Author SHA1 Message Date
Hide_D 2e23be1585 확장자 제거 2024-03-08 11:35:19 +00:00
Hide_D ca0bf296a6 simple test 2024-03-08 11:24:35 +09:00
Hide_D c713b858c1 delay 0 2024-03-08 11:08:05 +09:00
Hide_D 7885c8c8d7 test 2024-03-07 16:58:03 +00:00
Hide_D a8ddaac1ed worker_test init 2024-03-07 15:06:49 +00:00
Hide_D 21da4d14e6 wip: 개체 작업 진행 2024-03-06 16:33:23 +00:00
Hide_D be6e765d76 denque 2024-03-05 15:45:41 +00:00
Hide_D 23070ee19a dep: update 2024-03-05 14:50:55 +00:00
Hide_D 2fb1364282 php버전과의 비교 추가 2024-03-01 12:48:30 +00:00
Hide_D 9120943a1a 컴포넌트 문서 추가 2024-03-01 12:16:15 +00:00
Hide_D bd6158c0bc 실수 ㅜㅜ 2023-09-24 06:11:28 +00:00
Hide_D 3687d597c4 타입 정의 상세 2023-09-24 06:11:01 +00:00
Hide_D 45f14f0b72 che_필살 예시 구현 2023-09-24 05:43:06 +00:00
Hide_D 905b401aab ResourceController 란 것이 있을 것 2023-09-24 04:06:59 +00:00
Hide_D 4bc5407bf2 defs 준비 2023-09-23 18:41:45 +00:00
Hide_D e084835249 game_logic 정의 2023-09-23 18:34:27 +00:00
Hide_D cea888573c crypto 누락분 2023-09-23 17:53:46 +00:00
Hide_D 9495cff295 callClientAPI warning 제거 2023-09-23 17:34:56 +00:00
Hide_D c3d3a05f6e package.json author 2023-09-23 17:26:02 +00:00
Hide_D a0c730299c dep 수정 2023-09-23 17:24:30 +00:00
Hide_D 778f189a21 fix import 2023-09-23 17:12:20 +00:00
Hide_D 5ec1173f15 dep 정리 2023-09-23 17:05:18 +00:00
Hide_D 409e6513e3 fix: 누락 package 2023-09-23 16:34:23 +00:00
Hide_D e59f9a9659 monorepo 버전 준비
## @strpc
기존 RPC를 package화

### @strpc/express
express의 middleware + router 결함

## @sammo
게임 전체
- server, client
- gateway_server, gateway_client
2023-09-23 16:29:18 +00:00
71 changed files with 3256 additions and 1690 deletions
+2 -2
View File
@@ -11,7 +11,7 @@
"scripts": {
"build": "tsc --build"
},
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
@@ -21,7 +21,7 @@
"@strpc/def": "workspace:^"
},
"devDependencies": {
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"peerDependencies": {
"zod": "^3.22.2"
+6 -15
View File
@@ -2,24 +2,16 @@ import { GET, POST, type DefAPINamespace } from "@strpc/def";
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
import type {
InvalidResponse, ValidResponse
// eslint-disable-next-line @typescript-eslint/no-unused-vars
InvalidResponse, ValidResponse
} from "@strpc/def";
import type {
ArgDeleteAPI,
ArgGetAPI,
ArgHeadAPI,
ArgPatchAPI,
ArgPostAPI,
ArgPutAPI,
EmptyDeleteAPI,
EmptyGetAPI,
EmptyHeadAPI,
EmptyPatchAPI,
EmptyPostAPI,
EmptyPutAPI,
// 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,
} from "@strpc/def/types";
export type LoginResponse = {
result: true,
nextToken: [number, string] | undefined,
@@ -49,7 +41,6 @@ export type AutoLoginFailed = {
reason: string,
}
/** @internal */
export const structure = {
Login: {
LoginByID: POST<{
+8 -14
View File
@@ -1,26 +1,20 @@
import type {
DefAPINamespace,
} from "@strpc/def";
import { GET, POST } from '@strpc/def';
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
import type {
InvalidResponse, ValidResponse
// eslint-disable-next-line @typescript-eslint/no-unused-vars
InvalidResponse, ValidResponse
} from "@strpc/def";
import type {
ArgDeleteAPI,
ArgGetAPI,
ArgHeadAPI,
ArgPatchAPI,
ArgPostAPI,
ArgPutAPI,
EmptyDeleteAPI,
EmptyGetAPI,
EmptyHeadAPI,
EmptyPatchAPI,
EmptyPostAPI,
EmptyPutAPI,
// 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,
} from "@strpc/def/types";
import { GET, POST } from '@strpc/def';
export type {
InferResponse,
+24 -23
View File
@@ -5,46 +5,47 @@
"main": "dist/index.js",
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"build": "vue-tsc && vite build",
"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": "",
"author": "Hide_D <hided62@gmail.com>",
"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.1",
"bootstrap-vue-next": "^0.9.26",
"bson": "^5.4.0",
"date-fns": "^2.30.0",
"ky": "^1.0.1",
"bootstrap": "^5.3.3",
"bootstrap-vue-next": "^0.16.6",
"bson": "^6.4.0",
"date-fns": "^3.3.1",
"ky": "^1.2.2",
"lodash-es": "^4.17.21",
"pinia": "^2.1.4",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.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",
"@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",
"tslib": "^2.6.2",
"vite": "^4.4.6",
"vue-tsc": "^1.8.6",
"zod": "^3.22.2"
"vite": "^5.1.5",
"vue-tsc": "^2.0.5",
"zod": "^3.22.4"
},
"peerDependencies": {
"zod": "^3.22.2"
-12
View File
@@ -1,12 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+19 -24
View File
@@ -1,28 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"exclude": [
"extends": "../../tsconfig.vite.json",
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "../util"
},
{
"path": "../secure_token"
},
{
"path": "../../@strpc/def"
},
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "../util"
},
{
"path": "../secure_token"
},
{
"path": "../../@strpc/def"
},
{
"path": "./tsconfig.node.json"
}
]
}
}
+5 -13
View File
@@ -1,18 +1,10 @@
{
"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",
"types": ["node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+11 -11
View File
@@ -18,27 +18,27 @@
"build": "tsc --build",
"test": "jest"
},
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/util": "workspace:^"
},
"devDependencies": {
"@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",
"@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",
"buffer": "^6.0.3",
"eslint": "^8.49.0",
"eslint": "^8.57.0",
"jest": "^29.7.0",
"lodash-es": "^4.17.21",
"ts-jest": "^29.1.1",
"ts-jest": "^29.1.2",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
"typescript": "^5.3.3"
},
"peerDependencies": {
"bson": "^5.4.0",
+5
View File
@@ -1,4 +1,5 @@
import { InvalidArgument } from '@sammo/util';
import type { RandUtil } from './RandUtil.js';
export class InvalidVType extends InvalidArgument {
public override name = 'InvalidVType';
@@ -99,6 +100,10 @@ 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';
+5 -4
View File
@@ -6,18 +6,19 @@
"scripts": {
"build": "tsc --build"
},
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@sammo/api_def": "workspace:^",
"@sammo/crypto": "workspace:^",
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"dotenv": "^16.3.1",
"mongoose": "^7.4.3"
"dotenv": "^16.4.5",
"mongoose": "^8.2.1"
},
"devDependencies": {
"@types/node": "^20.6.3"
"@types/node": "^20.11.24"
}
}
+8
View File
@@ -0,0 +1,8 @@
import type { CityID } from "./defs.js";
export class City{
constructor(
public readonly id: CityID
){
}
}
View File
+3
View File
@@ -0,0 +1,3 @@
export class GameEnv{
}
+149
View File
@@ -0,0 +1,149 @@
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
@@ -0,0 +1,65 @@
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;
}
}
@@ -0,0 +1,19 @@
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;
}
@@ -0,0 +1,48 @@
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),
);
}
}
@@ -0,0 +1,6 @@
class SpecialityHelper {
}
export const specialityHelper = new SpecialityHelper();
@@ -0,0 +1,30 @@
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;
@@ -0,0 +1,8 @@
export { BaseSpecial } from "./BaseSpecial.js";
import { che_필살 } from "./SpecialWar/che_필살.js";
export const SpecialWar = {
che_필살
}
+23
View File
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,63 @@
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
@@ -0,0 +1,9 @@
import type { NationID, NationName, NationType } from "./defs.js";
export class Nation {
constructor(
public readonly id: NationID,
public readonly name: NationName,
public readonly type: NationType,
) { }
}
@@ -0,0 +1,64 @@
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;
}
@@ -0,0 +1,11 @@
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();
}
}
@@ -0,0 +1,43 @@
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();
}
}
@@ -0,0 +1,37 @@
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}`;
}
}
@@ -0,0 +1,9 @@
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;
}
}
@@ -0,0 +1,54 @@
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]));
}
}
}
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,7 @@
import type { NationID, TroopID } from "./defs.js";
export interface ITroopEntity {
id: TroopID;
nationID: NationID;
name: string;
}
+15
View File
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,5 @@
import { WarUnit } from "./WarUnit.js";
export class WarUnitCity extends WarUnit{
}
+5
View File
@@ -0,0 +1,5 @@
import { WarUnit } from "./WarUnit.js";
export class WarUnitGeneral extends WarUnit{
}
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,126 @@
//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;
+5 -1
View File
@@ -1 +1,5 @@
export * from "./generalStats.js";
export * from "./generalStats.js";
export * as defs from "./defs.js";
// NOTE: game_logic은 client, server 모두에서 사용되는 라이브러리이다.
// DB에 접근한다면 DI여야하나?
+3
View File
@@ -3,6 +3,9 @@
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
+24 -23
View File
@@ -5,45 +5,46 @@
"main": "dist/index.js",
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"build": "vue-tsc && vite build",
"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": "",
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@sammo/util": "workspace:^",
"@strpc/client_ky": "workspace:^",
"@strpc/def": "workspace:^",
"bootstrap": "^5.3.1",
"bootstrap-vue-next": "^0.9.26",
"bson": "^5.4.0",
"date-fns": "^2.30.0",
"ky": "^1.0.1",
"bootstrap": "^5.3.3",
"bootstrap-vue-next": "^0.16.6",
"bson": "^6.4.0",
"date-fns": "^3.3.1",
"ky": "^1.2.2",
"lodash-es": "^4.17.21",
"pinia": "^2.1.4",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.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",
"@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",
"tslib": "^2.6.2",
"vite": "^4.4.6",
"vue-tsc": "^1.8.6",
"zod": "^3.22.2"
"vite": "^5.1.5",
"vue-tsc": "^2.0.5",
"zod": "^3.22.4"
},
"peerDependencies": {
"zod": "^3.22.2"
-12
View File
@@ -1,12 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+7 -11
View File
@@ -1,11 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
},
"exclude": [
"extends": "../../tsconfig.vite.json",
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
@@ -20,9 +19,6 @@
},
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
}
]
}
+5 -13
View File
@@ -1,18 +1,10 @@
{
"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",
"types": ["node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
-1
View File
@@ -11,7 +11,6 @@ export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@util': fileURLToPath(new URL('./server/util', import.meta.url)),
}
}
})
+13 -12
View File
@@ -4,13 +4,14 @@
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build"
"build": "tsc --build",
"dev": "tsc --watch"
},
"exports": {
".": "./dist/index.js",
"./exports": "./dist/exports.js"
},
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
@@ -19,19 +20,19 @@
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-session": "^1.17.3",
"date-fns": "^3.3.1",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"express-session": "^1.18.0",
"lodash-es": "^4.17.21",
"mongoose": "^7.4.3",
"reflect-metadata": "^0.1.13",
"mongoose": "^8.2.1",
"reflect-metadata": "^0.2.1",
"tslib": "^2.6.2",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@types/node": "^20.6.3"
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/node": "^20.11.24"
}
}
+3 -3
View File
@@ -6,7 +6,7 @@
"scripts": {
"build": "tsc --build"
},
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
@@ -14,9 +14,9 @@
"@sammo/util": "workspace:^"
},
"devDependencies": {
"bson": "^5.4.0",
"bson": "^6.4.0",
"buffer": "^6.0.3",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"peerDependencies": {
"bson": "^5.4.0",
+16 -13
View File
@@ -4,9 +4,11 @@
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build"
"build": "tsc --build",
"dev": "tsc --watch",
"worker_test": "tsx src/worker_test.ts"
},
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"type": "module",
"license": "MIT",
"dependencies": {
@@ -16,22 +18,23 @@
"@sammo/server_util": "workspace:^",
"@sammo/util": "workspace:^",
"@strpc/express": "workspace:^",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"date-fns": "^3.3.1",
"denque": "^2.1.0",
"dotenv": "^16.4.5",
"eventemitter3": "^5.0.1",
"express": "^4.18.2",
"express-session": "^1.17.3",
"ky": "^1.0.1",
"express": "^4.18.3",
"express-session": "^1.18.0",
"ky": "^1.2.2",
"lodash-es": "^4.17.21",
"mongoose": "^7.4.3",
"reflect-metadata": "^0.1.13",
"mongoose": "^8.2.1",
"reflect-metadata": "^0.2.1",
"tslib": "^2.6.2",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/express-session": "^1.17.7",
"@sammo/gateway_server": "workspace:^",
"@types/node": "^20.6.3"
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/node": "^20.11.24"
}
}
+30
View File
@@ -0,0 +1,30 @@
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/config';
import './dotenv.js';
import { connect, Mongoose } from "mongoose";
import { unwrap } from '@sammo/util';
+49
View File
@@ -0,0 +1,49 @@
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
@@ -0,0 +1,40 @@
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
@@ -0,0 +1,5 @@
import type { RandomMsg } from "./worker_util.js";
export type RPCSampleList = {
do: (msg: RandomMsg) => string;
}
+32
View File
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,143 @@
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": "",
"author": "Hide_D <hided62@gmail.com>",
"license": "MIT",
"dependencies": {
"@sammo/api_def": "workspace:^",
@@ -16,7 +16,7 @@
"@strpc/express": "workspace:^"
},
"devDependencies": {
"@types/express-session": "^1.17.7",
"@types/express-session": "^1.18.0",
"lodash-es": "^4.17.21"
},
"peerDependencies": {
+4 -2
View File
@@ -16,13 +16,15 @@
},
"type": "module",
"keywords": [],
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.6.3",
"@types/node": "^20.11.24",
"date-fns": "^3.3.1",
"lodash-es": "^4.17.21"
},
"peerDependencies": {
"date-fns": "^2.30.0",
"lodash-es": "^4.17.21"
}
}
+4
View File
@@ -2,6 +2,7 @@ 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"
@@ -19,6 +20,9 @@ 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
@@ -0,0 +1,25 @@
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.0.1",
"ky": "^1.2.2",
"lodash-es": "^4.17.21"
},
"scripts": {
"build": "tsc --build"
},
"type": "module",
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"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}): ${err}`,
reason: `failed to fetch(${path}): ${infoText}`,
detail: err,
} as ErrorType;
}
+1 -1
View File
@@ -12,6 +12,6 @@
"build": "tsc --build"
},
"type": "module",
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"license": "MIT"
}
+5 -5
View File
@@ -12,17 +12,17 @@
"./proc_decorator": "./dist/proc_decorator.js"
},
"type": "module",
"author": "",
"author": "Hide_D <hided62@gmail.com>",
"license": "MIT",
"dependencies": {
"@strpc/def": "workspace:^"
},
"devDependencies": {
"@types/express": "^4.17.17",
"express": "^4.18.2",
"express-session": "^1.17.3",
"@types/express": "^4.17.21",
"express": "^4.18.3",
"express-session": "^1.18.0",
"lodash-es": "^4.17.21",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"peerDependencies": {
"express": "^4.18.2",
+137
View File
@@ -0,0 +1,137 @@
---
마지막수정일: 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
@@ -0,0 +1,302 @@
<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>

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

+15 -19
View File
@@ -5,43 +5,39 @@
"scripts": {
"dev": "run-p dev:client dev:server dev:gateway_server dev:gateway_client",
"build": "run-s build:ts build:vite",
"build:ts": "run-p build:server build:gateway_server",
"build:ts": "run-s 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.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",
"@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",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.0",
"ts-node": "^10.9.1",
"tsc-watch": "^6.0.4",
"typescript": "~5.1.6"
}
"prettier": "^3.2.5",
"tsx": "^4.7.1",
"typescript": "~5.3.3"
},
"packageManager": "pnpm@8.7.6"
}
+1096 -1443
View File
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -1,17 +1,11 @@
{
"extends": "./tsconfig.base.json",
"references": [
{
"path": "./@sammo/gateway_client"
},
{
"path": "./@sammo/gateway_server"
},
{
"path": "./@sammo/server"
},
{
"path": "./@sammo/client"
},
}
],
}
+29
View File
@@ -0,0 +1,29 @@
{
"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
},
}