Clamp battle save effect state

This commit is contained in:
2026-07-05 13:40:20 +09:00
parent d9a50ba064
commit 3a5ee11a6d
2 changed files with 32 additions and 8 deletions

View File

@@ -89,9 +89,17 @@ try {
['unknown item stock unit id', { itemStocks: { 'ghost-unit': { bean: 1 } } }], ['unknown item stock unit id', { itemStocks: { 'ghost-unit': { bean: 1 } } }],
['unknown item stock item id', { itemStocks: { 'liu-bei': { phantomItem: 1 } } }], ['unknown item stock item id', { itemStocks: { 'liu-bei': { phantomItem: 1 } } }],
['invalid buff turns', { battleBuffs: [{ ...validState.battleBuffs[0], turns: 0 }] }], ['invalid buff turns', { battleBuffs: [{ ...validState.battleBuffs[0], turns: 0 }] }],
['too many buff turns', { battleBuffs: [{ ...validState.battleBuffs[0], turns: 10 }] }],
['too long buff label', { battleBuffs: [{ ...validState.battleBuffs[0], label: 'x'.repeat(33) }] }],
['invalid buff bonus', { battleBuffs: [{ ...validState.battleBuffs[0], hitBonus: 1.5 }] }],
['too high buff bonus', { battleBuffs: [{ ...validState.battleBuffs[0], criticalBonus: 100 }] }],
['unknown buff unit id', { battleBuffs: [{ ...validState.battleBuffs[0], unitId: 'ghost-unit' }] }], ['unknown buff unit id', { battleBuffs: [{ ...validState.battleBuffs[0], unitId: 'ghost-unit' }] }],
['duplicate buff unit id', { battleBuffs: [validState.battleBuffs[0], { ...validState.battleBuffs[0] }] }], ['duplicate buff unit id', { battleBuffs: [validState.battleBuffs[0], { ...validState.battleBuffs[0] }] }],
['invalid status kind', { battleStatuses: [{ ...validState.battleStatuses[0], kind: 'poison' }] }], ['invalid status kind', { battleStatuses: [{ ...validState.battleStatuses[0], kind: 'poison' }] }],
['too many status turns', { battleStatuses: [{ ...validState.battleStatuses[0], turns: 10 }] }],
['too long status label', { battleStatuses: [{ ...validState.battleStatuses[0], label: 'x'.repeat(33) }] }],
['invalid status power', { battleStatuses: [{ ...validState.battleStatuses[0], power: 0 }] }],
['too high status power', { battleStatuses: [{ ...validState.battleStatuses[0], power: 100 }] }],
['unknown status unit id', { battleStatuses: [{ ...validState.battleStatuses[0], unitId: 'ghost-unit' }] }], ['unknown status unit id', { battleStatuses: [{ ...validState.battleStatuses[0], unitId: 'ghost-unit' }] }],
['duplicate status unit kind', { battleStatuses: [validState.battleStatuses[0], { ...validState.battleStatuses[0] }] }], ['duplicate status unit kind', { battleStatuses: [validState.battleStatuses[0], { ...validState.battleStatuses[0] }] }],
['invalid stats shape', { battleStats: { 'liu-bei': { damageDealt: 10 } } }], ['invalid stats shape', { battleStats: { 'liu-bei': { damageDealt: 10 } } }],

View File

@@ -87,6 +87,10 @@ const maxBattleBondEntries = 128;
const maxTriggeredBattleEventLength = 96; const maxTriggeredBattleEventLength = 96;
const defaultBattleSaveArrayLimit = 128; const defaultBattleSaveArrayLimit = 128;
const defaultBattleItemStockLimit = 9; const defaultBattleItemStockLimit = 9;
const maxBattleEffectLabelLength = 32;
const maxBattleEffectTurns = 9;
const maxBattleBuffBonus = 99;
const maxBattleStatusPower = 99;
const maxStatusKindsPerUnit = 2; const maxStatusKindsPerUnit = 2;
const maxBattleItemStockById: Record<string, number> = { const maxBattleItemStockById: Record<string, number> = {
bean: 3, bean: 3,
@@ -165,6 +169,10 @@ function isPositiveInteger(value: unknown) {
return Number.isInteger(value) && Number(value) >= 1; return Number.isInteger(value) && Number(value) >= 1;
} }
function isPositiveIntegerAtMost(value: unknown, max: number) {
return isPositiveInteger(value) && Number(value) <= max;
}
function isNonNegativeFiniteNumber(value: unknown) { function isNonNegativeFiniteNumber(value: unknown) {
return typeof value === 'number' && Number.isFinite(value) && value >= 0; return typeof value === 'number' && Number.isFinite(value) && value >= 0;
} }
@@ -177,6 +185,10 @@ function isFiniteNumber(value: unknown) {
return typeof value === 'number' && Number.isFinite(value); return typeof value === 'number' && Number.isFinite(value);
} }
function isIntegerInRange(value: unknown, min: number, max: number) {
return Number.isInteger(value) && Number(value) >= min && Number(value) <= max;
}
function isValidIsoLikeDate(value: unknown) { function isValidIsoLikeDate(value: unknown) {
return typeof value === 'string' && value.length > 0 && !Number.isNaN(Date.parse(value)); return typeof value === 'string' && value.length > 0 && !Number.isNaN(Date.parse(value));
} }
@@ -382,6 +394,10 @@ function isValidItemStockCount(itemId: string, count: unknown) {
return Number.isInteger(count) && Number(count) >= 0 && Number(count) <= (maxBattleItemStockById[itemId] ?? defaultBattleItemStockLimit); return Number.isInteger(count) && Number(count) >= 0 && Number(count) <= (maxBattleItemStockById[itemId] ?? defaultBattleItemStockLimit);
} }
function isValidEffectLabel(value: unknown) {
return typeof value === 'string' && value.length > 0 && value.length <= maxBattleEffectLabelLength;
}
function isOptionalBuffArray(value: unknown, options: BattleSaveValidationOptions) { function isOptionalBuffArray(value: unknown, options: BattleSaveValidationOptions) {
return ( return (
value === undefined || value === undefined ||
@@ -393,11 +409,11 @@ function isOptionalBuffArray(value: unknown, options: BattleSaveValidationOption
isRecord(buff) && isRecord(buff) &&
typeof buff.unitId === 'string' && typeof buff.unitId === 'string' &&
isKnownUnitId(buff.unitId, options) && isKnownUnitId(buff.unitId, options) &&
typeof buff.label === 'string' && isValidEffectLabel(buff.label) &&
isPositiveInteger(buff.turns) && isPositiveIntegerAtMost(buff.turns, maxBattleEffectTurns) &&
isFiniteNumber(buff.attackBonus) && isIntegerInRange(buff.attackBonus, 0, maxBattleBuffBonus) &&
isFiniteNumber(buff.hitBonus) && isIntegerInRange(buff.hitBonus, 0, maxBattleBuffBonus) &&
isFiniteNumber(buff.criticalBonus) isIntegerInRange(buff.criticalBonus, 0, maxBattleBuffBonus)
)) ))
); );
} }
@@ -414,9 +430,9 @@ function isOptionalStatusArray(value: unknown, options: BattleSaveValidationOpti
typeof status.unitId === 'string' && typeof status.unitId === 'string' &&
isKnownUnitId(status.unitId, options) && isKnownUnitId(status.unitId, options) &&
isBattleStatusKind(status.kind) && isBattleStatusKind(status.kind) &&
typeof status.label === 'string' && isValidEffectLabel(status.label) &&
isPositiveInteger(status.turns) && isPositiveIntegerAtMost(status.turns, maxBattleEffectTurns) &&
isFiniteNumber(status.power) isPositiveIntegerAtMost(status.power, maxBattleStatusPower)
)) ))
); );
} }