fix: preserve completion music and BGM ducking across next level
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
# iOS完成音播放中立即切关:诊断与修订
|
||||||
|
|
||||||
|
## 已知事实
|
||||||
|
|
||||||
|
用户在iOS微信小游戏中发现:通关短音乐和彩带仍播放时立即点击下一关会停顿,关闭音效则不卡。卡顿不应描述为单独的通关动画启动卡顿。
|
||||||
|
|
||||||
|
旧调用链为:下一关按钮→上游start适配入口→feedback.reset()→CasualAudio.stop()。stop先调用finishComplete解除BGM压低,再停止四个音效通道,其中包括正在播放完成WAV的第四路。
|
||||||
|
|
||||||
|
qa/jingle-next-trace.mjs以dc83712的真实游戏包和当前游戏包驱动同一个按钮流程,对音频接口做指令计数。旧包对活动完成通道调用stop为1次,切关后BGM为0.119;新版为0次,完成音src保持不变,切关后BGM仍为0.02975,待自然结束恢复0.119。这说明强制中断路径确实存在,并被移除;它不是对iOS底层stop耗时的直接测量。
|
||||||
|
|
||||||
|
## 修订
|
||||||
|
|
||||||
|
反馈reset增加显式preserveComplete选项,仅换关/重开使用;后台、静音、音频中断仍硬停止。完成音结束回调按具体完成播放对象的身份保护,独立于关卡输入事件serial。新关卡声音在完成音期间使用其余三路,避免节点或UI复用第四路时再次触发stop。未增加音效播放器数量,未改动WAV资源或A3绘制。
|
||||||
|
|
||||||
|
## 验收边界
|
||||||
|
|
||||||
|
自动化覆盖跨关续播、完成音自然结束后BGM恢复、连续完成与过期回调、UI/节点抢占、静音、后台和播放异常。iOS真机仍需重新验证“短音乐播放期间立刻点下一关”,不能凭这份调用链报告宣称真机帧率已达标。
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
## 2026-10-23:过关音乐跨关续播与iOS切关排查
|
||||||
|
|
||||||
|
版本2026-10-23-jingle-through-next,基于dc83712(已发布A3,包含6abbc37与8f2263b)。独立包:../releases/NumberSwipe-main-A3过关音乐跨关续播-20261023.zip。保留旧发布包和解压目录,不上传、不推送。
|
||||||
|
|
||||||
|
- 用户真机对照:iOS中完成音/彩带播放期间立即点击下一关会卡一下;关闭音效后不卡。旧代码start→feedback.reset→audio.stop会向正在播放的完成音发出stop,并立刻finishComplete→BGM恢复。实际旧/新游戏包调用链对照见qa/jingle-next-trace-20261023.json,属于原生调用指令取证,不是iOS耗时测量。
|
||||||
|
- 按用户新要求,切关只取消旧关视觉、震动和过期落格音;保留正在播的3秒过关短音乐,既不stop、不改src,也不重播。BGM继续保持基础音量25%,由这首短音乐自身的自然结束回调恢复正常。
|
||||||
|
- 完成音占用第四路期间,新关卡移动、节点、指路和按钮音效借用原有三路普通池,保持四路音效上限,不抢占完成音;保持短音乐音量0.6375、基础BGM音量0.119及音高规则。
|
||||||
|
- 普通换关和重开保留正在播放的完成音。关闭音效、后台/音频中断、播放失败仍按原规则取消;再次完成新关卡维持完成反馈优先级。旧结束回调不能把新一轮完成音的BGM提前恢复。
|
||||||
|
- 检查包含117项模块测试、200关触摸回归及旧/新调用链对照。针对用例覆盖真实下一关按钮、规则/UI/节点不抢占、自然结束恢复、后台与静音、连续完成和异常分支。日志见qa/jingle-next-check-20261023.txt。
|
||||||
|
- 本版已移除“点下一关时停止正在播的完成音”的原生调用,但尚无本机iOS性能追踪;不能将浏览器或测试替身的结果称为真机卡顿已消除。请用新包真机复测这一明确时机。
|
||||||
|
|
||||||
## 2026-10-23:A3明亮软糖指路(包含6abbc37)
|
## 2026-10-23:A3明亮软糖指路(包含6abbc37)
|
||||||
|
|
||||||
版本2026-10-23-soft-ribbon-a3-bright,以6abbc37为基础,包含iOS通关早点击负载优化、奇偶节点彩带、选关/规则/设置统一音效、设置冒号和“乐”字字重修复。独立包:../releases/NumberSwipe-main-A3明亮软糖指路-20261023.zip。旧包、旧解压目录不变;不上传、不推送。
|
版本2026-10-23-soft-ribbon-a3-bright,以6abbc37为基础,包含iOS通关早点击负载优化、奇偶节点彩带、选关/规则/设置统一音效、设置冒号和“乐”字字重修复。独立包:../releases/NumberSwipe-main-A3明亮软糖指路-20261023.zip。旧包、旧解压目录不变;不上传、不推送。
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
## 2026-10-23:过关音乐跨关续播与iOS切关排查
|
||||||
|
|
||||||
|
版本2026-10-23-jingle-through-next,基于dc83712(已发布A3,包含6abbc37与8f2263b)。独立包:../releases/NumberSwipe-main-A3过关音乐跨关续播-20261023.zip。保留旧发布包和解压目录,不上传、不推送。
|
||||||
|
|
||||||
|
- 用户真机对照:iOS中完成音/彩带播放期间立即点击下一关会卡一下;关闭音效后不卡。旧代码start→feedback.reset→audio.stop会向正在播放的完成音发出stop,并立刻finishComplete→BGM恢复。实际旧/新游戏包调用链对照见qa/jingle-next-trace-20261023.json,属于原生调用指令取证,不是iOS耗时测量。
|
||||||
|
- 按用户新要求,切关只取消旧关视觉、震动和过期落格音;保留正在播的3秒过关短音乐,既不stop、不改src,也不重播。BGM继续保持基础音量25%,由这首短音乐自身的自然结束回调恢复正常。
|
||||||
|
- 完成音占用第四路期间,新关卡移动、节点、指路和按钮音效借用原有三路普通池,保持四路音效上限,不抢占完成音;保持短音乐音量0.6375、基础BGM音量0.119及音高规则。
|
||||||
|
- 普通换关和重开保留正在播放的完成音。关闭音效、后台/音频中断、播放失败仍按原规则取消;再次完成新关卡维持完成反馈优先级。旧结束回调不能把新一轮完成音的BGM提前恢复。
|
||||||
|
- 检查包含117项模块测试、200关触摸回归及旧/新调用链对照。针对用例覆盖真实下一关按钮、规则/UI/节点不抢占、自然结束恢复、后台与静音、连续完成和异常分支。日志见qa/jingle-next-check-20261023.txt。
|
||||||
|
- 本版已移除“点下一关时停止正在播的完成音”的原生调用,但尚无本机iOS性能追踪;不能将浏览器或测试替身的结果称为真机卡顿已消除。请用新包真机复测这一明确时机。
|
||||||
|
|
||||||
## 2026-10-23:A3明亮软糖指路(包含6abbc37)
|
## 2026-10-23:A3明亮软糖指路(包含6abbc37)
|
||||||
|
|
||||||
版本2026-10-23-soft-ribbon-a3-bright,以6abbc37为基础,包含iOS通关早点击负载优化、奇偶节点彩带、选关/规则/设置统一音效、设置冒号和“乐”字字重修复。独立包:../releases/NumberSwipe-main-A3明亮软糖指路-20261023.zip。旧包、旧解压目录不变;不上传、不推送。
|
版本2026-10-23-soft-ribbon-a3-bright,以6abbc37为基础,包含iOS通关早点击负载优化、奇偶节点彩带、选关/规则/设置统一音效、设置冒号和“乐”字字重修复。独立包:../releases/NumberSwipe-main-A3明亮软糖指路-20261023.zip。旧包、旧解压目录不变;不上传、不推送。
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
## 2026-10-23:过关音乐跨关续播与iOS切关排查
|
||||||
|
|
||||||
|
版本2026-10-23-jingle-through-next,基于dc83712(已发布A3,包含6abbc37与8f2263b)。独立包:../releases/NumberSwipe-main-A3过关音乐跨关续播-20261023.zip。保留旧发布包和解压目录,不上传、不推送。
|
||||||
|
|
||||||
|
- 用户真机对照:iOS中完成音/彩带播放期间立即点击下一关会卡一下;关闭音效后不卡。旧代码start→feedback.reset→audio.stop会向正在播放的完成音发出stop,并立刻finishComplete→BGM恢复。实际旧/新游戏包调用链对照见qa/jingle-next-trace-20261023.json,属于原生调用指令取证,不是iOS耗时测量。
|
||||||
|
- 按用户新要求,切关只取消旧关视觉、震动和过期落格音;保留正在播的3秒过关短音乐,既不stop、不改src,也不重播。BGM继续保持基础音量25%,由这首短音乐自身的自然结束回调恢复正常。
|
||||||
|
- 完成音占用第四路期间,新关卡移动、节点、指路和按钮音效借用原有三路普通池,保持四路音效上限,不抢占完成音;保持短音乐音量0.6375、基础BGM音量0.119及音高规则。
|
||||||
|
- 普通换关和重开保留正在播放的完成音。关闭音效、后台/音频中断、播放失败仍按原规则取消;再次完成新关卡维持完成反馈优先级。旧结束回调不能把新一轮完成音的BGM提前恢复。
|
||||||
|
- 检查包含117项模块测试、200关触摸回归及旧/新调用链对照。针对用例覆盖真实下一关按钮、规则/UI/节点不抢占、自然结束恢复、后台与静音、连续完成和异常分支。日志见qa/jingle-next-check-20261023.txt。
|
||||||
|
- 本版已移除“点下一关时停止正在播的完成音”的原生调用,但尚无本机iOS性能追踪;不能将浏览器或测试替身的结果称为真机卡顿已消除。请用新包真机复测这一明确时机。
|
||||||
|
|
||||||
## 2026-10-23:A3明亮软糖指路(包含6abbc37)
|
## 2026-10-23:A3明亮软糖指路(包含6abbc37)
|
||||||
|
|
||||||
版本2026-10-23-soft-ribbon-a3-bright,以6abbc37为基础,包含iOS通关早点击负载优化、奇偶节点彩带、选关/规则/设置统一音效、设置冒号和“乐”字字重修复。独立包:../releases/NumberSwipe-main-A3明亮软糖指路-20261023.zip。旧包、旧解压目录不变;不上传、不推送。
|
版本2026-10-23-soft-ribbon-a3-bright,以6abbc37为基础,包含iOS通关早点击负载优化、奇偶节点彩带、选关/规则/设置统一音效、设置冒号和“乐”字字重修复。独立包:../releases/NumberSwipe-main-A3明亮软糖指路-20261023.zip。旧包、旧解压目录不变;不上传、不推送。
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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);});
|
||||||
@@ -2,14 +2,14 @@ export function applyLayeredFeedback(source){
|
|||||||
const replace=(old,value)=>{if(!source.includes(old))throw Error('Layered feedback seam changed: '+old.slice(0,80));source=source.replace(old,value);};
|
const replace=(old,value)=>{if(!source.includes(old))throw Error('Layered feedback seam changed: '+old.slice(0,80));source=source.replace(old,value);};
|
||||||
source="import {feedback} from '../feedback.mjs';\nimport {inBacktrackCore} from '../backtrack.mjs';\n"+source;
|
source="import {feedback} from '../feedback.mjs';\nimport {inBacktrackCore} from '../backtrack.mjs';\n"+source;
|
||||||
replace('const labels=new Map();',"feedback.attach(sound,wx,localStorage);\nconst labels=new Map();");
|
replace('const labels=new Map();',"feedback.attach(sound,wx,localStorage);\nconst labels=new Map();");
|
||||||
replace('function start(i,open=true){','function start(i,open=true){feedback.reset();');
|
replace('function start(i,open=true){','function start(i,open=true){feedback.reset({preserveComplete:true});');
|
||||||
replace("function markError(id,message){sound.play('error');","function markError(id,message){feedback.emit('error',level,id,path.at(-1)===id?path.length:path.length+1);");
|
replace("function markError(id,message){sound.play('error');","function markError(id,message){feedback.emit('error',level,id,path.at(-1)===id?path.length:path.length+1);");
|
||||||
replace("record.backtracks++;path.pop();sound.play('undo');","record.backtracks++;const removed=path.pop();feedback.emit('undo',level,removed,path.length,{from:path.at(-1)});");
|
replace("record.backtracks++;path.pop();sound.play('undo');","record.backtracks++;const removed=path.pop();feedback.emit('undo',level,removed,path.length,{from:path.at(-1)});");
|
||||||
replace("path.pop();sound.play('undo');record.backtracks++;","const removed=path.pop();feedback.emit('undo',level,removed,path.length,{from:path.at(-1)});record.backtracks++;");
|
replace("path.pop();sound.play('undo');record.backtracks++;","const removed=path.pop();feedback.emit('undo',level,removed,path.length,{from:path.at(-1)});record.backtracks++;");
|
||||||
const audio="if(path.length<level.cells.length)sound.play(cell.clue!==undefined&&cell.clue===path.length?(cell.clue>1?'checkpoint':'correct'):'move',0,path.length)";
|
const audio="if(path.length<level.cells.length)sound.play(cell.clue!==undefined&&cell.clue===path.length?(cell.clue>1?'checkpoint':'correct'):'move',0,path.length)";
|
||||||
replace(audio,"if(path.length<level.cells.length)feedback.emit((cell.clue>1&&cell.clue===path.length)||cell.parity||cell.direction?'checkpoint':'move',level,id,path.length,{from:path.at(-2),ribbon:Boolean(cell.parity)||(cell.clue>1&&cell.clue===path.length)})");
|
replace(audio,"if(path.length<level.cells.length)feedback.emit((cell.clue>1&&cell.clue===path.length)||cell.parity||cell.direction?'checkpoint':'move',level,id,path.length,{from:path.at(-2),ribbon:Boolean(cell.parity)||(cell.clue>1&&cell.clue===path.length)})");
|
||||||
replace("sound.play('complete',0)","feedback.emit('complete',level,id,path.length,{from:path.at(-2)})");
|
replace("sound.play('complete',0)","feedback.emit('complete',level,id,path.length,{from:path.at(-2)})");
|
||||||
replace("sound.unlock();sound.play('restart');","feedback.reset();sound.unlock();sound.play('restart');");
|
replace("sound.unlock();sound.play('restart');","feedback.reset({preserveComplete:true});sound.unlock();sound.play('restart');");
|
||||||
replace('advance(hit(a[0]+(p[0]-a[0])*i/steps,a[1]+(p[1]-a[1])*i/steps))','advanceAt(a[0]+(p[0]-a[0])*i/steps,a[1]+(p[1]-a[1])*i/steps)');
|
replace('advance(hit(a[0]+(p[0]-a[0])*i/steps,a[1]+(p[1]-a[1])*i/steps))','advanceAt(a[0]+(p[0]-a[0])*i/steps,a[1]+(p[1]-a[1])*i/steps)');
|
||||||
source+=`\nfunction advanceAt(x,y){const id=hit(x,y);if(path.length>=2&&id===path.at(-2)&&!inBacktrackCore(polygons,id,x,y,unit))return;advance(id);}\n`;
|
source+=`\nfunction advanceAt(x,y){const id=hit(x,y);if(path.length>=2&&id===path.at(-2)&&!inBacktrackCore(polygons,id,x,y,unit))return;advance(id);}\n`;
|
||||||
for(const event of ['pointerdown','pointermove']){
|
for(const event of ['pointerdown','pointermove']){
|
||||||
|
|||||||
@@ -135,6 +135,10 @@
|
|||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"value": "soft-hint.test.mjs"
|
"value": "soft-hint.test.mjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"value": "jingle-next.test.mjs"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
|
||||||
|
> numberswipe-main@1.0.0 check
|
||||||
|
> npm run build && npm test && npm run smoke
|
||||||
|
|
||||||
|
|
||||||
|
> numberswipe-main@1.0.0 build
|
||||||
|
> node build.mjs
|
||||||
|
|
||||||
|
Built campaign-200-r2: 200 levels
|
||||||
|
|
||||||
|
> numberswipe-main@1.0.0 test
|
||||||
|
> node --test *.test.mjs
|
||||||
|
|
||||||
|
✔ 手机没有浏览器音频接口时,原生播放器仍能播放全部 36 步和六类反馈音 (9.8918ms)
|
||||||
|
✔ 关闭音效停止播放;再次开启会试听并保存偏好;后台中断停止所有播放器 (1.0694ms)
|
||||||
|
✔ 延迟通关音在静音或进入后台时取消,不在返回后突然播放 (65.2169ms)
|
||||||
|
✔ 音频错误可诊断,不再静默吞掉 (1.1338ms)
|
||||||
|
✔ 所有音频均为非静音 PCM;原版第13步比第1步高一个八度 (108.5817ms)
|
||||||
|
✔ 教学和组合规则用具体动作及奇偶例子解释,没有未解释的术语 (15.5719ms)
|
||||||
|
✔ 过关提示同步使用实际播放起点,非请求时间 (0.8257ms)
|
||||||
|
✔ 过关短句使用用户提供的3秒WAV,不与旧和弦叠加 (2.1141ms)
|
||||||
|
✔ jingle end, cancellation and playback error restore BGM without restarting (11.547ms)
|
||||||
|
✔ muted effects never duck BGM and unrelated effect errors cannot end jingle ducking (0.6787ms)
|
||||||
|
✔ hide and music preference do not revive playback through a late jingle callback (1.9364ms)
|
||||||
|
✔ BGMB is absent from the runtime manifest and music package (1.6944ms)
|
||||||
|
✔ merged irregular outlines retain holes and never draw internal unit edges (89.6286ms)
|
||||||
|
✔ all 200 boards fit four screen budgets with stable safe anchors and real shared entrances (1515.6813ms)
|
||||||
|
✔ guides through every irregular block stay inside its occupied shape (63.4561ms)
|
||||||
|
✔ visited, conflicting and hinted category cells retain their material identity (19.0667ms)
|
||||||
|
✔ five font subsets match their manifest and fit the package budget (5.3115ms)
|
||||||
|
✔ upstream adapter fails closed if the inspected render seams change (1.3091ms)
|
||||||
|
✔ procedural cushion pixels preserve holes and light the upper bevel without a border (36.6093ms)
|
||||||
|
✔ visible glyph bounds are centered independently of font ascent and side bearings (2.7968ms)
|
||||||
|
✔ stage with zero depth casts alpha shadows without painting a solid backing (17.0614ms)
|
||||||
|
✔ native width-only TextMetrics still centers the rasterized glyph ink (4.0689ms)
|
||||||
|
✔ fixed channels remain anchored at their block centers after number enlargement (1664.2221ms)
|
||||||
|
✔ checkpoint ribbons trigger on correct arrival only and never duplicate per frame (5.5755ms)
|
||||||
|
✔ checkpoint pulse is small, centered in time and settles without looping (0.6015ms)
|
||||||
|
✔ event batches preserve every landing and sequence but commit only highest audio and haptic (13.6405ms)
|
||||||
|
✔ completion waits at most 120ms and ignores late audio callbacks (4.0339ms)
|
||||||
|
✔ light haptics are limited, checkpoint replaces light, and completion adds only one delayed short pulse (2.1007ms)
|
||||||
|
✔ unsupported graded haptic falls back to one short pulse, never long vibration (1.1535ms)
|
||||||
|
✔ haptic disable, preview and lifecycle cancellation suppress delayed or expired work (1.1302ms)
|
||||||
|
✔ late failed haptic requests cannot revive old pulses and ungraded completion falls back once (0.4838ms)
|
||||||
|
✔ rectangles, concave shapes and hexagons require geometric inward distance without treating internal seams as walls (11.9548ms)
|
||||||
|
✔ one high-speed native move records all intermediate cells and completion without duplicate final node (255.9046ms)
|
||||||
|
✔ legal but off-solution moves use public rules, while invalid clues only produce error (323.3563ms)
|
||||||
|
✔ drag near a previous-block boundary cannot undo; deeper entry can, and undo button remains immediate (253.6439ms)
|
||||||
|
✔ tutorial, redraw, hint and restored saved routes do not generate player feedback (256.1896ms)
|
||||||
|
✔ pressure and rebound have bounded amplitude and duration (0.5723ms)
|
||||||
|
✔ interpolation cannot duplicate the same failed-cell event within one native movement (0.6345ms)
|
||||||
|
✔ nodes are driven by explicit events even when multiple arrive before rendering; at most two ribbons survive (98.1338ms)
|
||||||
|
✔ audio fast moves merge to latest pitch and completion stops the whole four-channel pool (3.5604ms)
|
||||||
|
✔ settings persist haptic preference independently and chapter cards always use three columns (178.6448ms)
|
||||||
|
✔ original audio is preserved and high pitches lose energy rather than growing sharper (19.2762ms)
|
||||||
|
✔ empty, partial and wrong routes reveal the first incomplete full segment (6.9429ms)
|
||||||
|
✔ completion hides the hint; repeat clicks cannot skip; undo and restart return to earlier segments (0.8126ms)
|
||||||
|
✔ all 200 levels reveal consecutive segments without changing the player path (3113.4039ms)
|
||||||
|
✔ hint draws skip player edges and mask visited cells on their own layer; compositing is restored (2.9868ms)
|
||||||
|
✔ parity is a segment anchor; colour is not; flow moves continuously across cells (2.1693ms)
|
||||||
|
✔ soft teaching ribbon preserves visited masks without arrows in reduced motion (2.4006ms)
|
||||||
|
✔ hint rounds shared guide junctions and keeps separate runs disconnected (0.8447ms)
|
||||||
|
✔ level-transition reset keeps completion source, gain and native playback; natural end restores BGM (5.4094ms)
|
||||||
|
✔ UI, hints, errors, restart and numeric nodes share ordinary voices while completion owns fourth channel (0.8477ms)
|
||||||
|
✔ a newer completion retains priority and stale completion callbacks cannot unduck it (0.4534ms)
|
||||||
|
✔ mute, native error and hard lifecycle stop still terminate the preserved jingle (0.745ms)
|
||||||
|
✔ real next-level tap does not stop or reload playing jingle and does not restore BGM early (376.0563ms)
|
||||||
|
✔ background stops preserved completion and ignores its late end when resuming the unfinished next level (279.6686ms)
|
||||||
|
✔ a failing next-level ordinary voice cannot release completion ducking early (0.7784ms)
|
||||||
|
✔ 实际教学关动画:basic (6.571ms)
|
||||||
|
✔ 实际教学关动画:walls (33.4329ms)
|
||||||
|
✔ 实际教学关动画:straight (2.4198ms)
|
||||||
|
✔ 实际教学关动画:vertical (5.7743ms)
|
||||||
|
✔ 实际教学关动画:corner (1.0693ms)
|
||||||
|
✔ 实际教学关动画:colour (0.7618ms)
|
||||||
|
✔ 实际教学关动画:parity (0.6378ms)
|
||||||
|
✔ 实际教学关动画:rhythm-door (0.9217ms)
|
||||||
|
✔ 实际教学关动画:hex (0.8469ms)
|
||||||
|
✔ 实际教学关动画:unknown-start (1.5717ms)
|
||||||
|
✔ 200关的组合规则均能找到对应真实教学关 (318.4051ms)
|
||||||
|
✔ 教学减速,结束停留1秒后自动循环 (3.7093ms)
|
||||||
|
✔ music loops independently, persists mute and cancels in background (9.3779ms)
|
||||||
|
✔ old arrows retain distance, rush to endpoint and fresh arrows move at base speed (2.3741ms)
|
||||||
|
✔ settings have exactly three independent preferences, and hint has its own cue (415.6986ms)
|
||||||
|
✔ actual fixed numeric node uses checkpoint pitch, final cell uses complete chord (279.1492ms)
|
||||||
|
✔ selected menu and BGMA loop are stereo PCM WAV with complete musical durations (14.263ms)
|
||||||
|
✔ selected runtime WAVs retain the original durations after sample-rate adaptation (21.038ms)
|
||||||
|
✔ gameplay ambient uses lower scene volume and keeps independent mute and resume behavior (0.7343ms)
|
||||||
|
✔ all 200 levels: valid numeric, parity and direction arrivals play checkpoint pitch; final arrival only completes (37426.224ms)
|
||||||
|
✔ drag back and undo button both vibrate lightly; disabling vibration suppresses both (61.9501ms)
|
||||||
|
✔ haptic cadence is approximately 15 percent denser without changing light or medium grades (1.9877ms)
|
||||||
|
✔ regular and special scale settle at 90ms with a larger special peak (0.2442ms)
|
||||||
|
✔ rendered undo target and nonnumeric checkpoint animate immediately without adding ribbons (7.7073ms)
|
||||||
|
✔ quick undo cancels removed node rebound, including last-step removal (2.6317ms)
|
||||||
|
✔ navigation buttons use the same selection cue and settings include uniform colons (48.7545ms)
|
||||||
|
✔ early next during confetti cancels warming; settled ceremony reuses its background (202.4678ms)
|
||||||
|
✔ font corpus includes every settings label including music le glyph (0.6479ms)
|
||||||
|
✔ first launch goes directly to gameplay (186.7249ms)
|
||||||
|
✔ resume unfinished route after exit from gameplay (526.302ms)
|
||||||
|
✔ resume unfinished route after exit from selection (387.8133ms)
|
||||||
|
✔ completed level resumes next unfinished, including leaving from cleared screen (180.3974ms)
|
||||||
|
✔ replaying a cleared level does not replace the unfinished save (170.0196ms)
|
||||||
|
✔ legacy saves, malformed data and full completion have valid startup targets (152.2722ms)
|
||||||
|
✔ selection pages expose fifteen cards and fit phone safe areas (473.1698ms)
|
||||||
|
✔ hint-only frames reuse static UI, and player input invalidates it (100.3652ms)
|
||||||
|
✔ hint curves are cached and distance lookup is stable at boundaries (1.8835ms)
|
||||||
|
✔ new-mechanic lesson reuses the board while the continuous cursor advances (95.6831ms)
|
||||||
|
✔ BGMA stays continuous across level changes and completion redraws (5.0744ms)
|
||||||
|
✔ subpackage completion never plays a stale scene after hiding, muting, or clearing (0.6819ms)
|
||||||
|
✔ music load failure is silent and one real resume can retry; muted pending loads stay silent (0.9205ms)
|
||||||
|
✔ runtime uses supplied WAV hashes and ships music as a subpackage with a small main bundle (235.8265ms)
|
||||||
|
✔ title is centered and selection settings share the progress and level-range row (542.0741ms)
|
||||||
|
✔ completion ducks BGMA until jingle ends and next level never restarts it (247.148ms)
|
||||||
|
✔ title artwork is a transparent, high resolution PNG packaged locally (3.2742ms)
|
||||||
|
✔ asynchronous native image load requests a repaint and draws the artwork without a font substitute (0.7537ms)
|
||||||
|
✔ artwork stays above progress pills and clear of settings; fifteen cards still fit all phone sizes (659.6098ms)
|
||||||
|
✔ A3 width, one wide patch and brighter body preserve original design proportions (7.7278ms)
|
||||||
|
✔ ribbon caches its body, draws source-atop only offscreen and starts each frame clean (11.3619ms)
|
||||||
|
✔ one patch travels at 60px/s on the full route and does not restart when player advances (0.5258ms)
|
||||||
|
✔ reduced motion and missing canvas use a static ribbon without main-board compositing (0.7072ms)
|
||||||
|
✔ real target contours and smoothed routes stay attached to actual 200-level geometry (1801.2384ms)
|
||||||
|
✔ live target breath uses cached board layers, arrival and background stop obsolete visuals (206.814ms)
|
||||||
|
✔ undo button reverses seven played steps as 7,6,5,4,3,2,1 (587.6605ms)
|
||||||
|
✔ drag reverses seven played steps as 7,6,5,4,3,2,1 (254.5839ms)
|
||||||
|
✔ completion amplitude is reduced 15 percent while node, undo and restart volumes stay unchanged (1.7431ms)
|
||||||
|
✔ fast reverse coalesces to the latest descending pitch and cannot replay an old forward note (29.8648ms)
|
||||||
|
✔ all prepared routes match the immutable level hash and public rules (31.698ms)
|
||||||
|
✔ entering next level draws the live board once and preserves the running BGMA player (2261.4085ms)
|
||||||
|
✔ next-board warming advances one item per frame and cancels on deadline, replacement or hide (0.9236ms)
|
||||||
|
✔ idle material warming never changes the player path or emits sound and vibration (52.1869ms)
|
||||||
|
ℹ tests 117
|
||||||
|
ℹ suites 0
|
||||||
|
ℹ pass 117
|
||||||
|
ℹ fail 0
|
||||||
|
ℹ cancelled 0
|
||||||
|
ℹ skipped 0
|
||||||
|
ℹ todo 0
|
||||||
|
ℹ duration_ms 38422.2234
|
||||||
|
|
||||||
|
> numberswipe-main@1.0.0 smoke
|
||||||
|
> node smoke.mjs
|
||||||
|
|
||||||
|
PASS 200/200: r2源文件哈希一致、唯一解、微信发布包触摸通关、撤销/重开/指路、存档/音效设置恢复、前后台切换。
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"method": "Actual game bundle native-command trace using mocked audio API; not iOS timing",
|
||||||
|
"before": {
|
||||||
|
"level": 5,
|
||||||
|
"stopsOnPlayingComplete": 1,
|
||||||
|
"completeSourceUnchanged": true,
|
||||||
|
"bgmDucked": false,
|
||||||
|
"bgmVolume": 0.119
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"level": 5,
|
||||||
|
"stopsOnPlayingComplete": 0,
|
||||||
|
"completeSourceUnchanged": true,
|
||||||
|
"bgmDucked": true,
|
||||||
|
"bgmVolume": 0.02975
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import cp from 'node:child_process';import fs from 'node:fs';import {boot} from './runtime-harness.mjs';
|
||||||
|
const routes=JSON.parse(fs.readFileSync(new URL('../src/prepared-routes.json',import.meta.url))).routes;
|
||||||
|
function inspect(source){const ui=boot(new Map(),375,812,{source});ui.select(3);routes[ui.api.snapshot().level].forEach(ui.cell);const a=ui.audioContexts.find(a=>a.src==='audio/complete.wav');let stops=0;a.stop=()=>stops++;const originalSource=a.src;ui.button('下一关 →');return {level:ui.api.snapshot().displayOrder,stopsOnPlayingComplete:stops,completeSourceUnchanged:a.src===originalSource,bgmDucked:ui.api.snapshot().music.ducked,bgmVolume:ui.api.snapshot().music.volume};}
|
||||||
|
const old=cp.execFileSync('git',['show','dc83712:wechat-campaign-200-r2/game.js'],{encoding:'utf8',maxBuffer:4*1024*1024});console.log(JSON.stringify({method:'Actual game bundle native-command trace using mocked audio API; not iOS timing',before:inspect(old),after:inspect()},null,2));
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'node:fs';import vm from 'node:vm';import assert from 'node:assert/strict';import path from 'node:path';import {fileURLToPath} from 'node:url';
|
import fs from 'node:fs';import vm from 'node:vm';import assert from 'node:assert/strict';import path from 'node:path';import {fileURLToPath} from 'node:url';
|
||||||
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');const pack=JSON.parse(fs.readFileSync(path.join(root,'src/upstream/levels.json'))),ordered=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');const pack=JSON.parse(fs.readFileSync(path.join(root,'src/upstream/levels.json'))),ordered=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||||||
export function boot(storage = new Map(), width = 375, height = 812) {
|
export function boot(storage = new Map(), width = 375, height = 812, {source} = {}) {
|
||||||
const handlers = {}, frames = new Map(), played=[],vibrated=[],audioContexts=[];
|
const handlers = {}, frames = new Map(), played=[],vibrated=[],audioContexts=[];
|
||||||
let frameId = 0,now=100000;
|
let frameId = 0,now=100000;
|
||||||
class Clock extends Date {static now(){return now;}}
|
class Clock extends Date {static now(){return now;}}
|
||||||
@@ -11,7 +11,7 @@ export function boot(storage = new Map(), width = 375, height = 812) {
|
|||||||
for (const name of ['TouchStart', 'TouchMove', 'TouchEnd', 'TouchCancel', 'Hide', 'Show', 'WindowResize'])
|
for (const name of ['TouchStart', 'TouchMove', 'TouchEnd', 'TouchCancel', 'Hide', 'Show', 'WindowResize'])
|
||||||
wx['on' + name] = cb => handlers[name] = cb;
|
wx['on' + name] = cb => handlers[name] = cb;
|
||||||
const global = {};
|
const global = {};
|
||||||
vm.runInNewContext(fs.readFileSync(path.join(root, 'game.js'), 'utf8'), {Date:Clock, wx, GameGlobal: global, requestAnimationFrame: cb => { frames.set(++frameId, cb); return frameId; }, cancelAnimationFrame: id => frames.delete(id), setTimeout: () => 1, clearTimeout: () => { } }, { timeout: 10000 });
|
vm.runInNewContext(source||fs.readFileSync(path.join(root, 'game.js'), 'utf8'), {Date:Clock, wx, GameGlobal: global, requestAnimationFrame: cb => { frames.set(++frameId, cb); return frameId; }, cancelAnimationFrame: id => frames.delete(id), setTimeout: () => 1, clearTimeout: () => { } }, { timeout: 10000 });
|
||||||
const tick = () => { const callbacks=[...frames.values()];frames.clear();text.length=0;images.length=0;for(const fn of callbacks)fn(); };
|
const tick = () => { const callbacks=[...frames.values()];frames.clear();text.length=0;images.length=0;for(const fn of callbacks)fn(); };
|
||||||
tick();
|
tick();
|
||||||
const touch = (kind, x, y) => handlers[kind]({ changedTouches: [{ identifier: 1, clientX: x, clientY: y }], touches: [{ identifier: 1, clientX: x, clientY: y }] });
|
const touch = (kind, x, y) => handlers[kind]({ changedTouches: [{ identifier: 1, clientX: x, clientY: y }], touches: [{ identifier: 1, clientX: x, clientY: y }] });
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"channel": "main",
|
"channel": "main",
|
||||||
"version": "2026-10-23-soft-ribbon-a3-bright",
|
"version": "2026-10-23-jingle-through-next",
|
||||||
"date": "2026-10-23",
|
"date": "2026-10-23",
|
||||||
"baseVersion": "2026-10-22-resume-chapter-lessons",
|
"baseVersion": "2026-10-22-resume-chapter-lessons",
|
||||||
"baseMainSHA256": "bb35d5edf8d96eb3253c78052d219b32f9a4241efd9971537f6c9c8896188125",
|
"baseMainSHA256": "bb35d5edf8d96eb3253c78052d219b32f9a4241efd9971537f6c9c8896188125",
|
||||||
"package": "NumberSwipe-main-A3明亮软糖指路-20261023.zip",
|
"package": "NumberSwipe-main-A3过关音乐跨关续播-20261023.zip",
|
||||||
"promotedFrom": "2026-10-22-layered-feedback-music-flow",
|
"promotedFrom": "2026-10-22-layered-feedback-music-flow",
|
||||||
"previousVersion": "2026-10-23-ios-clear-ui-fix",
|
"previousVersion": "2026-10-23-soft-ribbon-a3-bright",
|
||||||
"baseCommit": "6abbc37",
|
"baseCommit": "dc83712",
|
||||||
"includes": [
|
"includes": [
|
||||||
"ios-clear-ui-fix",
|
"ios-clear-ui-fix",
|
||||||
"soft-ribbon-a3-bright"
|
"soft-ribbon-a3-bright",
|
||||||
|
"jingle-through-next"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import {FEEDBACK} from './feedback-params.mjs';
|
import {FEEDBACK} from './feedback-params.mjs';
|
||||||
const instances=new Set();
|
const instances=new Set();
|
||||||
export const audioDiagnostics={backend:'InnerAudioContext',lastError:null,plays:0,mergedMoves:0,completeRequestedAt:null,completeStartedAt:null};
|
export const audioDiagnostics={backend:'InnerAudioContext',lastError:null,plays:0,mergedMoves:0,completeRequestedAt:null,completeStartedAt:null,completePlaying:false,preservedTransitions:0};
|
||||||
export function stopAudio(){for(const instance of instances)instance.stop();}
|
export function stopAudio(){for(const instance of instances)instance.stop();}
|
||||||
export class CasualAudio{
|
export class CasualAudio{
|
||||||
constructor(host,storage,key){Object.assign(this,{storage,key,channels:[],cursor:0,timers:new Set(),lastEvent:null,lastAt:{},lastMove:-Infinity,serial:0,variant:'enhanced'});try{this.enabled=storage.getItem(key)!=='off';}catch{this.enabled=true;}instances.add(this);}
|
constructor(host,storage,key){Object.assign(this,{storage,key,channels:[],cursor:0,timers:new Set(),lastEvent:null,lastAt:{},lastMove:-Infinity,serial:0,variant:'enhanced'});try{this.enabled=storage.getItem(key)!=='off';}catch{this.enabled=true;}instances.add(this);}
|
||||||
unlock(){if(!this.enabled||!wx.createInnerAudioContext)return false;try{wx.setInnerAudioOption?.({obeyMuteSwitch:false,mixWithOther:true});if(!this.channels.length)for(let i=0;i<4;i++){const a=wx.createInnerAudioContext();a.obeyMuteSwitch=false;a.volume=.75;a.onError?.(e=>{if(a===this.channels[3])this.finishComplete();this.error(e);});this.channels.push(a);}return true;}catch(e){this.error(e);return false;}}
|
unlock(){if(!this.enabled||!wx.createInnerAudioContext)return false;try{wx.setInnerAudioOption?.({obeyMuteSwitch:false,mixWithOther:true});if(!this.channels.length)for(let i=0;i<4;i++){const a=wx.createInnerAudioContext();a.obeyMuteSwitch=false;a.volume=.75;a.onError?.(e=>{if(a===this.channels[3])this.finishComplete();this.error(e);});this.channels.push(a);}return true;}catch(e){this.error(e);return false;}}
|
||||||
error(e){audioDiagnostics.lastError={errCode:e?.errCode||0,errMsg:e?.errMsg||String(e)};}
|
error(e){audioDiagnostics.lastError={errCode:e?.errCode||0,errMsg:e?.errMsg||String(e)};}
|
||||||
finishComplete(){const finish=this.completeFinish;this.completeFinish=null;finish?.();}
|
finishComplete(){const finish=this.completeFinish;this.completeFinish=null;if(finish)audioDiagnostics.completePlaying=false;finish?.();}
|
||||||
stop(){this.finishComplete();this.serial++;for(const t of this.timers)clearTimeout(t);this.timers.clear();this.moveTimer=null;this.pendingMove=null;this.pendingKind=null;this.lastMove=-Infinity;this.highestUntil=0;for(const a of this.channels){try{a.stop();}catch{}}}
|
stop({preserveComplete=false}={}){const keep=preserveComplete&&Boolean(this.completeFinish);if(keep)audioDiagnostics.preservedTransitions++;else this.finishComplete();this.serial++;for(const t of this.timers)clearTimeout(t);this.timers.clear();this.moveTimer=null;this.pendingMove=null;this.pendingKind=null;this.lastMove=-Infinity;this.highestUntil=0;for(const a of this.channels){if(keep&&a===this.channels[3])continue;try{a.stop();}catch{}}}
|
||||||
toggle(){this.enabled=!this.enabled;try{this.storage.setItem(this.key,this.enabled?'on':'off');}catch{}if(this.enabled)this.play('correct');else this.stop();return this.enabled;}
|
toggle(){this.enabled=!this.enabled;try{this.storage.setItem(this.key,this.enabled?'on':'off');}catch{}if(this.enabled)this.play('correct');else this.stop();return this.enabled;}
|
||||||
play(kind,delay=0,step=1){
|
play(kind,delay=0,step=1){
|
||||||
if(!this.unlock())return;const now=Date.now();
|
if(!this.unlock())return;const now=Date.now();
|
||||||
if(kind==='error'&&now-(this.lastAt.error??-Infinity)<FEEDBACK.errorGapMs)return;this.lastAt[kind]=now;
|
if(kind==='error'&&now-(this.lastAt.error??-Infinity)<FEEDBACK.errorGapMs)return;this.lastAt[kind]=now;
|
||||||
if(delay>0){const serial=this.serial,t=setTimeout(()=>{this.timers.delete(t);if(serial===this.serial)this.play(kind,0,step);},delay*1000);this.timers.add(t);return;}
|
if(delay>0){const serial=this.serial,t=setTimeout(()=>{this.timers.delete(t);if(serial===this.serial)this.play(kind,0,step);},delay*1000);this.timers.add(t);return;}
|
||||||
if(kind==='complete'||kind==='restart')this.stop();
|
if(kind==='complete'||kind==='restart')this.stop({preserveComplete:kind==='restart'});
|
||||||
if(kind!=='complete'&&kind!=='ui'&&now<(this.highestUntil||0))return;
|
if(kind!=='complete'&&kind!=='ui'&&now<(this.highestUntil||0))return;
|
||||||
if(!['move','undo'].includes(kind)&&this.moveTimer){clearTimeout(this.moveTimer);this.timers.delete(this.moveTimer);this.moveTimer=null;this.pendingMove=null;}
|
if(!['move','undo'].includes(kind)&&this.moveTimer){clearTimeout(this.moveTimer);this.timers.delete(this.moveTimer);this.moveTimer=null;this.pendingMove=null;}
|
||||||
if(['move','undo'].includes(kind)&&this.variant!=='original'&&now-this.lastMove<FEEDBACK.audioGapMs){
|
if(['move','undo'].includes(kind)&&this.variant!=='original'&&now-this.lastMove<FEEDBACK.audioGapMs){
|
||||||
@@ -25,12 +25,13 @@ export class CasualAudio{
|
|||||||
}
|
}
|
||||||
run(kind,step){
|
run(kind,step){
|
||||||
if(!this.enabled)return;const now=Date.now();if(kind==='move'||kind==='undo')this.lastMove=now;
|
if(!this.enabled)return;const now=Date.now();if(kind==='move'||kind==='undo')this.lastMove=now;
|
||||||
const a=this.channels[kind==='move'||kind==='undo'&&this.variant!=='original'?this.cursor++%3:3],serial=this.serial,file=kind==='undo'&&this.variant!=='original'?'move-'+Math.max(1,Math.min(36,Math.floor(step)+1)):kind==='move'||kind==='checkpoint'&&this.variant!=='original'?kind+'-'+Math.max(1,Math.min(36,Math.floor(step))):kind;
|
const ordinary=kind==='move'||kind==='undo'&&this.variant!=='original'||kind!=='complete'&&Boolean(this.completeFinish);
|
||||||
|
const a=this.channels[ordinary?this.cursor++%3:3],serial=this.serial,file=kind==='undo'&&this.variant!=='original'?'move-'+Math.max(1,Math.min(36,Math.floor(step)+1)):kind==='move'||kind==='checkpoint'&&this.variant!=='original'?kind+'-'+Math.max(1,Math.min(36,Math.floor(step))):kind;
|
||||||
try{if(a===this.channels[3])this.finishComplete();a.stop();if(a._feedbackEnded)a.offEnded?.(a._feedbackEnded);if(a._feedbackPlay)a.offPlay?.(a._feedbackPlay);a.volume=.75*(kind==='complete'?FEEDBACK.completeGain:1);a.src=(this.variant==='original'&&!['ui','hint'].includes(kind)?'audio-original/':'audio/')+file+'.wav';
|
try{if(a===this.channels[3])this.finishComplete();a.stop();if(a._feedbackEnded)a.offEnded?.(a._feedbackEnded);if(a._feedbackPlay)a.offPlay?.(a._feedbackPlay);a.volume=.75*(kind==='complete'?FEEDBACK.completeGain:1);a.src=(this.variant==='original'&&!['ui','hint'].includes(kind)?'audio-original/':'audio/')+file+'.wav';
|
||||||
if(kind==='complete'){this.highestUntil=now+FEEDBACK.completeWaitMs;audioDiagnostics.completeRequestedAt=now;audioDiagnostics.completeStartedAt=typeof a.onPlay==='function'?null:now;}
|
if(kind==='complete'){this.highestUntil=now+FEEDBACK.completeWaitMs;audioDiagnostics.completeRequestedAt=now;audioDiagnostics.completeStartedAt=typeof a.onPlay==='function'?null:now;}
|
||||||
const callback=()=>{if(serial===this.serial&&kind==='complete')audioDiagnostics.completeStartedAt=Date.now();};a._feedbackPlay=callback;a.onPlay?.(callback);
|
const callback=()=>{if(serial===this.serial&&kind==='complete')audioDiagnostics.completeStartedAt=Date.now();};a._feedbackPlay=callback;a.onPlay?.(callback);
|
||||||
if(kind==='complete'){this.completeFinish=()=>this.afterComplete?.();const ended=()=>{if(serial===this.serial&&this.completeFinish===finish)this.finishComplete();};const finish=this.completeFinish;a._feedbackEnded=ended;a.onEnded?.(ended);this.beforeComplete?.();}
|
if(kind==='complete'){audioDiagnostics.completePlaying=true;this.completeFinish=()=>this.afterComplete?.();const ended=()=>{if(this.completeFinish===finish)this.finishComplete();};const finish=this.completeFinish;a._feedbackEnded=ended;a.onEnded?.(ended);this.beforeComplete?.();}
|
||||||
a.play();this.lastEvent=kind;audioDiagnostics.plays++;
|
a.play();this.lastEvent=kind;audioDiagnostics.plays++;
|
||||||
}catch(e){this.finishComplete();this.error(e);}
|
}catch(e){if(a===this.channels[3])this.finishComplete();this.error(e);}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class Feedback{
|
|||||||
this.audio?.play(e.kind==='checkpoint'?'checkpoint':e.kind,0,e.step);
|
this.audio?.play(e.kind==='checkpoint'?'checkpoint':e.kind,0,e.step);
|
||||||
if(this.enhanced){if(e.kind==='error')this.haptics?.stop();else this.haptics?.play(e.kind);}
|
if(this.enhanced){if(e.kind==='error')this.haptics?.stop();else this.haptics?.play(e.kind);}
|
||||||
}
|
}
|
||||||
reset(){this.epoch++;this.pending=null;this.visuals=[];this.completion=null;this.audio?.stop();this.haptics?.stop();}
|
reset({preserveComplete=false}={}){this.epoch++;this.pending=null;this.visuals=[];this.completion=null;this.audio?.stop({preserveComplete});this.haptics?.stop();}
|
||||||
completionStart(actual,soundEnabled=true){
|
completionStart(actual,soundEnabled=true){
|
||||||
const c=this.completion;if(!c)return null;if(c.locked)return c.start;
|
const c=this.completion;if(!c)return null;if(c.locked)return c.start;
|
||||||
if(soundEnabled&&actual!==null&&actual!==undefined&&actual>=c.event.time&&actual<=c.event.time+FEEDBACK.completeWaitMs){c.start=actual;c.locked=true;}
|
if(soundEnabled&&actual!==null&&actual!==undefined&&actual>=c.event.time&&actual<=c.event.time+FEEDBACK.completeWaitMs){c.start=actual;c.locked=true;}
|
||||||
|
|||||||
Reference in New Issue
Block a user