diff --git a/packages/logic/src/war/aftermath.ts b/packages/logic/src/war/aftermath.ts index 7ef74cb..fb0bc71 100644 --- a/packages/logic/src/war/aftermath.ts +++ b/packages/logic/src/war/aftermath.ts @@ -321,6 +321,14 @@ const resolveConquerCity = ( if (!nationCollapsed && defenderNation && defenderNation.capitalCityId === defenderCity.id) { const nextCapital = findNextCapital(cities, defenderNationId, defenderCity.id, defenderCity); if (nextCapital) { + const josaRo = JosaUtil.pick(nextCapital.name, '로'); + const josaYi = JosaUtil.pick(defenderNation.name, '이'); + attackerLogger.pushGlobalHistoryLog( + `【긴급천도】${defenderNation.name}${josaYi} 수도가 함락되어 ${nextCapital.name}${josaRo} 긴급천도하였습니다.` + ); + const moveLog = `수도가 함락되어 ${nextCapital.name}${josaRo} 긴급천도합니다.`; + const gatherLog = `수뇌는 ${nextCapital.name}${josaRo} 집합되었습니다.`; + defenderNation.capitalCityId = nextCapital.id; defenderNation.gold = round(defenderNation.gold * 0.5); defenderNation.rice = round(defenderNation.rice * 0.5); @@ -332,6 +340,16 @@ const resolveConquerCity = ( if (general.nationId !== defenderNationId) { continue; } + const defenderLogger = new ActionLogger({ + generalId: general.id, + nationId: defenderNationId, + }); + defenderLogger.pushGeneralActionLog(moveLog, LogFormat.PLAIN); + if (general.officerLevel >= 5) { + defenderLogger.pushGeneralActionLog(gatherLog, LogFormat.PLAIN); + } + pushLoggers([defenderLogger], logs); + general.atmos = round(general.atmos * 0.8); if (general.officerLevel >= 5) { general.cityId = nextCapital.id; diff --git a/packages/logic/test/warAftermath.test.ts b/packages/logic/test/warAftermath.test.ts index 3556d36..0c299d0 100644 --- a/packages/logic/test/warAftermath.test.ts +++ b/packages/logic/test/warAftermath.test.ts @@ -175,6 +175,61 @@ describe('war aftermath', () => { expect(defenderCity.meta.dead).toBe(90); }); + it('logs emergency relocation when a surviving nation loses its capital', () => { + const attackerNation = buildNation(1); + const defenderNation = buildNation(2); + const attackerCity = buildCity(1, 1); + const defenderCity = buildCity(2, 2); + const nextCapital = buildCity(3, 2); + const attacker = buildGeneral(1, 1, 1); + attacker.officerLevel = 12; + const defender = buildGeneral(2, 2, 2); + defender.officerLevel = 12; + + const outcome = resolveWarAftermath({ + battle: { + attacker, + defenders: [], + defenderCity, + logs: [], + conquered: true, + reports: [ + { + id: attacker.id, + type: 'general', + name: attacker.name, + isAttacker: true, + killed: 10, + dead: 5, + }, + ], + }, + attackerNation, + defenderNation, + attackerCity, + defenderCity, + nations: [attackerNation, defenderNation], + cities: [attackerCity, defenderCity, nextCapital], + generals: [attacker, defender], + unitSet: buildUnitSet(), + config: buildConfig(), + time: { + year: 200, + month: 1, + startYear: 180, + }, + }); + + expect(defenderNation.capitalCityId).toBe(nextCapital.id); + expect(outcome.logs.map((log) => log.text)).toEqual( + expect.arrayContaining([ + '【긴급천도】Nation2가 수도가 함락되어 City3으로 긴급천도하였습니다.', + '수도가 함락되어 City3으로 긴급천도합니다.', + '수뇌는 City3으로 집합되었습니다.', + ]) + ); + }); + it('applies conquest collapse rewards', () => { const rng = new RandUtil(new ConstantRNG(0)); const attackerNation = buildNation(1); diff --git a/tools/integration-tests/test/turnCommandCoreReference.integration.test.ts b/tools/integration-tests/test/turnCommandCoreReference.integration.test.ts index 5ccca51..c3202ba 100644 --- a/tools/integration-tests/test/turnCommandCoreReference.integration.test.ts +++ b/tools/integration-tests/test/turnCommandCoreReference.integration.test.ts @@ -83,6 +83,7 @@ integration('core ↔ legacy command-boundary differential', () => { ], ['live sortie supply retreat', 'fixtures/turn-differential/live-sortie-supply-retreat.json'], ['live sortie noncapital conquest', 'fixtures/turn-differential/live-sortie-noncapital-conquest.json'], + ['live sortie emergency capital', 'fixtures/turn-differential/live-sortie-emergency-capital.json'], ])( '%s matches command RNG and canonical state delta', async (_label, fixturePath) => { @@ -120,6 +121,18 @@ integration('core ↔ legacy command-boundary differential', () => { expect(reference.after.nations.some((nation) => nation.id === 2)).toBe(true); expect(reference.after.generals.find((general) => general.id === 2)?.nationId).toBe(2); } + if (fixturePath.endsWith('live-sortie-emergency-capital.json')) { + const defenderNation = reference.after.nations.find((nation) => nation.id === 2); + expect(defenderNation?.capitalCityId).toBe(71); + expect(defenderNation?.gold).toBe(50_000); + expect(defenderNation?.rice).toBe(40_000); + expect(reference.after.generals.find((general) => general.id === 2)).toMatchObject({ + nationId: 2, + cityId: 71, + atmos: 80, + }); + expect(reference.after.cities.find((city) => city.id === 71)?.supplyState).toBe(1); + } expect(core.execution.outcome).toMatchObject({ requestedAction: request.action,