Validate battle save unit roster

This commit is contained in:
2026-07-05 11:22:55 +09:00
parent 63fd404d77
commit 64efc3d488
3 changed files with 36 additions and 4 deletions

View File

@@ -14,7 +14,8 @@ try {
const options = {
expectedBattleId: 'first-battle-zhuo-commandery',
mapWidth: 12,
mapHeight: 8
mapHeight: 8,
validUnitIds: new Set(['liu-bei'])
};
const validState = createValidBattleSaveState();
@@ -40,6 +41,8 @@ try {
['invalid unit hp', { units: [{ ...validState.units[0], hp: 99, maxHp: 30 }] }],
['invalid unit x', { units: [{ ...validState.units[0], x: 12 }] }],
['invalid unit direction', { units: [{ ...validState.units[0], direction: 'down' }] }],
['unknown unit id', { units: [{ ...validState.units[0], id: 'ghost-unit' }] }],
['duplicate unit id', { units: [{ ...validState.units[0] }, { ...validState.units[0], x: 2 }] }],
['invalid bonds', { bonds: [{ ...validState.bonds[0], unitIds: ['liu-bei'] }] }],
['invalid item stock count', { itemStocks: { 'liu-bei': { bean: -1 } } }],
['invalid buff turns', { battleBuffs: [{ ...validState.battleBuffs[0], turns: 0 }] }],
@@ -54,6 +57,11 @@ try {
assert(!isValidBattleSaveState(candidate, options), `Expected invalid battle save to be rejected: ${label}`);
});
assert(
!isValidBattleSaveState(validState, { ...options, validUnitIds: new Set(['liu-bei', 'guan-yu']) }),
'Expected save missing a current battle unit to be rejected.'
);
console.log('Verified battle save normalization and corrupted battle save rejection.');
} finally {
await server.close();

View File

@@ -11348,7 +11348,8 @@ export class BattleScene extends Phaser.Scene {
return parseBattleSaveState(raw, {
expectedBattleId: battleScenario.id,
mapWidth: battleMap.width,
mapHeight: battleMap.height
mapHeight: battleMap.height,
validUnitIds: new Set(battleUnits.map((unit) => unit.id))
});
}

View File

@@ -71,6 +71,7 @@ type BattleSaveValidationOptions = {
expectedBattleId?: string;
mapWidth?: number;
mapHeight?: number;
validUnitIds?: ReadonlySet<string>;
};
const unitDirections = new Set<UnitDirection>(['south', 'east', 'north', 'west']);
@@ -118,7 +119,7 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida
!isStringArray(state.actedUnitIds) ||
!isBattleLog(state.battleLog) ||
!isAttackIntentArray(state.attackIntents) ||
!state.units.every((unit) => isSavedBattleUnitState(unit, options)) ||
!areSavedBattleUnitsValid(state.units, options) ||
!isBondArray(state.bonds)
) {
return false;
@@ -185,7 +186,29 @@ function isAttackIntentArray(value: unknown) {
);
}
function isSavedBattleUnitState(value: unknown, options: BattleSaveValidationOptions) {
function areSavedBattleUnitsValid(units: unknown[], options: BattleSaveValidationOptions) {
const seenUnitIds = new Set<string>();
for (const unit of units) {
if (!isSavedBattleUnitState(unit, options) || seenUnitIds.has(unit.id)) {
return false;
}
seenUnitIds.add(unit.id);
}
if (!options.validUnitIds) {
return true;
}
if (seenUnitIds.size !== options.validUnitIds.size) {
return false;
}
return [...seenUnitIds].every((unitId) => options.validUnitIds?.has(unitId));
}
function isSavedBattleUnitState(value: unknown, options: BattleSaveValidationOptions): value is SavedBattleUnitState {
if (!isRecord(value) || typeof value.id !== 'string' || value.id.length === 0 || !isRecord(value.equipment)) {
return false;
}