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 objectiveState = privateMethodBody(battleSource, 'objectiveState'); assert.match( objectiveState, /triggeredBattleEvents\.has\(this\.objectiveEventKey\(objective\.id, 'achieved'\)\)/, 'a secured terrain objective must remain complete after its achievement feedback is recorded' ); assert.match(objectiveState, /const secured = previouslySecured \|\| securedNow/); 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); }