diff --git a/scripts/verify-battle-save-normalization.mjs b/scripts/verify-battle-save-normalization.mjs index 6e6e922..521cf3e 100644 --- a/scripts/verify-battle-save-normalization.mjs +++ b/scripts/verify-battle-save-normalization.mjs @@ -15,7 +15,8 @@ try { expectedBattleId: 'first-battle-zhuo-commandery', mapWidth: 12, mapHeight: 8, - validUnitIds: new Set(['liu-bei', 'guan-yu', 'rebel-1']) + validUnitIds: new Set(['liu-bei', 'guan-yu', 'rebel-1']), + validUsableIds: new Set(['roar', 'fireTactic', 'aid']) }; const validState = createValidBattleSaveState(); const patchUnit = (index, patch) => validState.units.map((unit, unitIndex) => (unitIndex === index ? { ...unit, ...patch } : unit)); @@ -57,6 +58,10 @@ try { ['invalid stats shape', { battleStats: { 'liu-bei': { damageDealt: 10 } } }], ['unknown stats unit id', { battleStats: { 'ghost-unit': validState.battleStats['liu-bei'] } }], ['invalid enemy usable keys', { enemyUsableUseKeys: [1] }], + ['malformed enemy usable key', { enemyUsableUseKeys: ['rebel-1:shout'] }], + ['wrong battle enemy usable key', { enemyUsableUseKeys: ['other-battle:rebel-1:roar'] }], + ['unknown enemy usable unit id', { enemyUsableUseKeys: ['first-battle-zhuo-commandery:ghost-unit:roar'] }], + ['unknown enemy usable id', { enemyUsableUseKeys: ['first-battle-zhuo-commandery:rebel-1:phantom'] }], ['invalid triggered events', { triggeredBattleEvents: [1] }] ]; @@ -168,7 +173,7 @@ function createValidBattleSaveState() { support: 0 } }, - enemyUsableUseKeys: ['rebel-1:shout'], + enemyUsableUseKeys: ['first-battle-zhuo-commandery:rebel-1:roar'], triggeredBattleEvents: ['first-objective-near'] }; } diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index 217e1a4..e6a305f 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -11349,7 +11349,8 @@ export class BattleScene extends Phaser.Scene { expectedBattleId: battleScenario.id, mapWidth: battleMap.width, mapHeight: battleMap.height, - validUnitIds: new Set(battleUnits.map((unit) => unit.id)) + validUnitIds: new Set(battleUnits.map((unit) => unit.id)), + validUsableIds: new Set(Object.keys(usableCatalog)) }); } diff --git a/src/game/state/battleSaveState.ts b/src/game/state/battleSaveState.ts index 3efcb79..bedcd0f 100644 --- a/src/game/state/battleSaveState.ts +++ b/src/game/state/battleSaveState.ts @@ -72,6 +72,7 @@ type BattleSaveValidationOptions = { mapWidth?: number; mapHeight?: number; validUnitIds?: ReadonlySet; + validUsableIds?: ReadonlySet; }; const unitDirections = new Set(['south', 'east', 'north', 'west']); @@ -130,7 +131,7 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida !isOptionalBuffArray(state.battleBuffs, options) || !isOptionalStatusArray(state.battleStatuses, options) || !isOptionalStatsRecord(state.battleStats, options) || - !isOptionalStringArray(state.enemyUsableUseKeys) || + !isOptionalEnemyUsableUseKeyArray(state.enemyUsableUseKeys, state.battleId, options) || !isOptionalStringArray(state.triggeredBattleEvents) ) { return false; @@ -339,6 +340,28 @@ function isOptionalStatsRecord(value: unknown, options: BattleSaveValidationOpti ); } +function isOptionalEnemyUsableUseKeyArray(value: unknown, battleId: string, options: BattleSaveValidationOptions) { + return value === undefined || (Array.isArray(value) && value.every((entry) => isEnemyUsableUseKey(entry, battleId, options))); +} + +function isEnemyUsableUseKey(value: unknown, battleId: string, options: BattleSaveValidationOptions) { + if (typeof value !== 'string') { + return false; + } + + const [entryBattleId, unitId, usableId, extra] = value.split(':'); + return ( + extra === undefined && + entryBattleId === battleId && + typeof unitId === 'string' && + unitId.length > 0 && + typeof usableId === 'string' && + usableId.length > 0 && + isKnownUnitId(unitId, options) && + (!options.validUsableIds || options.validUsableIds.has(usableId)) + ); +} + function isKnownUnitId(unitId: string, options: BattleSaveValidationOptions) { return !options.validUnitIds || options.validUnitIds.has(unitId); }