fix: preserve completion music and BGM ducking across next level
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
|
||||
import {Music} from './src/music.mjs';import {CasualAudio} from './src/audio.mjs';import {boot} from './qa/runtime-harness.mjs';
|
||||
const pack=JSON.parse(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url))),routes=JSON.parse(fs.readFileSync(new URL('./src/prepared-routes.json',import.meta.url))).routes;
|
||||
function setup(){const contexts=[];const api={getStorageSync(){},setStorageSync(){},createInnerAudioContext(){const a={stops:0,plays:0,stop(){this.stops++;},play(){this.plays++;},onError(fn){this.error=fn;},onEnded(fn){this.ended=fn;},offEnded(fn){if(this.ended===fn)this.ended=null;},onPlay(fn){this.started=fn;},offPlay(fn){if(this.started===fn)this.started=null;}};contexts.push(a);return a;}};globalThis.wx=api;const m=new Music(api);m.scene('play','one');const sound=new CasualAudio({}, {getItem(){},setItem(){}},'test');sound.beforeComplete=()=>m.duck();sound.afterComplete=()=>m.unduck();return {sound,m,contexts};}
|
||||
test('level-transition reset keeps completion source, gain and native playback; natural end restores BGM',()=>{const {sound,m}=setup();sound.play('complete');const complete=sound.channels[3],stops=complete.stops;for(let i=0;i<3;i++){sound.stop({preserveComplete:true});m.scene('play','next');assert.equal(complete.stops,stops);assert.equal(complete.src,'audio/complete.wav');assert.equal(complete.volume,.75*.85);assert.equal(m.ducked,true);assert.equal(m.audio.volume,.119*.25);}complete.ended();assert.equal(m.ducked,false);assert.equal(m.audio.volume,.119);});
|
||||
test('UI, hints, errors, restart and numeric nodes share ordinary voices while completion owns fourth channel',()=>{const {sound,m,contexts}=setup();sound.play('complete');const complete=sound.channels[3],stops=complete.stops;for(const kind of ['ui','hint','error','correct','checkpoint','restart','move','undo']){sound.stop({preserveComplete:true});sound.play(kind,0,7);assert.equal(complete.src,'audio/complete.wav');assert.equal(complete.stops,stops);assert.equal(complete.plays,1);assert.equal(m.ducked,true);}assert.equal(contexts.length,5,'one BGM plus existing four effect channels');complete.ended();assert.equal(m.ducked,false);sound.play('checkpoint',0,8);assert.equal(complete.src,'audio/checkpoint-8.wav');});
|
||||
test('a newer completion retains priority and stale completion callbacks cannot unduck it',()=>{const {sound,m}=setup();sound.play('complete');const old=sound.channels[3].ended;sound.stop({preserveComplete:true});sound.play('complete');const latest=sound.channels[3].ended;old();assert.equal(m.ducked,true);latest();assert.equal(m.ducked,false);});
|
||||
test('mute, native error and hard lifecycle stop still terminate the preserved jingle',()=>{for(const action of ['mute','error','hide']){const {sound,m}=setup();sound.play('complete');const complete=sound.channels[3],late=complete.ended;sound.stop({preserveComplete:true});if(action==='mute')sound.toggle();else if(action==='error')complete.error({errMsg:'test'});else{m.suspend();sound.stop();}assert.equal(m.ducked,false);late();assert.equal(m.ducked,false);if(action==='hide')assert.equal(m.playing,null);}});
|
||||
test('real next-level tap does not stop or reload playing jingle and does not restore BGM early',()=>{const ui=boot();ui.select(3);const id=ui.api.snapshot().level;routes[id].forEach(ui.cell);const complete=ui.audioContexts.find(a=>a.src==='audio/complete.wav');let stops=0;complete.stop=()=>stops++;const plays=ui.played.filter(p=>p==='audio/complete.wav').length;ui.button('下一关 →');assert.equal(ui.api.snapshot().displayOrder,5);assert.equal(stops,0);assert.equal(ui.api.snapshot().music.ducked,true);assert.equal(ui.api.snapshot().music.volume,.119*.25);assert.equal(ui.api.snapshot().audio.completePlaying,true);if(ui.api.buttons().some(b=>b.label==='返回游戏'))ui.button('返回游戏');ui.cell(routes[ui.api.snapshot().level][0]);ui.button('设置');assert.equal(complete.src,'audio/complete.wav');assert.equal(stops,0);assert.equal(ui.played.filter(p=>p==='audio/complete.wav').length,plays);complete.ended();assert.equal(ui.api.snapshot().music.ducked,false);assert.equal(ui.api.snapshot().music.volume,.119);assert.equal(ui.api.snapshot().audio.completePlaying,false);});
|
||||
test('background stops preserved completion and ignores its late end when resuming the unfinished next level',()=>{const ui=boot();ui.select(3);routes[ui.api.snapshot().level].forEach(ui.cell);const complete=ui.audioContexts.find(a=>a.src==='audio/complete.wav');ui.button('下一关 →');const late=complete.ended;ui.handlers.Hide();assert.equal(ui.api.snapshot().music.ducked,false);assert.equal(ui.api.snapshot().audio.completePlaying,false);late();assert.equal(ui.api.snapshot().music.playing,null);ui.handlers.Show();assert.equal(ui.api.snapshot().displayOrder,5);assert.equal(ui.api.snapshot().music.volume,.119);});
|
||||
|
||||
test('a failing next-level ordinary voice cannot release completion ducking early',()=>{const {sound,m}=setup();sound.play('complete');sound.stop({preserveComplete:true});sound.channels[0].play=()=>{throw Error('ordinary voice unavailable');};sound.play('checkpoint',0,7);assert.equal(m.ducked,true);sound.channels[3].ended();assert.equal(m.ducked,false);});
|
||||
Reference in New Issue
Block a user