feat: improve tactical audiovisual clarity

This commit is contained in:
2026-07-22 20:08:23 +09:00
parent d3af88c2a9
commit 8bda36d46d
5 changed files with 663 additions and 58 deletions

View File

@@ -14,6 +14,8 @@ assert.match(tickStatuses, /const visibleOnMap = this\.isTileVisible\(unit\.x, u
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');
@@ -27,7 +29,12 @@ const turnStartCue = privateMethodBody(battleSource, 'playTurnStartFeedbackCue')
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 (burnTicked)', 'if (recoveryApplied)', "if (faction === 'ally')"];
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)),
@@ -47,6 +54,8 @@ const campSupply = privateMethodBody(campSource, 'useSupply');
});
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\(\)/);
@@ -58,6 +67,26 @@ const deploymentConfirmation = privateMethodBody(battleSource, 'confirmPreBattle
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');
@@ -77,6 +106,37 @@ assert.match(privateMethodBody(battleSource, 'unitStatusBadgePosition'), /0\.34
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'],
@@ -85,9 +145,16 @@ for (const [method, group, key] of [
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 semantic sound cues, map status feedback, capped status badges, and non-overlapping battle opening audio.');
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}`);