diff --git a/@sammo/game_logic/src/City.ts b/@sammo/game_logic/src/City.ts index 409145f..64fc1dc 100644 --- a/@sammo/game_logic/src/City.ts +++ b/@sammo/game_logic/src/City.ts @@ -16,6 +16,10 @@ export class City extends LazyEntityUpdater{ this._raw = raw; } + public bootstrap(){ + //do nothing; + } + get id(): CityID { return this._raw.id; } diff --git a/@sammo/game_logic/src/Entity/GameEnvEntity.ts b/@sammo/game_logic/src/Entity/GameEnvEntity.ts index d79f5bb..08ee0ef 100644 --- a/@sammo/game_logic/src/Entity/GameEnvEntity.ts +++ b/@sammo/game_logic/src/Entity/GameEnvEntity.ts @@ -1,3 +1,5 @@ +import type { IntYear, IntYearMonth } from "@/defs.js"; + export type UnitedGameState = 'onGame' | 'united' | 'onEvent' | 'endEvent'; export interface IGameEnvEntity { @@ -10,8 +12,9 @@ export interface IGameEnvEntity { gameStartYear: number; gameStartMonth: number; - year: number; - month: number; + year: IntYear; + month: IntYearMonth; + yearMonth: IntYearMonth; secondsPerTurn: number; diff --git a/@sammo/game_logic/src/Entity/GeneralEntity.ts b/@sammo/game_logic/src/Entity/GeneralEntity.ts index 65edd88..b066efc 100644 --- a/@sammo/game_logic/src/Entity/GeneralEntity.ts +++ b/@sammo/game_logic/src/Entity/GeneralEntity.ts @@ -1,4 +1,4 @@ -import type { CityID, DiplomaticPermission, FractionalRange, GeneralID, GeneralName, IntMonth, IntYear, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, SpecialityDomesticType, SpecialityWarType, SquadID, URILike, UserID, UserName } from "../defs.js"; +import type { CityID, DiplomaticPermission, FractionalRange, GeneralID, GeneralName, IntMonth, IntYear, IntYearMonth, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, SpecialityDomesticType, SpecialityWarType, SquadID, URILike, UserID, UserName } from "../defs.js"; export interface IGeneralEntity { id: GeneralID; @@ -49,7 +49,7 @@ export interface IGeneralEntity { age: IntYear; startAge: IntYear; - nationJoinedRelYear: IntYear; + nationJoinedYearMonth: IntYearMonth; betray: number; @@ -69,4 +69,9 @@ export interface IGeneralEntity { bornyear: number; deadyear: number; } + + aux: { + lastBattleDate?: Date; + lastBattleYearMonth?: IntYearMonth; + } } \ No newline at end of file diff --git a/@sammo/game_logic/src/Entity/NationEntity.ts b/@sammo/game_logic/src/Entity/NationEntity.ts index 6d7576d..0f615aa 100644 --- a/@sammo/game_logic/src/Entity/NationEntity.ts +++ b/@sammo/game_logic/src/Entity/NationEntity.ts @@ -51,6 +51,9 @@ export interface INationEntity{ availableWarSettingCnt?: number; prevIncomeGold: number; prevIncomeRice: number; + + lastBattleDate?: Date; + lastBattleYearMonth?: IntYearMonth; } //TODO: 타입 정의 diff --git a/@sammo/game_logic/src/Errors.ts b/@sammo/game_logic/src/Errors.ts index e69de29..8a938e0 100644 --- a/@sammo/game_logic/src/Errors.ts +++ b/@sammo/game_logic/src/Errors.ts @@ -0,0 +1,6 @@ +export class LogicError extends Error { + constructor(message?: string) { + super(message); + this.name = 'LogicError'; + } +} \ No newline at end of file diff --git a/@sammo/game_logic/src/GameEnv.ts b/@sammo/game_logic/src/GameEnv.ts index 839609a..413b083 100644 --- a/@sammo/game_logic/src/GameEnv.ts +++ b/@sammo/game_logic/src/GameEnv.ts @@ -17,4 +17,7 @@ export class GameEnv extends LazyEntityUpdater{ this._raw = raw; } + public bootstrap(){ + //do nothing; + } } \ No newline at end of file diff --git a/@sammo/game_logic/src/General.ts b/@sammo/game_logic/src/General.ts index c6e9ab0..d9d9692 100644 --- a/@sammo/game_logic/src/General.ts +++ b/@sammo/game_logic/src/General.ts @@ -2,7 +2,7 @@ import { InvalidArgument } from "@sammo/util"; import { City } from "./City.js"; import type { IGeneralEntity } from "./Entity/GeneralEntity.js"; import type { IResourceController } from "./IResourceController.js"; -import { DiplomaticPermission, type CityID, type GeneralID, type GeneralName, type NationID, OfficerLevel, type SquadID } from "./defs.js"; +import { DiplomaticPermission, type CityID, type GeneralID, type GeneralName, type NationID, OfficerLevel, type SquadID, NpcType, StaffLevel, CityOfficerLevel } from "./defs.js"; import { must } from "@sammo/util"; import { Nation } from "./Nation.js"; import { Squad } from "./Squad.js"; @@ -17,11 +17,11 @@ export class General extends LazyEntityUpdater { protected readonly _entityType = "General"; protected readonly _raw: IGeneralEntity; - protected _city: WeakRef; - protected _squad: WeakRef | undefined; - protected _nation: WeakRef; + protected _city!: WeakRef; + protected _squad!: WeakRef | undefined; + protected _nation!: WeakRef; - protected readonly iActionHandlers: Map; + protected readonly iActionHandlers = new Map; constructor( raw: IGeneralEntity, @@ -29,6 +29,11 @@ export class General extends LazyEntityUpdater { ){ super(); this._raw = raw; + } + + public bootstrap(){ + const rsc = this.rsc; + const raw = this._raw; this._city = new WeakRef(must(rsc.city(raw.cityID))); this._nation = new WeakRef(must(rsc.nation(raw.nationID))); @@ -36,8 +41,7 @@ export class General extends LazyEntityUpdater { this._squad = new WeakRef(must(rsc.squad(raw.squadID, raw.nationID))); } - //TODO: iAction 처리해야함 - this.iActionHandlers = new Map(); + //TODO: iActionHandlers 초기화 } public get id(): GeneralID { @@ -60,6 +64,28 @@ export class General extends LazyEntityUpdater { return must(this._nation.deref()); } + public get isUser(): boolean { + return this._raw.npcType <= NpcType.user_borrowed_npc; + } + + public get staffLevel(): StaffLevel | undefined { + if(this._raw.officerLevel < OfficerLevel.staffIntel3){ + return undefined; + } + return this._raw.officerLevel as number as StaffLevel; + } + + public get cityOfficerLevel(): CityOfficerLevel | undefined { + if(this._raw.officerLevel >= OfficerLevel.staffIntel3){ + return undefined; + } + if(this._raw.officerLevel < OfficerLevel.cityLeadership){ + return undefined; + } + + return this._raw.officerLevel as number as CityOfficerLevel; + } + public changeCity(city: CityID | City){ const cityObj: City = (()=>{ if(city instanceof City){ @@ -86,7 +112,7 @@ export class General extends LazyEntityUpdater { const squad = must(this._squad.deref()); if(squad.leader.id === this._raw.id){ - squad.destructSquad(false); + squad.destroySquad(false); } else{ squad.members.delete(this._raw.id); @@ -142,12 +168,12 @@ export class General extends LazyEntityUpdater { else{ raw.officerLevel = OfficerLevel.normal; } - raw.nationJoinedRelYear = 0; + raw.nationJoinedYearMonth = this.rsc.gameEnv.raw.yearMonth; }); } public updateTurnTime(){ - const secondsPerTurn = this.rsc.gameEnv().raw.secondsPerTurn; + const secondsPerTurn = this.rsc.gameEnv.raw.secondsPerTurn; this.forceUpdate((raw) => { raw.turnTime = addSeconds(raw.turnTime, secondsPerTurn); diff --git a/@sammo/game_logic/src/IResourceController.ts b/@sammo/game_logic/src/IResourceController.ts index c805694..9cb74db 100644 --- a/@sammo/game_logic/src/IResourceController.ts +++ b/@sammo/game_logic/src/IResourceController.ts @@ -22,21 +22,12 @@ export interface IResourceController { popGeneralTurnTimeQueue(): General | undefined; insertGeneralTurnTimeQueue(general: General, maybeTail: boolean): void; - gameEnv(): GameEnv; + readonly gameEnv: GameEnv; - allNations(): Map; - allCities(): Map; - allCitiesByNation(): Map>; - allSquads(): Map>; - allGenerals(): Map; - - generalByNation(id: NationID): Map; - generalByCity(id: CityID, nationID?: NationID): Map; - generalBySquad(id: SquadID): Map; - - cityByNation(id: NationID): Map; - - squadByNation(id: NationID): Map; + readonly nationList: Map; + readonly cityList: Map; + readonly squadList: Map; + readonly generalList: Map; //----Insert, Update, Delete diff --git a/@sammo/game_logic/src/LazyEntityUpdater.ts b/@sammo/game_logic/src/LazyEntityUpdater.ts index df339ad..4b6519a 100644 --- a/@sammo/game_logic/src/LazyEntityUpdater.ts +++ b/@sammo/game_logic/src/LazyEntityUpdater.ts @@ -9,6 +9,8 @@ export abstract class LazyEntityUpdater{ protected abstract readonly rsc: IResourceController; + public abstract bootstrap(): void; + public get raw(): Readonly { return this._raw; } diff --git a/@sammo/game_logic/src/LogicFunc/categorizeGeneral.ts b/@sammo/game_logic/src/LogicFunc/categorizeGeneral.ts new file mode 100644 index 0000000..8dfa155 --- /dev/null +++ b/@sammo/game_logic/src/LogicFunc/categorizeGeneral.ts @@ -0,0 +1,20 @@ +import type { General } from "@/General.js"; +import { NotYetImplemented } from "@sammo/util"; + +const battleMonthThreshold = 12; + +export function categorizeGeneral(general: General): 'war' | 'domestic' { + + const nationLastBattle = general.nation.raw.aux.lastBattleYearMonth; + const lastBattle = general.raw.aux.lastBattleYearMonth; + + if(lastBattle !== undefined && nationLastBattle !== undefined){ + if(lastBattle >= nationLastBattle - battleMonthThreshold){ + return 'war'; + } + } + + throw new NotYetImplemented(); + + return 'domestic'; +} \ No newline at end of file diff --git a/@sammo/game_logic/src/Nation.ts b/@sammo/game_logic/src/Nation.ts index dd90c70..4a7503f 100644 --- a/@sammo/game_logic/src/Nation.ts +++ b/@sammo/game_logic/src/Nation.ts @@ -1,21 +1,101 @@ import { LazyEntityUpdater } from "./LazyEntityUpdater.js"; import type { INationEntity } from "./Entity/NationEntity.js"; import type { IResourceController } from "./IResourceController.js"; -import type { NationID, NationName } from "./defs.js"; +import { StaffLevel, type CityID, type GeneralID, type NationID, type NationName, type SquadID } from "./defs.js"; +import type { City } from "./City.js"; +import { General } from "./General.js"; +import { must } from "@sammo/util"; +import type { Squad } from "./Squad.js"; +import { LogicError } from "./Errors.js"; +import { categorizeGeneral } from "./LogicFunc/categorizeGeneral.js"; export class Nation extends LazyEntityUpdater { protected readonly _indirectNames: (keyof INationEntity)[] = ["id"]; protected readonly _entityType = "Nation"; protected readonly _raw: INationEntity; + + protected readonly categorizedNPCGeneralList = { + war: new Map>(), + domestic: new Map>() + }; + protected readonly categorizedUserGeneralList = { + war: new Map>(), + domestic: new Map>() + }; + + + protected readonly cityList = new Map>; + protected readonly generalList = new Map>; + protected readonly squadList = new Map>; + + protected readonly npcGeneralList = new Map>(); + protected readonly userGeneralList = new Map>(); + + protected readonly warNPCGeneralList = new Map>(); + protected readonly warUserGeneralList = new Map>(); + protected readonly domesticNPCGeneralList = new Map>(); + protected readonly domesticUserGeneralList = new Map>(); + + protected readonly staffList = new Map>(); + constructor( raw: INationEntity, protected readonly rsc: IResourceController - ){ + ) { super(); this._raw = raw; } + bootstrap() { + const rsc = this.rsc; + const raw = this._raw; + + //raw값만 읽을것 + for (const city of rsc.cityList.values()) { + if (city.raw.nationID !== raw.id) { + continue; + } + this.cityList.set(city.id, new WeakRef(city)); + } + for (const squad of rsc.squadList.values()) { + if (squad.raw.nationID !== raw.id) { + continue; + } + this.squadList.set(squad.id, new WeakRef(squad)); + } + for (const general of rsc.generalList.values()) { + if (general.raw.nationID !== raw.id) { + continue; + } + this.generalList.set(general.id, new WeakRef(general)); + + if (general.isUser) { + this.userGeneralList.set(general.id, new WeakRef(general)); + if (categorizeGeneral(general) === "war") { + this.warUserGeneralList.set(general.id, new WeakRef(general)); + } + else { + this.domesticUserGeneralList.set(general.id, new WeakRef(general)); + } + } else { + this.npcGeneralList.set(general.id, new WeakRef(general)); + if (categorizeGeneral(general) === "war") { + this.warNPCGeneralList.set(general.id, new WeakRef(general)); + } + else { + this.domesticNPCGeneralList.set(general.id, new WeakRef(general)); + } + } + + const staffLevel = general.staffLevel; + if (staffLevel !== undefined) { + this.staffList.set(staffLevel, new WeakRef(general)); + } + } + + } + public get id(): NationID { return this._raw.id; } @@ -23,4 +103,64 @@ export class Nation extends LazyEntityUpdater { public get name(): NationName { return this._raw.name; } + + public _kickGeneral(general: General | GeneralID) { + //국가에서 장수가 빠져나가는 경우, class내 변수에 대한 처리. + if (!(general instanceof General)) { + general = must(this.generalList.get(general)?.deref()); + } + + const generalID = general.id; + + this.generalList.delete(generalID); + + if (general.isUser) { + this.userGeneralList.delete(generalID); + this.warUserGeneralList.delete(generalID); + this.domesticUserGeneralList.delete(generalID); + } else { + this.npcGeneralList.delete(generalID); + this.warNPCGeneralList.delete(generalID); + this.domesticNPCGeneralList.delete(generalID); + } + + const staffLevel = general.staffLevel; + + if (staffLevel !== undefined) { + this.staffList.delete(staffLevel); + + if (staffLevel === StaffLevel.lord) { + throw new LogicError(`Lord is kicked: nationID=${this.id}`); + } + } + + if (general.squad) { + general.quitSquad(); + } + } + + public notifyDestroySquad(squad: Squad) { + this.squadList.delete(squad.id); + } + + public _joinGeneral(general: General | GeneralID) { + //국가에 장수가 들어오는 경우, class내 변수에 대한 처리. + if (!(general instanceof General)) { + general = must(this.rsc.general(general)); + } + + const generalID = general.id; + + if (general.isUser) { + this.userGeneralList.set(generalID, new WeakRef(general)); + } else { + this.npcGeneralList.set(generalID, new WeakRef(general)); + } + + const staffLevel = general.staffLevel; + + if (staffLevel !== undefined) { + this.staffList.set(staffLevel, new WeakRef(general)); + } + } } \ No newline at end of file diff --git a/@sammo/game_logic/src/Squad.ts b/@sammo/game_logic/src/Squad.ts index 707ec0f..8fad8e1 100644 --- a/@sammo/game_logic/src/Squad.ts +++ b/@sammo/game_logic/src/Squad.ts @@ -9,21 +9,27 @@ export class Squad extends LazyEntityUpdater { protected readonly _indirectNames: (keyof ISquadEntity)[] = ["id"]; protected readonly _entityType = "Squad"; protected readonly _raw: ISquadEntity; - private _leader: WeakRef; - private _members: Map>; + private _leader!: WeakRef; + private _members!: Map>; constructor( raw: ISquadEntity, - protected rsc: IResourceController + protected rsc: IResourceController, + private _rawMembers: GeneralID[] ){ super(); this._raw = raw; + } - const members = rsc.generalBySquad(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 bootstrap(){ + this._members = new Map(this._rawMembers.map((id)=>{ + return [id, new WeakRef(must(this.rsc.general(id)))]; + })); + + const leaderID = this._raw.id as GeneralID; + + this._leader = must(this._members.get(leaderID)); + this._members.delete(leaderID); } public get id(){ @@ -38,7 +44,7 @@ export class Squad extends LazyEntityUpdater { return this._members; } - public destructSquad(withLeader = true){ + public destroySquad(withLeader = true){ this._members.forEach((member)=>{ const general = must(member.deref()); general.quitSquad(); @@ -47,6 +53,8 @@ export class Squad extends LazyEntityUpdater { this.leader.quitSquad(); } + this.leader.nation.notifyDestroySquad(this); + this.rsc.reserveDelete(this); } diff --git a/@sammo/server/src/GameEngine.ts b/@sammo/server/src/GameEngine.ts index 4d9256e..4d5dcb8 100644 --- a/@sammo/server/src/GameEngine.ts +++ b/@sammo/server/src/GameEngine.ts @@ -81,7 +81,7 @@ export class GameEngine { upcomingGeneral.updateTurnTime(); this.rsc.insertGeneralTurnTimeQueue(upcomingGeneral, true); - this.rsc.gameEnv().update((env) => { + this.rsc.gameEnv.update((env) => { env.lastExecuted = generalTurnTime; }) processedGeneralCount++; @@ -89,7 +89,7 @@ export class GameEngine { if (processedGeneralCount > 0) { await this.prevSaveWaiter; - const lastExecuted = this.rsc.gameEnv().raw.lastExecuted; + const lastExecuted = this.rsc.gameEnv.raw.lastExecuted; this.loggerEngine.flushAll(); this.prevSaveWaiter = this.rsc.saveAll().then(() => { this.postStatus({ diff --git a/@sammo/server/src/ResourceController.ts b/@sammo/server/src/ResourceController.ts index 80f0eeb..9ab1288 100644 --- a/@sammo/server/src/ResourceController.ts +++ b/@sammo/server/src/ResourceController.ts @@ -1,19 +1,20 @@ import type { IResourceController } from "@sammo/game_logic"; -import type { City } from "@sammo/game_logic/src/City.js"; +import { City } from "@sammo/game_logic/src/City.js"; import type { CityID, GeneralID, NationID, SquadID } from "@sammo/game_logic/src/defs.js"; import type { IGeneralEntity } from "@sammo/game_logic/src/Entity/GeneralEntity.js"; import type { ICityEntity, ValidEntityType } from "@sammo/game_logic/src/Entity/index.js"; import type { INationEntity } from "@sammo/game_logic/src/Entity/NationEntity.js"; import type { ReservedTurn } from "@sammo/game_logic/src/defs.js"; import type { ISquadEntity } from "@sammo/game_logic/src/Entity/SquadEntity.js"; -import type { GameEnv } from "@sammo/game_logic/src/GameEnv.js"; +import { GameEnv } from "@sammo/game_logic/src/GameEnv.js"; import { General } from "@sammo/game_logic/src/General.js"; import type { LazyEntityUpdater } from "@sammo/game_logic/src/LazyEntityUpdater.js"; -import type { Nation } from "@sammo/game_logic/src/Nation.js"; -import type { Squad } from "@sammo/game_logic/src/Squad.js"; -import { insertItemAtArrayLike, NotYetImplemented } from "@sammo/util"; +import { Nation } from "@sammo/game_logic/src/Nation.js"; +import { Squad } from "@sammo/game_logic/src/Squad.js"; +import { insertItemAtArrayLike, must, NotYetImplemented } from "@sammo/util"; import Denque from "denque"; import type { Mongoose } from "mongoose"; +import type { IGameEnvEntity } from "@sammo/game_logic/src/Entity/GameEnvEntity.js"; type ChangeType = "insert" | "update" | "delete"; @@ -22,13 +23,17 @@ export class ResourceController implements IResourceController{ constructor( private readonly db: Mongoose ) { - } - private _nation: Map = new Map(); - private _city: Map = new Map(); - private _squad: Map = new Map(); - private _general: Map = new Map(); + private _gameEnv: GameEnv = new GameEnv({} as IGameEnvEntity, this); + public get gameEnv(): GameEnv { + return this._gameEnv; + } + + public readonly nationList: Map = new Map(); + public readonly cityList: Map = new Map(); + public readonly squadList: Map = new Map(); + public readonly generalList: Map = new Map(); //내부적으로 turnTime asc, id asc로 동작하는 PriorityQueue private _generalByTurnTime = new Denque(); @@ -45,19 +50,26 @@ export class ResourceController implements IResourceController{ } as const satisfies Record]>>; nation(id: NationID): Nation | undefined { - throw new NotYetImplemented(); + return this.nationList.get(id); } city(id: CityID): City | undefined { - throw new NotYetImplemented(); + return this.cityList.get(id); } squad(id: SquadID, nation: NationID): Squad | undefined { - throw new NotYetImplemented(); + const squad = this.squadList.get(id); + if(!squad){ + return undefined; + } + if(squad.raw.nationID !== nation){ + return undefined; + } + return squad; } general(id: GeneralID): General | undefined { - throw new NotYetImplemented(); + return this.generalList.get(id); } peekGeneralTurnTimeQueue(): General | undefined { @@ -72,10 +84,6 @@ export class ResourceController implements IResourceController{ insertItemAtArrayLike(this._generalByTurnTime, general, (a, b)=>a.compareTurnTime(b), maybeTail); } - gameEnv(): GameEnv { - throw new NotYetImplemented(); - } - allNations(): Map { throw new NotYetImplemented(); } @@ -159,6 +167,63 @@ export class ResourceController implements IResourceController{ } resetAndFill(): Promise { + this._generalByTurnTime.clear(); + this.nationList.clear(); + this.cityList.clear(); + this.squadList.clear(); + this.generalList.clear(); + + //MongoDB로부터 데이터를 가져와서 채워넣는다. + + const rawGameEnv = {} as IGameEnvEntity; + //TODO: Implement! + + + const rawNationList = new Map(); + const rawCityList = new Map(); + const rawSquadList = new Map(); + const rawGeneralList = new Map(); + + + const rawGeneralListBySquad = new Map(); + for(const rawSquad of rawSquadList.values()){ + rawGeneralListBySquad.set(rawSquad.id, []); + } + for(const rawGeneral of rawGeneralList.values()){ + if(!rawGeneral.squadID){ + continue; + } + const list = must(rawGeneralListBySquad.get(rawGeneral.squadID)); + list.push(rawGeneral.id); + } + + this._gameEnv = new GameEnv(rawGameEnv, this); + this._gameEnv.bootstrap(); + + for(const rawNation of rawNationList.values()){ + const nation = new Nation(rawNation, this); + this.nationList.set(nation.id, nation); + } + for(const rawCity of rawCityList.values()){ + const city = new City(rawCity, this); + this.cityList.set(city.id, city); + } + for(const rawSquad of rawSquadList.values()){ + const squad = new Squad(rawSquad, this, must(rawGeneralListBySquad.get(rawSquad.id))); + this.squadList.set(squad.id, squad); + } + for(const rawGeneral of rawGeneralList.values()){ + const general = new General(rawGeneral, this); + this.generalList.set(general.id, general); + } + + //lazyInit()을 호출한다. + this.nationList.forEach(nation=>nation.bootstrap()); + this.cityList.forEach(city=>city.bootstrap()); + this.squadList.forEach(squad=>squad.bootstrap()); + //iAction때문에 general의 lazyInit이 가장 마지막 + this.generalList.forEach(general=>general.bootstrap()); + throw new NotYetImplemented(); }