fix tied conflict conquest order

This commit is contained in:
2026-07-26 23:14:27 +00:00
parent 5a9b85e821
commit fc019a786c
7 changed files with 128 additions and 20 deletions
+15 -10
View File
@@ -13,7 +13,16 @@ import type {
WarAftermathTechContext,
WarDiplomacyDelta,
} from './types.js';
import { clamp, clampMin, getMetaNumber, parseConflict, round, simpleSerialize } from './utils.js';
import {
clamp,
clampMin,
getMetaNumber,
parseConflict,
readConflictOrder,
round,
simpleSerialize,
sortConflictEntries,
} from './utils.js';
const META_DEAD = 'dead';
const MAX_EXP_LEVEL = 255;
@@ -124,15 +133,10 @@ const resolveConquerNation = (city: City, attackerNationId: number, nations: Nat
return attackerNationId;
}
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);
const entries = sortConflictEntries(conflict, readConflictOrder(city.meta)).filter(
([key, value]) =>
Number.isFinite(key) && typeof value === 'number' && (key === attackerNationId || activeNationIds.has(key))
);
if (!entries.length) {
return attackerNationId;
}
@@ -396,6 +400,7 @@ const resolveConquerCity = <TriggerState extends GeneralTriggerState>(
defenderCity.security = round(defenderCity.security * 0.7);
defenderCity.nationId = conquerNationId;
defenderCity.conflict = {};
defenderCity.meta.conflict_order = [];
if (defenderCity.level > 3) {
defenderCity.defence = config.defaultCityWall;
+11 -2
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 } from '../utils.js';
import { clampMin, parseConflict, readConflictOrder, round, sortConflict, sortConflictEntries } from '../utils.js';
import { WarUnit } from './base.js';
// 도시 성벽 전투 유닛(legacy WarUnitCity 포팅).
@@ -144,6 +144,12 @@ export class WarUnitCity extends WarUnit {
let dead = Math.max(1, this.dead);
let isNew = false;
const conflictOrder = readConflictOrder(this.city.meta).filter((id) => conflict[id] !== undefined);
for (const id of Object.keys(conflict).map(Number)) {
if (!conflictOrder.includes(id)) {
conflictOrder.push(id);
}
}
if (Object.keys(conflict).length === 0 || this.getHP() === 0) {
dead *= 1.05;
@@ -157,10 +163,13 @@ export class WarUnitCity extends WarUnit {
conflict[nationId] += dead;
} else {
conflict[nationId] = dead;
conflictOrder.push(nationId);
isNew = true;
}
const sorted = sortConflict(conflict);
const sortedEntries = sortConflictEntries(conflict, conflictOrder);
this.city.meta.conflict_order = sortedEntries.map(([id]) => id);
const sorted = sortConflict(conflict, conflictOrder);
this.city.conflict = sorted;
return isNew;
+30 -7
View File
@@ -77,6 +77,31 @@ export const parseConflict = (raw: TriggerValue | undefined): Record<number, num
return Object.keys(result).length ? result : null;
};
export const readConflictOrder = (meta: Record<string, TriggerValue>): number[] => {
const raw = meta.conflict_order;
if (!Array.isArray(raw)) {
return [];
}
return raw.filter((value): value is number => typeof value === 'number' && Number.isInteger(value) && value > 0);
};
export const sortConflictEntries = (
conflict: Record<number, number>,
preferredOrder: number[] = []
): Array<readonly [number, number]> => {
const orderIndex = new Map(preferredOrder.map((nationId, index) => [nationId, index]));
return Object.entries(conflict)
.map(([key, value], index) => [Number(key), value, index] as const)
.filter(([key, value]) => Number.isFinite(key) && typeof value === 'number')
.sort(
([lhsKey, lhsValue, lhsIndex], [rhsKey, rhsValue, rhsIndex]) =>
rhsValue - lhsValue ||
(orderIndex.get(lhsKey) ?? preferredOrder.length + lhsIndex) -
(orderIndex.get(rhsKey) ?? preferredOrder.length + rhsIndex)
)
.map(([key, value]) => [key, value] as const);
};
export const stringifyConflict = (conflict: Record<number, number> | null): string => {
if (!conflict) {
return '{}';
@@ -94,14 +119,12 @@ export const stringifyConflict = (conflict: Record<number, number> | null): stri
return JSON.stringify(ordered);
};
export const sortConflict = (conflict: Record<number, number>): Record<number, number> => {
export const sortConflict = (
conflict: Record<number, number>,
preferredOrder: number[] = []
): Record<number, number> => {
const ordered: Record<number, number> = {};
const entries = Object.entries(conflict)
.map(([key, value]) => [Number(key), value] as const)
.filter(([key, value]) => Number.isFinite(key) && typeof value === 'number')
.sort(([, lhs], [, rhs]) => rhs - lhs);
for (const [key, value] of entries) {
for (const [key, value] of sortConflictEntries(conflict, preferredOrder)) {
ordered[key] = value;
}
+44
View File
@@ -304,4 +304,48 @@ describe('war aftermath', () => {
])
);
});
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);
});
});
+2
View File
@@ -301,6 +301,7 @@ describe('war triggers', () => {
cityUnit.setOppose(firstAttacker);
expect(cityUnit.addConflict()).toBe(false);
expect(city.conflict).toEqual({ 1: 1.05 });
expect(city.meta.conflict_order).toEqual([1]);
expect(city.meta.conflict).toBeUndefined();
const secondNation = { ...buildNation(), id: 2 };
@@ -319,6 +320,7 @@ describe('war triggers', () => {
cityUnit.setOppose(secondAttacker);
expect(cityUnit.addConflict()).toBe(true);
expect(city.conflict).toEqual({ 1: 1.05, 2: 1 });
expect(city.meta.conflict_order).toEqual([1, 2]);
});
});