fix: align scenario 2400 battle replay parity

This commit is contained in:
2026-08-05 03:08:12 +00:00
parent bbb6de6971
commit 0dc860feee
12 changed files with 668 additions and 51 deletions
+17 -10
View File
@@ -134,11 +134,14 @@ const mapCityPayload = (payload: BattleSimJobPayload['attackerCity']): City => (
},
});
const mapGeneralPayload = (payload: BattleSimJobPayload['attackerGeneral']): General => ({
const mapGeneralPayload = (
payload: BattleSimJobPayload['attackerGeneral'],
currentCityId: number
): General => ({
id: payload.no,
name: payload.name,
nationId: payload.nation,
cityId: payload.officer_city,
cityId: payload.city ?? currentCityId,
troopId: 0,
stats: {
leadership: payload.leadership,
@@ -184,10 +187,12 @@ const mapGeneralPayload = (payload: BattleSimJobPayload['attackerGeneral']): Gen
dex3: payload.dex3,
dex4: payload.dex4,
dex5: payload.dex5,
intelExp: payload.intel_exp,
strengthExp: payload.strength_exp,
leadershipExp: payload.leadership_exp,
defenceTrain: payload.defence_train,
intel_exp: payload.intel_exp,
strength_exp: payload.strength_exp,
leadership_exp: payload.leadership_exp,
defence_train: payload.defence_train,
officerCity: payload.officer_city,
officer_city: payload.officer_city,
rank_warnum: payload.warnum,
rank_killnum: payload.killnum,
rank_killcrew: payload.killcrew,
@@ -304,8 +309,8 @@ const resolveDefenderOrderPayload = (payload: BattleSimJobPayload): number[] =>
const defenderNation = mapNationPayload(payload.defenderNation);
const attackerCity = mapCityPayload(payload.attackerCity);
const defenderCity = mapCityPayload(payload.defenderCity);
const attacker = mapGeneralPayload(payload.attackerGeneral);
const defenders = payload.defenderGenerals.map(mapGeneralPayload);
const attacker = mapGeneralPayload(payload.attackerGeneral, attackerCity.id);
const defenders = payload.defenderGenerals.map((general) => mapGeneralPayload(general, defenderCity.id));
const warActionModules = buildWarActionModules(payload.unitSet, payload.scenarioEffect);
return resolveDefenderOrder({
@@ -376,8 +381,10 @@ export const processBattleSimJob = (
const defenderNation = mapNationPayload(payload.defenderNation);
const attackerCity = mapCityPayload(payload.attackerCity);
const defenderCity = mapCityPayload(payload.defenderCity);
const attackerGeneral = mapGeneralPayload(payload.attackerGeneral);
const defenderGenerals = payload.defenderGenerals.map(mapGeneralPayload);
const attackerGeneral = mapGeneralPayload(payload.attackerGeneral, attackerCity.id);
const defenderGenerals = payload.defenderGenerals.map((general) =>
mapGeneralPayload(general, defenderCity.id)
);
const initialRice = new Map<number, number>();
initialRice.set(attackerGeneral.id, attackerGeneral.rice);
+1
View File
@@ -6,6 +6,7 @@ export const zBattleSimGeneral = z.object({
no: z.number().int().positive(),
name: z.string().min(1),
nation: z.number().int().positive(),
city: z.number().int().min(0).optional(),
turntime: z.string().min(1),
personal: z.string().nullable(),
special: z.string().nullable().optional(),
+2
View File
@@ -8,6 +8,8 @@ export interface BattleSimGeneralPayload {
no: number;
name: string;
nation: number;
/** Current city. Older clients omit this; the surrounding city payload is authoritative then. */
city?: number;
turntime: string;
personal: string | null;
special?: string | null;
+27 -3
View File
@@ -237,9 +237,9 @@ describe('battle sim processor', () => {
datetime: '2026-01-01 00:00:00',
avgWar: 1,
phase: 2,
killed: 625,
maxKilled: 625,
minKilled: 625,
killed: 626,
maxKilled: 626,
minKilled: 626,
dead: 1000,
maxDead: 1000,
minDead: 1000,
@@ -267,6 +267,30 @@ describe('battle sim processor', () => {
expect(result.order).toEqual([2]);
});
it('uses the legacy defence training threshold when ordering defenders', () => {
const payload = buildPayload('reorder');
payload.defenderGenerals[0]!.defence_train = 101;
const result = processBattleSimJob(payload);
expect(result.result).toBe(true);
expect(result.order).toEqual([]);
});
it('keeps current city separate from the officer assignment city', () => {
const localOfficer = buildPayload('battle');
localOfficer.attackerGeneral.city = localOfficer.attackerCity.city;
const remoteOfficer = buildPayload('battle');
remoteOfficer.attackerGeneral.city = remoteOfficer.attackerCity.city;
remoteOfficer.attackerGeneral.officer_city = 99;
const local = processBattleSimJob(localOfficer);
const remote = processBattleSimJob(remoteOfficer);
expect(local.killed).toBeGreaterThan(remote.killed ?? 0);
expect(local).not.toEqual(remote);
});
it('executes crew trigger handlers in simulator battles', () => {
const payload = buildPayload('battle');
payload.unitSet.crewTypes![0]!.phaseSkillTrigger = ['che_선제사격시도', 'che_선제사격발동'];