Author SHA1 Message Date
Hide_D 2997d5dc21 package.json 수정 2023-09-23 16:26:30 +00:00
Hide_D bfbc0134de wip 2023-09-23 16:24:32 +00:00
Hide_D ffd3ab681e wip 2023-09-23 15:44:35 +00:00
Hide_D 4e629e0b1e 파일 위치 이동 2023-09-23 14:21:21 +00:00
Hide_D 438df6cd3c wip 2023-09-23 13:24:37 +00:00
Hide_D 9c9d9e9545 wip 2023-09-23 12:46:52 +00:00
Hide_D 93686c4912 gateway 코드 이식 중 2023-09-22 21:34:01 +09:00
Hide_D 438ee85ed2 이동 2023-09-22 21:13:33 +09:00
Hide_D 295d39327b api_def 2023-09-22 20:51:08 +09:00
Hide_D b8d57a014a 코드 이식 2023-09-22 20:46:23 +09:00
Hide_D 0f9d5c2dc9 @strpc ready 2023-09-22 20:32:41 +09:00
Hide_D 3013554cef 사소한 수정 2023-08-20 07:57:05 +00:00
Hide_D 32b41dd07a josonify 2023-08-20 07:55:54 +00:00
Hide_D f4640abe17 rootAPI to gatewayAPI 2023-08-20 06:07:06 +00:00
Hide_D 56a9c5bc45 parseSecurePlaintextToken validUntil 2023-08-19 07:05:50 +00:00
Hide_D 65bf02a3aa generateSecureToken 2023-08-19 07:05:01 +00:00
Hide_D 08890309c2 SecureToken을 하나로 2023-08-19 06:58:38 +00:00
Hide_D 194b213f61 securetoken 타입 변경
- 웹 통신시 혹시 모르니
2023-08-19 06:54:53 +00:00
Hide_D f8b8ea6cbe SecureToken 구현
- gateway - server 통신용
2023-08-19 06:50:32 +00:00
Hide_D 0ee7066679 aes 수정, 전통적으로. 2023-08-19 05:42:56 +00:00
Hide_D b850eb285b bson 2023-08-19 04:50:42 +00:00
Hide_D 6cbbc3222a ts type error 2023-08-19 04:49:49 +00:00
Hide_D ae7701ee18 ts type error 수정 2023-08-19 04:49:26 +00:00
Hide_D 72c6f5ae64 aes에 aad 2023-08-19 04:49:12 +00:00
Hide_D ad4ac9dfb2 aes 2023-08-19 04:39:36 +00:00
Hide_D 635d7f167d gateway 구분중 2023-08-18 16:20:10 +00:00
Hide_D 87c239610a gamelogintoken 2023-08-18 15:52:27 +00:00
Hide_D 199938f2b2 gateway 2023-08-18 15:47:10 +00:00
Hide_D 60848987eb no typeorm, 2023-08-18 15:35:22 +00:00
Hide_D a37baa64fe gateway schema 2023-08-18 15:29:18 +00:00
Hide_D ae22fc7c28 경로 이동 2023-08-18 14:50:09 +00:00
Hide_D 17977b4d96 UserLog 2023-08-18 14:49:45 +00:00
Hide_D 0fda257ca0 User Schema 2023-08-18 14:38:22 +00:00
Hide_D aec88333c2 경로 이동 2023-08-18 14:13:46 +00:00
Hide_D df8b5e854b ProcDecorator에서 chain 실패시 never 반환해야함 2023-08-18 13:56:02 +00:00
Hide_D c012a81377 버그 수정 2023-08-18 13:25:57 +00:00
Hide_D b6cdd49449 dep 2023-08-18 13:14:26 +00:00
Hide_D 837cff225b dep 정리 2023-08-18 13:13:31 +00:00
Hide_D de0b851217 api 구조 변경 2023-08-18 13:11:59 +00:00
Hide_D bfe0b99d44 api 설계 변경 2023-08-18 13:02:40 +00:00
Hide_D 7716c6936b dep 2023-08-18 12:39:25 +00:00
Hide_D 8e96f6082d fix: 소소한 import 실수 2023-08-13 09:39:20 +00:00
Hide_D 302bab518e UniqueNumberAllocator 구현
- DB에 원자적 값 증가 호출이 가능하다는 가정
2023-08-13 09:39:02 +00:00
Hide_D 0ba0b83433 rwlock 변경 2023-08-13 09:38:31 +00:00
Hide_D 3a06e44e85 fix 2023-08-09 16:23:31 +00:00
Hide_D 81d16aaba2 refac: api 시스템 교체
- procDecorator의 타입 복잡도를 낮춤
- 타입 에러가 살짝 더 깔끔함
2023-08-09 16:10:53 +00:00
Hide_D 0c511d27c5 info 2023-08-09 16:14:07 +09:00
Hide_D 1214036091 리팩토링 2023-08-08 17:54:19 +00:00
Hide_D 13d9662439 빌드 2023-08-08 17:46:39 +00:00
Hide_D e381dd5a7e 대충이런느낌 2023-08-08 17:10:15 +00:00
Hide_D c7d0702e66 안이쁜데 2023-08-08 16:08:23 +00:00
Hide_D bf35eb4c09 ng2 2023-08-08 16:00:18 +00:00
Hide_D deb989e45b 꼬이네.. 2023-08-08 15:41:08 +00:00
Hide_D 96b18ad5ff ProcDecorator 정리 2023-08-08 14:44:42 +00:00
Hide_D 0d57ff9783 업그레이드
- 이름 변경 ProcDecorator
- Promise
- Pre, Post 모드 분리
2023-08-08 14:16:03 +00:00
Hide_D ff423189e9 Promise 2023-08-08 13:20:44 +00:00
Hide_D 507cba34eb 정리 2023-08-07 18:48:26 +00:00
Hide_D b30dde442b Solve 2023-08-07 18:23:37 +00:00
Hide_D 3f7e645e40 테스트 2023-08-07 17:30:45 +00:00
Hide_D d928241e00 declconverter 연습 2023-08-07 17:14:27 +00:00
Hide_D 35f5726659 tsconfig 2023-08-07 15:42:48 +00:00
Hide_D ce1749e090 lint 수정 2023-08-06 14:39:53 +00:00
Hide_D 4b6d48850e no-explicit-any 등록 2023-08-06 14:27:30 +00:00
Hide_D 31fa264160 t 2023-08-06 13:54:23 +00:00
Hide_D 69de10642e express.js 준비 2023-08-06 13:53:12 +00:00
Hide_D 691f5e79ce ArgType 변경 2023-08-06 13:39:22 +00:00
Hide_D 3256620276 argValidator 타입 처리 2023-08-06 13:22:21 +00:00
Hide_D a1502ebc16 test api 2023-08-06 12:58:53 +00:00
Hide_D 1d751ea045 node.js이므로 generator 형식 변경 2023-08-06 12:50:56 +00:00
Hide_D cca7214446 정리 2023-08-06 12:40:09 +00:00
Hide_D 257644aff7 api 타입 시스템 준비 완료 2023-08-06 12:38:38 +00:00
Hide_D 5e216e5f67 API Structure 재정비 2023-08-06 12:31:51 +00:00
Hide_D 19a19d8a2f 위치 이동 2023-08-06 11:02:03 +00:00
Hide_D 3432009213 경로 이동 2023-08-06 09:29:16 +00:00
Hide_D 826b4212e5 강력한 API 타입 체크 2023-08-06 09:27:24 +00:00
Hide_D 7352cdbf23 작업중 2023-08-06 08:03:29 +00:00
Hide_D 2b27b8b427 api 구성 준비 2023-08-06 05:49:25 +00:00
Hide_D 5b6751f420 dep 2023-08-06 04:10:19 +00:00
Hide_D 2fb22ed6a9 fix: LiteHashDRBG에서 state 초기화 문제 2023-08-05 17:23:21 +00:00
Hide_D 743ab3e1aa fix 2023-08-05 16:03:31 +00:00
Hide_D 92a67315f3 LiteHashDRBG를 Promise로 변경
- subtleCrypto를 쓰다보니..
- genNextBlock()을 최대한 지연
2023-08-05 15:35:34 +00:00
Hide_D 8130c731e0 이전 util 이전 2023-08-05 15:34:26 +00:00
Hide_D 5ba073c125 path 2023-08-05 12:22:51 +00:00
Hide_D 45d05f900f dotenv config 2023-08-05 12:14:36 +00:00
Hide_D 580dd4aeee tmp 2023-08-05 12:14:15 +00:00
Hide_D b88fdf85e1 copy files 2023-08-05 12:12:18 +00:00
Hide_D 421d75f665 tsconfig 2023-08-05 11:11:22 +00:00
Hide_D a261b45e5d dto work
- vscode쪽에서 강제로 내는 에러
- .vscode/settings.json 에서
"js/ts.implicitProjectConfig.experimentalDecorators": true
필요
2023-08-04 17:33:16 +00:00
Hide_D a39543e718 typecheck 2023-08-04 17:10:34 +00:00
Hide_D f2ad6ad897 ts-node setting 2023-08-04 16:55:23 +00:00
Hide_D bb28815f93 no readme 2023-08-04 16:23:37 +00:00
Hide_D 9557e9b099 package 2023-08-04 16:23:00 +00:00
Hide_D 0622c534bc 일부 수정 2023-08-04 16:08:48 +00:00
Hide_D 58562e2c99 single package 2023-08-04 16:02:54 +00:00
Hide_D 1287486d74 test 2023-08-04 15:50:21 +00:00
71 changed files with 1695 additions and 3261 deletions
+2 -2
View File
@@ -11,7 +11,7 @@
"scripts": {
"build": "tsc --build"
},
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
@@ -21,7 +21,7 @@
"@strpc/def": "workspace:^"
},
"devDependencies": {
"zod": "^3.22.4"
"zod": "^3.22.2"
},
"peerDependencies": {
"zod": "^3.22.2"
+15 -6
View File
@@ -2,16 +2,24 @@ import { GET, POST, type DefAPINamespace } from "@strpc/def";
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
import type {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
InvalidResponse, ValidResponse
InvalidResponse, ValidResponse
} from "@strpc/def";
import type {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ArgDeleteAPI, ArgGetAPI, ArgHeadAPI, ArgPatchAPI, ArgPostAPI, ArgPutAPI,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
EmptyDeleteAPI, EmptyGetAPI, EmptyHeadAPI, EmptyPatchAPI, EmptyPostAPI, EmptyPutAPI,
ArgDeleteAPI,
ArgGetAPI,
ArgHeadAPI,
ArgPatchAPI,
ArgPostAPI,
ArgPutAPI,
EmptyDeleteAPI,
EmptyGetAPI,
EmptyHeadAPI,
EmptyPatchAPI,
EmptyPostAPI,
EmptyPutAPI,
} from "@strpc/def/types";
export type LoginResponse = {
result: true,
nextToken: [number, string] | undefined,
@@ -41,6 +49,7 @@ export type AutoLoginFailed = {
reason: string,
}
/** @internal */
export const structure = {
Login: {
LoginByID: POST<{
+14 -8
View File
@@ -1,20 +1,26 @@
import type {
DefAPINamespace,
} from "@strpc/def";
import { GET, POST } from '@strpc/def';
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
import type {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
InvalidResponse, ValidResponse
InvalidResponse, ValidResponse
} from "@strpc/def";
import type {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ArgDeleteAPI, ArgGetAPI, ArgHeadAPI, ArgPatchAPI, ArgPostAPI, ArgPutAPI,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
EmptyDeleteAPI, EmptyGetAPI, EmptyHeadAPI, EmptyPatchAPI, EmptyPostAPI, EmptyPutAPI,
ArgDeleteAPI,
ArgGetAPI,
ArgHeadAPI,
ArgPatchAPI,
ArgPostAPI,
ArgPutAPI,
EmptyDeleteAPI,
EmptyGetAPI,
EmptyHeadAPI,
EmptyPatchAPI,
EmptyPostAPI,
EmptyPutAPI,
} from "@strpc/def/types";
import { GET, POST } from '@strpc/def';
export type {
InferResponse,
+23 -24
View File
@@ -5,47 +5,46 @@
"main": "dist/index.js",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "run-p type-check build-only",
"preview": "vite preview",
"type-check": "vue-tsc",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@sammo/game_logic": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/client_ky": "workspace:^",
"@strpc/def": "workspace:^",
"bootstrap": "^5.3.3",
"bootstrap-vue-next": "^0.16.6",
"bson": "^6.4.0",
"date-fns": "^3.3.1",
"ky": "^1.2.2",
"bootstrap": "^5.3.1",
"bootstrap-vue-next": "^0.9.26",
"bson": "^5.4.0",
"date-fns": "^2.30.0",
"ky": "^1.0.1",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
"pinia": "^2.1.4",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.22.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"@types/lodash-es": "^4.17.9",
"@types/node": "^20.6.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.50.0",
"eslint-plugin-vue": "^9.15.1",
"prettier": "^3.0.0",
"ts-node": "^10.9.1",
"tslib": "^2.6.2",
"vite": "^5.1.5",
"vue-tsc": "^2.0.5",
"zod": "^3.22.4"
"vite": "^4.4.6",
"vue-tsc": "^1.8.6",
"zod": "^3.22.2"
},
"peerDependencies": {
"zod": "^3.22.2"
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+24 -19
View File
@@ -1,23 +1,28 @@
{
"extends": "../../tsconfig.vite.json",
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"exclude": [
],
"references": [
{
"path": "../util"
},
{
"path": "../secure_token"
},
{
"path": "../../@strpc/def"
},
{
"path": "./tsconfig.node.json"
}
{
"path": "../util"
},
{
"path": "../secure_token"
},
{
"path": "../../@strpc/def"
},
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
]
}
}
+13 -5
View File
@@ -1,10 +1,18 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
"moduleResolution": "Bundler",
"types": ["node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
+11 -11
View File
@@ -18,27 +18,27 @@
"build": "tsc --build",
"test": "jest"
},
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/util": "workspace:^"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"bson": "^6.4.0",
"@types/jest": "^29.5.4",
"@types/lodash": "^4.14.198",
"@types/lodash-es": "^4.17.9",
"@types/node": "^20.6.0",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"bson": "^5.4.0",
"buffer": "^6.0.3",
"eslint": "^8.57.0",
"eslint": "^8.49.0",
"jest": "^29.7.0",
"lodash-es": "^4.17.21",
"ts-jest": "^29.1.2",
"ts-jest": "^29.1.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3"
"typescript": "^5.2.2"
},
"peerDependencies": {
"bson": "^5.4.0",
-5
View File
@@ -1,5 +1,4 @@
import { InvalidArgument } from '@sammo/util';
import type { RandUtil } from './RandUtil.js';
export class InvalidVType extends InvalidArgument {
public override name = 'InvalidVType';
@@ -100,10 +99,6 @@ export * as SHA2 from './SHA2.js';
export * as ECDHe from './ECDHe.js';
export * as ECDHe_AES from './ECDHe_AES.js';
export { RandUtil } from './RandUtil.js';
export { LiteHashDRBG } from './LiteHashDRBG.js';
export type { RNG } from './RNG.js';
export * as RawTypes from './RawTypes.js';
export * from './utils.js';
+4 -5
View File
@@ -6,19 +6,18 @@
"scripts": {
"build": "tsc --build"
},
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/api_def": "workspace:^",
"@sammo/crypto": "workspace:^",
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"dotenv": "^16.4.5",
"mongoose": "^8.2.1"
"dotenv": "^16.3.1",
"mongoose": "^7.4.3"
},
"devDependencies": {
"@types/node": "^20.11.24"
"@types/node": "^20.6.3"
}
}
-8
View File
@@ -1,8 +0,0 @@
import type { CityID } from "./defs.js";
export class City{
constructor(
public readonly id: CityID
){
}
}
View File
-3
View File
@@ -1,3 +0,0 @@
export class GameEnv{
}
-149
View File
@@ -1,149 +0,0 @@
import { InvalidArgument } from "@sammo/util";
import { City } from "./City.js";
import type { IGeneralEntity } from "./GeneralEntity.js";
import type { IResourceController } from "./ResourceController.js";
import { DiplomaticPermission, type CityID, type GeneralID, type GeneralName, type NationID, OfficerLevel, type TroopID } from "./defs.js";
import { must } from "@sammo/util";
import { Nation } from "./Nation.js";
import { Troop } from "./Troop.js";
export class General {
private _raw: IGeneralEntity;
private _city: WeakRef<City>;
private _troop: WeakRef<Troop> | undefined;
private _nation: WeakRef<Nation>;
public get raw(): Readonly<IGeneralEntity> {
return this._raw;
}
constructor(
raw: IGeneralEntity,
private resourceController: IResourceController
){
this._raw = raw;
this._city = new WeakRef(must(resourceController.getCity(raw.cityID)));
this._nation = new WeakRef(must(resourceController.getNation(raw.nationID)));
if(raw.troopID){
this._troop = new WeakRef(must(resourceController.getTroop(raw.troopID, raw.nationID)));
}
}
public get id(): GeneralID {
return this._raw.id;
}
public get name(): GeneralName {
return this._raw.name;
}
public update(callback: (oldRaw: IGeneralEntity) => void, force = false) {
const oldCityID = this._raw.cityID;
const oldTroopID = this._raw.troopID;
const oldNationID = this._raw.nationID;
callback(this._raw);
if(!force){
if(oldCityID !== this._raw.cityID){
throw new Error("City change by update() is not allowed");
}
if(oldTroopID !== this._raw.troopID){
throw new Error("Troop change by update() is not allowed");
}
if(oldNationID !== this._raw.nationID){
throw new Error("Nation change by update() is not allowed");
}
}
this.resourceController.setDirtyGeneral(this._raw);
}
public changeCity(city: CityID | City){
const cityObj: City = (()=>{
if(city instanceof City){
return city;
}
const cityObj = this.resourceController.getCity(city);
if(!cityObj){
throw new InvalidArgument(`City not found: ${city}`);
}
return cityObj;
})();
this._city = new WeakRef(cityObj);
//TODO: ResourceController에게 알려야 하는가?
this.update((raw) => {
raw.cityID = cityObj.id;
}, true);
}
public quitTroop(){
if(!this._troop){
return;
}
const troop = must(this._troop.deref());
if(troop.leader.id === this._raw.id){
troop.destructTroop(false);
}
else{
troop.members.delete(this._raw.id);
}
this._troop = undefined;
this.update((raw) => {
raw.troopID = undefined;
}, true);
}
public changeTroop(troop: TroopID | Troop){
this.quitTroop();
const troopObj: Troop = (()=>{
if(troop instanceof Troop){
return troop;
}
const troopObj = this.resourceController.getTroop(troop, this._raw.nationID);
if(!troopObj){
throw new InvalidArgument(`Troop not found: ${troop}`);
}
return troopObj;
})();
troopObj.members.set(this._raw.id, new WeakRef(this));
this._troop = new WeakRef(troopObj);
this.update((raw) => {
raw.troopID = troopObj.id;
}, true);
}
public changeNation(nation: NationID | Nation){
const nationObj: Nation = (()=>{
if(nation instanceof Nation){
return nation;
}
const nationObj = this.resourceController.getNation(nation);
if(!nationObj){
throw new InvalidArgument(`Nation not found: ${nation}`);
}
return nationObj;
})();
this._nation = new WeakRef(nationObj);
this.quitTroop();
this.update((raw) => {
raw.nationID = nationObj.id;
raw.diplomaticPermission = DiplomaticPermission.none;
if(nationObj.id == 0){
raw.officerLevel = OfficerLevel.none;
}
else{
raw.officerLevel = OfficerLevel.normal;
}
raw.nationBelong = 0;
}, true);
}
}
-65
View File
@@ -1,65 +0,0 @@
import type { DiplomaticPermission, GeneralID, GeneralName, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, OwnerID, OwnerName, PersonalityType, SpecialityDomesticType, SpecialityWarType, TroopID } from "./defs.js";
export interface IGeneralEntity {
id: GeneralID;
owner?: OwnerID;
npcType: NpcType;
affinity?: number;
picture?: string;
name: GeneralName;
ownerName?: OwnerName;
nationID: NationID;
cityID: number;
troopID?: TroopID;
leadership: number;
leadershipExp: number;
strength: number;
strengthExp: number;
intel: number;
intelExp: number;
injury: number;
experience: number;
dedication: number;
experienceLevel: number;
dedicationLevel: number;
dex: number[];
officerLevel: OfficerLevel;
diplomaticPermission: DiplomaticPermission;
gold: number;
rice: number;
item: Record<ItemKeyType, {
id: ItemID,
option: Record<string, unknown>,
}>;
turntime: Date;
killturn: number;
age: number;
startAge: number;
nationBelong: number;
betray: number;
personality: PersonalityType;
specialityDomestic: SpecialityDomesticType;
specialityWar: SpecialityWarType;
defenceTrain: number;
npcSpec?: {
msg?: string;
bornyear: number;
deadyear: number;
}
}
@@ -1,19 +0,0 @@
import type { IAction } from "../IAction.js";
import type { SpecialReqType, WeightType } from "./defs.js";
export abstract class BaseSpecial implements IAction {
public abstract readonly name: string;
static readonly selectWeight: WeightType = {
type: "normal",
weight: 1,
}
static readonly type: SpecialReqType[] = [];
getName(): string {
return this.name;
}
abstract getInfo(): string[]|string;
}
@@ -1,48 +0,0 @@
import type { General } from "../../General.js";
import type { BattlePhaseTrigger, CalcStat, CalcStatRange } from "../../IActionMethod.js";
import { WarUnitTriggerCaller } from "../../TriggerCaller/WarUnitTriggerCaller.js";
import type { WarUnit } from "../../WarUnit.js";
import { che_필살강화_회피불가 } from "../../WarUnitTrigger/che_필살강화_회피불가.js";
import type { CalcStatName, CalcStatRangeName } from "@/defs.js";
import { BaseSpecial } from "../BaseSpecial.js";
import type { SpecialReqType, WeightNormal } from "../defs.js";
export class che_필살 extends BaseSpecial implements CalcStat, CalcStatRange, BattlePhaseTrigger {
public readonly name = "필살";
static override readonly selectWeight: WeightNormal = {
type: "normal",
weight: 1,
}
static override type: SpecialReqType[] = [
'statLeadership',
'statStrength',
'statIntel',
];
getInfo() {
return "[전투] 필살 확률 +30%p, 필살 발동시 대상 회피 불가, 필살 계수 향상";
}
onCalcStat(general: General, statName: CalcStatName, value: number, aux?: unknown[] | undefined): number {
if (statName == 'warCriticalRatio') {
return value + 0.3;
}
return value;
}
onCalcStatRange(general: General, statName: CalcStatRangeName, value: [number, number], aux?: unknown[] | undefined): [number, number] {
if (statName === 'criticalDamageRange'){
const [rangeMin, rangeMax] = value;
return [(rangeMin + rangeMax) / 2, rangeMax];
}
return value;
}
getBattlePhaseSkillTriggerList(unit: WarUnit): WarUnitTriggerCaller | undefined {
return new WarUnitTriggerCaller(
new che_필살강화_회피불가(unit),
);
}
}
@@ -1,6 +0,0 @@
class SpecialityHelper {
}
export const specialityHelper = new SpecialityHelper();
@@ -1,30 +0,0 @@
export type SpecialReqType =
"statLeadership" |
"statStrength" |
"statIntel" |
"armyFootMan" |
"armyArcher" |
"armyCavalry" |
"armyWizard" |
"armySiege" |
"reqDexterity" |
"statNotLeadership" |
"statNotStrength" |
"statNotIntel"
;
export type WeightNormal = {
type: "normal";
weight: number; // 1을 기준으로 한 가중치 곱
};
export type WeightPercent = {
type: "percent";
weight: number; // 1이면 100%
};
export type WeightType = WeightNormal | WeightPercent;
@@ -1,8 +0,0 @@
export { BaseSpecial } from "./BaseSpecial.js";
import { che_필살 } from "./SpecialWar/che_필살.js";
export const SpecialWar = {
che_필살
}
-23
View File
@@ -1,23 +0,0 @@
import type * as IMethod from "./IActionMethod.js";
export type * as IMethod from "./IActionMethod.js";
export interface IActionMethod extends
IMethod.TurnExecuteTriggerList,
IMethod.CalcDomestic,
IMethod.CalcStat,
IMethod.CalcStatRange,
IMethod.CalcOpposeStat,
IMethod.CalcStrategic,
IMethod.CalcIncome,
IMethod.WarPowerMultiplier,
IMethod.BattleInitTrigger,
IMethod.BattlePhaseTrigger,
IMethod.ArbitraryAction {
}
export interface IAction extends Partial<IActionMethod> {
readonly name: string;
getName(): string;
getInfo(): string[] | string;
}
-63
View File
@@ -1,63 +0,0 @@
import type { RandUtil } from "@sammo/crypto";
import type { WarUnitTriggerCaller } from "./TriggerCaller/WarUnitTriggerCaller.js";
import type { GeneralTriggerCaller } from "./TriggerCaller/GeneralTriggerCaller.js";
import type { General } from "./General.js";
import type { WarUnit } from "./WarUnit.js";
import type { ArbitraryActionType, CalcIncomeType, CalcStatName, CalcStatRangeName, CalcStrategicVarType, StrategicTurnType } from "./defs.js";
//TODO: turnType, varType이 string이면 안됨. enum에 가까워야함!
type onCalcDomesticFunc = (turnType: string, varType: string, value: number, aux?: unknown[]) => number;
type onCalcStatFunc = (general: General, statName: CalcStatName, value: number, aux?: unknown[]) => number;
type onCalcStatRangeFunc = (general: General, statName: CalcStatRangeName, value: [number, number], aux?: unknown[]) => [number, number];
type onCalcStrategicFunc = (turnType: StrategicTurnType, varType: CalcStrategicVarType, value: number) => number;
type onCalcIncomeFunc = (type: CalcIncomeType, value: number) => number;
type getWarPowerMultiplierFunc = (unit: WarUnit) => [number, number];
type getWarUnitTriggerListFunc = (unit: WarUnit) => WarUnitTriggerCaller | undefined;
type onArbitraryActionFunc = (general: General, rng: RandUtil, actionType: ArbitraryActionType, phase?: string, aux?: unknown[]) => null | string[];
export interface TurnExecuteTriggerList {
getPreTurnExecuteTriggerList(general: General): GeneralTriggerCaller | undefined;
}
export interface CalcDomestic {
onCalcDomestic: onCalcDomesticFunc;
}
export interface CalcStat {
onCalcStat: onCalcStatFunc;
}
export interface CalcStatRange {
onCalcStatRange: onCalcStatRangeFunc;
}
export interface CalcOpposeStat {
onCalcOpposeStat: onCalcStatFunc;
}
export interface CalcStrategic {
onCalcStrategic: onCalcStrategicFunc;
}
export interface CalcIncome {
onCalcNationalIncome: onCalcIncomeFunc;
}
export interface WarPowerMultiplier {
getWarPowerMultiplier: getWarPowerMultiplierFunc;
}
export interface BattleInitTrigger {
getBatteInitSkillTriggerList: getWarUnitTriggerListFunc;
}
export interface BattlePhaseTrigger {
getBattlePhaseSkillTriggerList: getWarUnitTriggerListFunc;
}
export interface ArbitraryAction {
onArbitraryAction: onArbitraryActionFunc;
}
-9
View File
@@ -1,9 +0,0 @@
import type { NationID, NationName, NationType } from "./defs.js";
export class Nation {
constructor(
public readonly id: NationID,
public readonly name: NationName,
public readonly type: NationType,
) { }
}
@@ -1,64 +0,0 @@
import type { General } from "./General.js";
import type { City } from "./City.js";
import type { Nation } from "./Nation.js";
import type { CityID, GeneralID, NationID, TroopID } from "./defs.js";
import type { GameEnv } from "./GameEnv.js";
import type { IGeneralEntity } from "./GeneralEntity.js";
import type { Troop } from "./Troop.js";
import type { ITroopEntity } from "./TroopEntity.js";
export interface IResourceController{
addCity(city: City): boolean;
addNation(nation: Nation): boolean;
addGeneral(general: General): boolean;
getCity(id: CityID): City | undefined;
getNation(id: NationID): Nation | undefined;
getGeneral(id: GeneralID): General | undefined;
getGameEnv(): Promise<GameEnv>;
findAllNations(): Map<NationID, Nation>;
findAllCities(): Map<CityID, City>;
findAllGenerals(): Map<GeneralID, General>;
findGeneralByTroop(id: TroopID): Map<GeneralID, General>;
findCityByNation(id: NationID): Map<CityID, City>;
findGeneralByNation(id: NationID): Map<GeneralID, General>;
findGeneralByCity(id: CityID, nationID?: NationID): Map<GeneralID, General>;
getTroop(id: TroopID, nation: NationID): Troop | undefined;
_getAllChanges(): Map<unknown, unknown>;
setDirtyGeneral(rawGeneral: IGeneralEntity): void;
setDirtyCity(city: City): void;
setDirtyTroop(troop: Troop | TroopID): void;
setDirtyNation(nation: Nation): void;
createGeneral(raw: IGeneralEntity): General;
createTroop(raw: ITroopEntity): Troop;
//createNation
reserveDeleteTroop(id: TroopID): void;
reserveDeleteGeneral(id: GeneralID): void;
reserveDeleteNation(id: NationID): void;
resetAndFill(): Promise<void>;
saveAll(): Promise<void>;
}
let _resourceController: IResourceController | undefined = undefined;
export const setResourceController = (resourceController: IResourceController) => {
_resourceController = resourceController;
}
export const getResourceController = () => {
if (_resourceController === undefined) {
throw new Error("ResourceController not set");
}
return _resourceController;
}
@@ -1,11 +0,0 @@
import type { General } from "../General.js";
import { ObjectTrigger } from "./ObjectTrigger.js";
export abstract class BaseGeneralTrigger extends ObjectTrigger<General> {
constructor(
protected object: General,
protected priority: number
) {
super();
}
}
@@ -1,43 +0,0 @@
import type { RandUtil } from "@sammo/crypto";
import type { WarUnit, WarUnitEnv } from "../WarUnit.js";
import { ObjectTrigger, PRIORITY } from "./ObjectTrigger.js";
import type { GameEnv } from "../GameEnv.js";
import { NotYetImplemented } from "@sammo/util";
export const RaiseType = {
None: 0,
Item: 1,
ConsumableItem: 3,
DedupTypeBase: 1024,
} as const;
export abstract class BaseWarUnitTrigger extends ObjectTrigger<WarUnit> {
protected override priority: number = PRIORITY.BODY;
constructor(
protected object: WarUnit,
protected raiseType: number = RaiseType.None,
) {
super();
}
override getUniqueID(): string {
const priority = this.getPriority();
const name = this.constructor.name;
const unitName = this.object?.constructor.name ?? "undefined";
return `${priority}:${name}:${unitName}`;
}
action(rng: RandUtil, env: GameEnv, arg: unknown[]): void {
throw new NotYetImplemented();
console.log(rng, env, arg);
}
abstract actionWar(self: WarUnit, oppose: WarUnit, selfEnv: WarUnitEnv, opposeEnv: WarUnitEnv, rng: RandUtil): boolean;
processConsumableItem(): boolean {
throw new NotYetImplemented();
}
}
@@ -1,37 +0,0 @@
import type { RandUtil } from "@sammo/crypto";
import type { GameEnv } from "../GameEnv.js";
export const PRIORITY = {
MIN: 0,
BEGIN: 10000,
PRE: 20000,
BODY: 30000,
POST: 40000,
FINAL: 50000,
} as const;
export abstract class ObjectTrigger<T extends object> {
protected abstract priority: number;
protected abstract object?: T;
getPriority(): number {
return this.priority;
}
setPriority(priority: number): this {
this.priority = priority;
return this;
}
//FIXME: arg array. generic이어야하나?
abstract action(rng: RandUtil, env: GameEnv, arg: unknown[]): void;
//FIXME: 재설계 필요.
getUniqueID(): string{
const priority = this.getPriority();
//FIXME: PHP 버전에서 namespace 개념도 옮겨와야하는가?
const objName = this.constructor.name;
return `${priority}:${objName}`;
}
}
@@ -1,9 +0,0 @@
import { BaseGeneralTrigger } from "../Trigger/BaseGeneralTrigger.js";
import type { ObjectTrigger } from "../Trigger/ObjectTrigger.js";
import { TriggerCaller } from "./TriggerCaller.js";
export class GeneralTriggerCaller extends TriggerCaller<BaseGeneralTrigger> {
checkValidTrigger(trigger: ObjectTrigger<object>): boolean {
return trigger instanceof BaseGeneralTrigger;
}
}
@@ -1,54 +0,0 @@
import { ObjectTrigger, PRIORITY } from "../Trigger/ObjectTrigger.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export abstract class TriggerCaller<T extends ObjectTrigger<any>> {
protected triggerListByPriority: Map<number, Map<string, T>>;
abstract checkValidTrigger(trigger: T): boolean;
isEmpty(): boolean {
return this.triggerListByPriority.size === 0;
}
constructor(...triggerList: T[]) {
if(triggerList.length === 0){
this.triggerListByPriority = new Map();
return;
}
this.triggerListByPriority = new Map();
let sorted = true;
let maxPriority: number = PRIORITY.MIN;
for (const trigger of triggerList) {
const priority = trigger.getPriority();
const uniqueID = trigger.getUniqueID();
if(sorted){
if(maxPriority < priority){
maxPriority = priority;
}
else if(maxPriority > priority){
sorted = false;
}
}
if(this.triggerListByPriority.has(priority)){
const subTriggerList = this.triggerListByPriority.get(priority) as Map<string, T>;
if(subTriggerList.has(uniqueID)){
//TODO: 완성되기 전까진..
throw `Duplicated trigger: ${uniqueID}`;
}
subTriggerList.set(uniqueID, trigger);
}
else {
this.triggerListByPriority.set(priority, new Map([[uniqueID, trigger]]));
}
}
if(!sorted){
this.triggerListByPriority = new Map([...this.triggerListByPriority.entries()].sort((a, b) => b[0] - a[0]));
}
}
}
@@ -1,9 +0,0 @@
import { BaseWarUnitTrigger } from "../Trigger/BaseWarUnitTrigger.js";
import type { ObjectTrigger } from "../Trigger/ObjectTrigger.js";
import { TriggerCaller } from "./TriggerCaller.js";
export class WarUnitTriggerCaller extends TriggerCaller<BaseWarUnitTrigger> {
checkValidTrigger(trigger: ObjectTrigger<object>): boolean {
return trigger instanceof BaseWarUnitTrigger;
}
}
-53
View File
@@ -1,53 +0,0 @@
import { must } from "@sammo/util";
import type { General } from "./General.js";
import type { IResourceController } from "./ResourceController.js";
import type { ITroopEntity } from "./TroopEntity.js";
import type { GeneralID } from "./defs.js";
export class Troop {
private _raw: ITroopEntity
private _leader: WeakRef<General>;
private _members: Map<GeneralID, WeakRef<General>>;
public get raw(): Readonly<ITroopEntity> {
return this._raw;
}
constructor(
raw: ITroopEntity,
private resourceController: IResourceController
){
this._raw = raw;
const members = resourceController.findGeneralByTroop(raw.id);
const leaderID: GeneralID = raw.id as GeneralID;
this._leader = new WeakRef(must(members.get(leaderID)));
members.delete(leaderID);
this._members = new Map([...members].map(([k,v])=>[k, new WeakRef(v)]));
}
public get id(){
return this._raw.id;
}
public get leader(){
return must(this._leader.deref());
}
public get members(): Map<GeneralID, WeakRef<General>> {
return this._members;
}
public destructTroop(withLeader = true){
this._members.forEach((member)=>{
const general = must(member.deref());
general.quitTroop();
});
if(withLeader){
this.leader.quitTroop();
}
this.resourceController.reserveDeleteTroop(this._raw.id);
}
}
-7
View File
@@ -1,7 +0,0 @@
import type { NationID, TroopID } from "./defs.js";
export interface ITroopEntity {
id: TroopID;
nationID: NationID;
name: string;
}
-15
View File
@@ -1,15 +0,0 @@
import type { WarSkillName } from "./defs.js";
export class WarUnitEnv {
}
export abstract class WarUnit{
hasActivatedSkill(skillName: WarSkillName): boolean {
throw new Error("Method not implemented.");
}
activateSkill(skillName: WarSkillName): void {
throw new Error("Method not implemented.");
}
}
-5
View File
@@ -1,5 +0,0 @@
import { WarUnit } from "./WarUnit.js";
export class WarUnitCity extends WarUnit{
}
-5
View File
@@ -1,5 +0,0 @@
import { WarUnit } from "./WarUnit.js";
export class WarUnitGeneral extends WarUnit{
}
@@ -1,17 +0,0 @@
import type { RandUtil } from "@sammo/crypto";
import { BaseWarUnitTrigger } from "../Trigger/BaseWarUnitTrigger.js";
import type { WarUnit, WarUnitEnv } from "../WarUnit.js";
import { PRIORITY } from "../Trigger/ObjectTrigger.js";
export class che_필살강화_회피불가 extends BaseWarUnitTrigger {
protected override priority = PRIORITY.PRE + 150;
actionWar(self: WarUnit, oppose: WarUnit, selfEnv: WarUnitEnv, opposeEnv: WarUnitEnv, rng: RandUtil): boolean {
if(!self.hasActivatedSkill("필살")){
return true;
}
oppose.activateSkill("회피불가");
return true;
}
}
-126
View File
@@ -1,126 +0,0 @@
//Type 정의
export type OwnerID = number & { zz_t?: "OwnerID" };
export type OwnerName = string & { zz_t?: "OwnerName" };
export type GeneralID = number & { zz_t?: "GeneralID" };
export type GeneralName = string & { zz_t?: "GeneralName" };
export type CityID = number & { zz_t?: "CityID" };
export type CityName = string & { zz_t?: "CityName" };
export type TroopID = number & { zz_t?: "TroopID" };
export type TroopName = string & { zz_t?: "TroopName" };
export type NationID = number & { zz_t?: "NationID" };
export type NationName = string & { zz_t?: "NationName" };
//TODO: 무언가의 keyof 로 바꿔야함
export type NationType = string & { zz_t?: "NationType" };
export type StrategicTurnType = string & { zz_t?: "StrategicTurnType" };
export type ItemKeyType = 'horse' | 'weapon' | 'book' | 'item';
export type ItemID = string & { zz_t?: "ItemID" };
export type PersonalityType = string & { zz_t?: "PersonalityType" };
export type SpecialityDomesticType = string & { zz_t?: "SpecialityDomesticType" };
export type SpecialityWarType = string & { zz_t?: "SpecialityWarType" };
export const enum NpcType {
user = 0,
user_borrowed_npc = 1,
npc_scenario = 2,
npc_generated = 3,
npc_strategic = 4,
npc_troop_leader = 5,
npc_unselectable = 6,
npc_invader = 9,
}
export const enum OfficerLevel {
//재야
none = 0,
//일반
normal = 1,
//도시관직
cityLeadership = 2,
cityIntel = 3,
cityStrength = 4,
//지력관직
staffIntel3 = 5,
staffIntel2 = 7,
staffIntel1 = 9,
//무력관직
staffStrength3 = 6,
staffStrength2 = 8,
staffStrength1 = 10,
//참모
staffChief = 11,
//군주
lord = 12,
}
export const enum DiplomaticPermission {
none = 0,
audit = 1,
ambassador = 2,
}
/* 전용 stat이어서 확장하고자 하는경우 아래와 같이 확장 가능
```ts
declare module "@/defs.js" {
export interface CalcStatNameBase {
statName: unknown
}
export interface CalcStatRangeNameBase {
statName: unknown
}
}
```
*/
//필요할 때 확장
export interface CalcStatNameBase {
leadership: unknown,
strength: unknown,
intel: unknown,
warCriticalRatio: unknown;
}
export type CalcStatName = keyof CalcStatNameBase;
//필요할 때 확장
export interface CalcStatRangeNameBase {
criticalDamageRange: unknown;
}
export type CalcStatRangeName = keyof CalcStatRangeNameBase;
export interface CalcStrategicVarTypeBase {
globalDelay: unknown;
}
export type CalcStrategicVarType = keyof CalcStrategicVarTypeBase;
export interface CalcIncomeTypeBase {
gold: unknown;
rice: unknown;
}
export type CalcIncomeType = keyof CalcIncomeTypeBase;
export interface ArbitraryActionTypeBase {
}
export type ArbitraryActionType = keyof ArbitraryActionTypeBase;
export interface WarSkillNameBase {
필살: unknown;
회피불가: unknown;
회피: unknown;
}
export type WarSkillName = keyof WarSkillNameBase;
+1 -5
View File
@@ -1,5 +1 @@
export * from "./generalStats.js";
export * as defs from "./defs.js";
// NOTE: game_logic은 client, server 모두에서 사용되는 라이브러리이다.
// DB에 접근한다면 DI여야하나?
export * from "./generalStats.js";
-3
View File
@@ -3,9 +3,6 @@
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
+23 -24
View File
@@ -5,46 +5,45 @@
"main": "dist/index.js",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build": "run-p type-check build-only",
"preview": "vite preview",
"type-check": "vue-tsc",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@sammo/util": "workspace:^",
"@strpc/client_ky": "workspace:^",
"@strpc/def": "workspace:^",
"bootstrap": "^5.3.3",
"bootstrap-vue-next": "^0.16.6",
"bson": "^6.4.0",
"date-fns": "^3.3.1",
"ky": "^1.2.2",
"bootstrap": "^5.3.1",
"bootstrap-vue-next": "^0.9.26",
"bson": "^5.4.0",
"date-fns": "^2.30.0",
"ky": "^1.0.1",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
"pinia": "^2.1.4",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.22.0",
"prettier": "^3.2.5",
"ts-node": "^10.9.2",
"@types/lodash-es": "^4.17.9",
"@types/node": "^20.6.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.50.0",
"eslint-plugin-vue": "^9.15.1",
"prettier": "^3.0.0",
"ts-node": "^10.9.1",
"tslib": "^2.6.2",
"vite": "^5.1.5",
"vue-tsc": "^2.0.5",
"zod": "^3.22.4"
"vite": "^4.4.6",
"vue-tsc": "^1.8.6",
"zod": "^3.22.2"
},
"peerDependencies": {
"zod": "^3.22.2"
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+11 -7
View File
@@ -1,10 +1,11 @@
{
"extends": "../../tsconfig.vite.json",
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"exclude": [
],
"references": [
{
@@ -19,6 +20,9 @@
},
{
"path": "./tsconfig.node.json"
}
},
{
"path": "./tsconfig.app.json"
},
]
}
+13 -5
View File
@@ -1,10 +1,18 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
"moduleResolution": "Bundler",
"types": ["node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
+1
View File
@@ -11,6 +11,7 @@ export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@util': fileURLToPath(new URL('./server/util', import.meta.url)),
}
}
})
+12 -13
View File
@@ -4,14 +4,13 @@
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build",
"dev": "tsc --watch"
"build": "tsc --build"
},
"exports": {
".": "./dist/index.js",
"./exports": "./dist/exports.js"
},
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
@@ -20,19 +19,19 @@
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"date-fns": "^3.3.1",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"express-session": "^1.18.0",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-session": "^1.17.3",
"lodash-es": "^4.17.21",
"mongoose": "^8.2.1",
"reflect-metadata": "^0.2.1",
"mongoose": "^7.4.3",
"reflect-metadata": "^0.1.13",
"tslib": "^2.6.2",
"zod": "^3.22.4"
"zod": "^3.22.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/node": "^20.11.24"
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@types/node": "^20.6.3"
}
}
+3 -3
View File
@@ -6,7 +6,7 @@
"scripts": {
"build": "tsc --build"
},
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
@@ -14,9 +14,9 @@
"@sammo/util": "workspace:^"
},
"devDependencies": {
"bson": "^6.4.0",
"bson": "^5.4.0",
"buffer": "^6.0.3",
"zod": "^3.22.4"
"zod": "^3.22.2"
},
"peerDependencies": {
"bson": "^5.4.0",
+13 -16
View File
@@ -4,11 +4,9 @@
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build",
"dev": "tsc --watch",
"worker_test": "tsx src/worker_test.ts"
"build": "tsc --build"
},
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"type": "module",
"license": "MIT",
"dependencies": {
@@ -18,23 +16,22 @@
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"date-fns": "^3.3.1",
"denque": "^2.1.0",
"dotenv": "^16.4.5",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"eventemitter3": "^5.0.1",
"express": "^4.18.3",
"express-session": "^1.18.0",
"ky": "^1.2.2",
"express": "^4.18.2",
"express-session": "^1.17.3",
"ky": "^1.0.1",
"lodash-es": "^4.17.21",
"mongoose": "^8.2.1",
"reflect-metadata": "^0.2.1",
"mongoose": "^7.4.3",
"reflect-metadata": "^0.1.13",
"tslib": "^2.6.2",
"zod": "^3.22.4"
"zod": "^3.22.2"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@sammo/gateway_server": "workspace:^",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/node": "^20.11.24"
"@types/node": "^20.6.3"
}
}
-30
View File
@@ -1,30 +0,0 @@
import Denque from "denque";
export class GameEngine {
private static instance: GameEngine | null = null;
private npcActionQueue = new Denque<unknown>();
private apiActionQueue = new Denque<unknown>();
private constructor() {
}
public static getInstance() {
if (!GameEngine.instance) {
GameEngine.instance = new GameEngine();
}
return GameEngine.instance;
}
public async start() {
console.log('GameEngine started');
}
public async stop() {
console.log('GameEngine stopped');
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import './dotenv.js';
import 'dotenv/config';
import { connect, Mongoose } from "mongoose";
import { unwrap } from '@sammo/util';
-49
View File
@@ -1,49 +0,0 @@
import { delay, must } from "@sammo/util";
import { parentPort as _parentPort } from "node:worker_threads";
import { type MessagePort } from "node:worker_threads";
import { generateMessage, PortRPCClient, type InitPort, type RandomMsg } from "./worker_util.js";
import type { RPCSampleList } from "./worker_common.js";
if (!_parentPort) {
throw new Error("parentPort is not defined");
}
const parentPort = must(_parentPort);
let run = true;
async function worker_a(port: MessagePort) {
const rpcClient = new PortRPCClient<RPCSampleList>(port);
console.log("Hello from worker_a.ts!");
let cnt = 0;
const now = Date.now();
while (run && cnt < 10000) {
const message = generateMessage();
await rpcClient.callFunction('do', message);
cnt++;
if(cnt % 100 === 0){
console.log(`a_cnt: ${cnt}`);
}
}
const elapsed = Date.now() - now;
console.log(`Elapsed: ${elapsed}, cnt: ${cnt}`);
parentPort.postMessage(elapsed);
}
await new Promise<void>((resolve) => {
parentPort.once('message', async (ports: InitPort) => {
await worker_a(ports.oppose);
resolve();
});
parentPort.once('close', () => {
run = false;
});
});
-40
View File
@@ -1,40 +0,0 @@
import { delay, must } from "@sammo/util";
import { parentPort as _parentPort } from "node:worker_threads";
import { type MessagePort } from "node:worker_threads";
import { PortRPCServer, type InitPort, type RandomMsg } from "./worker_util.js";
import { unknown } from "zod";
if (!_parentPort) {
throw new Error("parentPort is not defined");
}
const parentPort = must(_parentPort);
let run = true;
function doFunc(msg: RandomMsg): string {
return msg.key;
}
let rpcServer: PortRPCServer<{}>|null = null;
async function worker_b(port: MessagePort) {
console.log("Hello from worker_b.ts!");
rpcServer = new PortRPCServer(port, {
do: doFunc
});
parentPort.postMessage("ready");
}
await new Promise<void>((resolve) => {
parentPort.once('message', async (ports: InitPort) => {
await worker_b(ports.oppose);
resolve();
});
parentPort.once('close', () => {
run = false;
});
});
-5
View File
@@ -1,5 +0,0 @@
import type { RandomMsg } from "./worker_util.js";
export type RPCSampleList = {
do: (msg: RandomMsg) => string;
}
-32
View File
@@ -1,32 +0,0 @@
import { Worker, MessageChannel } from "node:worker_threads";
import * as nodePath from "node:path";
import { fileURLToPath } from "node:url";
console.log("Hello from worker_test.ts!");
const __filename = fileURLToPath(new URL(import.meta.url));
const __dirname = nodePath.dirname(__filename);
const worker_b_ts = nodePath.join(__dirname, "worker_b");
const worker_a_ts = nodePath.join(__dirname, "worker_a");
const worker_b = new Worker(worker_b_ts);
const worker_a = new Worker(worker_a_ts);
{
const betweenPort = new MessageChannel();
worker_b.postMessage({ oppose: betweenPort.port2 }, [betweenPort.port2]);
worker_b.once('message', ()=>{
worker_a.postMessage({ oppose: betweenPort.port1 }, [betweenPort.port1]);
});
}
await new Promise<void>((resolve) => {
worker_a.once('message', async (cnt: number) => {
await worker_b.terminate();
resolve();
});
});
-143
View File
@@ -1,143 +0,0 @@
import { delay, must } from "@sammo/util";
import { type MessagePort } from "node:worker_threads";
export function randomString(len: number): string {
const charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charSetLen = charSet.length;
const str: string[] = [];
for (let i = 0; i < len; i++) {
str.push(charSet.charAt(Math.floor(Math.random() * charSetLen)));
}
return str.join("");
}
export type RandomMsg = {
key: string;
cnt: number;
arr: string[];
};
export function ArrayBufferFromString(str: string): ArrayBuffer {
const buf = Buffer.from(str);
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
export function generateMessage(): RandomMsg {
const randomObjCnt = Math.floor(Math.random() * 10);
const key = randomString(60);
const obj = {
key,
cnt: randomObjCnt,
arr: [] as string[],
};
for (let i = 0; i < randomObjCnt; i++) {
const randomStrLen = Math.floor(Math.random() * 20) + 4;
obj.arr.push(randomString(randomStrLen));
}
return obj;
}
export type RawRPCCall = [name: string, id: number, arg: unknown];
export type RawRPCResponse = [id: number, success: 1 | 0, result: unknown];
export type RPCLists = {
[name: string]: (arg: any) => any | Promise<any>;
}
export class PortRPCServer<RPCL extends RPCLists>{
private listeners: Map<string, (arg: unknown) => unknown | Promise<unknown>> = new Map();
constructor(private port: MessagePort, rpcLists: RPCL) {
port.on('message', async (buffer: ArrayBuffer) => {
if(!(buffer instanceof ArrayBuffer)){
throw new Error("RPCServer: buffer is not ArrayBuffer");
}
const rawText = Buffer.from(buffer).toString();
const data = JSON.parse(rawText) as RawRPCCall;
const [name, id, arg] = data;
const _func = this.listeners.get(name);
if (!_func) {
const result: RawRPCResponse = [id, 0, `Function ${name} not found`];
const resultBuffer = ArrayBufferFromString(JSON.stringify(result));
port.postMessage(resultBuffer, [resultBuffer]);
}
const func = must(_func);
try {
const result: RawRPCResponse = [id, 1, await func(arg)];
const resultBuffer = ArrayBufferFromString(JSON.stringify(result));
port.postMessage(resultBuffer, [resultBuffer]);
}
catch (e) {
const errMsg = e instanceof Error ? e.message : e;
const result: RawRPCResponse = [id, 0, String(errMsg)];
const resultBuffer = ArrayBufferFromString(JSON.stringify(result));
port.postMessage(resultBuffer, [resultBuffer]);
}
});
for (const [name, func] of Object.entries(rpcLists)) {
this.listeners.set(name, func);
}
console.info("RPC: PortRPCServer started");
}
}
export class PortRPCClient<RPCL extends RPCLists> {
private prevID = 0;
private waiters = new Map<number, [resolve: (result: unknown)=>void, reject: (reason: unknown)=>void]>();
constructor(private port: MessagePort) {
port.on('message', (buffer: ArrayBuffer) => {
const data = JSON.parse(Buffer.from(buffer).toString()) as RawRPCResponse;
const [id, success, result] = data;
const waiter = this.waiters.get(id);
if (!waiter) {
console.error(`RPC: Response for unknown id: ${id}`);
return;
}
this.waiters.delete(id);
if (success) {
waiter[0](result);
}
else {
waiter[1](result);
}
});
console.info("RPC: PortRPCClient started");
}
public async callFunction<T extends keyof RPCL & string>(name: T, arg: Parameters<RPCL[T]>[0]): Promise<ReturnType<RPCL[T]>> {
const id = this.prevID++;
let done = false;
const waiter = new Promise((resolve, reject) => {
const call: RawRPCCall = [name, id, arg];
const callBuffer = ArrayBufferFromString(JSON.stringify(call));
this.waiters.set(id, [resolve, reject]);
done = true;
this.port.postMessage(callBuffer, [callBuffer]);
});
await delay(0);
if(!done){
throw new Error("RPC: callFunction failed");
}
return waiter as Promise<ReturnType<RPCL[T]>>;
}
}
export type InitPort = {
oppose: MessagePort;
};
+2 -2
View File
@@ -8,7 +8,7 @@
},
"type": "module",
"keywords": [],
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"license": "MIT",
"dependencies": {
"@sammo/api_def": "workspace:^",
@@ -16,7 +16,7 @@
"@strpc/express": "workspace:^"
},
"devDependencies": {
"@types/express-session": "^1.18.0",
"@types/express-session": "^1.17.7",
"lodash-es": "^4.17.21"
},
"peerDependencies": {
+2 -4
View File
@@ -16,15 +16,13 @@
},
"type": "module",
"keywords": [],
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.11.24",
"date-fns": "^3.3.1",
"@types/node": "^20.6.3",
"lodash-es": "^4.17.21"
},
"peerDependencies": {
"date-fns": "^2.30.0",
"lodash-es": "^4.17.21"
}
}
-4
View File
@@ -2,7 +2,6 @@ export * from "./bsonify.js"
export * from "./jsonify.js"
export * from "./error.js"
export * from "./unwrap.js"
export * from "./must.js"
export * from "./types.js"
export * from "./web.js"
@@ -20,9 +19,6 @@ export function calcBase64Len(length: number) {
export function delay(time: number): Promise<void>;
export function delay<T>(time: number, result: T): Promise<T>;
export function delay<T = undefined>(time: number, result?: T): Promise<T | void> {
if(time <= 0){
return new Promise(r => r(result));
}
return new Promise(resolve =>
setTimeout(() => {
resolve(result);
-25
View File
@@ -1,25 +0,0 @@
import type { Nullable } from './types.js';
import { NotNullExpected } from "./error.js";
export function must<T>(result: Nullable<T>): T {
if (result === null || result === undefined) {
throw new NotNullExpected();
}
return result;
}
export function must_any<T>(result: Nullable<unknown>): T {
if (result === null || result === undefined) {
throw new NotNullExpected();
}
return result as T;
}
type ErrType<T> = { new(msg?: string): T }
export function must_err<T, ErrT extends Error>(result: Nullable<T>, errType: ErrType<ErrT>, errMsg?: string): T {
if (result === null || result === undefined) {
throw new errType(errMsg);
}
return result;
}
+2 -2
View File
@@ -11,14 +11,14 @@
"@strpc/def": "workspace:^"
},
"devDependencies": {
"ky": "^1.2.2",
"ky": "^1.0.1",
"lodash-es": "^4.17.21"
},
"scripts": {
"build": "tsc --build"
},
"type": "module",
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"license": "MIT",
"peerDependencies": {
"ky": "^1.0.1",
+1 -1
View File
@@ -151,7 +151,7 @@ export async function callClientAPI<ResultType extends ValidResponse, ErrorType
return {
result: false,
reason: `failed to fetch(${path}): ${infoText}`,
reason: `failed to fetch(${path}): ${err}`,
detail: err,
} as ErrorType;
}
+1 -1
View File
@@ -12,6 +12,6 @@
"build": "tsc --build"
},
"type": "module",
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"license": "MIT"
}
+5 -5
View File
@@ -12,17 +12,17 @@
"./proc_decorator": "./dist/proc_decorator.js"
},
"type": "module",
"author": "Hide_D <hided62@gmail.com>",
"author": "",
"license": "MIT",
"dependencies": {
"@strpc/def": "workspace:^"
},
"devDependencies": {
"@types/express": "^4.17.21",
"express": "^4.18.3",
"express-session": "^1.18.0",
"@types/express": "^4.17.17",
"express": "^4.18.2",
"express-session": "^1.17.3",
"lodash-es": "^4.17.21",
"zod": "^3.22.4"
"zod": "^3.22.2"
},
"peerDependencies": {
"express": "^4.18.2",
-137
View File
@@ -1,137 +0,0 @@
---
마지막수정일: 2024-03-01
---
# Component 구성
컴포넌트 구성과 역할을 설명합니다.
## 객체
서버 내에 객체는 다음 그림과 같습니다.
![구조](./threads.drawio.svg)
### MongoDB
메인 DB는 MongoDB입니다.
Collection은 다양하나, 게임 엔진에 필요한 값과 게임 엔진에 필요하지 않은 값이 분리됩니다.
분리되는 예시는 다음과 같습니다.
- 게임 내부 Collection
- 장수의 능력치
- 장수의 금/쌀
- 장수의 수비 켬/끔 여부
- 도시의 현재 스탯
- 국가의 금/쌀, 전략 수치
- 국가의 게임 내 정책(세율, 지급률)
- 게임 외부 Collection
- 장수의 메시지함
- 국가의 방침 게시물
- 국가의 회의록
- 국가간 외교 메시지
- 로그인 정보, 벌점
- 유니크 경매장 진행상태
### 게임 엔진
턴 실행과 관련된 게임 핵심 로직을 처리하는 엔진 컴포넌트이며, 게임 내부 Collection에 대한 쓰기 명령은 게임 엔진에서만 가능합니다.
게임 엔진은 예외적인 경우에 게임 외부 Collection의 쓰기 명령을 할 수 있지만, 게임 내부에 영향을 주지 않는 경우여야만 합니다. 만약 예전에는 게임 외부 Collection에 기록되는 데이터였다 할지라도, 게임 엔진에서 다루기 시작한다면 게임 내부 Collection으로 이관됩니다.
#### 명령 처리 순서
![명령처리순서](./engine_issue_port.drawio.svg)
게임 엔진은 내부적으로 세개의 처리 큐를 가집니다.
- 장수 처리 Queue: 턴마다 처리되는 장수 명령 처리 Queue입니다.
- NPC Action Queue: NPC의 명령을 처리하는 Queue입니다.
- NPC 명령 선택기의 입력 요청이 이곳에 쌓입니다.
- 유저 Action Queue: 유저의 명령을 처리합니다.
- API 처리기를 통해 전달됩니다.
세개의 처리 큐는 장수 처리 Queue > NPC Action Queue > 유저 Action Queue 순의 우선순위를 가집니다.
#### DB 반영 시점
MongoDB에 데이터가 기록되는 시점은 Queue의 아이템 하나를 해소했을 때 이루어집니다.
즉, 장수의 턴 하나가 완전히 실행되었을 때에만 DB에 반영된다고 할 수 있습니다.
게임 엔진 내에서 변경되는 내용은 메모리 내의 저장소에만 담아두고, 처리를 마치는 시점에 변경된 내용을 정리하여 한번에 쓰기 작업을 수행하여 Transaction을 모사합니다.
### NPC 명령 선택기
NPC 명령 선택기는 NPC가 어떤 턴을 실행해야할지만을 연산하는 컴포넌트입니다.
NPC의 실행턴보다 일정 시간 이전에 적절한 턴이 무엇인지 계산하고, 게임 엔진으로 결과값을 전송합니다.
이때 명령의 규격은 일반 유저가 내리는 명령 규격과 유사한 형태를 갖습니다.
> [!NOTE]
>
> 기존 삼모전 PHP에서는 명령 선택기가 게임 엔진과 동기적으로 구성되어있었습니다.
### API 처리기
API 처리기는 유저의 명령을 받아 1차적으로 처리합니다.
게임 외부 데이터에 대한 처리라면 읽기/쓰기를 모두 수행하며, 게임 내부 데이터이면 읽기 작업은 직접할 수 있습니다.
턴 입력 등 쓰기 작업이 요구되는 경우라면 게임 엔진에 해당 작업을 요청하고, 작업이 끝날때까지 기다린 다음, 엔진으로부터 결과값을 받아 반환합니다.
> [!WARNING]
>
> **어떠한 경우에도** 게임 내부 DB에 직접 쓰기를 해서는 안됩니다.
>
> 게임 엔진은 메모리 cache에 강력히 의존하므로, 내부 DB에 직접 쓰는 작업은 게임 엔진의 처리 결과를 훼손할 것입니다.
## 기존 삼국지 모의전투 PHP와 비교하여 달라진 부분
### 게임 엔진
PHP 버전에서 장수 처리 Queue는 `sammo::TurnExecutionHelper`에 해당합니다.
턴 실행 도중에 장수의 행동 또는 국가 정보가 바뀔 수 있으므로, 처리 시점에 DBMS로부터 최신 정보를 가져와 처리하고, 장수 한명의 실행이 마무리되면 모든 cache를 invalidate합니다.
이러한 처리 방법은 장수 처리 Queue 하나만으로 운영되는 형태로 생각할 수 있습니다.
### NPC 명령 선택기
NPC의 실행 턴이 무엇이 될지는 엔진내에 NPC 턴 실행 직전에 결정되므로, 게임 엔진 에서 처리되어있다고 볼 수 있습니다.
`sammo::GeneralAI`가 처리를 전담하고 있으며, `sammo::TurnExecutionHelper``sammo::GeneralAI::chooseNationTurn()`, `sammo::GeneralAI::chooseGeneralTurn()`를 호출하는 방식입니다.
게임 엔진과 분리되어 있지 않으므로, 턴 입력, 세율 조정 등의 작업이 직접 이루어졌습니다.
### API 처리기
유기체 서버 시절에서는 `c_*.php` 계열 함수들이 API를 처리하는 형태였고, 이후 HiDCHe 에서 json web api 처리를 위해 `j_*.php` 계열 함수들이 추가되었습니다.
0.23 버전부터는 `api.php`를 통해 처리하는 (제대로 된)API 처리기가 도입되었습니다.
`api.php?path=**/*`의 경로는 `sammo::API::**::*.php` 이하의 클래스를 생성하고 호출합니다.
이때의 API 처리기는 게임 엔진에서 다루어야 할 데이터 변경도 같이 하고 있어 게임 엔진 측에서 조심해서 DB를 조회하도록 하여야 했습니다.
### DBMS
PHP 버전에서는 MariaDB를 사용하였습니다.
다만, 게임 내의 모든 데이터를 column으로 처리하는 데에는 무리가 있어 크게 두가지 방식으로 나누어 확장 데이터를 처리하였습니다.
- `general`, `city`, `nation` 테이블에 `aux` 필드를 만들어 json으로 처리
- `storage`, `nation_env`와 같은 Key/Value(json) 테이블을 만들어 처리
게임 엔진의 턴 실행과 연계된 부분인가, 유저 API에 연계된 부분인가에 따라서 다르게 저장되고 있어 일관성이 떨어지는 문제가 있었습니다.
"어차피 많은 데이터가 json으로 따로 관리되니, 굳이 RDBMS일 이유가 있는가?"가 DBMS 전환의 가장 큰 이유입니다.
-302
View File
@@ -1,302 +0,0 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="441px" height="271px" viewBox="-0.5 -0.5 441 271" content="&lt;mxfile&gt;&lt;diagram id=&quot;ovDIAAWsHlNK0DibyGEH&quot; name=&quot;페이지-1&quot;&gt;zZjBjpswEIafxlJ7iQAbYh9twraXVq320LMTvAGJhIiQJrtP37ExCQSiUikFLsjM2GB/8zNjg3C4u3wp5CH5lscqQ54TXxBeIc8LAgxXbXivDJQElWFbpHFlcm+G1/RDWaNjrac0VsdWxzLPszI9tI2bfL9Xm7Jlk0WRn9vd3vKs/daD3KqO4XUjs671VxqXiV2Ft7zZv6p0m9RvdgNWeXay7mxXckxknJ8bJhwhHBZ5Xlat3SVUmWZXc6nGvTzwXidWqH05ZIBXDfgts5NdG4pCxCLEfd2gFDEqoYPjIsw9G7Zj+V6TKPLTPlb6YQ7C4pykpXo9yI32niH0YEvKXQZ3MF7Yd6miVJeH83WvFEA9Kt+psniHLnaARy04qxzi2PvzLQ5uDTdpxABbm7Sh314ffaMDDQuoHxYeAAv8ui083Y4E4gHi8GDn50nBoMf43EnwYTwQH3kCPjIA31prjYDWfDY/rQ2F9Qyt+R1Yi8VifkiCEZEEA/TzAR1AOvPUDxsR1rIfVoi4yU9w1bNz+KZM8/1s8lPgTJif6MD8hKIVogQJAmsJIamFwDrIYCJiXUBrq1sm9XPETQ0QGDHXFgYqTAQ4EksdgYZ36dSLn1Cz9wEYXF+fEQDWCcD3H+H8RErIhCKtt8BtSLJWF8iS1h+63ovoafiTq+qe2Ki7Ntd9TOz2IXc/YNjEBZpj6wMWejcniEVMyfzQjirG7vEB0G7+ivaa/VaIMbNpnhvF5ZgUh5wr3gxUrg8WYmV0uESc1JbKhZEI+0sRdBCuHUWpjQ/jiJpHMZD3avIYdEoPGTMG/YcTqNY6A0CDG9ogal9betOF3lO5pk9ganyTdhXOKkzUGOdGm45Ju3u6MWR8xFemoXensp9yp8ThWZS4e56+NybPIUejy6cu4/XnB5A9Xecis1cVrJnMHZtOWMM9O/ZDT6VPYd89aUENjHtgSw1b7vT69+vjwaz9n+HDzYtxQz+s883U8O9Lp+/+P/hwe/s1anyN/8s4+gM=&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs/>
<g>
<rect x="0" y="70" width="120" height="30" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 85px; margin-left: 1px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
장수a 01:23
</div>
</div>
</div>
</foreignObject>
<text x="60" y="89" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
장수a 01:23
</text>
</switch>
</g>
<rect x="0" y="0" width="120" height="40" rx="6" ry="6" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 20px; margin-left: 1px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
장수 처리 Queue
</div>
</div>
</div>
</foreignObject>
<text x="60" y="24" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
장수 처리 Queue
</text>
</switch>
</g>
<rect x="0" y="100" width="120" height="30" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 115px; margin-left: 1px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
장수b 04:59
</div>
</div>
</div>
</foreignObject>
<text x="60" y="119" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
장수b 04:59
</text>
</switch>
</g>
<rect x="0" y="130" width="120" height="30" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 145px; margin-left: 1px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
...
</div>
</div>
</div>
</foreignObject>
<text x="60" y="149" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
...
</text>
</switch>
</g>
<rect x="0" y="160" width="120" height="30" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 175px; margin-left: 1px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
장수z 59:59
</div>
</div>
</div>
</foreignObject>
<text x="60" y="179" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
장수z 59:59
</text>
</switch>
</g>
<rect x="320" y="0" width="120" height="40" rx="6" ry="6" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 20px; margin-left: 321px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
유저 Action Queue
</div>
</div>
</div>
</foreignObject>
<text x="380" y="24" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
유저 Action Queue
</text>
</switch>
</g>
<rect x="320" y="70" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 90px; margin-left: 321px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
장수b 턴1,4,7
<br/>
모병 신귀병 7000
</div>
</div>
</div>
</foreignObject>
<text x="380" y="94" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
장수b 턴1,4,7...
</text>
</switch>
</g>
<rect x="160" y="0" width="120" height="40" rx="6" ry="6" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 20px; margin-left: 161px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
NPC Action Queue
</div>
</div>
</div>
</foreignObject>
<text x="220" y="24" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
NPC Action Queue
</text>
</switch>
</g>
<rect x="160" y="70" width="120" height="30" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 85px; margin-left: 161px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
NPCa 세율 20%
</div>
</div>
</div>
</foreignObject>
<text x="220" y="89" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
NPCa 세율 20%
</text>
</switch>
</g>
<rect x="160" y="100" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 120px; margin-left: 161px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
NPCa 턴1
<br/>
출병 무위
</div>
</div>
</div>
</foreignObject>
<text x="220" y="124" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
NPCa 턴1...
</text>
</switch>
</g>
<rect x="160" y="140" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 160px; margin-left: 161px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
NPCc 턴1
<br/>
귀환
</div>
</div>
</div>
</foreignObject>
<text x="220" y="164" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
NPCc 턴1...
</text>
</switch>
</g>
<rect x="320" y="110" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 130px; margin-left: 321px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
장수f 경매결과
<br/>
백마 획득
</div>
</div>
</div>
</foreignObject>
<text x="380" y="134" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
장수f 경매결과...
</text>
</switch>
</g>
<rect x="320" y="150" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 170px; margin-left: 321px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
시스템
<br/>
적토마 잠금
</div>
</div>
</div>
</foreignObject>
<text x="380" y="174" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
시스템
적토마 잠금
</text>
</switch>
</g>
<rect x="320" y="190" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 210px; margin-left: 321px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
국가a
<br/>
세율 30%
</div>
</div>
</div>
</foreignObject>
<text x="380" y="214" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
국가a...
</text>
</switch>
</g>
<rect x="320" y="230" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 250px; margin-left: 321px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
장수x(국가b)
<br/>
사령턴1 발령
</div>
</div>
</div>
</foreignObject>
<text x="380" y="254" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
장수x(국가b)...
</text>
</switch>
</g>
<rect x="160" y="180" width="120" height="40" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 200px; margin-left: 161px;">
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
NPCd(국가a)
<br/>
사령턴1 포상
</div>
</div>
</div>
</foreignObject>
<text x="220" y="204" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
NPCd(국가a)...
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Text is not SVG - cannot display
</text>
</a>
</switch>
</svg>

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

+19 -15
View File
@@ -5,39 +5,43 @@
"scripts": {
"dev": "run-p dev:client dev:server dev:gateway_server dev:gateway_client",
"build": "run-s build:ts build:vite",
"build:ts": "run-s build:server build:gateway_server",
"build:ts": "run-p build:server build:gateway_server",
"build:vite": "run-s build:client build:gateway_client",
"type-check": "run-p type-check:server type-check:client type-check:gateway_server type-check:gateway_client",
"dev:server": "pnpm --filter @sammo/server dev",
"dev:client": "pnpm --filter @sammo/client dev",
"dev:gateway_server": "pnpm --filter @sammo/gateway_server dev",
"dev:gateway_client": "pnpm --filter @sammo/gateway_client dev",
"build:def": "pnpm --filter @sammo/api_def build --force",
"build:server": "pnpm --filter @sammo/server build",
"build:client": "pnpm --filter @sammo/client build",
"build:gateway_server": "pnpm --filter @sammo/gateway_server build",
"build:gateway_client": "pnpm --filter @sammo/gateway_client build",
"type-check:server": "pnpm --filter @sammo/server type-check",
"type-check:client": "pnpm --filter @sammo/client type-check",
"type-check:gateway_server": "pnpm --filter @sammo/gateway_server type-check",
"type-check:gateway_client": "pnpm --filter @sammo/gateway_client type-check",
"lint": "lint",
"test": "jest"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.7.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.24",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.22.0",
"@rushstack/eslint-patch": "^1.3.2",
"@types/lodash-es": "^4.17.8",
"@types/node": "^18.17.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.45.0",
"eslint-plugin-vue": "^9.15.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"tsx": "^4.7.1",
"typescript": "~5.3.3"
},
"packageManager": "pnpm@8.7.6"
"prettier": "^3.0.0",
"ts-node": "^10.9.1",
"tsc-watch": "^6.0.4",
"typescript": "~5.1.6"
}
}
+1448 -1101
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -1,11 +1,17 @@
{
"extends": "./tsconfig.base.json",
"references": [
{
"path": "./@sammo/gateway_client"
},
{
"path": "./@sammo/gateway_server"
},
{
"path": "./@sammo/server"
}
},
{
"path": "./@sammo/client"
},
],
}
-29
View File
@@ -1,29 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
}