feat: deepen audiovisual story immersion

This commit is contained in:
2026-07-23 10:57:07 +09:00
parent a9e401aedf
commit 8cf0886d7d
38 changed files with 2037 additions and 144 deletions

View File

@@ -85,6 +85,17 @@ export type BattleSaveCoreResonanceStats = {
additionalDamage: number;
};
export type BattleSaveEventPriority = 'critical' | 'high' | 'normal' | 'low';
export type BattleSavePendingEvent = {
key: string;
title: string;
lines: string[];
priority: BattleSaveEventPriority;
playCue: boolean;
presented?: boolean;
};
export type BattleSaveState = {
version: 1;
battleId: string;
@@ -111,6 +122,7 @@ export type BattleSaveState = {
tacticalCommand?: BattleSaveTacticalCommandState;
enemyUsableUseKeys?: string[];
triggeredBattleEvents?: string[];
pendingBattleEvents?: BattleSavePendingEvent[];
};
export type BattleSaveValidationOptions = {
@@ -135,6 +147,9 @@ const maxBattleLogEntries = 10;
const maxBattleLogEntryLength = 96;
const maxBattleBondEntries = 128;
const maxTriggeredBattleEventLength = 96;
const maxPendingBattleEventTitleLength = 96;
const maxPendingBattleEventLineLength = 256;
const maxPendingBattleEventLines = 12;
const defaultBattleSaveArrayLimit = 128;
const defaultBattleItemStockLimit = 9;
const maxBattleEffectLabelLength = 32;
@@ -272,7 +287,8 @@ export function isValidBattleSaveState(state: unknown, options: BattleSaveValida
options
) ||
!isOptionalEnemyUsableUseKeyArray(state.enemyUsableUseKeys, state.battleId, options) ||
!isOptionalTriggeredBattleEventArray(state.triggeredBattleEvents, options)
!isOptionalTriggeredBattleEventArray(state.triggeredBattleEvents, options) ||
!isOptionalPendingBattleEventArray(state.pendingBattleEvents, state.triggeredBattleEvents, options)
) {
return false;
}
@@ -922,6 +938,58 @@ function isOptionalTriggeredBattleEventArray(value: unknown, options: BattleSave
);
}
function isOptionalPendingBattleEventArray(
value: unknown,
triggeredEvents: unknown,
options: BattleSaveValidationOptions
) {
if (value === undefined) {
return true;
}
if (
!Array.isArray(value) ||
value.length > triggeredBattleEventLimit(options) ||
!hasUniqueRecordStrings(value, 'key')
) {
return false;
}
const presentedIndices = value
.map((event, index) => (isRecord(event) && event.presented === true ? index : -1))
.filter((index) => index >= 0);
if (presentedIndices.length > 1 || (presentedIndices.length === 1 && presentedIndices[0] !== 0)) {
return false;
}
const completedKeys = new Set(Array.isArray(triggeredEvents) ? triggeredEvents : []);
return value.every((event) => (
isRecord(event) &&
typeof event.key === 'string' &&
event.key.length > 0 &&
event.key.length <= maxTriggeredBattleEventLength &&
!completedKeys.has(event.key) &&
(!options.validTriggeredBattleEventIds || options.validTriggeredBattleEventIds.has(event.key)) &&
typeof event.title === 'string' &&
event.title.trim().length > 0 &&
event.title.length <= maxPendingBattleEventTitleLength &&
Array.isArray(event.lines) &&
event.lines.length > 0 &&
event.lines.length <= maxPendingBattleEventLines &&
event.lines.every((line) => (
typeof line === 'string' &&
line.trim().length > 0 &&
line.length <= maxPendingBattleEventLineLength
)) &&
isBattleSaveEventPriority(event.priority) &&
typeof event.playCue === 'boolean' &&
(event.presented === undefined || typeof event.presented === 'boolean')
));
}
function isBattleSaveEventPriority(value: unknown): value is BattleSaveEventPriority {
return value === 'critical' || value === 'high' || value === 'normal' || value === 'low';
}
function isEnemyUsableUseKey(value: unknown, battleId: string, options: BattleSaveValidationOptions) {
if (typeof value !== 'string') {
return false;