diff --git a/app/game-engine/src/turn/reservedTurnHandler.ts b/app/game-engine/src/turn/reservedTurnHandler.ts index 270f0fdf..5a3d4438 100644 --- a/app/game-engine/src/turn/reservedTurnHandler.ts +++ b/app/game-engine/src/turn/reservedTurnHandler.ts @@ -1993,7 +1993,7 @@ export const createReservedTurnHandler = async (options: { let deleteGeneral = false; const deletedTroopIds = Array.from(commandDeletedTroopIds); const lifecycleSnapshot = cloneTurnGeneral(currentGeneral); - if (currentGeneral.meta.killturn <= 0 && typeof currentGeneral.deadYear === 'number') { + if (currentGeneral.meta.killturn <= 0) { if ( currentGeneral.npcState === 1 && typeof currentGeneral.deadYear === 'number' && diff --git a/app/game-engine/test/generalTurnLifecycle.test.ts b/app/game-engine/test/generalTurnLifecycle.test.ts index 05f6b330..ce7a1eb8 100644 --- a/app/game-engine/test/generalTurnLifecycle.test.ts +++ b/app/game-engine/test/generalTurnLifecycle.test.ts @@ -316,12 +316,13 @@ describe('legacy general turn lifecycle', () => { expect(harness.world.peekDirtyState().deletedGenerals).toContain(1); }); - it('keeps compatibility fixtures without legacy lifespan metadata alive', async () => { + it('deletes an expired NPC even when its in-memory lifespan metadata is missing', async () => { const harness = await createTurnTestHarness({ snapshot: makeSnapshot([ makeGeneral({ deadYear: undefined, - npcState: 2, + npcState: 4, + name: 'ⓖ의병', meta: { killturn: 1 }, }), ]), @@ -332,9 +333,9 @@ describe('legacy general turn lifecycle', () => { await harness.runOneTick(); - expect(harness.world.getGeneralById(1)).not.toBeNull(); - expect(harness.world.getGeneralById(1)!.meta.killturn).toBe(0); - expect(harness.world.peekDirtyState().lifecycleEvents[0]?.outcome).toBe('active'); + expect(harness.world.getGeneralById(1)).toBeNull(); + expect(harness.world.peekDirtyState().deletedGenerals).toContain(1); + expect(harness.world.peekDirtyState().lifecycleEvents[0]?.outcome).toBe('deleted'); }); it('retires a player general and resets inherited stats and rank state', async () => { diff --git a/packages/logic/src/actions/turn/nation/che_의병모집.ts b/packages/logic/src/actions/turn/nation/che_의병모집.ts index 4228849c..c2ac20e2 100644 --- a/packages/logic/src/actions/turn/nation/che_의병모집.ts +++ b/packages/logic/src/actions/turn/nation/che_의병모집.ts @@ -452,6 +452,8 @@ export class ActionResolver< }), turnTime, ...(turnTick === undefined ? {} : { turnTick }), + bornYear: birthYear, + deadYear: deathYear, }; effects.push(createGeneralAddEffect(newGeneral)); } diff --git a/packages/logic/test/actions/turn/nationVolunteerRecruit.test.ts b/packages/logic/test/actions/turn/nationVolunteerRecruit.test.ts new file mode 100644 index 00000000..290612c3 --- /dev/null +++ b/packages/logic/test/actions/turn/nationVolunteerRecruit.test.ts @@ -0,0 +1,113 @@ +import { ConstantRNG, RandUtil } from '@sammo-ts/common'; +import { describe, expect, it } from 'vitest'; + +import type { General, Nation } from '../../../src/domain/entities.js'; +import { + ActionResolver, + type VolunteerRecruitEnvironment, + type VolunteerRecruitResolveContext, +} from '../../../src/actions/turn/nation/che_의병모집.js'; + +const general: General = { + id: 1, + name: '군주', + nationId: 1, + cityId: 3, + troopId: 0, + stats: { leadership: 70, strength: 70, intelligence: 70 }, + experience: 1_000, + dedication: 1_000, + officerLevel: 12, + role: { + personality: null, + specialDomestic: null, + specialWar: null, + items: { horse: null, weapon: null, book: null, item: null }, + }, + injury: 0, + gold: 1_000, + rice: 1_000, + crew: 0, + crewTypeId: 0, + train: 0, + atmos: 0, + age: 30, + npcState: 0, + triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} }, + meta: { killturn: 24 }, +}; + +const nation: Nation = { + id: 1, + name: '테스트국', + color: '#000000', + capitalCityId: 3, + chiefGeneralId: 1, + gold: 10_000, + rice: 10_000, + power: 0, + level: 1, + typeCode: 'che_중립', + meta: { gennum: 1, strategic_cmd_limit: 0 }, +}; + +const environment: VolunteerRecruitEnvironment = { + openingPartYear: 0, + initialNationGenLimit: 10, + defaultNpcGold: 1_000, + defaultNpcRice: 1_000, + defaultCrewTypeId: 0, + defaultSpecialDomestic: null, + defaultSpecialWar: null, + createCountBase: 1, + createCountDivisor: 8, + npcAge: 20, + npcDeathYears: 10, + randomGeneralFirstNames: ['장'], + randomGeneralMiddleNames: [''], + randomGeneralLastNames: ['수'], + availablePersonalities: ['che_안전'], +}; + +describe('nation volunteer recruitment lifespan', () => { + it('places the Ref birth and death years on the created general entity', () => { + const resolver = new ActionResolver([], environment); + const context = { + general: structuredClone(general), + nation: structuredClone(nation), + rng: new RandUtil(new ConstantRNG(0)), + addLog: () => undefined, + currentYear: 190, + currentMonth: 1, + startYear: 180, + averageNationGeneralCount: 0, + nationAverageStats: { leadership: 50, strength: 50, intelligence: 50 }, + nationAverageExperience: 1_000, + nationAverageDedication: 1_000, + nationAverageDex: [100, 100, 100, 100, 100], + friendlyGenerals: [general], + createGeneralId: () => 2, + turnTermSeconds: 60, + turnTimeBase: new Date('0190-01-01T00:00:00.000Z'), + ticksPerSecond: 1, + } as VolunteerRecruitResolveContext; + + const outcome = resolver.resolve(context, {}); + const createdEffect = outcome.effects.find((effect) => effect.type === 'general:add'); + expect(createdEffect?.type).toBe('general:add'); + if (!createdEffect || createdEffect.type !== 'general:add') { + return; + } + + const created = createdEffect.general as General & { bornYear?: number; deadYear?: number }; + expect(created).toMatchObject({ + name: 'ⓖ장수', + bornYear: 170, + deadYear: 200, + meta: { + birthYear: 170, + deathYear: 200, + }, + }); + }); +});