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

@@ -0,0 +1,75 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const battleSource = readFileSync('src/game/scenes/BattleScene.ts', 'utf8');
const saveSource = readFileSync('src/game/state/battleSaveState.ts', 'utf8');
const triggerEvent = privateMethodBody(battleSource, 'triggerBattleEvent');
assert.match(triggerEvent, /this\.isBattleEventKnown\(key\)/, 'event detection must reject completed, active, and queued duplicates');
assert.match(triggerEvent, /battleEventPriorityRank/, 'event insertion must compare explicit priorities');
assert.match(triggerEvent, /this\.battleEventQueue\.splice\(insertionIndex, 0, event\)/, 'higher-priority events must enter ahead of lower priorities');
assert.match(triggerEvent, /this\.battleEventQueue\.push\(event\)/, 'equal priorities must retain FIFO order');
assert.match(triggerEvent, /if \(!this\.deferBattleEventPresentation\)/, 'batched event detection must defer presentation until priority sorting finishes');
assert.doesNotMatch(triggerEvent, /triggeredBattleEvents\.add/, 'queued events must not complete before presentation');
const showNextEvent = privateMethodBody(battleSource, 'showNextBattleEvent');
assert.match(showNextEvent, /this\.battleEventQueue\.shift\(\)/, 'presentation must consume the queue head');
assert.match(showNextEvent, /this\.activeBattleEvent = event/, 'presentation must retain the active event until dismissal');
assert.match(showNextEvent, /queuedEvent\.presented = true/, 'lower-priority notices in the same batch must be recorded without opening more modals');
assert.match(showNextEvent, /title: `\$\{event\.title\} · 외 \$\{groupedEvents\.length\}건`/, 'the visible notice must disclose how many grouped updates were logged');
assert.match(showNextEvent, /this\.showBattleEventBanner\(/, 'the queue head must be rendered');
const completeEvent = privateMethodBody(battleSource, 'completeActiveBattleEvent');
assert.match(completeEvent, /this\.triggeredBattleEvents\.add\(completedEvent\.key\)/, 'an event may complete only after its banner closes');
assert.doesNotMatch(completeEvent, /showNextBattleEvent/, 'closing one banner must not chain every queued notice into a modal burst');
assert.match(completeEvent, /this\.battleEventQueue\.splice\(0\)/, 'closing the grouped banner must drain the same-action notice batch');
assert.match(completeEvent, /this\.triggeredBattleEvents\.add\(groupedEvent\.key\)/, 'grouped notices recorded in the battle log must complete with the visible banner');
const checkEvents = privateMethodBody(battleSource, 'checkBattleEvents');
assert.match(checkEvents, /this\.deferBattleEventPresentation = true/, 'one action must collect every newly satisfied event before choosing what to show');
assert.match(checkEvents, /this\.collectBattleEvents\(\)/, 'one action must collect event candidates as a batch');
assert.match(checkEvents, /this\.deferBattleEventPresentation = false/, 'event presentation deferral must always be released');
assert.match(checkEvents, /this\.showNextBattleEvent\(\)/, 'one action may present the highest-priority queued notice after collection');
const clearEvents = privateMethodBody(battleSource, 'clearBattleEvents');
assert.match(clearEvents, /this\.battleEventQueue = \[\]/, 'battle shutdown must discard pending events');
assert.match(clearEvents, /this\.activeBattleEvent = undefined/, 'battle shutdown must discard the active event');
const createSave = privateMethodBody(battleSource, 'createBattleSaveState');
const applySave = privateMethodBody(battleSource, 'applyBattleSaveState');
assert.match(createSave, /pendingBattleEvents: this\.pendingBattleEventsForSave\(\)/, 'battle saves must retain active and queued events');
assert.match(applySave, /state\.pendingBattleEvents \?\? \[\]/, 'battle loads must restore pending events');
assert.match(applySave, /showNextBattleEvent\(\)/, 'battle loads must resume presentation');
assert.match(saveSource, /pendingBattleEvents\?: BattleSavePendingEvent\[\]/, 'pending event saves must remain an optional version-1 field');
const pendingOutcome = privateMethodBody(battleSource, 'pendingBattleOutcome');
assert.match(pendingOutcome, /requiredVictoryObjectiveStates\(\)/, 'victory must consult required narrative objectives');
assert.match(pendingOutcome, /victory-gate-pending/, 'an unmet narrative gate must explain why battle continues');
const triggerTacticalEvent = privateMethodBody(battleSource, 'triggerTacticalEvent');
const tacticalReaction = privateMethodBody(battleSource, 'tacticalEventReaction');
assert.match(triggerTacticalEvent, /reaction \? \[reaction\.line\] : \[\]/, 'a valid deployed officer reaction must be appended to tactical event lines');
assert.match(tacticalReaction, /unit\.faction === 'ally' && unit\.hp > 0/, 'reaction candidates must be deployed, allied, and alive');
assert.match(battleSource, /tacticalEventReactions: this\.tacticalEventReactionHistory\.map/, 'debug state must expose the selected tactical speakers');
assert.match(battleSource, /const maxBodyLines = 3/, 'the tactical reaction line must remain visible in the event banner');
const reactionTable = sourceBlock(battleSource, 'const tacticalEventReactionCandidates', '\n};');
const reactionUnitIds = new Set(Array.from(reactionTable.matchAll(/unitId: '([^']+)'/g), (match) => match[1]));
assert.equal(reactionUnitIds.size, 8, 'the focused tactical reaction table should use exactly eight representative officers');
console.log('Verified prioritized FIFO battle-event presentation, one-modal grouped pacing, completion timing, save resume, cleanup, narrative victory gates, and deployed-officer reactions.');
function privateMethodBody(source, name) {
const marker = new RegExp(`^ private (?:async )?${name}\\b`, 'm').exec(source);
assert(marker, `missing private method: ${name}`);
const start = marker.index;
const next = source.indexOf('\n private ', start + marker[0].length);
return source.slice(start, next > start ? next : source.length);
}
function sourceBlock(source, startMarker, endMarker) {
const start = source.indexOf(startMarker);
assert(start >= 0, `missing source marker: ${startMarker}`);
const end = source.indexOf(endMarker, start);
assert(end > start, `missing source end marker after: ${startMarker}`);
return source.slice(start, end + endMarker.length);
}