feat: connect Xuzhou equipment to battle outcomes

This commit is contained in:
2026-07-27 15:39:11 +09:00
parent adf9e65668
commit 2737354e30
13 changed files with 5664 additions and 68 deletions

View File

@@ -16,6 +16,9 @@ try {
thirdCampPreparationSelectionRecordId,
thirdCampPreparationTargetBattleId
} = await server.ssrLoadModule('/src/game/data/thirdCampPreparation.ts');
const {
cityEquipmentPreparationSelectionRecordId
} = await server.ssrLoadModule('/src/game/data/cityEquipmentPreparation.ts');
const options = {
expectedBattleId: 'first-battle-zhuo-commandery',
@@ -406,6 +409,227 @@ try {
parsedLegacyFourthBattleState?.thirdCampPreparation === undefined,
'Expected legacy fourth-battle saves without preparation progress to remain valid.'
);
const eighthBattleId = 'eighth-battle-xiaopei-supply-road';
const eighthBattleOptions = {
...options,
expectedBattleId: eighthBattleId,
validEquipmentItemIds: new Set([
...options.validEquipmentItemIds,
'iron-spear'
]),
validEquipmentSlotItems: {
...options.validEquipmentSlotItems,
weapon: new Set(['training-sword', 'iron-spear'])
}
};
const eighthBattleUnits = validState.units.map((unit) => ({
...unit,
stats: unit.stats ? { ...unit.stats } : undefined,
equipment: {
weapon: {
...(unit.id === 'liu-bei'
? { itemId: 'iron-spear', level: 2, exp: 17 }
: unit.equipment.weapon)
},
armor: { ...unit.equipment.armor },
accessory: { ...unit.equipment.accessory }
}
}));
const validCityEquipmentContribution = {
version: 1,
selectionRecordId: cityEquipmentPreparationSelectionRecordId,
cityStayId: 'xuzhou',
sourceBattleId: 'seventh-battle-xuzhou-rescue',
targetBattleId: eighthBattleId,
offerId: 'city-xuzhou-iron-spear',
itemId: 'iron-spear',
slot: 'weapon',
price: 620,
purchaseNumber: 2,
unitId: 'liu-bei',
previousItemId: 'training-sword',
deployed: true,
offensiveActions: 3,
defensiveHits: 0,
bonusDamage: 7,
preventedDamage: 0
};
const eighthBattleBaseState = {
...validState,
battleId: eighthBattleId,
campaignStep: 'eighth-battle',
units: eighthBattleUnits,
enemyUsableUseKeys: []
};
const validCityEquipmentState = {
...eighthBattleBaseState,
cityEquipmentContribution: validCityEquipmentContribution
};
const parsedCityEquipmentState = parseBattleSaveState(
JSON.stringify(validCityEquipmentState),
eighthBattleOptions
);
assert(
isValidBattleSaveState(
validCityEquipmentState,
eighthBattleOptions
) &&
JSON.stringify(
parsedCityEquipmentState?.cityEquipmentContribution
) === JSON.stringify(validCityEquipmentContribution) &&
parsedCityEquipmentState?.cityEquipmentContribution !==
validCityEquipmentContribution,
'Expected a deployed city-purchase contribution to round-trip as a deep-cloned battle-save field.'
);
const unsafeCityEquipmentState = {
...validCityEquipmentState,
cityEquipmentContribution: {
...validCityEquipmentContribution,
offensiveActions: 2.9,
defensiveHits: -4,
bonusDamage: Number.MAX_SAFE_INTEGER,
preventedDamage: 18
}
};
const normalizedUnsafeCityEquipment = parseBattleSaveState(
JSON.stringify(unsafeCityEquipmentState),
eighthBattleOptions
)?.cityEquipmentContribution;
assert(
normalizedUnsafeCityEquipment?.offensiveActions === 2 &&
normalizedUnsafeCityEquipment.defensiveHits === 0 &&
normalizedUnsafeCityEquipment.bonusDamage === 999999 &&
normalizedUnsafeCityEquipment.preventedDamage === 0 &&
!isValidBattleSaveState(
unsafeCityEquipmentState,
eighthBattleOptions
),
`Expected unsafe city-equipment counters to normalize safely and fail strict direct validation: ${JSON.stringify(normalizedUnsafeCityEquipment)}`
);
const noRecordedUseCityEquipment = parseBattleSaveState(
JSON.stringify({
...validCityEquipmentState,
cityEquipmentContribution: {
...validCityEquipmentContribution,
offensiveActions: 0,
bonusDamage: 77,
defensiveHits: 0,
preventedDamage: 88
}
}),
eighthBattleOptions
)?.cityEquipmentContribution;
assert(
noRecordedUseCityEquipment?.bonusDamage === 0 &&
noRecordedUseCityEquipment.preventedDamage === 0,
'Expected contribution damage totals to clear when no matching actions or hits were recorded.'
);
const wrongBattleCityEquipment = {
...validState,
cityEquipmentContribution: validCityEquipmentContribution
};
const normalizedWrongBattleCityEquipment =
normalizeBattleSaveState(
wrongBattleCityEquipment,
options
);
assert(
normalizedWrongBattleCityEquipment?.cityEquipmentContribution ===
undefined &&
!isValidBattleSaveState(
wrongBattleCityEquipment,
options
),
'Expected city-equipment progress attached to another battle to be discarded and rejected by direct validation.'
);
const mismatchedEquipmentCityState = {
...validCityEquipmentState,
units: validState.units.map((unit) => ({
...unit,
equipment: {
weapon: { ...unit.equipment.weapon },
armor: { ...unit.equipment.armor },
accessory: { ...unit.equipment.accessory }
}
}))
};
const normalizedMismatchedEquipment = normalizeBattleSaveState(
mismatchedEquipmentCityState,
eighthBattleOptions
);
assert(
normalizedMismatchedEquipment?.cityEquipmentContribution ===
undefined &&
!isValidBattleSaveState(
mismatchedEquipmentCityState,
eighthBattleOptions
),
'Expected a save whose recorded unit no longer equips the purchased item to discard and reject the contribution.'
);
const corruptedCityEquipmentCases = [
{
label: 'forged record id',
contribution: {
...validCityEquipmentContribution,
selectionRecordId: 'forged-city-equipment'
}
},
{
label: 'wrong-slot previous item',
contribution: {
...validCityEquipmentContribution,
previousItemId: 'cloth-armor'
}
},
{
label: 'undeployed runtime',
contribution: {
...validCityEquipmentContribution,
deployed: false
}
},
{
label: 'unknown prepared unit',
contribution: {
...validCityEquipmentContribution,
unitId: 'missing-unit'
}
}
];
corruptedCityEquipmentCases.forEach(({ label, contribution }) => {
const candidate = {
...eighthBattleBaseState,
cityEquipmentContribution: contribution
};
const normalized = normalizeBattleSaveState(
candidate,
eighthBattleOptions
);
assert(
normalized?.cityEquipmentContribution === undefined &&
!isValidBattleSaveState(candidate, eighthBattleOptions),
`Expected ${label} city-equipment progress to be discarded and rejected.`
);
});
assert(
isValidBattleSaveState(
eighthBattleBaseState,
eighthBattleOptions
) &&
parseBattleSaveState(
JSON.stringify(eighthBattleBaseState),
eighthBattleOptions
)?.cityEquipmentContribution === undefined,
'Expected legacy eighth-battle saves without city-equipment contribution progress to remain valid.'
);
const validSortieStatsState = {
...validState,
battleStats: {