fix: reduce active celebration workload and restore node and navigation feedback
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
## 2026-10-23:iOS通关早点击与界面反馈修复
|
||||
|
||||
版本:2026-10-23-ios-clear-ui-fix,基于0cefce4。独立新包:../releases/NumberSwipe-main-iOS通关切换与界面反馈修复-20261023.zip;旧包及旧包解压目录保持原样,不上传微信,不推送远端。
|
||||
|
||||
- 用户真机复现条件为iOS微信小游戏,彩带播放后立即点击下一关会卡顿,稍后点击正常。上版通关时以“clear”新键重启下一关材质预热,与动画争抢时间。本版删除重复启动,通关停止预热;点击下一关先取消旧动画帧和预热,再执行切换。
|
||||
- 通关静态背景在落格动画收束后保存一次,庆祝帧只合成背景和彩带,不反复绘制全部UI。设置、规则、尺寸变化、后台及换关会使背景失效;不等待2.6秒庆祝结束即可切换。
|
||||
- 奇偶节点的有效落格现在开启原有局部彩带,方向节点仍保留音效、缩放与振动;最后一格只执行完成反馈,两组节点彩带上限不变。
|
||||
- 游戏内选关、规则、设置和返回游戏统一使用现有audio/ui.wav。通关最初120ms内也允许按钮音效;不改变静音偏好。
|
||||
- 设置统一为音乐:开/关、音效:开/关、振动:开/关。“乐”在旧Sans字体子集中缺失,已补齐500/600/700三个实际字重并保留原字库覆盖;数字和标题字形资源保持原样。
|
||||
- 验收:104项模块测试;200关触摸回归;新增通关后0/40/110/300/3000ms切关及演出背景复用、统一按钮音效和字体覆盖测试。浏览器真实Canvas20组早点击切关记录见qa/ios-clear-browser-20261023.txt。
|
||||
- iOS真机待复测。浏览器不替代真机性能验收;交专门打包上传会话上传后,重点验证连续过关立即切换、静音/后台恢复和不同密度关卡。
|
||||
|
||||
## 2026-10-23:回退音阶与流畅切关
|
||||
|
||||
版本:2026-10-23-descending-undo-fast-next;基于90d83b3的BGMA连续播放与节点缩放版。仅本地开发与打包,不上传微信,不开启开发者工具服务端口,不推送远端。
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
## 2026-10-23:iOS通关早点击与界面反馈修复
|
||||
|
||||
版本:2026-10-23-ios-clear-ui-fix,基于0cefce4。独立新包:../releases/NumberSwipe-main-iOS通关切换与界面反馈修复-20261023.zip;旧包及旧包解压目录保持原样,不上传微信,不推送远端。
|
||||
|
||||
- 用户真机复现条件为iOS微信小游戏,彩带播放后立即点击下一关会卡顿,稍后点击正常。上版通关时以“clear”新键重启下一关材质预热,与动画争抢时间。本版删除重复启动,通关停止预热;点击下一关先取消旧动画帧和预热,再执行切换。
|
||||
- 通关静态背景在落格动画收束后保存一次,庆祝帧只合成背景和彩带,不反复绘制全部UI。设置、规则、尺寸变化、后台及换关会使背景失效;不等待2.6秒庆祝结束即可切换。
|
||||
- 奇偶节点的有效落格现在开启原有局部彩带,方向节点仍保留音效、缩放与振动;最后一格只执行完成反馈,两组节点彩带上限不变。
|
||||
- 游戏内选关、规则、设置和返回游戏统一使用现有audio/ui.wav。通关最初120ms内也允许按钮音效;不改变静音偏好。
|
||||
- 设置统一为音乐:开/关、音效:开/关、振动:开/关。“乐”在旧Sans字体子集中缺失,已补齐500/600/700三个实际字重并保留原字库覆盖;数字和标题字形资源保持原样。
|
||||
- 验收:104项模块测试;200关触摸回归;新增通关后0/40/110/300/3000ms切关及演出背景复用、统一按钮音效和字体覆盖测试。浏览器真实Canvas20组早点击切关记录见qa/ios-clear-browser-20261023.txt。
|
||||
- iOS真机待复测。浏览器不替代真机性能验收;交专门打包上传会话上传后,重点验证连续过关立即切换、静音/后台恢复和不同密度关卡。
|
||||
|
||||
## 2026-10-23:回退音阶与流畅切关
|
||||
|
||||
版本:2026-10-23-descending-undo-fast-next;基于90d83b3的BGMA连续播放与节点缩放版。仅本地开发与打包,不上传微信,不开启开发者工具服务端口,不推送远端。
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
## 2026-10-23:iOS通关早点击与界面反馈修复
|
||||
|
||||
版本:2026-10-23-ios-clear-ui-fix,基于0cefce4。独立新包:../releases/NumberSwipe-main-iOS通关切换与界面反馈修复-20261023.zip;旧包及旧包解压目录保持原样,不上传微信,不推送远端。
|
||||
|
||||
- 用户真机复现条件为iOS微信小游戏,彩带播放后立即点击下一关会卡顿,稍后点击正常。上版通关时以“clear”新键重启下一关材质预热,与动画争抢时间。本版删除重复启动,通关停止预热;点击下一关先取消旧动画帧和预热,再执行切换。
|
||||
- 通关静态背景在落格动画收束后保存一次,庆祝帧只合成背景和彩带,不反复绘制全部UI。设置、规则、尺寸变化、后台及换关会使背景失效;不等待2.6秒庆祝结束即可切换。
|
||||
- 奇偶节点的有效落格现在开启原有局部彩带,方向节点仍保留音效、缩放与振动;最后一格只执行完成反馈,两组节点彩带上限不变。
|
||||
- 游戏内选关、规则、设置和返回游戏统一使用现有audio/ui.wav。通关最初120ms内也允许按钮音效;不改变静音偏好。
|
||||
- 设置统一为音乐:开/关、音效:开/关、振动:开/关。“乐”在旧Sans字体子集中缺失,已补齐500/600/700三个实际字重并保留原字库覆盖;数字和标题字形资源保持原样。
|
||||
- 验收:104项模块测试;200关触摸回归;新增通关后0/40/110/300/3000ms切关及演出背景复用、统一按钮音效和字体覆盖测试。浏览器真实Canvas20组早点击切关记录见qa/ios-clear-browser-20261023.txt。
|
||||
- iOS真机待复测。浏览器不替代真机性能验收;交专门打包上传会话上传后,重点验证连续过关立即切换、静音/后台恢复和不同密度关卡。
|
||||
|
||||
## 2026-10-23:回退音阶与流畅切关
|
||||
|
||||
版本:2026-10-23-descending-undo-fast-next;基于90d83b3的BGMA连续播放与节点缩放版。仅本地开发与打包,不上传微信,不开启开发者工具服务端口,不推送远端。
|
||||
|
||||
@@ -13,7 +13,8 @@ from fontTools.varLib.instancer import instantiateVariableFont
|
||||
root = Path(__file__).resolve().parent
|
||||
corpus = ''.join(p.read_text(encoding='utf-8') for p in (root / 'src').rglob('*')
|
||||
if p.suffix in ('.mjs', '.json'))
|
||||
chars = ''.join(sorted(set(corpus + ''.join(chr(i) for i in range(32, 127)) + '✓→…!:·')))
|
||||
previous_chars = json.loads((root / 'fonts' / 'manifest.json').read_text(encoding='utf-8')).get('characters', '')
|
||||
chars = ''.join(sorted(set(corpus + previous_chars + ''.join(chr(i) for i in range(32, 127)) + '✓→…!:·')))
|
||||
manifest = {'generator': 'fonttools', 'sources': [], 'outputs': [], 'characters': chars}
|
||||
for family, weights in [('notosanssc', [500, 600, 700]), ('nunito', [800])]:
|
||||
source = root / '.font-sources' / (family + '.ttf')
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,59 +1,59 @@
|
||||
{
|
||||
"generator": "fonttools",
|
||||
"sources": [
|
||||
{
|
||||
"family": "notosanssc",
|
||||
"sha256": "a3041811a78c361b1de50f953c805e0244951c21c5bd412f7232ef0d899af0da",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/notosanssc"
|
||||
},
|
||||
{
|
||||
"family": "nunito",
|
||||
"sha256": "bb55a5ca5c2042335b3991af27c4d0705d0ef41cac6164ac737fd8f2a1e85207",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/nunito"
|
||||
},
|
||||
{
|
||||
"family": "zcoolkuaile",
|
||||
"sha256": "812a6fc1fe54b6d73a419245c32dfeba8aa33104d5be90d1cf6af082007cb71d",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/zcoolkuaile"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"file": "NumberSwipeSans500.ttf",
|
||||
"weight": 500,
|
||||
"sha256": "a02c66351825f2af3e15de47e332d0dff11c60854ce37b0c0c8e2ae3de52aca5",
|
||||
"bytes": 121952,
|
||||
"glyphs": 503
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeSans600.ttf",
|
||||
"weight": 600,
|
||||
"sha256": "6bd45b2532ee4afed2ff8c1bbb43cac6cc8490983df8c7312a3b57aa53241a9f",
|
||||
"bytes": 121940,
|
||||
"glyphs": 503
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeSans700.ttf",
|
||||
"weight": 700,
|
||||
"sha256": "cf4834799e8c9277902654f90ca9b86165eb8afcf62e8ce08a3b0c33f09eba35",
|
||||
"bytes": 121972,
|
||||
"glyphs": 503
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeDigits800.ttf",
|
||||
"weight": 800,
|
||||
"sha256": "7ee176cbb7311f4f8e794fe660a7f58148479eba0528b9ec97563354b0360b79",
|
||||
"bytes": 4652,
|
||||
"glyphs": 10
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipePlayful.ttf",
|
||||
"weight": 400,
|
||||
"sha256": "a376431996b69449de38604a73d4fc7253092316bea8e9f1774b9ef768388c7b",
|
||||
"bytes": 4832,
|
||||
"glyphs": 26,
|
||||
"characters": "重开撤销指路已走步还剩块第关0123456789 ·"
|
||||
}
|
||||
],
|
||||
"characters": "\n !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~·×–“”…→✓、。一三上下不与且两个中为久么之也习了交亮从他以们会位住余作使例供依侧保信修候值偶允先入全六共关其具再冲准减几出分切划则刚别到前剩力加动包区单卡即原去又双反发变叠口另只可右号合同后向否启和哪唯喜器回园围固图在坐块域堵墙处备复多大太失奇好如始子字存学它完定察对导封小少尚就尽尾局峰工左差己已布并序应座开异式引弯当录形往径循微必态怎思恢恰想戏成或战所才打找把拍拐择拼持指按挑挡挨振换接推提撤播支收改放效教数整斜新方旁时是显替最有未束条来果枢标核格框桥楚横橙次欢正此步段每永求没沿法浏深混清渐游源滑点版状玩环现理生用画留略的盘直相看着知矩短确示离种积称移空穿突窗竖章端笔符第算类索红约纹纽线练终经绕继续综缺置者耗能脚自至色节花范荡落蓝行表被要观规视览角解计订认让记许设访评试该误请读调败资赖走起越趣跨路转载边达过近返还这进连退选逐通道那邻部都配里重链销错门闭问间随难需面音页顺须预频题颜馈验黄黑!,/:;?"
|
||||
}
|
||||
{
|
||||
"generator": "fonttools",
|
||||
"sources": [
|
||||
{
|
||||
"family": "notosanssc",
|
||||
"sha256": "a3041811a78c361b1de50f953c805e0244951c21c5bd412f7232ef0d899af0da",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/notosanssc"
|
||||
},
|
||||
{
|
||||
"family": "nunito",
|
||||
"sha256": "bb55a5ca5c2042335b3991af27c4d0705d0ef41cac6164ac737fd8f2a1e85207",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/nunito"
|
||||
},
|
||||
{
|
||||
"family": "zcoolkuaile",
|
||||
"sha256": "812a6fc1fe54b6d73a419245c32dfeba8aa33104d5be90d1cf6af082007cb71d",
|
||||
"url": "https://github.com/google/fonts/tree/main/ofl/zcoolkuaile"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"file": "NumberSwipeSans500.ttf",
|
||||
"weight": 500,
|
||||
"sha256": "5b6d256d43bea106cadc32edeeb4c4b21042f0e3bf8122eac5ed0e68fd9b532f",
|
||||
"bytes": 122760,
|
||||
"glyphs": 506
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeSans600.ttf",
|
||||
"weight": 600,
|
||||
"sha256": "66c9321439aff83dfed12bd0d61c785c07e9562d0b93ff765def4591cc209043",
|
||||
"bytes": 122748,
|
||||
"glyphs": 506
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeSans700.ttf",
|
||||
"weight": 700,
|
||||
"sha256": "ae15bd1dec2c724363e3faec928c0641b103f8ab30003f040b93cf9ac5b0d88c",
|
||||
"bytes": 122776,
|
||||
"glyphs": 506
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipeDigits800.ttf",
|
||||
"weight": 800,
|
||||
"sha256": "7ee176cbb7311f4f8e794fe660a7f58148479eba0528b9ec97563354b0360b79",
|
||||
"bytes": 4652,
|
||||
"glyphs": 10
|
||||
},
|
||||
{
|
||||
"file": "NumberSwipePlayful.ttf",
|
||||
"weight": 400,
|
||||
"sha256": "a376431996b69449de38604a73d4fc7253092316bea8e9f1774b9ef768388c7b",
|
||||
"bytes": 4832,
|
||||
"glyphs": 26,
|
||||
"characters": "重开撤销指路已走步还剩块第关0123456789 ·"
|
||||
}
|
||||
],
|
||||
"characters": "\n !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~·×–“”…→✓、。一三上下不与且两个中为久么之乐也习了交亮从他以们会位住余作使例供依侧保信修候值偶允先入全六共关其具再冲准减几出分切划则刚别到前剩力加动包区单卡即原去又双反发变叠口另只可右号合同后向否启和哪唯喜器回园围固图在坐块域堵墙处备复多大太失奇好如始子字存学它完定察对导封小少尚就尽尾局峰工左差己已布并序应度座开异式引弯当录形往径循微必态怎思总恢恰想戏成或战所才打找把拍拐择拼持指按挑挡挨振换接推提撤播支收改放效教数整斜新方旁时是显替最有未束条来果枢标核格框桥楚横橙次欢正此步段每永求没沿法浏深混清渐游源滑点版状玩环现理生用画留略的盘直相看着知矩短确示离种积称移空穿突窗竖章端笔符第算类索红约纹纽线练终经绕继续综缺置者耗能脚自至色节花范荡落蓝行表被要观规视览角解计订认让记许设访评试该误请读调败资赖走起越趣跨路转载边达过近返还这进连退选逐通道那邻部都配里重链销错门闭问间随难需面音页顺须预频题颜馈验黄黑!,/:;?"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@ export function applyLayeredFeedback(source){
|
||||
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++;");
|
||||
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: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.unlock();sound.play('restart');","feedback.reset();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)');
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from 'node:test';import assert from 'node:assert/strict';import fs f
|
||||
import {Music} from './src/music.mjs';import {HintArrows} from './src/hint-arrows.mjs';import {boot} from './qa/runtime-harness.mjs';import {solve} from './src/upstream/rules.mjs';
|
||||
test('music loops independently, persists mute and cancels in background',()=>{const stored=new Map(),calls=[];const api={getStorageSync:k=>stored.get(k),setStorageSync:(k,v)=>stored.set(k,v),createInnerAudioContext:()=>({stop(){calls.push('stop');},play(){calls.push(this.src);},onError(){}})};const m=new Music(api);m.scene('select');m.scene('select');assert.equal(calls.filter(x=>x==='music/select.wav').length,1);assert.equal(m.audio.loop,true);m.scene('play');m.suspend();m.scene('select');assert.equal(m.playing,null);m.resume();assert.equal(m.playing,'select');m.toggle();assert.equal(new Music(api).enabled,false);});
|
||||
test('old arrows retain distance, rush to endpoint and fresh arrows move at base speed',()=>{const a=new HintArrows(),motion={total:300},events=[];let f=a.frame('a',0,events);a.heads(f,motion,0);f=a.frame('a',100,events);assert.equal(a.heads(f,motion,0)[0],6.5);events.push({sequence:1,kind:'move'});f=a.frame('a',116,events);const at=a.heads(f,motion,30);assert.ok(at[0]>6.5&&at[0]<10);assert.equal(at[1],30);f=a.frame('a',216,events);const next=a.heads(f,motion,30);assert.ok(next[0]>150);assert.equal(next[1],36.5);for(let t=316;t<=416;t+=100)a.heads(a.frame('a',t,events),motion,30);assert.ok(a.particles.every(p=>!p.rush));a.reset();assert.equal(a.key,null);});
|
||||
test('settings have exactly three independent preferences, and hint has its own cue',()=>{const ui=boot();ui.select(3);ui.button('设置');assert.deepEqual(Array.from(ui.api.buttons(),b=>b.label),['音乐:开','音效开','振动:开','完成']);ui.button('完成');ui.button('指路');assert.equal(ui.played.at(-1),'audio/hint.wav');ui.button('选关');assert.ok(ui.played.includes('music/select.wav'));ui.button('下一页');assert.equal(ui.played.at(-1),'audio/ui.wav');});
|
||||
test('settings have exactly three independent preferences, and hint has its own cue',()=>{const ui=boot();ui.select(3);ui.button('设置');assert.deepEqual(Array.from(ui.api.buttons(),b=>b.label),['音乐:开','音效:开','振动:开','完成']);ui.button('完成');ui.button('指路');assert.equal(ui.played.at(-1),'audio/hint.wav');ui.button('选关');assert.ok(ui.played.includes('music/select.wav'));ui.button('下一页');assert.equal(ui.played.at(-1),'audio/ui.wav');});
|
||||
test('actual fixed numeric node uses checkpoint pitch, final cell uses complete chord',()=>{const pack=JSON.parse(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url)));const levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));const i=levels.findIndex(l=>l.cells.some(c=>c.clue>1&&c.clue<l.cells.length));const l=levels[i],ui=boot();ui.select(i);const route=solve(l)[0];for(const id of route){ui.advance(80);ui.cell(id);const e=ui.api.snapshot().feedback.events.at(-1);if(e.kind==='checkpoint')assert.equal(ui.played.at(-1),'audio/checkpoint-'+e.step+'.wav');}assert.equal(ui.played.at(-1),'audio/complete.wav');assert.ok(ui.api.snapshot().feedback.events.some(e=>e.kind==='checkpoint'));});
|
||||
test('selected menu and BGMA loop are stereo PCM WAV with complete musical durations',()=>{const meta=JSON.parse(fs.readFileSync(new URL('./.audio-sources/manifest.json',import.meta.url)));for(const name of ['select','play-a']){const b=fs.readFileSync(new URL('./'+meta[name].output,import.meta.url));assert.equal(b.readUInt32LE(24),22050);assert.equal(b.readUInt16LE(22),2);assert.ok(meta[name].seconds>29);assert.equal(b.length,meta[name].bytes);}});
|
||||
test('selected runtime WAVs retain the original durations after sample-rate adaptation',()=>{const meta=JSON.parse(fs.readFileSync(new URL('./.audio-sources/manifest.json',import.meta.url)));for(const [name,item]of Object.entries(meta)){const b=fs.readFileSync(new URL('./.audio-sources/'+name+'.wav',import.meta.url)),seconds=(b.length-44)/(44100*4);assert.ok(Math.abs(seconds-item.seconds)<1/22050);}});
|
||||
|
||||
@@ -3,7 +3,7 @@ import {boot} from './qa/runtime-harness.mjs';import {solve} from './src/upstrea
|
||||
const pack=JSON.parse(fs.readFileSync(new URL('./src/upstream/levels.json',import.meta.url))),levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||||
test('all 200 levels: valid numeric, parity and direction arrivals play checkpoint pitch; final arrival only completes',()=>{
|
||||
const ui=boot();let numeric=0,parity=0,direction=0;
|
||||
for(let i=0;i<levels.length;i++){const level=levels[i],route=solve(level)[0];ui.select(i);for(let step=1;step<=route.length;step++){ui.advance(80);const id=route[step-1],cell=level.cells.find(c=>c.id===id);ui.cell(id);const e=ui.api.snapshot().feedback.events.at(-1),last=step===route.length,node=(cell.clue>1&&cell.clue===step)||cell.parity||cell.direction;assert.equal(e.cell,id);assert.equal(e.kind,last?'complete':node?'checkpoint':'move','level '+(i+1)+' step '+step);assert.equal(ui.played.at(-1),last?'audio/complete.wav':node?'audio/checkpoint-'+step+'.wav':'audio/move-'+step+'.wav');if(node&&!last){assert.equal(e.ribbon,Boolean(cell.clue>1&&cell.clue===step));if(cell.clue>1)numeric++;if(cell.parity)parity++;if(cell.direction)direction++;}}}
|
||||
for(let i=0;i<levels.length;i++){const level=levels[i],route=solve(level)[0];ui.select(i);for(let step=1;step<=route.length;step++){ui.advance(80);const id=route[step-1],cell=level.cells.find(c=>c.id===id);ui.cell(id);const e=ui.api.snapshot().feedback.events.at(-1),last=step===route.length,node=(cell.clue>1&&cell.clue===step)||cell.parity||cell.direction;assert.equal(e.cell,id);assert.equal(e.kind,last?'complete':node?'checkpoint':'move','level '+(i+1)+' step '+step);assert.equal(ui.played.at(-1),last?'audio/complete.wav':node?'audio/checkpoint-'+step+'.wav':'audio/move-'+step+'.wav');if(node&&!last){assert.equal(e.ribbon,Boolean(cell.parity)||Boolean(cell.clue>1&&cell.clue===step));if(cell.clue>1)numeric++;if(cell.parity)parity++;if(cell.direction)direction++;}}}
|
||||
assert.ok(numeric>0&&parity>0&&direction>0);});
|
||||
test('drag back and undo button both vibrate lightly; disabling vibration suppresses both',()=>{
|
||||
const ui=boot(),route=solve(levels[3])[0];ui.select(3);for(const id of route.slice(0,3)){ui.advance(80);ui.cell(id);}ui.advance(61);let n=ui.vibrated.length;ui.cell(route[1]);assert.equal(ui.api.snapshot().path.length,2);assert.equal(ui.vibrated.length,n+1);assert.equal(ui.vibrated.at(-1).type,'light');ui.advance(61);ui.button('撤销');assert.equal(ui.vibrated.length,n+2);assert.equal(ui.vibrated.at(-1).type,'light');ui.button('设置');ui.button('振动:开');ui.button('完成');ui.advance(80);ui.cell(route[1]);n=ui.vibrated.length;ui.advance(80);ui.button('撤销');assert.equal(ui.vibrated.length,n);assert.equal(ui.api.snapshot().music.playing,'play');});
|
||||
@@ -13,3 +13,12 @@ test('rendered undo target and nonnumeric checkpoint animate immediately without
|
||||
const old=feedback.now;t.after(()=>{feedback.reset();feedback.now=old;});const level=levels[3],route=solve(level)[0],scales=[],ctx=new Proxy({measureText:()=>({width:20}),scale:(x,y)=>scales.push([x,y])},{get:(o,k)=>o[k]??(()=>{}),set:(o,k,v)=>(o[k]=v,true)});feedback.reset();feedback.now=()=>1000;feedback.emit('undo',level,route[1],1,{from:route[0]});renderBrightBoard(ctx,level,route.slice(0,1),375,400,{now:1045});assert.ok(scales.some(([x])=>x===1.1));scales.length=0;feedback.now=()=>1100;feedback.emit('checkpoint',level,route[0],1,{ribbon:false});const scene=renderBrightBoard(ctx,level,route.slice(0,1),375,400,{now:1145});assert.ok(scales.some(([x])=>x===1.3));assert.equal(scene.bursts.length,0);});
|
||||
|
||||
test('quick undo cancels removed node rebound, including last-step removal',t=>{const old=feedback.now;t.after(()=>{feedback.reset();feedback.now=old;});const level=levels[3],route=solve(level)[0],scales=[],ctx=new Proxy({measureText:()=>({width:20}),scale:x=>scales.push(x)},{get:(o,k)=>o[k]??(()=>{}),set:(o,k,v)=>(o[k]=v,true)});feedback.reset();feedback.now=()=>1000;feedback.emit('checkpoint',level,route[1],2,{ribbon:false});feedback.now=()=>1010;feedback.emit('undo',level,route[1],1,{from:route[0]});renderBrightBoard(ctx,level,route.slice(0,1),375,400,{now:1045});assert.equal(scales.length,1);assert.ok(scales[0]>1&&scales[0]<=1.1);scales.length=0;feedback.now=()=>1100;feedback.emit('undo',level,route[0],0,{});renderBrightBoard(ctx,level,[],375,400,{now:1145});assert.deepEqual(scales,[1.1]);});
|
||||
|
||||
test('navigation buttons use the same selection cue and settings include uniform colons',()=>{
|
||||
const ui=boot();ui.select(0);for(const action of ['规则','返回游戏','设置']){const n=ui.played.length;ui.button(action);assert.equal(ui.played.length,n+1);assert.equal(ui.played.at(-1),'audio/ui.wav');}
|
||||
assert.deepEqual(Array.from(ui.api.buttons(),b=>b.label),['音乐:开','音效:开','振动:开','完成']);ui.button('完成');const n=ui.played.filter(p=>p==='audio/ui.wav').length;ui.button('选关');assert.equal(ui.played.filter(p=>p==='audio/ui.wav').length,n+1);assert.equal(ui.played.filter(p=>p.startsWith('audio/')).at(-1),'audio/ui.wav');
|
||||
});
|
||||
test('early next during confetti cancels warming; settled ceremony reuses its background',()=>{
|
||||
const ui=boot();for(const delay of [0,40,110,300,3000]){ui.select(3);solve(levels[3])[0].forEach(ui.cell);assert.equal(ui.api.snapshot().warming,false);ui.advance(delay);const before=ui.api.snapshot().clearBaseHits;ui.advance(16);if(delay>=300&&delay<2600)assert.ok(ui.api.snapshot().clearBaseHits>before);ui.button('下一关 →');assert.equal(ui.api.snapshot().displayOrder,5);assert.equal(ui.api.snapshot().cleared,false);ui.advance(100);assert.equal(ui.api.snapshot().displayOrder,5);}
|
||||
});
|
||||
test('font corpus includes every settings label including music le glyph',()=>{const m=JSON.parse(fs.readFileSync(new URL('./fonts/manifest.json',import.meta.url)));for(const c of '音乐音效振动开关设置:')assert.ok(m.characters.includes(c),'missing '+c);});
|
||||
|
||||
@@ -54,4 +54,8 @@ document.querySelector('#hint').onclick=()=>{runtime.button('指路');update();}
|
||||
document.querySelector('#clear').onclick=()=>{const l=levels.find(l=>l.id===runtime.api.snapshot().level);runtime.button('重开');for(const id of solve(l)[0])runtime.cell(id);runtime.tick(600);update();};
|
||||
document.querySelector('#select').onclick=()=>{runtime.button('选关');update();};
|
||||
show(3);function pump(){runtime.tick();requestAnimationFrame(pump);}requestAnimationFrame(pump);
|
||||
|
||||
const settingsCheck=document.createElement('button');settingsCheck.textContent='设置文字验收';settingsCheck.onclick=()=>{runtime.button('设置');update();};document.querySelector('.tools').append(settingsCheck);
|
||||
const timingCheck=document.createElement('button');timingCheck.textContent='通关早点击验收';document.querySelector('.tools').append(timingCheck);
|
||||
timingCheck.onclick=()=>{const results=[];for(const i of [3,90,162,191])for(const delay of [0,40,110,300,3000]){runtime.select(i);for(const id of solve(levels[i])[0])runtime.cell(id);runtime.tick(delay);const start=performance.now();runtime.button('下一关 →');const elapsed=performance.now()-start;if(runtime.api.snapshot().displayOrder!==i+2||runtime.api.snapshot().cleared)throw Error('切关失败');results.push({level:i+1,delay,ms:Number(elapsed.toFixed(1))});}status.textContent='PASS 20/20:通关后0/40/110/300/3000ms切关,上一关演出均取消。真实Canvas浏览器数据,iOS真机待复测。\n'+JSON.stringify(results);};
|
||||
</script></html>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
PASS 20/20:通关后0/40/110/300/3000ms切关,上一关演出均取消。真实Canvas浏览器数据,iOS真机待复测。 [{"level":4,"delay":0,"ms":26.3},{"level":4,"delay":40,"ms":3.7},{"level":4,"delay":110,"ms":6.6},{"level":4,"delay":300,"ms":2.7},{"level":4,"delay":3000,"ms":2.5},{"level":91,"delay":0,"ms":3.4},{"level":91,"delay":40,"ms":4.2},{"level":91,"delay":110,"ms":3.8},{"level":91,"delay":300,"ms":2.4},{"level":91,"delay":3000,"ms":2.6},{"level":163,"delay":0,"ms":4.4},{"level":163,"delay":40,"ms":4.1},{"level":163,"delay":110,"ms":4.2},{"level":163,"delay":300,"ms":3.9},{"level":163,"delay":3000,"ms":3.9},{"level":192,"delay":0,"ms":5.9},{"level":192,"delay":40,"ms":3.7},{"level":192,"delay":110,"ms":3.5},{"level":192,"delay":300,"ms":6},{"level":192,"delay":3000,"ms":3.4}]
|
||||
@@ -0,0 +1,130 @@
|
||||
|
||||
> 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 步和六类反馈音 (5.9102ms)
|
||||
✔ 关闭音效停止播放;再次开启会试听并保存偏好;后台中断停止所有播放器 (0.8733ms)
|
||||
✔ 延迟通关音在静音或进入后台时取消,不在返回后突然播放 (46.9464ms)
|
||||
✔ 音频错误可诊断,不再静默吞掉 (0.7341ms)
|
||||
✔ 所有音频均为非静音 PCM;原版第13步比第1步高一个八度 (37.8488ms)
|
||||
✔ 教学和组合规则用具体动作及奇偶例子解释,没有未解释的术语 (23.6071ms)
|
||||
✔ 过关提示同步使用实际播放起点,非请求时间 (0.8329ms)
|
||||
✔ 过关短句使用用户提供的3秒WAV,不与旧和弦叠加 (2.4877ms)
|
||||
✔ jingle end, cancellation and playback error restore BGM without restarting (8.08ms)
|
||||
✔ muted effects never duck BGM and unrelated effect errors cannot end jingle ducking (0.5505ms)
|
||||
✔ hide and music preference do not revive playback through a late jingle callback (0.7296ms)
|
||||
✔ BGMB is absent from the runtime manifest and music package (1.0822ms)
|
||||
✔ merged irregular outlines retain holes and never draw internal unit edges (91.9791ms)
|
||||
✔ all 200 boards fit four screen budgets with stable safe anchors and real shared entrances (993.2033ms)
|
||||
✔ guides through every irregular block stay inside its occupied shape (55.1344ms)
|
||||
✔ visited, conflicting and hinted category cells retain their material identity (17.1667ms)
|
||||
✔ five font subsets match their manifest and fit the package budget (5.7186ms)
|
||||
✔ upstream adapter fails closed if the inspected render seams change (2.9204ms)
|
||||
✔ procedural cushion pixels preserve holes and light the upper bevel without a border (32.5702ms)
|
||||
✔ visible glyph bounds are centered independently of font ascent and side bearings (2.2832ms)
|
||||
✔ stage with zero depth casts alpha shadows without painting a solid backing (9.4926ms)
|
||||
✔ native width-only TextMetrics still centers the rasterized glyph ink (5.6814ms)
|
||||
✔ fixed channels remain anchored at their block centers after number enlargement (1574.5104ms)
|
||||
✔ checkpoint ribbons trigger on correct arrival only and never duplicate per frame (5.6473ms)
|
||||
✔ checkpoint pulse is small, centered in time and settles without looping (0.6263ms)
|
||||
✔ event batches preserve every landing and sequence but commit only highest audio and haptic (5.1006ms)
|
||||
✔ completion waits at most 120ms and ignores late audio callbacks (1.0139ms)
|
||||
✔ light haptics are limited, checkpoint replaces light, and completion adds only one delayed short pulse (1.1484ms)
|
||||
✔ unsupported graded haptic falls back to one short pulse, never long vibration (0.9185ms)
|
||||
✔ haptic disable, preview and lifecycle cancellation suppress delayed or expired work (1.7776ms)
|
||||
✔ late failed haptic requests cannot revive old pulses and ungraded completion falls back once (0.5243ms)
|
||||
✔ rectangles, concave shapes and hexagons require geometric inward distance without treating internal seams as walls (1.8735ms)
|
||||
✔ one high-speed native move records all intermediate cells and completion without duplicate final node (225.2218ms)
|
||||
✔ legal but off-solution moves use public rules, while invalid clues only produce error (107.8661ms)
|
||||
✔ drag near a previous-block boundary cannot undo; deeper entry can, and undo button remains immediate (118.274ms)
|
||||
✔ tutorial, redraw, hint and restored saved routes do not generate player feedback (151.5673ms)
|
||||
✔ pressure and rebound have bounded amplitude and duration (0.8228ms)
|
||||
✔ interpolation cannot duplicate the same failed-cell event within one native movement (0.4525ms)
|
||||
✔ nodes are driven by explicit events even when multiple arrive before rendering; at most two ribbons survive (47.005ms)
|
||||
✔ audio fast moves merge to latest pitch and completion stops the whole four-channel pool (3.1293ms)
|
||||
✔ settings persist haptic preference independently and chapter cards always use three columns (121.6619ms)
|
||||
✔ original audio is preserved and high pitches lose energy rather than growing sharper (14.463ms)
|
||||
✔ empty, partial and wrong routes reveal the first incomplete full segment (3.2962ms)
|
||||
✔ completion hides the hint; repeat clicks cannot skip; undo and restart return to earlier segments (0.7181ms)
|
||||
✔ all 200 levels reveal consecutive segments without changing the player path (2441.6873ms)
|
||||
✔ hint draws skip player edges and mask visited cells on their own layer; compositing is restored (1.7172ms)
|
||||
✔ parity is a segment anchor; colour is not; flow moves continuously across cells (0.6727ms)
|
||||
✔ coral teaching ribbon preserves visited masks and offers direction in reduced motion (2.9315ms)
|
||||
✔ hint rounds shared guide junctions and keeps separate runs disconnected (0.6576ms)
|
||||
✔ 实际教学关动画:basic (10.2107ms)
|
||||
✔ 实际教学关动画:walls (1.4202ms)
|
||||
✔ 实际教学关动画:straight (6.6802ms)
|
||||
✔ 实际教学关动画:vertical (3.9633ms)
|
||||
✔ 实际教学关动画:corner (1.5938ms)
|
||||
✔ 实际教学关动画:colour (0.8024ms)
|
||||
✔ 实际教学关动画:parity (0.8455ms)
|
||||
✔ 实际教学关动画:rhythm-door (1.0885ms)
|
||||
✔ 实际教学关动画:hex (0.8294ms)
|
||||
✔ 实际教学关动画:unknown-start (1.8172ms)
|
||||
✔ 200关的组合规则均能找到对应真实教学关 (147.4251ms)
|
||||
✔ 教学减速,结束停留1秒后自动循环 (8.2263ms)
|
||||
✔ music loops independently, persists mute and cancels in background (5.8993ms)
|
||||
✔ old arrows retain distance, rush to endpoint and fresh arrows move at base speed (0.9877ms)
|
||||
✔ settings have exactly three independent preferences, and hint has its own cue (236.4223ms)
|
||||
✔ actual fixed numeric node uses checkpoint pitch, final cell uses complete chord (195.7976ms)
|
||||
✔ selected menu and BGMA loop are stereo PCM WAV with complete musical durations (10.0711ms)
|
||||
✔ selected runtime WAVs retain the original durations after sample-rate adaptation (28.814ms)
|
||||
✔ gameplay ambient uses lower scene volume and keeps independent mute and resume behavior (0.753ms)
|
||||
✔ all 200 levels: valid numeric, parity and direction arrivals play checkpoint pitch; final arrival only completes (31077.6635ms)
|
||||
✔ drag back and undo button both vibrate lightly; disabling vibration suppresses both (52.4789ms)
|
||||
✔ haptic cadence is approximately 15 percent denser without changing light or medium grades (1.176ms)
|
||||
✔ regular and special scale settle at 90ms with a larger special peak (0.1979ms)
|
||||
✔ rendered undo target and nonnumeric checkpoint animate immediately without adding ribbons (6.4606ms)
|
||||
✔ quick undo cancels removed node rebound, including last-step removal (1.8244ms)
|
||||
✔ navigation buttons use the same selection cue and settings include uniform colons (38.8292ms)
|
||||
✔ early next during confetti cancels warming; settled ceremony reuses its background (174.7924ms)
|
||||
✔ font corpus includes every settings label including music le glyph (0.6513ms)
|
||||
✔ first launch goes directly to gameplay (123.2517ms)
|
||||
✔ resume unfinished route after exit from gameplay (261.6998ms)
|
||||
✔ resume unfinished route after exit from selection (197.5838ms)
|
||||
✔ completed level resumes next unfinished, including leaving from cleared screen (142.5187ms)
|
||||
✔ replaying a cleared level does not replace the unfinished save (123.7502ms)
|
||||
✔ legacy saves, malformed data and full completion have valid startup targets (97.8607ms)
|
||||
✔ selection pages expose fifteen cards and fit phone safe areas (336.2497ms)
|
||||
✔ hint-only frames reuse static UI, and player input invalidates it (77.9032ms)
|
||||
✔ hint curves are cached and distance lookup is stable at boundaries (1.2382ms)
|
||||
✔ new-mechanic lesson reuses the board while the continuous cursor advances (69.3608ms)
|
||||
✔ BGMA stays continuous across level changes and completion redraws (2.4107ms)
|
||||
✔ subpackage completion never plays a stale scene after hiding, muting, or clearing (0.7081ms)
|
||||
✔ music load failure is silent and one real resume can retry; muted pending loads stay silent (0.4878ms)
|
||||
✔ runtime uses supplied WAV hashes and ships music as a subpackage with a small main bundle (181.1113ms)
|
||||
✔ title is centered and selection settings share the progress and level-range row (350.0215ms)
|
||||
✔ completion ducks BGMA until jingle ends and next level never restarts it (171.2163ms)
|
||||
✔ title artwork is a transparent, high resolution PNG packaged locally (4.8826ms)
|
||||
✔ asynchronous native image load requests a repaint and draws the artwork without a font substitute (0.9964ms)
|
||||
✔ artwork stays above progress pills and clear of settings; fifteen cards still fit all phone sizes (477.1889ms)
|
||||
✔ undo button reverses seven played steps as 7,6,5,4,3,2,1 (272.0277ms)
|
||||
✔ drag reverses seven played steps as 7,6,5,4,3,2,1 (195.1025ms)
|
||||
✔ completion amplitude is reduced 15 percent while node, undo and restart volumes stay unchanged (1.5768ms)
|
||||
✔ fast reverse coalesces to the latest descending pitch and cannot replay an old forward note (2.1951ms)
|
||||
✔ all prepared routes match the immutable level hash and public rules (25.8434ms)
|
||||
✔ entering next level draws the live board once and preserves the running BGMA player (1930.219ms)
|
||||
✔ next-board warming advances one item per frame and cancels on deadline, replacement or hide (0.9028ms)
|
||||
✔ idle material warming never changes the player path or emits sound and vibration (106.5281ms)
|
||||
ℹ tests 104
|
||||
ℹ suites 0
|
||||
ℹ pass 104
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 31744.7934
|
||||
|
||||
> numberswipe-main@1.0.0 smoke
|
||||
> node smoke.mjs
|
||||
|
||||
PASS 200/200: r2源文件哈希一致、唯一解、微信发布包触摸通关、撤销/重开/指路、存档/音效设置恢复、前后台切换。
|
||||
@@ -0,0 +1 @@
|
||||
PASS:200/200关,点击指路前后和撤销重画后,玩家已走格子的实际Canvas填色像素一致。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"channel": "main",
|
||||
"version": "2026-10-23-descending-undo-fast-next",
|
||||
"version": "2026-10-23-ios-clear-ui-fix",
|
||||
"date": "2026-10-23",
|
||||
"baseVersion": "2026-10-22-resume-chapter-lessons",
|
||||
"baseMainSHA256": "bb35d5edf8d96eb3253c78052d219b32f9a4241efd9971537f6c9c8896188125",
|
||||
"package": "NumberSwipe-main-回退音阶与流畅切关-20261023.zip",
|
||||
"package": "NumberSwipe-main-iOS通关切换与界面反馈修复-20261023.zip",
|
||||
"promotedFrom": "2026-10-22-layered-feedback-music-flow",
|
||||
"previousVersion": "2026-10-23-bgma-duck-node-scale"
|
||||
"previousVersion": "2026-10-23-descending-undo-fast-next"
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ const restored = boot(ui.storage, 320, 640);
|
||||
assert.equal(restored.api.snapshot().level, ordered[199].id);
|
||||
assert.equal(JSON.parse(ui.storage.get('variant:campaign-200-r2:v1')).length, 200);
|
||||
restored.button('规则');assert.ok(restored.api.buttons().every(b=>b.y>=0&&b.y+b.h<=640));const smallBefore=restored.api.snapshot();restored.advance(5200);assert.deepEqual(restored.api.snapshot().path,smallBefore.path);restored.button('返回游戏');
|
||||
restored.button('设置');restored.button('音效开');restored.button('完成');
|
||||
restored.button('设置');restored.button('音效:开');restored.button('完成');
|
||||
const reopen = boot(ui.storage);
|
||||
assert.equal(reopen.api.snapshot().soundEnabled, false);
|
||||
// 在多种手机尺寸下验证通关后的真实按钮、绘制区域与点击跳转。
|
||||
|
||||
@@ -14,7 +14,7 @@ export class CasualAudio{
|
||||
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(kind==='complete'||kind==='restart')this.stop();
|
||||
if(kind!=='complete'&&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.variant!=='original'&&now-this.lastMove<FEEDBACK.audioGapMs){
|
||||
this.pendingMove=step;this.pendingKind=kind;audioDiagnostics.mergedMoves++;
|
||||
|
||||
@@ -26,10 +26,11 @@ const music=new Music(wx),hintArrows=new HintArrows();
|
||||
let hintAnimation={key:'',started:0};
|
||||
let settingsOpen=false,pressedLabel='',pressedUntil=0;
|
||||
let celebrationSuppressed=false;
|
||||
const clearBaseCanvas=wx.createCanvas();let clearBaseKey='',clearBaseHits=0;
|
||||
let renderedFrames=0,clearCelebration=null,selectWasOpen=false;
|
||||
function requestPaint(){if(visible&&!raf)raf=requestAnimationFrame(draw);}
|
||||
const hintBaseCanvas=wx.createCanvas();let hintBaseKey='',hintBaseHits=0;
|
||||
setViewInvalidator(()=>{hintBaseKey='';requestPaint();});
|
||||
setViewInvalidator(()=>{hintBaseKey='';clearBaseKey='';requestPaint();});
|
||||
preferences.reducedMotion=false;
|
||||
const ordered = PACK.playOrder.map(id => PACK.levels.find(l => l.id === id));
|
||||
function resize() { hintBaseKey=''; const info = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync(); width = info.windowWidth; height = info.windowHeight; top = Math.max(info.safeArea?.top || 0, wx.getMenuButtonBoundingClientRect?.().bottom || 0) + 12; bottom = Math.max(16, height - (info.safeArea?.bottom || height)); window.devicePixelRatio = Math.min(info.pixelRatio || 1, 3); mainCanvas.width = width * window.devicePixelRatio; mainCanvas.height = height * window.devicePixelRatio; $('board').clientWidth = width; $('board').clientHeight = Math.max(140, height - top - bottom - 218); window.handlers.resize?.(); requestPaint(); }
|
||||
@@ -62,13 +63,20 @@ function draw() {
|
||||
const d = window.devicePixelRatio;
|
||||
ctx.setTransform(d, 0, 0, d, 0, 0);
|
||||
const state=window.variantSnapshot();
|
||||
const nextWarm=ordered[state.displayOrder],nextWarmKey=state.level+':'+width+':'+$('board').clientHeight+(state.cleared?':clear':'');
|
||||
if(state.screen==='play'&&nextWarm){if(warmupKey!==nextWarmKey){warmupKey=nextWarmKey;warmup.start(nextWarmKey,warmBoardMaterials(warmCanvas.getContext('2d'),nextWarm,width,$('board').clientHeight));}}else if(warmupKey){warmup.stop();warmupKey='';}
|
||||
const nextWarm=ordered[state.displayOrder],nextWarmKey=state.level+':'+width+':'+$('board').clientHeight;
|
||||
if(state.screen==='play'&&nextWarm&&!state.cleared){if(warmupKey!==nextWarmKey){warmupKey=nextWarmKey;warmup.start(nextWarmKey,warmBoardMaterials(warmCanvas.getContext('2d'),nextWarm,width,$('board').clientHeight));}}else if(state.cleared){warmup.stop();}else if(warmupKey){warmup.stop();warmupKey='';}
|
||||
music.scene(state.screen==='select'?'select':state.cleared?'complete':'play',state.level);
|
||||
if(!state.cleared||state.screen!=='play'){clearCelebration=null;celebrationSuppressed=false;}
|
||||
else if(!celebrationSuppressed&&(!clearCelebration||clearCelebration.key!==state.level))clearCelebration={key:state.level,started:Date.now(),requested:audioDiagnostics.completeRequestedAt};
|
||||
if(clearCelebration)clearCelebration.started=feedback.completionStart(audioDiagnostics.completeStartedAt,state.soundEnabled);
|
||||
const animationHint=hintView();
|
||||
const clearKey=state.cleared&&state.screen==='play'&&!settingsOpen&&$('rules-sheet').hidden&&!celebrationSuppressed?state.level+':'+width+':'+height:'';
|
||||
if(clearKey&&clearBaseKey===clearKey&&!brightAnimating()&&Date.now()>=pressedUntil){
|
||||
clearBaseHits++;ctx.drawImage(clearBaseCanvas,0,0,width,height);
|
||||
if(clearCelebration?.started!==null&&!preferences.reducedMotion)drawClearCelebration(ctx,width,top+82,$('board').clientHeight,Date.now()-clearCelebration.started);
|
||||
if(clearCelebration&&(clearCelebration.started===null||Date.now()-clearCelebration.started<CLEAR_DURATION))requestPaint();
|
||||
return;
|
||||
}
|
||||
const cacheEligible=Boolean(animationHint)&&state.screen==='play'&&!state.cleared&&!settingsOpen&&$('rules-sheet').hidden&&!brightAnimating()&&Date.now()>=pressedUntil;
|
||||
const baseKey=JSON.stringify([state.level,state.path,width,height,d,preferences.reducedMotion]);
|
||||
if(cacheEligible&&hintBaseKey===baseKey){
|
||||
@@ -133,6 +141,11 @@ function draw() {
|
||||
}
|
||||
button('规则', 16, top + 43, 56, 30, () => click('btn-rules'));
|
||||
}
|
||||
if(clearKey&&!brightAnimating()&&Date.now()>=pressedUntil){
|
||||
if(clearBaseCanvas.width!==mainCanvas.width)clearBaseCanvas.width=mainCanvas.width;
|
||||
if(clearBaseCanvas.height!==mainCanvas.height)clearBaseCanvas.height=mainCanvas.height;
|
||||
clearBaseCanvas.getContext('2d').drawImage(mainCanvas,0,0);clearBaseKey=clearKey;
|
||||
}
|
||||
if(clearCelebration&&clearCelebration.started===null&&!settingsOpen&&$('rules-sheet').hidden){const x=width/2,y=top+82+$('board').clientHeight*.45;panel(ctx,x-74,y-40,148,80,25,C.surface,2);text('完成!',x,y,24,C.success);}
|
||||
if(clearCelebration&&clearCelebration.started!==null&&!preferences.reducedMotion&&!settingsOpen&&$('rules-sheet').hidden)drawClearCelebration(ctx,width,top+82,$('board').clientHeight,Date.now()-clearCelebration.started);
|
||||
if (!$('rules-sheet').hidden) {
|
||||
@@ -172,7 +185,7 @@ function draw() {
|
||||
const y=Math.max(top,height/2-185);panel(ctx,24,y,width-48,340,24,C.surface,5);
|
||||
text('设置',width/2,y+32,24);
|
||||
button('音乐:'+(music.enabled?'开':'关'),44,y+65,width-88,48,()=>music.toggle());
|
||||
button($('btn-sound').textContent,44,y+125,width-88,48,()=>click('btn-sound'));
|
||||
button('音效:'+(state.soundEnabled?'开':'关'),44,y+125,width-88,48,()=>click('btn-sound'));
|
||||
button('振动:'+(feedback.haptics.enabled?'开':'关'),44,y+185,width-88,48,()=>feedback.haptics.setEnabled(!feedback.haptics.enabled));
|
||||
button('完成',44,y+265,width-88,48,()=>settingsOpen=false,{primary:true});
|
||||
}
|
||||
@@ -197,7 +210,9 @@ wx.onTouchStart(e => { music.resume();if (pointer !== null)
|
||||
return; const p = e.changedTouches[0]; if (!p)
|
||||
return; const b = buttons.find(b => p.clientX >= b.x && p.clientX <= b.x + b.w && p.clientY >= b.y && p.clientY <= b.y + b.h); if (b) {
|
||||
if (b.enabled){music.resume();hintBaseKey='';pressedLabel=b.label;pressedUntil=Date.now()+120;
|
||||
const uiCue=$('screen-play').className.includes('hidden')||settingsOpen;
|
||||
const uiCue=$('screen-play').className.includes('hidden')||settingsOpen||['选关','规则','设置','返回游戏'].includes(b.label);
|
||||
clearBaseKey='';
|
||||
if(b.label==='下一关 →'||b.label==='返回选关'){warmup.stop();warmupKey='';celebrationSuppressed=true;clearCelebration=null;cancelAnimationFrame(raf);raf=0;}
|
||||
b.run();if(uiCue)feedback.audio.play('ui');
|
||||
requestPaint();
|
||||
}
|
||||
@@ -213,7 +228,7 @@ const release = e => { if (e.changedTouches.some(p => p.identifier === pointer))
|
||||
} };
|
||||
wx.onTouchEnd(release);
|
||||
wx.onTouchCancel(release);
|
||||
wx.onHide(() => { visible = false;warmup.stop();warmupKey='';music.suspend();hintArrows.reset(); lessonStarted=Date.now(); celebrationSuppressed=window.variantSnapshot().cleared;clearCelebration=null;feedback.reset();stopAudio(); cancelAnimationFrame(raf);raf=0; pointer = null; $('board').handlers.pointercancel?.(); window.handlers.pagehide?.(); });
|
||||
wx.onHide(() => { visible = false;clearBaseKey='';warmup.stop();warmupKey='';music.suspend();hintArrows.reset(); lessonStarted=Date.now(); celebrationSuppressed=window.variantSnapshot().cleared;clearCelebration=null;feedback.reset();stopAudio(); cancelAnimationFrame(raf);raf=0; pointer = null; $('board').handlers.pointercancel?.(); window.handlers.pagehide?.(); });
|
||||
wx.onShow(() => { if (!visible) {
|
||||
visible = true;lessonStarted=Date.now();settingsOpen=false;resumeGameplay();
|
||||
const resumed=window.variantSnapshot();music.scene(resumed.screen==='select'?'select':resumed.cleared?'complete':'play',resumed.level);music.resume();
|
||||
@@ -224,7 +239,7 @@ wx.onAudioInterruptionBegin?.(()=>{music.suspend();feedback.reset();stopAudio();
|
||||
wx.onAudioInterruptionEnd?.(()=>{if(visible){music.resume();requestPaint();}});
|
||||
wx.onWindowResize?.(resize);
|
||||
// 只读状态,供微信开发者工具和发布包自动验收定位问题。
|
||||
GameGlobal.numberSwipeR2 = { snapshot: () => ({...window.variantSnapshot(),theme:'bright-classroom',release:releaseInfo,fonts:{...fontDiagnostics},renderedFrames,boardRenders:boardDiagnostics.liveRenders,hintBaseHits,lessonBoardRenders,music:music.snapshot(),selection:{page:page+1,pages:Math.ceil(ordered.length/15),pageSize:15,chapter:page+1,chapters:Math.ceil(ordered.length/15)},reducedMotion:preferences.reducedMotion,boardRect:{x:0,y:top+82,w:width,h:$('board').clientHeight},hint:hintView(),audio:{...audioDiagnostics},feedback:feedback.snapshot(),rules: $('rule-text').textContent,lesson:!$('rules-sheet').hidden&&lessonState?{key:lesson?.key,finished:lessonState.finished,duration:lesson?.duration,path:[...lessonState.route],message:lessonState.message}:null}), buttons: () => buttons.map(({ label, x, y, w, h, enabled }) => ({ label, x, y, w, h, enabled })) };
|
||||
GameGlobal.numberSwipeR2 = { snapshot: () => ({...window.variantSnapshot(),theme:'bright-classroom',release:releaseInfo,fonts:{...fontDiagnostics},renderedFrames,clearBaseHits,warming: Boolean(warmup.pending),boardRenders:boardDiagnostics.liveRenders,hintBaseHits,lessonBoardRenders,music:music.snapshot(),selection:{page:page+1,pages:Math.ceil(ordered.length/15),pageSize:15,chapter:page+1,chapters:Math.ceil(ordered.length/15)},reducedMotion:preferences.reducedMotion,boardRect:{x:0,y:top+82,w:width,h:$('board').clientHeight},hint:hintView(),audio:{...audioDiagnostics},feedback:feedback.snapshot(),rules: $('rule-text').textContent,lesson:!$('rules-sheet').hidden&&lessonState?{key:lesson?.key,finished:lessonState.finished,duration:lesson?.duration,path:[...lessonState.route],message:lessonState.message}:null}), buttons: () => buttons.map(({ label, x, y, w, h, enabled }) => ({ label, x, y, w, h, enabled })) };
|
||||
feedback.audio.beforeComplete=()=>music.duck();
|
||||
feedback.audio.afterComplete=()=>music.unduck();
|
||||
loadSelectionTitle(wx,requestPaint);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"undoRestartHint": true,
|
||||
"storageRestore": true,
|
||||
"foregroundLoop": true,
|
||||
"nativeAudioPlaybackCalls": 2043,
|
||||
"nativeAudioPlaybackCalls": 2654,
|
||||
"plainLanguageRules": true,
|
||||
"pagedRules": true,
|
||||
"clearActionLayout": true,
|
||||
|
||||
Reference in New Issue
Block a user