fix: align audiovisual and narrative feedback

This commit is contained in:
2026-07-23 23:20:50 +09:00
parent 8cf0886d7d
commit e06b15ce7a
27 changed files with 1368 additions and 142 deletions

View File

@@ -4,6 +4,15 @@ 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 storySource = readFileSync('src/game/scenes/StoryScene.ts', 'utf8');
const applyStoryPage = privateMethodBody(storySource, 'applyPage');
assert.match(
applyStoryPage,
/chapterText\?\.setText\(page\.chapter\)\.setVisible\(!page\.cutscene\)/,
'cutscenes must hide the generic chapter label so it cannot overlap the result or reward strip'
);
assert.match(storySource, /chapterVisible: this\.chapterText\?\.visible \?\? false/);
const tickStatuses = privateMethodBody(battleSource, 'tickBattleStatuses');
assert.match(tickStatuses, /let burnTicked = false/);
@@ -74,6 +83,17 @@ const deploymentConfirmation = privateMethodBody(battleSource, 'confirmPreBattle
assert.match(deploymentConfirmation, /showOpeningBattleEvent\(\)/);
assert.doesNotMatch(deploymentConfirmation, /soundDirector\.playSelect\(\)/, 'opening alert must not overlap a selection cue');
const combatAssetLoading = privateMethodBody(battleSource, 'ensureScenarioCombatAssets');
assert.match(
combatAssetLoading,
/if \(this\.phase === 'deployment'\) \{\s*this\.renderSortieOrderHud\(\);\s*this\.renderDeploymentPanel\(\);/s,
'deployment text and its status HUD must be rebuilt once delayed combat assets settle'
);
const deploymentPanel = privateMethodBody(battleSource, 'renderDeploymentPanel');
assert.match(deploymentPanel, /const combatAssetsReady =/);
assert.match(deploymentPanel, /'준비 중 · 누르면 자동 시작'/);
assert.match(deploymentPanel, /combatAssetsReady\s*\? ' '/s);
const resolveDamageTarget = privateMethodBody(battleSource, 'tryResolveDamageTarget');
assert.match(resolveDamageTarget, /triggerBattleEvent\('first-engagement'[\s\S]*\{ playCue: false \}\)/);
const triggerBattleEvent = privateMethodBody(battleSource, 'triggerBattleEvent');
@@ -83,13 +103,27 @@ const movementSound = privateMethodBody(battleSource, 'playMovementSound');
assert.match(movementSound, /const minInterval = isMounted \? 125 : 180/);
assert.match(movementSound, /Math\.floor\(duration \/ minInterval\) \+ 1/);
assert.match(movementSound, /pulseCount > 1 \? duration \/ \(pulseCount - 1\) : 0/);
assert.match(movementSound, /playMovementStep\(isMounted, index, \{\s*terrain,/s);
assert.match(
movementSound,
/playMovementStep\(isMounted, index, \{\s*terrain: movementAudioTerrainForPulse\(terrainPath, index, pulseCount\),/s
);
assert.doesNotMatch(movementSound, /stopAfterMs/, 'movement one-shots should clean up on ended instead of truncating each step');
assert.equal(
countMatches(battleSource, /playMovementSound\(unit, movementDuration, movementDistance, battleMap\.terrain\[y\]\?\.\[x\]\)/g),
countMatches(battleSource, /this\.movementAudioTerrainPath\(unit, fromX, fromY, x, y\)/g),
2,
'both synchronous and asynchronous movement paths must pass destination terrain to movement audio'
'both synchronous and asynchronous movement paths must pass a sampled terrain route to movement audio'
);
assert.match(battleSource, /particles\?\.kind === 'rain'/, 'rain battles should route movement through the wet pool');
const movementPlayback = privateMethodBody(soundSource, 'playMovementEffect');
assert.match(movementPlayback, /activeVoices\.length >= this\.movementVoiceCap[\s\S]*return false/);
assert.doesNotMatch(
movementPlayback,
/retireEffectElement/,
'movement voice limiting should skip an excess pulse instead of hard-cutting a playing sample'
);
const meleeRushPlayback = publicMethodBody(soundSource, 'playMeleeRush');
assert.match(meleeRushPlayback, /return this\.playEffect\(/, 'melee rush should bypass the saturated movement-step voice group');
assert.doesNotMatch(meleeRushPlayback, /playMovementEffect/, 'melee rush must not be dropped by the movement-step ceiling');
const mapCombat = privateMethodBody(battleSource, 'playCombatMapPresentation');
assert.match(mapCombat, /contactDelayMs = Math\.max\(70, Math\.round\(duration \* 0\.42\)\)/);