diff --git a/scripts/verify-battle-save-normalization.mjs b/scripts/verify-battle-save-normalization.mjs index 680c560..656fc19 100644 --- a/scripts/verify-battle-save-normalization.mjs +++ b/scripts/verify-battle-save-normalization.mjs @@ -54,6 +54,8 @@ try { ['unknown attack intent unit id', { attackIntents: [{ attackerId: 'liu-bei', targetId: 'ghost-unit' }] }], ['self-targeting attack intent', { attackIntents: [{ attackerId: 'liu-bei', targetId: 'liu-bei' }] }], ['duplicate attack intent attacker id', { attackIntents: [{ attackerId: 'liu-bei', targetId: 'rebel-1' }, { attackerId: 'liu-bei', targetId: 'rebel-1' }] }], + ['defeated attack intent attacker', { units: patchUnit(0, { hp: 0 }) }], + ['defeated attack intent target', { units: patchUnit(2, { hp: 0 }) }], ['invalid units array', { units: {} }], ['too high unit level', { units: patchUnit(0, { level: 100, exp: 0 }) }], ['invalid unit exp threshold', { units: patchUnit(0, { exp: 100 }) }], diff --git a/src/game/state/battleSaveState.ts b/src/game/state/battleSaveState.ts index e008cf3..492d825 100644 --- a/src/game/state/battleSaveState.ts +++ b/src/game/state/battleSaveState.ts @@ -142,16 +142,23 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida return false; } + const attackIntents = state.attackIntents; + const units = state.units; + if ( !isUnitIdArray(state.actedUnitIds, options) || !isBattleLog(state.battleLog) || - !isAttackIntentArray(state.attackIntents, options) || - !areSavedBattleUnitsValid(state.units, options) || + !isAttackIntentArray(attackIntents, options) || + !areSavedBattleUnitsValid(units, options) || !isBondArray(state.bonds, options) ) { return false; } + if (!areAttackIntentsTargetingLiveUnits(attackIntents, units)) { + return false; + } + if ( !isOptionalItemStockRecord(state.itemStocks, options) || !isOptionalBuffArray(state.battleBuffs, options) || @@ -219,7 +226,7 @@ function isBattleLog(value: unknown) { ); } -function isAttackIntentArray(value: unknown, options: BattleSaveValidationOptions) { +function isAttackIntentArray(value: unknown, options: BattleSaveValidationOptions): value is BattleSaveAttackIntent[] { return ( Array.isArray(value) && value.length <= unitReferenceLimit(options) && @@ -236,7 +243,7 @@ function isAttackIntentArray(value: unknown, options: BattleSaveValidationOption ); } -function areSavedBattleUnitsValid(units: unknown[], options: BattleSaveValidationOptions) { +function areSavedBattleUnitsValid(units: unknown[], options: BattleSaveValidationOptions): units is SavedBattleUnitState[] { const seenUnitIds = new Set(); const seenLiveTileKeys = new Set(); @@ -266,6 +273,11 @@ function areSavedBattleUnitsValid(units: unknown[], options: BattleSaveValidatio return [...seenUnitIds].every((unitId) => options.validUnitIds?.has(unitId)); } +function areAttackIntentsTargetingLiveUnits(attackIntents: BattleSaveAttackIntent[], units: SavedBattleUnitState[]) { + const liveUnitIds = new Set(units.filter((unit) => unit.hp > 0).map((unit) => unit.id)); + return attackIntents.every((intent) => liveUnitIds.has(intent.attackerId) && liveUnitIds.has(intent.targetId)); +} + function isSavedBattleUnitState(value: unknown, options: BattleSaveValidationOptions): value is SavedBattleUnitState { if (!isRecord(value) || typeof value.id !== 'string' || value.id.length === 0 || !isEquipmentSet(value.equipment, options)) { return false;