fix war command differential parity

This commit is contained in:
2026-07-26 22:26:09 +00:00
parent 1011822463
commit 789b9c8c8f
9 changed files with 69 additions and 52 deletions
+16 -22
View File
@@ -13,10 +13,9 @@ import type {
WarAftermathTechContext,
WarDiplomacyDelta,
} from './types.js';
import { clamp, clampMin, getMetaNumber, round, simpleSerialize } from './utils.js';
import { clamp, clampMin, getMetaNumber, parseConflict, round, simpleSerialize } from './utils.js';
const META_DEAD = 'dead';
const META_CONFLICT = 'conflict';
const MAX_EXP_LEVEL = 255;
const MAX_DEDICATION_LEVEL = 30;
@@ -120,29 +119,24 @@ const applyNationTechGain = <TriggerState extends GeneralTriggerState>(
};
const resolveConquerNation = (city: City, attackerNationId: number, nations: Nation[]): number => {
const rawConflict = city.meta[META_CONFLICT];
if (!rawConflict) {
const conflict = parseConflict(city.conflict);
if (!conflict) {
return attackerNationId;
}
try {
const parsed = JSON.parse(String(rawConflict)) as Record<string, number>;
const activeNationIds = new Set(nations.map((nation) => nation.id));
const entries = Object.entries(parsed)
.map(([key, value]) => [Number(key), value] as const)
.filter(
([key, value]) =>
Number.isFinite(key) &&
typeof value === 'number' &&
(key === attackerNationId || activeNationIds.has(key))
)
.sort(([, lhs], [, rhs]) => rhs - lhs);
if (!entries.length) {
return attackerNationId;
}
return entries[0]![0];
} catch {
const activeNationIds = new Set(nations.map((nation) => nation.id));
const entries = Object.entries(conflict)
.map(([key, value]) => [Number(key), value] as const)
.filter(
([key, value]) =>
Number.isFinite(key) &&
typeof value === 'number' &&
(key === attackerNationId || activeNationIds.has(key))
)
.sort(([, lhs], [, rhs]) => rhs - lhs);
if (!entries.length) {
return attackerNationId;
}
return entries[0]![0];
};
const getCityPosition = (city: City): { x: number; y: number } | null => {
@@ -383,7 +377,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
defenderCity.commerce = round(defenderCity.commerce * 0.7);
defenderCity.security = round(defenderCity.security * 0.7);
defenderCity.nationId = conquerNationId;
defenderCity.meta[META_CONFLICT] = '{}';
defenderCity.conflict = {};
if (defenderCity.level > 3) {
defenderCity.defence = config.defaultCityWall;
+3 -3
View File
@@ -4,7 +4,7 @@ import type { City, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
import type { WarEngineConfig } from '../types.js';
import type { WarCrewType } from '../crewType.js';
import { clampMin, round, parseConflict, sortConflict, stringifyConflict } from '../utils.js';
import { clampMin, round, parseConflict, sortConflict } from '../utils.js';
import { WarUnit } from './base.js';
// 도시 성벽 전투 유닛(legacy WarUnitCity 포팅).
@@ -133,7 +133,7 @@ export class WarUnitCity extends WarUnit {
}
public addConflict(): boolean {
const raw = this.city.meta.conflict;
const raw = this.city.conflict;
const conflict = parseConflict(raw) ?? {};
const oppose = this.getOppose();
@@ -161,7 +161,7 @@ export class WarUnitCity extends WarUnit {
}
const sorted = sortConflict(conflict);
this.city.meta.conflict = stringifyConflict(sorted);
this.city.conflict = sorted;
return isNew;
}
+14 -11
View File
@@ -55,23 +55,26 @@ export const parseConflict = (raw: TriggerValue | undefined): Record<number, num
if (raw === undefined || raw === null) {
return null;
}
let parsed: unknown = raw;
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw) as Record<string, number>;
const result: Record<number, number> = {};
for (const [key, value] of Object.entries(parsed)) {
const id = Number(key);
if (!Number.isFinite(id) || typeof value !== 'number') {
continue;
}
result[id] = value;
}
return Object.keys(result).length ? result : null;
parsed = JSON.parse(raw) as unknown;
} catch {
return null;
}
}
return null;
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
return null;
}
const result: Record<number, number> = {};
for (const [key, value] of Object.entries(parsed)) {
const id = Number(key);
if (!Number.isFinite(id) || typeof value !== 'number') {
continue;
}
result[id] = value;
}
return Object.keys(result).length ? result : null;
};
export const stringifyConflict = (conflict: Record<number, number> | null): string => {
+3 -3
View File
@@ -183,7 +183,7 @@ describe('war aftermath', () => {
defenderNation.rice = 6000;
const attackerCity = buildCity(1, 1);
const defenderCity = buildCity(2, 2);
defenderCity.meta.conflict = JSON.stringify({ 99: 200, 1: 100 });
defenderCity.conflict = { 99: 200, 1: 100 };
const attacker = buildGeneral(1, 1, 1);
attacker.officerLevel = 12;
const defender = buildGeneral(2, 2, 2);
@@ -236,10 +236,10 @@ describe('war aftermath', () => {
expect(attackerNation.rice).toBe(4600);
expect(defender.experience).toBe(90);
expect(defender.dedication).toBe(50);
// Removed nations can remain in old persisted conflict metadata. They
// 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.meta.conflict).toBe('{}');
expect(defenderCity.conflict).toEqual({});
expect(outcome.logs.map((log) => log.text)).toEqual(
expect.arrayContaining([
'<D><b>Nation2</b></>를 정복',
+3
View File
@@ -300,6 +300,8 @@ describe('war triggers', () => {
);
cityUnit.setOppose(firstAttacker);
expect(cityUnit.addConflict()).toBe(false);
expect(city.conflict).toEqual({ 1: 1.05 });
expect(city.meta.conflict).toBeUndefined();
const secondNation = { ...buildNation(), id: 2 };
const secondGeneral = { ...buildGeneral(80), id: 2, nationId: 2 };
@@ -316,6 +318,7 @@ describe('war triggers', () => {
);
cityUnit.setOppose(secondAttacker);
expect(cityUnit.addConflict()).toBe(true);
expect(city.conflict).toEqual({ 1: 1.05, 2: 1 });
});
});