diff --git a/app/game-engine/src/turn/databaseHooks.ts b/app/game-engine/src/turn/databaseHooks.ts index 7f058a7..5e0e876 100644 --- a/app/game-engine/src/turn/databaseHooks.ts +++ b/app/game-engine/src/turn/databaseHooks.ts @@ -32,11 +32,7 @@ import { ensureItemInventory, withSerializedItemInventory } from '@sammo-ts/logi import { persistGeneralLifecycleEvents } from './generalTurnLifecyclePersistence.js'; import type { DatabaseTurnDaemonLease } from '../lifecycle/databaseTurnDaemonLease.js'; import { calculateNationBettingRewards } from '../betting/nationBettingSettlement.js'; -import type { - NationBettingCandidate, - PendingNationBettingFinish, - PendingNationBettingOpen, -} from './types.js'; +import type { NationBettingCandidate, PendingNationBettingFinish, PendingNationBettingOpen } from './types.js'; export interface DatabaseTurnHooks { hooks: TurnDaemonHooks; @@ -53,11 +49,7 @@ const readBettingCandidates = (value: unknown): NationBettingCandidate[] => { return value.flatMap((candidate) => { const item = asRecord(candidate); const aux = asRecord(item.aux); - if ( - typeof item.title !== 'string' || - typeof aux.nation !== 'number' || - !Number.isInteger(aux.nation) - ) { + if (typeof item.title !== 'string' || typeof aux.nation !== 'number' || !Number.isInteger(aux.nation)) { return []; } return [ @@ -369,6 +361,9 @@ const buildRankRows = ( ['betwingold', readMeta('betwingold')], ['inherit_earned', readMeta('inherit_earned')], ['inherit_spent', readMeta('inherit_spent')], + ['inherit_earned_dyn', readMeta('inherit_earned_dyn')], + ['inherit_earned_act', readMeta('inherit_earned_act')], + ['inherit_spent_dyn', readMeta('inherit_spent_dyn')], ]; return entries.map(([type, value]) => ({ diff --git a/app/game-engine/src/turn/types.ts b/app/game-engine/src/turn/types.ts index 957a5b4..4703e1d 100644 --- a/app/game-engine/src/turn/types.ts +++ b/app/game-engine/src/turn/types.ts @@ -31,6 +31,7 @@ export interface TurnGeneral extends General { recentWarTime?: Date | null; lastTurn?: GeneralLastTurn; penalty?: unknown; + inheritancePoints?: Record; } export interface TurnDiplomacy { diff --git a/app/game-engine/src/turn/worldLoader.ts b/app/game-engine/src/turn/worldLoader.ts index 0567674..63c7b45 100644 --- a/app/game-engine/src/turn/worldLoader.ts +++ b/app/game-engine/src/turn/worldLoader.ts @@ -5,6 +5,8 @@ import { type TurnEngineDatabaseClient, type TurnEngineDiplomacyRow, type TurnEngineGeneralRow, + type TurnEngineInheritancePointRow, + type TurnEngineRankDataRow, type TurnEngineNationRow, type TurnEngineTroopRow, } from '@sammo-ts/infra'; @@ -154,14 +156,36 @@ const mapScenarioConfig = (raw: JsonValue): ScenarioConfig => { return parsed.data; }; -const mapGeneralRow = (row: TurnEngineGeneralRow): TurnGeneral => { +const GENERAL_RANK_META_PREFIX_TYPES = new Set([ + 'warnum', + 'killnum', + 'deathnum', + 'occupied', + 'killcrew', + 'deathcrew', + 'killcrew_person', + 'deathcrew_person', +]); + +const mapGeneralRow = ( + row: TurnEngineGeneralRow, + rankRows: readonly TurnEngineRankDataRow[], + inheritanceRows: readonly TurnEngineInheritancePointRow[] +): TurnGeneral => { const legacySlots: GeneralItemSlots = { horse: normalizeCode(row.horseCode), weapon: normalizeCode(row.weaponCode), book: normalizeCode(row.bookCode), item: normalizeCode(row.itemCode), }; - const rawMeta = asTriggerRecord(row.meta) as Record; + const rawMeta = { ...(asTriggerRecord(row.meta) as Record) }; + for (const rank of rankRows) { + if (rank.type === 'experience' || rank.type === 'dedication') { + continue; + } + rawMeta[GENERAL_RANK_META_PREFIX_TYPES.has(rank.type) ? `rank_${rank.type}` : rank.type] = rank.value; + } + const inheritancePoints = Object.fromEntries(inheritanceRows.map((entry) => [entry.key, entry.value])); const itemInventory = readItemInventoryFromMeta(rawMeta, legacySlots); return { ...((): { meta: TurnGeneral['meta'] } => { @@ -218,6 +242,7 @@ const mapGeneralRow = (row: TurnEngineGeneralRow): TurnGeneral => { // meta는 상단에서 보장 처리됨. turnTime: row.turnTime, recentWarTime: row.recentWarTime ?? null, + inheritancePoints, }; }; @@ -315,18 +340,39 @@ export const loadTurnWorldFromDatabase = async (options: TurnWorldLoaderOptions) throw new Error('world_state row is required to start turn daemon.'); } - const [generalRows, cityRows, nationRows, diplomacyRows, troopRows, eventRows] = await Promise.all([ - prisma.general.findMany(), - prisma.city.findMany(), - prisma.nation.findMany(), - prisma.diplomacy.findMany(), - prisma.troop.findMany(), - prisma.event.findMany({ - orderBy: [{ priority: 'desc' }, { id: 'asc' }], - }), - ]); + const [generalRows, rankRows, inheritanceRows, cityRows, nationRows, diplomacyRows, troopRows, eventRows] = + await Promise.all([ + prisma.general.findMany(), + prisma.rankData.findMany(), + prisma.inheritancePoint.findMany(), + prisma.city.findMany(), + prisma.nation.findMany(), + prisma.diplomacy.findMany(), + prisma.troop.findMany(), + prisma.event.findMany({ + orderBy: [{ priority: 'desc' }, { id: 'asc' }], + }), + ]); - const generals = generalRows.map(mapGeneralRow); + const ranksByGeneral = new Map(); + for (const row of rankRows) { + const bucket = ranksByGeneral.get(row.generalId) ?? []; + bucket.push(row); + ranksByGeneral.set(row.generalId, bucket); + } + const inheritanceByUser = new Map(); + for (const row of inheritanceRows) { + const bucket = inheritanceByUser.get(row.userId) ?? []; + bucket.push(row); + inheritanceByUser.set(row.userId, bucket); + } + const generals = generalRows.map((row) => + mapGeneralRow( + row, + ranksByGeneral.get(row.id) ?? [], + row.userId ? (inheritanceByUser.get(row.userId) ?? []) : [] + ) + ); const cities = cityRows.map(mapCityRow); const nations = nationRows.map(mapNationRow); const diplomacy = diplomacyRows.map(mapDiplomacyRow); diff --git a/app/game-engine/test/monthlyCreateManyNpcPersistence.integration.test.ts b/app/game-engine/test/monthlyCreateManyNpcPersistence.integration.test.ts index b183b08..3472b23 100644 --- a/app/game-engine/test/monthlyCreateManyNpcPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyCreateManyNpcPersistence.integration.test.ts @@ -213,7 +213,7 @@ integration('CreateManyNPC database persistence', () => { const ranks = await db.rankData.findMany({ where: { generalId: createdGeneralId } }); // Legacy inserts 37 RankColumn rows. Core's canonical rank model // additionally projects experience/dedication/dex, so it owns 41. - expect(ranks).toHaveLength(41); + expect(ranks).toHaveLength(44); expect(ranks.every((rank) => rank.nationId === 0 && rank.value === 0)).toBe(true); expect( await db.logEntry.findMany({ diff --git a/app/game-engine/test/monthlyInvaderPersistence.integration.test.ts b/app/game-engine/test/monthlyInvaderPersistence.integration.test.ts index 4a6ac03..97d64f6 100644 --- a/app/game-engine/test/monthlyInvaderPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyInvaderPersistence.integration.test.ts @@ -320,7 +320,7 @@ integration('RaiseInvader database persistence', () => { }); expect(await db.general.count({ where: { nationId: createdNationId } })).toBe(10); expect(await db.generalTurn.count({ where: { generalId: { gte: firstCreatedGeneralId } } })).toBe(300); - expect(await db.rankData.count({ where: { generalId: { gte: firstCreatedGeneralId } } })).toBe(410); + expect(await db.rankData.count({ where: { generalId: { gte: firstCreatedGeneralId } } })).toBe(440); expect(await db.nationTurn.count({ where: { nationId: createdNationId } })).toBe(48); expect( await db.diplomacy.count({ diff --git a/app/game-engine/test/monthlyNpcSupportPersistence.integration.test.ts b/app/game-engine/test/monthlyNpcSupportPersistence.integration.test.ts index d9af8a5..b7e3d91 100644 --- a/app/game-engine/test/monthlyNpcSupportPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyNpcSupportPersistence.integration.test.ts @@ -228,16 +228,14 @@ integration('monthly NPC support database persistence', () => { affinity: 999, meta: expect.objectContaining({ killturn: 70 }), }); - expect( - await db.troop.findUniqueOrThrow({ where: { troopLeaderId: generalId } }) - ).toMatchObject({ + expect(await db.troop.findUniqueOrThrow({ where: { troopLeaderId: generalId } })).toMatchObject({ nationId, name: '㉥부대장 41', }); const turns = await db.generalTurn.findMany({ where: { generalId } }); expect(turns).toHaveLength(30); expect(new Set(turns.map((turn) => turn.actionCode))).toEqual(new Set(['che_집합'])); - expect(await db.rankData.count({ where: { generalId } })).toBe(41); + expect(await db.rankData.count({ where: { generalId } })).toBe(44); expect((await db.worldState.findUniqueOrThrow({ where: { id: stateRow.id } })).meta).toMatchObject({ lastNPCTroopLeaderID: 41, }); diff --git a/app/game-engine/test/monthlyRaiseNpcNationPersistence.integration.test.ts b/app/game-engine/test/monthlyRaiseNpcNationPersistence.integration.test.ts index 4a8dde7..f4bbb73 100644 --- a/app/game-engine/test/monthlyRaiseNpcNationPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyRaiseNpcNationPersistence.integration.test.ts @@ -65,10 +65,7 @@ const map: MapDefinition = { level: 5, region: 1, position: { x: index, y: 0 }, - connections: [ - ...(index > 0 ? [rows[index - 1]!] : []), - ...(index + 1 < rows.length ? [rows[index + 1]!] : []), - ], + connections: [...(index > 0 ? [rows[index - 1]!] : []), ...(index + 1 < rows.length ? [rows[index + 1]!] : [])], max: { population: 50_000, agriculture: 5_000, @@ -294,7 +291,7 @@ integration('RaiseNPCNation database persistence', () => { }); expect(await db.generalTurn.count({ where: { generalId: createdGeneralId } })).toBe(30); expect(await db.nationTurn.count({ where: { nationId: createdNationId } })).toBe(48); - expect(await db.rankData.count({ where: { generalId: createdGeneralId } })).toBe(41); + expect(await db.rankData.count({ where: { generalId: createdGeneralId } })).toBe(44); expect( await db.diplomacy.count({ where: { diff --git a/app/game-engine/test/monthlyRegisterNpcPersistence.integration.test.ts b/app/game-engine/test/monthlyRegisterNpcPersistence.integration.test.ts index b239a36..a99dc5f 100644 --- a/app/game-engine/test/monthlyRegisterNpcPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyRegisterNpcPersistence.integration.test.ts @@ -53,10 +53,7 @@ integration('RegNPC database persistence', () => { const clean = async () => { await db.logEntry.deleteMany({ where: { - OR: [ - { generalId: createdGeneralId }, - { year: 200, month: 1, text: { contains: 'ⓝ저장장수' } }, - ], + OR: [{ generalId: createdGeneralId }, { year: 200, month: 1, text: { contains: 'ⓝ저장장수' } }], }, }); await db.generalTurn.deleteMany({ where: { generalId: createdGeneralId } }); @@ -207,7 +204,7 @@ integration('RegNPC database persistence', () => { expect(turns).toHaveLength(30); expect(new Set(turns.map((turn) => turn.actionCode))).toEqual(new Set(['휴식'])); const ranks = await db.rankData.findMany({ where: { generalId: createdGeneralId } }); - expect(ranks).toHaveLength(41); + expect(ranks).toHaveLength(44); expect(ranks.every((rank) => rank.nationId === 0 && rank.value === 0)).toBe(true); expect( await db.logEntry.findFirst({ diff --git a/packages/common/src/ranking/types.ts b/packages/common/src/ranking/types.ts index d50ae6a..6350075 100644 --- a/packages/common/src/ranking/types.ts +++ b/packages/common/src/ranking/types.ts @@ -40,6 +40,9 @@ export const RANK_DATA_TYPES = [ 'betwingold', 'inherit_earned', 'inherit_spent', + 'inherit_earned_dyn', + 'inherit_earned_act', + 'inherit_spent_dyn', ] as const; export type RankDataType = (typeof RANK_DATA_TYPES)[number]; diff --git a/packages/infra/src/turnEngineDb.ts b/packages/infra/src/turnEngineDb.ts index 18d3429..9efea38 100644 --- a/packages/infra/src/turnEngineDb.ts +++ b/packages/infra/src/turnEngineDb.ts @@ -55,6 +55,19 @@ export interface TurnEngineGeneralRow { recentWarTime: Date | null; } +export interface TurnEngineRankDataRow { + generalId: number; + nationId: number; + type: string; + value: number; +} + +export interface TurnEngineInheritancePointRow { + userId: string; + key: string; + value: number; +} + export interface TurnEngineCityRow { id: number; name: string; @@ -363,6 +376,12 @@ export interface TurnEngineDatabaseClient { update(args: { where: { id: number }; data: TurnEngineGeneralUpdateInput }): Promise; deleteMany(args?: unknown): Promise; }; + rankData: { + findMany(args?: unknown): Promise; + }; + inheritancePoint: { + findMany(args?: unknown): Promise; + }; city: { findMany(args?: unknown): Promise; createMany(args: { data: TurnEngineCityCreateManyInput[] }): Promise;