feat: add narrative page turn audio cue

This commit is contained in:
2026-07-22 01:21:36 +09:00
parent 3a41f8b671
commit fa26992b56
6 changed files with 91 additions and 7 deletions

View File

@@ -19,6 +19,7 @@ const requiredTacticalCueKeys = [
'objective-success',
'objective-failure'
];
const requiredNarrativeCueKeys = ['story-page-turn'];
const requiredBattleSceneAudioMethods = [
'playSoundscape',
'playAllyTurnCue',
@@ -49,6 +50,20 @@ const battlefieldSourceRecords = [
source: 'https://pixabay.com/sound-effects/film-special-effects-failure-1-89170/'
}
];
const narrativeSourceRecord = {
projectFile: 'src/assets/audio/sfx/story-page-turn.mp3',
source: 'https://pixabay.com/sound-effects/film-special-effects-flipping-book-page-499646/',
requiredDocumentation: [
'2026-07-22',
'Flipping Book Page',
'DRAGON-STUDIO',
'48 kHz stereo MP3',
'1.0-second',
'silence-trimmed',
'fade-in/out',
'high-pass and low-pass filtered'
]
};
const server = await createServer({
logLevel: 'error',
@@ -69,7 +84,9 @@ try {
validateTrackMap(errors, 'effect', effectTracks, 'sfx');
validateRequiredTrackKeys(errors, 'battlefield ambience', ambienceTracks, requiredBattlefieldAmbienceKeys);
validateRequiredTrackKeys(errors, 'tactical cue', effectTracks, requiredTacticalCueKeys);
validateRequiredTrackKeys(errors, 'narrative cue', effectTracks, requiredNarrativeCueKeys);
validateBattlefieldSourceDocumentation(errors);
validateNarrativeSourceDocumentation(errors);
validateBattleSceneAudioIntegration(errors);
validateEffectPools(errors, effectPools, effectTracks);
validateEffectPoolRegistration(errors, effectPools);
@@ -145,6 +162,21 @@ function validateBattlefieldSourceDocumentation(errors) {
});
}
function validateNarrativeSourceDocumentation(errors) {
const docs = readFileSync(join('docs', 'audio-sources.md'), 'utf8');
if (!docs.includes(narrativeSourceRecord.projectFile)) {
errors.push(`docs/audio-sources.md is missing narrative audio file "${narrativeSourceRecord.projectFile}"`);
}
if (!docs.includes(narrativeSourceRecord.source)) {
errors.push(`docs/audio-sources.md is missing Pixabay source "${narrativeSourceRecord.source}"`);
}
narrativeSourceRecord.requiredDocumentation.forEach((value) => {
if (!docs.includes(value)) {
errors.push(`docs/audio-sources.md is missing narrative audio detail "${value}"`);
}
});
}
function validateBattleSceneAudioIntegration(errors) {
const path = join('src', 'game', 'scenes', 'BattleScene.ts');
const source = readFileSync(path, 'utf8');

View File

@@ -102,6 +102,7 @@ try {
'operation-alert': '/audio/operation-alert.ogg',
'objective-success': '/audio/objective-success.ogg',
'objective-failure': '/audio/objective-failure.ogg',
'story-page-turn': '/audio/story-page-turn.ogg',
'ui-select': '/audio/ui-select.ogg'
});
director.registerEffectPools({
@@ -387,7 +388,7 @@ try {
debug = director.getDebugState();
assert.deepEqual(
debug.semanticCues.cooldownMs,
{ turn: 900, operation: 650, objective: 1200 },
{ turn: 900, operation: 650, objective: 1200, narrative: 220 },
'semantic cue cooldowns should be visible in debug state'
);
assert.deepEqual(
@@ -417,6 +418,35 @@ try {
assert.equal(debug.activeEffectCount, 0, 'tactical cues should retire after their playback windows');
assert.equal(audioHarness.activeInstances().length, 1, 'tactical cue cleanup should leave only music active');
const narrativeCueStart = audioHarness.instances.length;
const musicDuckRevisionBeforeNarrativeCue = debug.musicDuck.revision;
assert.equal(director.playStoryAdvanceCue(), true, 'the first story advance cue should play');
assertClose(audioHarness.byOriginalSrc('/audio/story-page-turn.ogg').volume, 0.15, 'story advance cue volume');
debug = director.getDebugState();
assert.equal(
debug.musicDuck.revision,
musicDuckRevisionBeforeNarrativeCue,
'story advance cues must not duck music'
);
assert.deepEqual(
debug.semanticCues.lastPlayed,
{ group: 'narrative', key: 'story-page-turn', at: clock.now },
'debug state should expose a played story advance cue'
);
assert.equal(director.playStoryAdvanceCue(), false, 'rapid story advance cues should be coalesced');
debug = director.getDebugState();
assert.deepEqual(
debug.semanticCues.lastSuppressed,
{ group: 'narrative', key: 'story-page-turn', at: clock.now, remainingMs: 220 },
'debug state should expose a coalesced story advance cue'
);
assert.equal(audioHarness.instances.length, narrativeCueStart + 1, 'coalesced story cues must not construct Audio');
clock.advance(220);
assert.equal(director.playStoryAdvanceCue(), true, 'story advance cues should replay after the narrative cooldown');
assert.equal(audioHarness.instances.length, narrativeCueStart + 2, 'expired narrative cooldown should permit new Audio');
clock.advance(1100);
assert.equal(director.getDebugState().activeEffectCount, 0, 'story advance cues should retire after playback');
const semanticEffectStart = audioHarness.instances.length;
director.playMeleeSwing(true, false);
assert.equal(director.getDebugState().lastEffect?.key, 'melee-swing', 'melee API should use the swing pool');
@@ -455,7 +485,7 @@ try {
console.log(
`Verified dual SoundDirector channels across ${debug.music.elementRevision} music and ` +
`${debug.ambience.elementRevision} ambience element revisions, no-repeat effect pools, ` +
'coalesced tactical cues, semantic combat effects, category multipliers, and deterministic music ducking.'
'coalesced tactical and narrative cues, semantic combat effects, category multipliers, and deterministic music ducking.'
);
} finally {
restoreGlobals();