From 6dec59430a7f30b18185b95a4b5119e708d59771 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 26 Jul 2026 13:13:10 +0000 Subject: [PATCH] fix live sortie legacy logs --- packages/logic/src/war/aftermath.ts | 33 +++++++++++++++ packages/logic/src/war/units/base.ts | 33 ++++++++++++++- packages/logic/src/war/units/city.ts | 6 ++- packages/logic/src/war/units/general.ts | 16 ++++++- packages/logic/test/warAftermath.test.ts | 9 ++++ packages/logic/test/warEngine.test.ts | 54 +++++++++++++++++++++++- 6 files changed, 145 insertions(+), 6 deletions(-) diff --git a/packages/logic/src/war/aftermath.ts b/packages/logic/src/war/aftermath.ts index f2a026c..106fb41 100644 --- a/packages/logic/src/war/aftermath.ts +++ b/packages/logic/src/war/aftermath.ts @@ -254,6 +254,14 @@ const resolveConquerCity = ( let totalGoldLoss = 0; let totalRiceLoss = 0; + const defenderNationJosaUl = JosaUtil.pick(defenderNationName, '을'); + const defenderNationJosaUn = JosaUtil.pick(defenderNationName, '은'); + const defenderNationJosaYi = JosaUtil.pick(defenderNationName, '이'); + attackerLogger.pushNationHistoryLog(`${defenderNationName}${defenderNationJosaUl} 정복`); + attackerLogger.pushGlobalHistoryLog( + `【멸망】${defenderNationName}${defenderNationJosaUn} 멸망했습니다.` + ); + for (const general of defenderGenerals) { // Legacy Util::toInt truncates these losses rather than rounding. const loseGold = Math.trunc(general.gold * rng.nextRange(0.2, 0.5)); @@ -271,6 +279,11 @@ const resolveConquerCity = ( generalId: general.id, nationId: general.nationId, }); + generalLogger.pushGeneralActionLog( + `${defenderNationName}${defenderNationJosaYi} 멸망했습니다.`, + LogFormat.PLAIN + ); + generalLogger.pushGeneralHistoryLog(`${defenderNationName}${defenderNationJosaYi} 멸망`); generalLogger.pushGeneralActionLog( `도주하며 금${loseGold} 쌀${loseRice}을 분실했습니다.`, LogFormat.PLAIN @@ -285,6 +298,26 @@ const resolveConquerCity = ( attackerNation.gold = round(attackerNation.gold + collapseRewardGold); attackerNation.rice = round(attackerNation.rice + collapseRewardRice); + const resourceLog = + `${defenderNationName} 정복으로 ` + + `금${collapseRewardGold.toLocaleString('en-US')} ` + + `쌀${collapseRewardRice.toLocaleString('en-US')}을 획득했습니다.`; + for (const general of generals) { + if (general.nationId !== attackerNation.id || general.officerLevel < 5) { + continue; + } + if (general.id === attacker.id) { + attackerLogger.pushGeneralActionLog(resourceLog, LogFormat.PLAIN); + continue; + } + const chiefLogger = new ActionLogger({ + generalId: general.id, + nationId: attackerNation.id, + }); + chiefLogger.pushGeneralActionLog(resourceLog, LogFormat.PLAIN); + pushLoggers([chiefLogger], logs); + } + defenderNation.meta.collapsed = true; affectedNations.add(defenderNation); affectedNations.add(attackerNation); diff --git a/packages/logic/src/war/units/base.ts b/packages/logic/src/war/units/base.ts index 870dce4..b9bba73 100644 --- a/packages/logic/src/war/units/base.ts +++ b/packages/logic/src/war/units/base.ts @@ -36,6 +36,36 @@ const resolveNationVar = (nation: Nation | null, key: string): TriggerValue | nu } }; +const isCityWarUnit = (unit: WarUnit): boolean => + typeof (unit as WarUnit & { getCityId?: unknown }).getCityId === 'function'; + +const buildLegacySmallWarLog = (me: WarUnit, oppose: WarUnit): string => { + const warType = !me.isAttacker() ? 'defense' : isCityWarUnit(oppose) ? 'siege' : 'attack'; + const warTypeStr = me.isAttacker() ? '→' : '←'; + return [ + '
', + ' ', + ' ', + `${me.getCrewTypeShortName()} `, + '【', + `${me.getName()} `, + '', + `${me.getHP()}`, + '(', + `${-me.getDeadCurrentBattle()}) `, + `${warTypeStr} `, + ' ', + '', + `${oppose.getHP()}`, + '(', + `${-oppose.getDeadCurrentBattle()}) `, + ' ', + `${oppose.getCrewTypeShortName()} `, + '【', + `${oppose.getName()}
`, + ].join(''); +}; + // 전투 유닛 공통 상태와 수치 계산(legacy WarUnit 계열 포팅). export abstract class WarUnit { private static nextUnitId = 1; @@ -317,8 +347,7 @@ export abstract class WarUnit previousExpLevel + ? `Lv ${resolvedExpLevel}${josaRo} 레벨업!` + : `Lv ${resolvedExpLevel}${josaRo} 레벨다운!`, + LogFormat.PLAIN + ); } public addDedication(value: number): void { diff --git a/packages/logic/test/warAftermath.test.ts b/packages/logic/test/warAftermath.test.ts index db0ba84..67a1638 100644 --- a/packages/logic/test/warAftermath.test.ts +++ b/packages/logic/test/warAftermath.test.ts @@ -185,6 +185,7 @@ describe('war aftermath', () => { const defenderCity = buildCity(2, 2); defenderCity.meta.conflict = JSON.stringify({ 99: 200, 1: 100 }); const attacker = buildGeneral(1, 1, 1); + attacker.officerLevel = 12; const defender = buildGeneral(2, 2, 2); const outcome = resolveWarAftermath({ @@ -239,5 +240,13 @@ describe('war aftermath', () => { // must never receive ownership during a later conquest. expect(defenderCity.nationId).toBe(attackerNation.id); expect(defenderCity.meta.conflict).toBe('{}'); + expect(outcome.logs.map((log) => log.text)).toEqual( + expect.arrayContaining([ + 'Nation2를 정복', + '【멸망】Nation2멸망했습니다.', + 'Nation2멸망했습니다.', + 'Nation2 정복으로 금2,600 쌀3,600을 획득했습니다.', + ]) + ); }); }); diff --git a/packages/logic/test/warEngine.test.ts b/packages/logic/test/warEngine.test.ts index 185e045..4c4655b 100644 --- a/packages/logic/test/warEngine.test.ts +++ b/packages/logic/test/warEngine.test.ts @@ -171,6 +171,7 @@ describe('war triggers', () => { general.experience = 90; general.meta.explevel = 0; const crewType = new WarCrewType(buildUnitSet().crewTypes![0]!); + const logger = new ActionLogger({ generalId: 1, nationId: 1 }); const unit = new WarUnitGeneral( new RandUtil(new ConstantRNG(0)), buildConfig(), @@ -179,7 +180,7 @@ describe('war triggers', () => { buildNation(), true, crewType, - new ActionLogger({ generalId: 1, nationId: 1 }), + logger, new WarActionPipeline([ { onCalcStat: (_context, statName, value) => @@ -191,6 +192,11 @@ describe('war triggers', () => { unit.addLevelExp(10); expect(general.experience).toBe(102); expect(general.meta.explevel).toBe(1); + expect(logger.flush()).toContainEqual( + expect.objectContaining({ + text: 'Lv 1로 레벨업!', + }) + ); }); it('applies the legacy 90% dexterity gain for wizard and siege arms', () => { @@ -265,6 +271,52 @@ describe('war triggers', () => { expect(attacker.hasActivatedSkill('필살')).toBe(true); expect(attacker.getWarPowerMultiply()).toBeCloseTo(1.3); }); + + it('treats the first conflict nation as the baseline', () => { + const rng = new RandUtil(new ConstantRNG(0)); + const config = buildConfig(); + const city = buildCity(); + const cityUnit = new WarUnitCity( + rng, + config, + city, + buildNation(), + new WarCrewType(buildUnitSet().crewTypes![1]!), + new ActionLogger({}), + 200, + 180 + ); + const firstNation = buildNation(); + const firstAttacker = new WarUnitGeneral( + rng, + config, + buildGeneral(80), + city, + firstNation, + true, + new WarCrewType(buildUnitSet().crewTypes![0]!), + new ActionLogger({ generalId: 1, nationId: 1 }), + new WarActionPipeline([]) + ); + cityUnit.setOppose(firstAttacker); + expect(cityUnit.addConflict()).toBe(false); + + const secondNation = { ...buildNation(), id: 2 }; + const secondGeneral = { ...buildGeneral(80), id: 2, nationId: 2 }; + const secondAttacker = new WarUnitGeneral( + rng, + config, + secondGeneral, + city, + secondNation, + true, + new WarCrewType(buildUnitSet().crewTypes![0]!), + new ActionLogger({ generalId: 2, nationId: 2 }), + new WarActionPipeline([]) + ); + cityUnit.setOppose(secondAttacker); + expect(cityUnit.addConflict()).toBe(true); + }); }); describe('resolveWarBattle', () => {