import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; const battleSource = readFileSync('src/game/scenes/BattleScene.ts', 'utf8'); const campSource = readFileSync('src/game/scenes/CampScene.ts', 'utf8'); const soundSource = readFileSync('src/game/audio/SoundDirector.ts', 'utf8'); const tickStatuses = privateMethodBody(battleSource, 'tickBattleStatuses'); assert.match(tickStatuses, /let burnTicked = false/); assert.match(tickStatuses, /showMapResultPopup\s*\(/); assert.match(tickStatuses, /`\$\{status\.label\} -\$\{damage\}`/); assert.match(tickStatuses, /flashDamage\s*\(unit, previousHp\)/); assert.match(tickStatuses, /const visibleOnMap = this\.isTileVisible\(unit\.x, unit\.y\)/); assert.match(tickStatuses, /if \(visibleOnMap\)\s*\{\s*this\.showMapResultPopup/s); assert.match(tickStatuses, /if \(visibleOnMap\)\s*\{\s*this\.flashDamage/s); assert.match(tickStatuses, /if \(playCue && burnTicked\)\s*\{\s*soundDirector\.playBurnTickCue\(\)/s); assert.match(tickStatuses, /offscreenBurnSources\.push/); assert.match(tickStatuses, /showOffscreenBurnCaption/); assert.equal(countMatches(tickStatuses, /soundDirector\.playBurnTickCue\(\)/g), 1, 'a direct status tick must play one cue per faction batch'); const endAllyTurn = privateMethodBody(battleSource, 'endAllyTurn'); const beginNextAllyTurn = privateMethodBody(battleSource, 'beginNextAllyTurn'); const turnStartCue = privateMethodBody(battleSource, 'playTurnStartFeedbackCue'); [endAllyTurn, beginNextAllyTurn].forEach((body) => { assert.match(body, /tickBattleStatuses\('(ally|enemy)', false\)/); assert.match(body, /applyTerrainRecovery\('(ally|enemy)', false\)/); assert.match(body, /playTurnStartFeedbackCue/); assert.match(body, /statusOutcomeDelayMs = statusResult\.burnTicked \? 720 : 0/); assert.match(body, /resolveBattleOutcomeIfNeeded\(statusOutcomeDelayMs\)/); assert.match(body, /statusResult\.burnTicked[\s\S]*playBurnTickCue\(\)/, 'lethal burn must retain its cue before an early return'); }); const cuePriorityMarkers = [ 'if (operationCuePlayed)', 'if (statusResult.burnTicked)', 'if (recoveryResult.applied)', "if (faction === 'ally')" ]; cuePriorityMarkers.forEach((marker) => assert.match(turnStartCue, new RegExp(escapeRegExp(marker)))); assert.deepEqual( cuePriorityMarkers.map((marker) => turnStartCue.indexOf(marker)), cuePriorityMarkers.map((marker) => turnStartCue.indexOf(marker)).toSorted((left, right) => left - right), 'turn-start cues must prioritize operation, burn, recovery, then the generic faction cue' ); assert.doesNotMatch(turnStartCue, /delayedCall/, 'turn-start feedback must select one cue instead of layering delayed cues'); const supportMap = privateMethodBody(battleSource, 'playSupportMapPresentation'); const supportCutIn = privateMethodBody(battleSource, 'playSupportCutIn'); const terrainRecovery = privateMethodBody(battleSource, 'applyTerrainRecovery'); const tacticalInitiative = privateMethodBody(battleSource, 'activateTacticalInitiativeCommand'); const campSupply = privateMethodBody(campSource, 'useSupply'); [supportMap, supportCutIn].forEach((body) => { assert.match(body, /result\.usable\.effect === 'heal'/); assert.match(body, /soundDirector\.playRecoveryCue\(\)/); }); assert.match(terrainRecovery, /soundDirector\.playRecoveryCue\(\)/); assert.match(terrainRecovery, /if \(playCue\)/); assert.match(terrainRecovery, /offscreenRecoverySources\.push/); assert.match(terrainRecovery, /showOffscreenRecoveryCaption/); assert.match(tacticalInitiative, /role === 'support'/); assert.match(tacticalInitiative, /soundDirector\.playRecoveryCue\(\)/); assert.match(campSupply, /soundDirector\.playRecoveryCue\(\)/); const victoryReward = privateMethodBody(campSource, 'showVictoryRewardAcknowledgement'); assert.equal(countMatches(victoryReward, /soundDirector\.playRewardRevealCue\(\)/g), 1, 'victory reward should reveal once'); const deploymentConfirmation = privateMethodBody(battleSource, 'confirmPreBattleDeployment'); assert.match(deploymentConfirmation, /showOpeningBattleEvent\(\)/); assert.doesNotMatch(deploymentConfirmation, /soundDirector\.playSelect\(\)/, 'opening alert must not overlap a selection cue'); const resolveDamageTarget = privateMethodBody(battleSource, 'tryResolveDamageTarget'); assert.match(resolveDamageTarget, /triggerBattleEvent\('first-engagement'[\s\S]*\{ playCue: false \}\)/); const triggerBattleEvent = privateMethodBody(battleSource, 'triggerBattleEvent'); assert.match(triggerBattleEvent, /options\.playCue !== false/); const mapCombat = privateMethodBody(battleSource, 'playCombatMapPresentation'); assert.match(mapCombat, /contactDelayMs = Math\.max\(70, Math\.round\(duration \* 0\.42\)\)/); assert.match(mapCombat, /await this\.waitSceneDuration\(contactDelayMs\);\s*this\.playCombatContactSound\(result\)/s); assert( mapCombat.indexOf('playMeleeSwing') < mapCombat.indexOf('await this.waitSceneDuration(contactDelayMs)') && mapCombat.indexOf('await this.waitSceneDuration(contactDelayMs)') < mapCombat.indexOf('playCombatContactSound(result)'), 'map combat must play launch motion before delayed contact audio' ); assert.match(mapCombat, /await Promise\.all\(\[this\.waitSceneDuration\(impactDuration \+ 20\), actorFramePromise\]\)/); const contactSound = privateMethodBody(battleSource, 'playCombatContactSound'); assert.match(contactSound, /soundDirector\.playStrategyImpact/); assert.match(contactSound, /soundDirector\.playArrowImpact\(result\.hit, result\.critical\)/); assert.match(contactSound, /soundDirector\.playCombatImpact\(result\.critical\)/); assert.match(privateMethodBody(battleSource, 'showCombatImpact'), /playCombatContactSound\(result\)/); const mapResultPopup = privateMethodBody(battleSource, 'showMapResultPopup'); assert.match(mapResultPopup, /!view \|\| !this\.isTileVisible\(unit\.x, unit\.y\)/, 'offscreen units must not create edge-clamped popups'); const statusBadges = privateMethodBody(battleSource, 'syncUnitStatusBadges'); assert.match(statusBadges, /\.slice\(0, 2\)/, 'map status badges must remain capped at two per unit'); assert.match(statusBadges, /createBattleUiIcon/); assert.match(statusBadges, /statusEffectIcon/); assert.match(statusBadges, /statusEffectTone/); assert.match(statusBadges, /setMask\(this\.mapMask\)/); assert.match(privateMethodBody(battleSource, 'applyBattleStatus'), /syncUnitStatusBadges/); assert.match(tickStatuses, /syncUnitStatusBadges/); assert.match(tacticalInitiative, /battleStatuses\.delete\(supportTarget\.id\)[\s\S]*syncUnitStatusBadges\(supportTarget\)/); assert.match(privateMethodBody(battleSource, 'applyDefeatedStyle'), /syncUnitStatusBadges/); assert.match(privateMethodBody(battleSource, 'moveUnitViewAsync'), /unitStatusBadgePosition/); assert.match(privateMethodBody(battleSource, 'moveUnitView'), /unitStatusBadgePosition/); assert.match(privateMethodBody(battleSource, 'unitStatusBadgePosition'), /0\.34 \+ index \* spacing/, 'paired badges should fan away from the unit instead of folding inward'); assert.match(battleSource, /statusBadgeSide: unit\.faction === 'ally' \? 1 : -1/); assert.match(battleSource, /statusBadges: \(this\.unitViews\.get\(unit\.id\)\?\.statusBadges \?\? \[\]\)\.map/); const facingIndicator = privateMethodBody(battleSource, 'showFacingIndicator'); assert.match(facingIndicator, /add\.ellipse/); assert.match(facingIndicator, /directionSymbol/); assert.match(facingIndicator, /strokeThickness: this\.battleUiLength\(4\)/); assert.match(facingIndicator, /if \(!isVisualMotionReduced\(\)\)/); assert.match(privateMethodBody(battleSource, 'startUnitIdleMotion'), /if \(isVisualMotionReduced\(\)\)\s*\{\s*return;/s); assert.match(privateMethodBody(battleSource, 'moveUnitViewAsync'), /facingIndicatorUnitId === unit\.id[\s\S]*clearFacingIndicator\(\)/); assert.match(privateMethodBody(battleSource, 'moveUnitView'), /facingIndicatorUnitId === unit\.id[\s\S]*clearFacingIndicator\(\)/); assert.match(privateMethodBody(battleSource, 'faceUnitToward'), /syncUnitMotion[\s\S]*refreshFacingIndicator\(unit\.id\)/); const actedStyle = privateMethodBody(battleSource, 'applyUnitLegibilityStyle'); assert.match(actedStyle, /const acted = this\.actedUnitIds\.has\(unit\.id\)/); assert.match(actedStyle, /view\.sprite\.setAlpha\(acted \? 0\.62 : 1\)/); assert.match(actedStyle, /view\.actedMarker\?\.setVisible\(visible && acted\)/); assert.match(privateMethodBody(battleSource, 'createUnitActedMarker'), /'success'/); assert.match(privateMethodBody(battleSource, 'applyDefeatedStyle'), /view\.actedMarker\?\.setAlpha\(0\)\.setVisible\(false\)/); const drawMiniMap = privateMethodBody(battleSource, 'drawMiniMap'); assert.match(drawMiniMap, /objective\.kind === 'secure-terrain'/); assert.match(drawMiniMap, /marker\.setAngle\(45\)/); assert.match(drawMiniMap, /miniMapObjectiveMarkers\.push\(marker\)/); const soundCaption = privateMethodBody(battleSource, 'showBattleSoundCaption'); assert.match(soundCaption, /battle-sound-caption/); assert.match(soundCaption, /kind === 'status' \? 'burn' : kind === 'recovery' \? 'heal' : 'success'/); assert.match(soundCaption, /this\.layout\.gridY \+ this\.layout\.gridHeight/); assert.match(privateMethodBody(battleSource, 'offscreenTileDirection'), /cameraTileX/); assert.match(privateMethodBody(battleSource, 'battleSoundCaptionDirectionLabel'), /'north-east': '↗'/); assert.match(privateMethodBody(battleSource, 'showObjectiveMapFeedback'), /showBattleSoundCaption\('objective'/); assert.doesNotMatch(soundCaption, /battleOutcome/, 'a lethal offscreen cue must remain captioned during the delayed result reveal'); for (const [method, group, key] of [ ['playRecoveryCue', 'recovery', 'recovery-cue'], ['playBurnTickCue', 'status', 'burn-tick'], ['playRewardRevealCue', 'reward', 'reward-reveal'] ]) { const body = publicMethodBody(soundSource, method); assert.match(body, new RegExp(`playSemanticCue\\('${group}', '${key}'`)); } const strategyImpact = publicMethodBody(soundSource, 'playStrategyImpact'); assert.match(strategyImpact, /kind === 'fire'/); assert.match(strategyImpact, /kind === 'roar'/); assert.match(strategyImpact, /playEffect\('burn-tick'/); assert.match(strategyImpact, /playEffect\('combat-impact'/); assert.match(strategyImpact, /playEffect\('strategy-cast'/); assert.match(soundSource, /recentEffects: this\.recentEffects\.map/); assert.match(soundSource, /recovery:\s*1900/, 'recovery cue cooldown should cover its playback window'); console.log('Verified synchronized combat audio, offscreen captions, tactical focus markers, mini-map objectives, status badges, and semantic cues.'); function privateMethodBody(source, name) { return methodBody(source, new RegExp(`^ private (?:async )?${name}\\b`, 'm'), `private ${name}`); } function publicMethodBody(source, name) { return methodBody(source, new RegExp(`^ (?:async )?${name}\\b`, 'm'), name); } function methodBody(source, pattern, label) { const match = pattern.exec(source); assert(match, `missing method marker: ${label}`); const start = match.index; const next = source.indexOf('\n private ', start + match[0].length); const nextPublic = source.indexOf('\n play', start + match[0].length); const candidates = [next, nextPublic].filter((index) => index > start); const end = candidates.length > 0 ? Math.min(...candidates) : source.length; return source.slice(start, end); } function countMatches(source, pattern) { return Array.from(source.matchAll(pattern)).length; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }