import { describe, expect, it } from 'vitest'; import { ConstantRNG, RandUtil } from '@sammo-ts/common'; import type { City, General, Nation } from '../src/domain/entities.js'; import type { UnitSetDefinition } from '../src/world/types.js'; import { resolveWarAftermath } from '../src/war/aftermath.js'; import type { WarAftermathConfig } from '../src/war/types.js'; const buildUnitSet = (): UnitSetDefinition => ({ id: 'test', name: 'test', crewTypes: [ { id: 999, armType: 9, name: '성벽', attack: 0, defence: 0, speed: 1, avoid: 0, magicCoef: 0, cost: 0, rice: 10, requirements: [], attackCoef: {}, defenceCoef: {}, info: [], initSkillTrigger: null, phaseSkillTrigger: null, iActionList: null, }, ], }); const buildConfig = (): WarAftermathConfig => ({ initialNationGenLimit: 1, techLevelIncYear: 5, initialAllowedTechLevel: 1, maxTechLevel: 12, defaultCityWall: 1000, baseGold: 0, baseRice: 0, castleCrewTypeId: 999, }); const buildCity = (id: number, nationId: number): City => ({ id, name: `City${id}`, nationId, level: 2, state: 0, population: 10000, populationMax: 10000, agriculture: 1000, agricultureMax: 1000, commerce: 1000, commerceMax: 1000, security: 1000, securityMax: 1000, defence: 100, defenceMax: 200, supplyState: 1, frontState: 0, wall: 100, wallMax: 200, meta: {}, }); const buildNation = (id: number): Nation => ({ id, name: `Nation${id}`, color: '#000000', capitalCityId: id, chiefGeneralId: null, gold: 1000, rice: 1000, power: 0, level: 1, typeCode: 'test', meta: { tech: 1000, }, }); const buildGeneral = (id: number, nationId: number, cityId: number): General => ({ id, name: `General${id}`, nationId, cityId, troopId: 0, stats: { leadership: 70, strength: 70, intelligence: 70, }, experience: 100, dedication: 100, officerLevel: 3, role: { personality: null, specialDomestic: null, specialWar: null, items: { horse: null, weapon: null, book: null, item: null, }, }, injury: 0, gold: 1000, rice: 1000, crew: 1000, crewTypeId: 999, train: 80, atmos: 80, age: 20, npcState: 0, triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {}, }, meta: { killturn: 24 }, }); describe('war aftermath', () => { it('updates tech and diplomacy deltas', () => { const attackerNation = buildNation(1); const defenderNation = buildNation(2); const attackerCity = buildCity(1, 1); const defenderCity = buildCity(2, 2); const attacker = buildGeneral(1, 1, 1); const outcome = resolveWarAftermath({ battle: { attacker, defenders: [], defenderCity, logs: [], conquered: false, reports: [ { id: attacker.id, type: 'general', name: attacker.name, isAttacker: true, killed: 100, dead: 50, }, ], }, attackerNation, defenderNation, attackerCity, defenderCity, nations: [attackerNation, defenderNation], cities: [attackerCity, defenderCity], generals: [attacker], unitSet: buildUnitSet(), config: buildConfig(), time: { year: 200, month: 1, startYear: 180, }, }); expect(attackerNation.meta.tech).toBe(1000.6); expect(defenderNation.meta.tech).toBe(1000.9); expect(outcome.diplomacyDeltas).toHaveLength(2); expect(attackerCity.meta.dead).toBe(60); 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); const defenderNation = buildNation(2); defenderNation.gold = 5000; defenderNation.rice = 6000; const attackerCity = buildCity(1, 1); const defenderCity = buildCity(2, 2); defenderCity.conflict = { 99: 200, 1: 100 }; const attacker = buildGeneral(1, 1, 1); attacker.officerLevel = 12; const defender = buildGeneral(2, 2, 2); const outcome = resolveWarAftermath({ battle: { attacker, defenders: [defender], defenderCity, logs: [], conquered: true, reports: [ { id: attacker.id, type: 'general', name: attacker.name, isAttacker: true, killed: 10, dead: 5, }, { id: defenderCity.id, type: 'city', name: defenderCity.name, isAttacker: false, killed: 0, dead: 0, }, ], }, attackerNation, defenderNation, attackerCity, defenderCity, nations: [attackerNation, defenderNation], cities: [attackerCity, defenderCity], generals: [attacker, defender], unitSet: buildUnitSet(), config: buildConfig(), time: { year: 200, month: 1, startYear: 180, }, rng, }); 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); // Removed nations can remain in old persisted conflict data. They // must never receive ownership during a later conquest. expect(defenderCity.nationId).toBe(attackerNation.id); expect(defenderCity.conflict).toEqual({}); expect(outcome.logs.map((log) => log.text)).toEqual( expect.arrayContaining([ 'Nation2를 정복', '【멸망】Nation2멸망했습니다.', 'Nation2멸망했습니다.', 'Nation2 정복으로 금2,600 쌀3,600을 획득했습니다.', ]) ); }); it('preserves the first contributor when conflict values are tied', () => { const attackerNation = buildNation(1); const defenderNation = buildNation(2); defenderNation.capitalCityId = 5; const laterNation = buildNation(3); const firstNation = buildNation(4); const attackerCity = buildCity(1, 1); const defenderCity = buildCity(2, 2); defenderCity.conflict = { 3: 100, 4: 100 }; defenderCity.meta.conflict_order = [4, 3]; const defenderCapital = buildCity(5, 2); const attacker = buildGeneral(1, 1, 1); const outcome = resolveWarAftermath({ battle: { attacker, defenders: [], defenderCity, logs: [], conquered: true, reports: [], }, attackerNation, defenderNation, attackerCity, defenderCity, nations: [attackerNation, defenderNation, laterNation, firstNation], cities: [attackerCity, defenderCity, defenderCapital], generals: [attacker], unitSet: buildUnitSet(), config: buildConfig(), time: { year: 200, month: 1, startYear: 180, }, }); expect(outcome.conquest?.conquerNationId).toBe(4); expect(defenderCity.nationId).toBe(4); expect(defenderCity.meta.conflict_order).toEqual([]); expect(attacker.cityId).toBe(1); }); });