fix: 커맨드 차등 생명주기와 로그 그래프를 보강

장수 55종과 수뇌 35종의 상태, 로그, 메시지, 예약 턴 비교를 닫습니다.

실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
This commit is contained in:
2026-08-23 21:48:29 +00:00
parent 85591c68ad
commit c63a49bd07
128 changed files with 8615 additions and 848 deletions
+87 -3
View File
@@ -8,9 +8,12 @@ import type { UnitSetDefinition } from '../src/world/types.js';
import { resolveWarAftermath } from '../src/war/aftermath.js';
import type { WarAftermathConfig } from '../src/war/types.js';
import { LogFormat } from '../src/logging/types.js';
import { LegacyWarLogFlushSequence } from '../src/war/legacyFlushSequence.js';
import { buildWarAftermathConfig, buildWarConfig } from '../src/actions/turn/actionContextHelpers.js';
import type { ScenarioConfig } from '../src/scenario/types.js';
const MESSAGE_TIME = new Date('0185-01-01T00:00:00.000Z');
const buildUnitSet = (): UnitSetDefinition => ({
id: 'test',
name: 'test',
@@ -188,6 +191,7 @@ describe('war aftermath', () => {
unitSet: buildUnitSet(),
config: { ...buildConfig(), maxTechLevel: 15 },
time: { year: 200, month: 1, startYear: 180 },
messageTime: MESSAGE_TIME,
});
expect(defenderNation.rice).toBe(985);
@@ -232,6 +236,7 @@ describe('war aftermath', () => {
month: 1,
startYear: 180,
},
messageTime: MESSAGE_TIME,
});
expect(attackerNation.meta.tech).toBe(Math.fround(1000.6));
@@ -271,6 +276,7 @@ describe('war aftermath', () => {
unitSet: buildUnitSet(),
config: buildConfig(),
time: { year: 200, month: 1, startYear: 180 },
messageTime: MESSAGE_TIME,
});
expect(attackerCity.meta.dead).toBe(71);
@@ -320,6 +326,7 @@ describe('war aftermath', () => {
month: 1,
startYear: 180,
},
messageTime: MESSAGE_TIME,
});
expect(defenderNation.capitalCityId).toBe(nextCapital.id);
@@ -330,6 +337,7 @@ describe('war aftermath', () => {
'수뇌는 <G><b>City3</b></>으로 집합되었습니다.',
])
);
expect(outcome.logs.find((log) => log.text.startsWith('수뇌는'))?.format).toBe(LogFormat.MONTH);
});
it('uses the city battle phase, not retained casualties, for conquered supply-city rice', () => {
@@ -372,6 +380,7 @@ describe('war aftermath', () => {
unitSet: buildUnitSet(),
config: buildConfig(),
time: { year: 200, month: 1, startYear: 180 },
messageTime: MESSAGE_TIME,
});
expect(defenderNation.rice).toBe(6500);
@@ -389,6 +398,11 @@ describe('war aftermath', () => {
const attacker = buildGeneral(1, 1, 1);
attacker.officerLevel = 12;
const defender = buildGeneral(2, 2, 2);
defender.officerLevel = 12;
defender.experience = 2_000;
defender.dedication = 2_000;
defender.meta.explevel = 14;
defender.meta.dedlevel = 3;
const outcome = resolveWarAftermath({
battle: {
@@ -430,14 +444,16 @@ describe('war aftermath', () => {
month: 1,
startYear: 180,
},
messageTime: MESSAGE_TIME,
rng,
legacyFlushSequence: new LegacyWarLogFlushSequence(100),
});
expect(outcome.conquest?.nationCollapsed).toBe(true);
expect(attackerNation.gold).toBe(3600);
expect(attackerNation.rice).toBe(4600);
expect(defender.experience).toBe(90);
expect(defender.dedication).toBe(50);
expect(defender.experience).toBe(1_800);
expect(defender.dedication).toBe(1_000);
// Removed nations can remain in old persisted conflict data. They
// must never receive ownership during a later conquest.
expect(defenderCity.nationId).toBe(attackerNation.id);
@@ -447,9 +463,14 @@ describe('war aftermath', () => {
'<D><b>Nation2</b></>를 정복',
'<R><b>【멸망】</b></><D><b>Nation2</b></>는 <R>멸망</>했습니다.',
'<D><b>Nation2</b></>가 <R>멸망</>했습니다.',
'<C>Lv 13</>으로 <R>레벨다운</>!',
'<Y>27품관</>으로 <C>승급</>하여 봉록이 <C>1,200</>으로 <C>상승</>했습니다!',
'<D><b>Nation2</b></> 정복으로 금<C>2,600</> 쌀<C>3,600</>을 획득했습니다.',
])
);
expect(outcome.logs.filter((log) => log.generalId === defender.id).map((log) => log.legacyFlushGroup)).toEqual([
103, 103, 103, 103, 103,
]);
});
it('matches ruined-nation lord ordering and NPC appointment draws', () => {
@@ -464,7 +485,7 @@ describe('war aftermath', () => {
npc.npcState = 2;
const rangeDraws = [0.2, 0.21, 0.4, 0.41];
const nextBool = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true).mockReturnValueOnce(false);
const nextBool = vi.fn().mockReturnValueOnce(true).mockReturnValueOnce(true).mockReturnValueOnce(false);
const rng = {
nextRange: vi.fn(() => rangeDraws.shift()!),
nextBool,
@@ -495,6 +516,8 @@ describe('war aftermath', () => {
joinRuinedNpcProbability: 0.1,
},
time: { year: 186, month: 1, startYear: 179 },
messageTime: MESSAGE_TIME,
messageSharedIconBaseUrl: 'https://ref.example/image/icons',
rng,
});
@@ -503,6 +526,32 @@ describe('war aftermath', () => {
expect(lord.gold).toBe(600);
expect(lord.rice).toBe(590);
expect(nextBool.mock.calls.map(([probability]) => probability)).toEqual([0.5, 0.1, 0.5]);
expect(outcome.conquest?.messages).toEqual([
{
msgType: 'private',
src: {
generalId: attacker.id,
generalName: attacker.name,
nationId: attackerNation.id,
nationName: attackerNation.name,
color: attackerNation.color,
icon: 'https://ref.example/image/icons/default.jpg',
},
dest: {
generalId: npc.id,
generalName: npc.name,
nationId: 0,
nationName: '재야',
color: '#000000',
icon: 'https://ref.example/image/icons/default.jpg',
},
text: 'Nation1로 망명 권유 서신',
time: MESSAGE_TIME,
validUntil: new Date('9999-12-31T12:59:59.000Z'),
option: { action: 'scout' },
sendDestOnly: true,
},
]);
expect(outcome.conquest?.ruinedNpcJoinPlans).toEqual([
{ generalId: npc.id, destNationId: attackerNation.id, joinTurn: 6 },
]);
@@ -523,10 +572,12 @@ describe('war aftermath', () => {
const attackerCity = buildCity(1, 1);
const defenderCity = buildCity(2, 2);
const attacker = buildGeneral(1, 1, 1);
attacker.officerLevel = 12;
const firstDefender = buildGeneral(2, 2, 2);
const secondDefender = buildGeneral(3, 2, 2);
secondDefender.crew = 0;
const elsewhere = buildGeneral(4, 2, 3);
elsewhere.officerLevel = 12;
const dispatchOrder: number[] = [];
const module: GeneralActionModule = {
eventHandlers: {
@@ -562,8 +613,10 @@ describe('war aftermath', () => {
month: 1,
startYear: 180,
},
messageTime: MESSAGE_TIME,
rng,
generalActionModules: [module],
legacyFlushSequence: new LegacyWarLogFlushSequence(200),
});
expect(dispatchOrder).toEqual([firstDefender.id, secondDefender.id]);
@@ -585,6 +638,27 @@ describe('war aftermath', () => {
LogFormat.MONTH,
LogFormat.MONTH,
]);
expect(
outcome.logs.filter((log) => log.text.startsWith('점령 이벤트')).map((log) => log.legacyFlushGroup)
).toEqual([200, 201]);
expect(
outcome.logs.find((log) => log.nationId === defenderNation.id && log.text.includes('<O>함락</>'))
?.legacyFlushGroup
).toBe(202);
expect(
outcome.logs.find((log) => log.text.includes(`Nation${defenderNation.id}</b></>를 정복`))?.legacyFlushGroup
).toBe(203);
expect(
outcome.logs
.filter((log) => log.text.includes('도주하며'))
.map((log) => [log.generalId, log.legacyFlushGroup])
).toEqual([
[firstDefender.id, 204],
[secondDefender.id, 205],
[elsewhere.id, 206],
]);
expect(outcome.logs.find((log) => log.text.includes('【멸망】'))?.legacyFlushGroup).toBe(206);
expect(outcome.logs.find((log) => log.text.includes('정복으로 금'))?.legacyFlushGroup).toBeUndefined();
expect(outcome.generals.map((general) => general.id)).toEqual(
expect.arrayContaining([firstDefender.id, secondDefender.id])
);
@@ -626,11 +700,21 @@ describe('war aftermath', () => {
month: 1,
startYear: 180,
},
messageTime: MESSAGE_TIME,
legacyFlushSequence: new LegacyWarLogFlushSequence(300),
});
expect(outcome.conquest?.conquerNationId).toBe(4);
expect(defenderCity.nationId).toBe(4);
expect(defenderCity.meta.conflict_order).toEqual([]);
expect(attacker.cityId).toBe(1);
expect(outcome.logs.find((log) => log.nationId === defenderNation.id)?.legacyFlushGroup).toBe(300);
expect(outcome.logs.find((log) => log.nationId === firstNation.id)?.legacyFlushGroup).toBe(301);
expect(
outcome.logs
.filter((log) => log.generalId === attacker.id || log.scope === 'SYSTEM')
.filter((log) => log.text.includes('영토분쟁') || log.text.includes('점령'))
.every((log) => log.legacyFlushGroup === undefined)
).toBe(true);
});
});