fix: guard incomplete general lifespans

This commit is contained in:
2026-07-25 08:57:53 +00:00
parent c94c5011f4
commit a83f7a44e3
2 changed files with 43 additions and 12 deletions
+22 -12
View File
@@ -1159,22 +1159,32 @@ export const createReservedTurnHandler = async (options: {
)
);
currentCity = currentCity ? { ...currentCity, meta: { ...currentCity.meta } } : currentCity;
const cityGeneralCopies = new Map<number, TurnGeneral>();
for (const general of worldView?.listGenerals() ?? []) {
cityGeneralCopies.set(
general.id,
general.id === currentGeneral.id ? currentGeneral : cloneTurnGeneral(general)
);
}
cityGeneralCopies.set(currentGeneral.id, currentGeneral);
let cityGeneralCopies: Map<number, TurnGeneral> | undefined;
const getCityGeneralCopies = (): Map<number, TurnGeneral> => {
if (cityGeneralCopies) {
return cityGeneralCopies;
}
cityGeneralCopies = new Map<number, TurnGeneral>();
for (const general of worldView?.listGenerals() ?? []) {
if (general.cityId !== currentGeneral.cityId) {
continue;
}
cityGeneralCopies.set(
general.id,
general.id === currentGeneral.id ? currentGeneral : cloneTurnGeneral(general)
);
}
cityGeneralCopies.set(currentGeneral.id, currentGeneral);
return cityGeneralCopies;
};
const preTurnPipeline = new GeneralActionPipeline(env.generalActionModules ?? []);
const preTurnContext = createGeneralTriggerContext({
general: currentGeneral,
nation: currentNation,
worldView: {
listGenerals: () => Array.from(cityGeneralCopies.values()),
listGenerals: () => Array.from(getCityGeneralCopies().values()),
listGeneralsByCity: (cityId) =>
Array.from(cityGeneralCopies.values()).filter((general) => general.cityId === cityId),
Array.from(getCityGeneralCopies().values()).filter((general) => general.cityId === cityId),
},
rng: preprocessRng,
log: {
@@ -1209,7 +1219,7 @@ export const createReservedTurnHandler = async (options: {
}
preTurnContext.skill.activate('pre.병력군량소모');
}
for (const [generalId, next] of cityGeneralCopies) {
for (const [generalId, next] of cityGeneralCopies ?? []) {
if (generalId === currentGeneral.id) {
continue;
}
@@ -1399,7 +1409,7 @@ export const createReservedTurnHandler = async (options: {
let deleteGeneral = false;
const deletedTroopIds: number[] = [];
const lifecycleSnapshot = cloneTurnGeneral(currentGeneral);
if (currentGeneral.meta.killturn <= 0) {
if (currentGeneral.meta.killturn <= 0 && typeof currentGeneral.deadYear === 'number') {
if (
currentGeneral.npcState === 1 &&
typeof currentGeneral.deadYear === 'number' &&
@@ -272,6 +272,27 @@ describe('legacy general turn lifecycle', () => {
expect(harness.world.peekDirtyState().deletedGenerals).toContain(1);
});
it('keeps compatibility fixtures without legacy lifespan metadata alive', async () => {
const harness = await createTurnTestHarness({
snapshot: makeSnapshot([
makeGeneral({
deadYear: undefined,
npcState: 2,
meta: { killturn: 1 },
}),
]),
state: makeState(),
schedule,
map,
});
await harness.runOneTick();
expect(harness.world.getGeneralById(1)).not.toBeNull();
expect(harness.world.getGeneralById(1)!.meta.killturn).toBe(0);
expect(harness.world.peekDirtyState().lifecycleEvents[0]?.outcome).toBe('active');
});
it('retires a player general and resets inherited stats and rank state', async () => {
const harness = await createTurnTestHarness({
snapshot: makeSnapshot([