From 2a8a579b751ae8fa294fc3b81971c671fdd96291 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 26 Jul 2026 21:49:40 +0000 Subject: [PATCH] fix active action inheritance accounting --- .../src/turn/reservedTurnHandler.ts | 6 -- .../src/turn-differential/canonical.ts | 1 + .../src/turn-differential/coreCommandTrace.ts | 1 + ...rnCommandGeneralMatrix.integration.test.ts | 83 +++++++++++++++++++ ...urnCommandNationMatrix.integration.test.ts | 74 +++++++++++++++++ 5 files changed, 159 insertions(+), 6 deletions(-) diff --git a/app/game-engine/src/turn/reservedTurnHandler.ts b/app/game-engine/src/turn/reservedTurnHandler.ts index 417fe73..f00c22c 100644 --- a/app/game-engine/src/turn/reservedTurnHandler.ts +++ b/app/game-engine/src/turn/reservedTurnHandler.ts @@ -1573,12 +1573,6 @@ export const createReservedTurnHandler = async (options: { } else { meta.killturn = worldKillturn; } - const active = typeof meta.inherit_active_action === 'number' ? meta.inherit_active_action : 0; - if (generalResult.actionKey !== DEFAULT_ACTION) { - meta.inherit_active_action = active + 1; - } else { - meta.inherit_active_action = active; - } currentGeneral = { ...currentGeneral, meta }; worldOverlay?.syncGeneral(currentGeneral); } diff --git a/tools/integration-tests/src/turn-differential/canonical.ts b/tools/integration-tests/src/turn-differential/canonical.ts index 6cd5b03..0c63964 100644 --- a/tools/integration-tests/src/turn-differential/canonical.ts +++ b/tools/integration-tests/src/turn-differential/canonical.ts @@ -159,6 +159,7 @@ export const projectCoreDatabaseSnapshot = (rows: { dex5: readNumber(meta, 'dex5'), specAge: readNumber(meta, 'specage'), specAge2: readNumber(meta, 'specage2'), + inheritActiveActionPoints: readNumber(meta, 'inherit_active_action') * 3, makeLimit: readNumber(meta, 'makelimit'), penalty: asRecord(row.penalty), killTurn: readNumber(meta, 'killturn'), diff --git a/tools/integration-tests/src/turn-differential/coreCommandTrace.ts b/tools/integration-tests/src/turn-differential/coreCommandTrace.ts index ad8e8bf..1cdd71a 100644 --- a/tools/integration-tests/src/turn-differential/coreCommandTrace.ts +++ b/tools/integration-tests/src/turn-differential/coreCommandTrace.ts @@ -580,6 +580,7 @@ const projectWorld = ( dex5: toDatabaseInt(readNumber(general.meta, 'dex5')), specAge: toDatabaseInt(readNumber(general.meta, 'specage')), specAge2: toDatabaseInt(readNumber(general.meta, 'specage2')), + inheritActiveActionPoints: readNumber(general.meta, 'inherit_active_action') * 3, makeLimit: toDatabaseInt(readNumber(general.meta, 'makelimit')), penalty: asRecord(general.penalty), killTurn: readNumber(general.meta, 'killturn'), diff --git a/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts b/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts index 92b27e8..50391a9 100644 --- a/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts +++ b/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts @@ -426,6 +426,89 @@ integration('general command success matrix', () => { ); }); +type GeneralActiveActionInheritanceCase = { + name: string; + action: string; + args?: Record; + expectedPointDelta?: number; +}; + +const generalActiveActionInheritanceCases: GeneralActiveActionInheritanceCase[] = [ + { + name: 'ordinary training does not count as a legacy active action', + action: 'che_훈련', + expectedPointDelta: 0, + }, + { + name: 'spy contributes the legacy half active action', + action: 'che_첩보', + args: { destCityID: 70 }, + expectedPointDelta: 1.5, + }, + { + name: 'abdication contributes one active action', + action: 'che_선양', + args: { destGeneralID: 3 }, + expectedPointDelta: 3, + }, + { + name: 'rest does not count as a legacy active action', + action: '휴식', + expectedPointDelta: 0, + }, + { + name: 'talent scouting preserves its probability-weighted active action', + action: 'che_인재탐색', + }, +]; + +const readActiveActionPoints = ( + snapshot: { generals: Array> }, + generalId: number +): number => { + const general = snapshot.generals.find((entry) => entry.id === generalId); + const value = general?.inheritActiveActionPoints; + return typeof value === 'number' && Number.isFinite(value) ? value : 0; +}; + +integration('general active-action inheritance point parity', () => { + it.each(generalActiveActionInheritanceCases)( + '$name', + async ({ name, action, args, expectedPointDelta }) => { + const request = buildRequest(action, args); + request.setup!.world!.hiddenSeed = `general-active-action-${name}`; + const reference = runReferenceTurnCommandTraceRequest( + workspaceRoot!, + request as unknown as Record + ); + const core = await runCoreTurnCommandTrace(request, reference.before); + const referencePointDelta = + readActiveActionPoints(reference.after, 1) - readActiveActionPoints(reference.before, 1); + const corePointDelta = readActiveActionPoints(core.after, 1) - readActiveActionPoints(core.before, 1); + + expect(reference.execution.outcome).toMatchObject({ completed: true }); + expect(core.execution.outcome).toMatchObject({ + requestedAction: action, + actionKey: action, + usedFallback: false, + }); + if (expectedPointDelta !== undefined) { + expect(referencePointDelta).toBe(expectedPointDelta); + } else { + expect(referencePointDelta).toBeGreaterThan(0); + } + expect(corePointDelta).toBe(referencePointDelta); + expect(core.rng).toEqual(reference.rng); + expect( + compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, { + ignoredPathPatterns: ignoredLifecyclePaths, + }) + ).toEqual([]); + }, + 120_000 + ); +}); + interface NpcActiveBoundaryCase { name: string; args: Record; diff --git a/tools/integration-tests/test/turnCommandNationMatrix.integration.test.ts b/tools/integration-tests/test/turnCommandNationMatrix.integration.test.ts index 2e04141..54a0df9 100644 --- a/tools/integration-tests/test/turnCommandNationMatrix.integration.test.ts +++ b/tools/integration-tests/test/turnCommandNationMatrix.integration.test.ts @@ -702,6 +702,80 @@ integration('nation command success matrix', () => { ); }); +type NationActiveActionInheritanceCase = { + name: string; + request: NationMatrixCase; + expectedPointDelta: number; +}; + +const nationActiveActionInheritanceCases: NationActiveActionInheritanceCase[] = [ + { + name: 'ordinary award does not count as a legacy active action', + request: ['che_포상', { isGold: true, amount: 100, destGeneralID: 3 }], + expectedPointDelta: 0, + }, + { + name: 'nation rename contributes one active action', + request: ['che_국호변경', { nationName: '능동아국' }], + expectedPointDelta: 3, + }, + { + name: 'event research contributes one active action on completion', + request: researchCase('event_화시병연구', '화시병 연구', 11), + expectedPointDelta: 3, + }, + { + name: 'nation rest does not count as a legacy active action', + request: ['휴식', undefined], + expectedPointDelta: 0, + }, +]; + +const readNationActorActiveActionPoints = ( + snapshot: { generals: Array> }, + generalId: number +): number => { + const general = snapshot.generals.find((entry) => entry.id === generalId); + const value = general?.inheritActiveActionPoints; + return typeof value === 'number' && Number.isFinite(value) ? value : 0; +}; + +integration('nation active-action inheritance point parity', () => { + it.each(nationActiveActionInheritanceCases)( + '$name', + async ({ name, request: [action, args, fixturePatches], expectedPointDelta }) => { + const request = buildRequest(action, args, fixturePatches); + request.setup!.world!.hiddenSeed = `nation-active-action-${name}`; + const reference = runReferenceTurnCommandTraceRequest( + workspaceRoot!, + request as unknown as Record + ); + const core = await runCoreTurnCommandTrace(request, reference.before); + const referencePointDelta = + readNationActorActiveActionPoints(reference.after, 1) - + readNationActorActiveActionPoints(reference.before, 1); + const corePointDelta = + readNationActorActiveActionPoints(core.after, 1) - readNationActorActiveActionPoints(core.before, 1); + + expect(reference.execution.outcome).toMatchObject({ completed: true }); + expect(core.execution.outcome).toMatchObject({ + requestedAction: action, + actionKey: action, + usedFallback: false, + }); + expect(referencePointDelta).toBe(expectedPointDelta); + expect(corePointDelta).toBe(referencePointDelta); + expect(core.rng).toEqual(reference.rng); + expect( + compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, { + ignoredPathPatterns: ignoredLifecyclePaths, + }) + ).toEqual([]); + }, + 120_000 + ); +}); + const nationNameBoundaryCases: Array<{ name: string; nationName: string;