修复特殊节点反馈并强化跟手缩放和选关标题
This commit is contained in:
Binary file not shown.
@@ -1,3 +1,20 @@
|
||||
## 2026-10-23:节点反馈与立体标题
|
||||
|
||||
版本:2026-10-23-responsive-nodes-sticker-header。开发分支:codex/node-feedback-responsive。以当前正式主工程为基线;没有从历史副本覆盖文件。
|
||||
|
||||
- 修复只有固定数字触发checkpoint的问题:合法到达数字、奇偶、方向节点均使用独立checkpoint-N确认音及中震;最后一格只触发完成和弦。错误不冒充节点,也不参照答案判断。
|
||||
- 所有普通落格、回退落点(撤销最后一步时为被撤销格)、错误局部提示改为90ms小回弹,峰值1.025;特殊节点90ms峰值1.065。只缩放块体表面,文字锚点、规则线和命中区域保持稳定。
|
||||
- 回退和撤销加入轻震。经用户选择保留轻/中强度,把普通震动最短间隔70ms改为61ms,连续触发密度约增14.8%;不宣称单次物理强度提高15%。短震降级、关闭开关、预览禁震及后台取消继续有效。
|
||||
- 原数字节点彩带的触发、范围、最多两组及1500ms时长保留。新增奇偶/方向节点使用较大缩放和确认音,不额外扩大彩带。
|
||||
- 选关标题按参考图使用橙紫双色、奶油描边和叠层立体处理;进度奶油标签、关卡范围橙色标签。保持每页15关及320×568安全区域。
|
||||
- 选关与关卡BGM正常保留,音乐资源、音量及独立开关本轮没有更换;音效、震动和回退不停止音乐。
|
||||
|
||||
验证:78项模块测试通过(含200关每一步特殊节点音效断言),200关模拟触摸回归通过;375宽、320×568浏览器排版通过。200关提示填色像素复核记录见qa。真机音效起播、振动手感与高密度帧率仍待iPhone和Android验收。
|
||||
|
||||
BGM待选方向:①温暖无节拍氛围,低音区软垫与缓慢和声、弱旋律(优先推荐);②稀疏毛毡钢琴,少量低中音、长留白;③柔和木质环境音,去固定节拍、极少敲击,保留教具室气质。当前16秒循环暂留,未擅自替换。
|
||||
|
||||
包:../releases/NumberSwipe-main-节点跟手与立体标题-20261023.zip。之前主版本包保留。
|
||||
|
||||
# NumberSwipe 正式主版本
|
||||
|
||||
本目录是唯一正式微信主工程,属于 NumberSwipe_New 仓库 main 分支。2026-10-23 已将分层反馈、音乐与流动指路强化版提升为主版本。版本以 release-info.json 为准。
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
## 2026-10-23:节点反馈与立体标题
|
||||
|
||||
版本:2026-10-23-responsive-nodes-sticker-header。开发分支:codex/node-feedback-responsive。以当前正式主工程为基线;没有从历史副本覆盖文件。
|
||||
|
||||
- 修复只有固定数字触发checkpoint的问题:合法到达数字、奇偶、方向节点均使用独立checkpoint-N确认音及中震;最后一格只触发完成和弦。错误不冒充节点,也不参照答案判断。
|
||||
- 所有普通落格、回退落点(撤销最后一步时为被撤销格)、错误局部提示改为90ms小回弹,峰值1.025;特殊节点90ms峰值1.065。只缩放块体表面,文字锚点、规则线和命中区域保持稳定。
|
||||
- 回退和撤销加入轻震。经用户选择保留轻/中强度,把普通震动最短间隔70ms改为61ms,连续触发密度约增14.8%;不宣称单次物理强度提高15%。短震降级、关闭开关、预览禁震及后台取消继续有效。
|
||||
- 原数字节点彩带的触发、范围、最多两组及1500ms时长保留。新增奇偶/方向节点使用较大缩放和确认音,不额外扩大彩带。
|
||||
- 选关标题按参考图使用橙紫双色、奶油描边和叠层立体处理;进度奶油标签、关卡范围橙色标签。保持每页15关及320×568安全区域。
|
||||
- 选关与关卡BGM正常保留,音乐资源、音量及独立开关本轮没有更换;音效、震动和回退不停止音乐。
|
||||
|
||||
验证:78项模块测试通过(含200关每一步特殊节点音效断言),200关模拟触摸回归通过;375宽、320×568浏览器排版通过。200关提示填色像素复核记录见qa。真机音效起播、振动手感与高密度帧率仍待iPhone和Android验收。
|
||||
|
||||
BGM待选方向:①温暖无节拍氛围,低音区软垫与缓慢和声、弱旋律(优先推荐);②稀疏毛毡钢琴,少量低中音、长留白;③柔和木质环境音,去固定节拍、极少敲击,保留教具室气质。当前16秒循环暂留,未擅自替换。
|
||||
|
||||
包:../releases/NumberSwipe-main-节点跟手与立体标题-20261023.zip。之前主版本包保留。
|
||||
|
||||
# 历史开发记录
|
||||
|
||||
2026-10-23 本版本已提升为正式 main。以下独立实验版和不替换主版的说明仅描述提升前历史;当前入口以 README.md 和 release-info.json 为准。
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
## 2026-10-23:节点反馈与立体标题
|
||||
|
||||
版本:2026-10-23-responsive-nodes-sticker-header。开发分支:codex/node-feedback-responsive。以当前正式主工程为基线;没有从历史副本覆盖文件。
|
||||
|
||||
- 修复只有固定数字触发checkpoint的问题:合法到达数字、奇偶、方向节点均使用独立checkpoint-N确认音及中震;最后一格只触发完成和弦。错误不冒充节点,也不参照答案判断。
|
||||
- 所有普通落格、回退落点(撤销最后一步时为被撤销格)、错误局部提示改为90ms小回弹,峰值1.025;特殊节点90ms峰值1.065。只缩放块体表面,文字锚点、规则线和命中区域保持稳定。
|
||||
- 回退和撤销加入轻震。经用户选择保留轻/中强度,把普通震动最短间隔70ms改为61ms,连续触发密度约增14.8%;不宣称单次物理强度提高15%。短震降级、关闭开关、预览禁震及后台取消继续有效。
|
||||
- 原数字节点彩带的触发、范围、最多两组及1500ms时长保留。新增奇偶/方向节点使用较大缩放和确认音,不额外扩大彩带。
|
||||
- 选关标题按参考图使用橙紫双色、奶油描边和叠层立体处理;进度奶油标签、关卡范围橙色标签。保持每页15关及320×568安全区域。
|
||||
- 选关与关卡BGM正常保留,音乐资源、音量及独立开关本轮没有更换;音效、震动和回退不停止音乐。
|
||||
|
||||
验证:78项模块测试通过(含200关每一步特殊节点音效断言),200关模拟触摸回归通过;375宽、320×568浏览器排版通过。200关提示填色像素复核记录见qa。真机音效起播、振动手感与高密度帧率仍待iPhone和Android验收。
|
||||
|
||||
BGM待选方向:①温暖无节拍氛围,低音区软垫与缓慢和声、弱旋律(优先推荐);②稀疏毛毡钢琴,少量低中音、长留白;③柔和木质环境音,去固定节拍、极少敲击,保留教具室气质。当前16秒循环暂留,未擅自替换。
|
||||
|
||||
包:../releases/NumberSwipe-main-节点跟手与立体标题-20261023.zip。之前主版本包保留。
|
||||
|
||||
# NumberSwipe 正式主版本
|
||||
|
||||
本目录是唯一正式微信主工程,属于 NumberSwipe_New 仓库 main 分支。2026-10-23 已将分层反馈、音乐与流动指路强化版提升为主版本。版本以 release-info.json 为准。
|
||||
|
||||
@@ -133,5 +133,5 @@ test('checkpoint ribbons trigger on correct arrival only and never duplicate per
|
||||
|
||||
test('checkpoint pulse is small, centered in time and settles without looping',async()=>{
|
||||
const {checkpointScale}=await import('./src/bright-classroom/board-renderer.mjs');
|
||||
assert.equal(checkpointScale(0),1);assert.equal(checkpointScale(130),1.035);assert.equal(checkpointScale(260),1);assert.equal(checkpointScale(1000),1);assert.equal(checkpointScale(130,true),1);
|
||||
assert.equal(checkpointScale(0),1);assert.equal(checkpointScale(45),1.065);assert.equal(checkpointScale(90),1);assert.equal(checkpointScale(1000),1);assert.equal(checkpointScale(45,true),1);
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ test('drag near a previous-block boundary cannot undo; deeper entry can, and und
|
||||
test('tutorial, redraw, hint and restored saved routes do not generate player feedback',()=>{
|
||||
const ui=boot();ui.select(4);const n=ui.api.snapshot().feedback.sequence;ui.button('规则');ui.advance(1000);ui.advance(1000);ui.button('返回游戏');ui.button('指路');ui.advance(400);assert.equal(ui.api.snapshot().feedback.sequence,n);ui.cell(solve(levels[4])[0][0]);const restored=boot(ui.storage);assert.equal(restored.api.snapshot().feedback.sequence,0);
|
||||
});
|
||||
test('pressure and rebound have bounded amplitude and duration',()=>{assert.equal(landingScale(60),.985);assert.equal(landingScale(120),1);assert.equal(checkpointScale(130),1.035);assert.equal(checkpointScale(260),1);});
|
||||
test('pressure and rebound have bounded amplitude and duration',()=>{assert.equal(landingScale(45),1.025);assert.equal(landingScale(90),1);assert.equal(checkpointScale(45),1.065);assert.equal(checkpointScale(90),1);});
|
||||
test('interpolation cannot duplicate the same failed-cell event within one native movement',()=>{
|
||||
const f=new Feedback();f.begin();for(let i=0;i<40;i++)f.emit('error','l','wall-cell',3);f.end();assert.equal(f.sequence,1);f.begin();f.emit('error','l','wall-cell',3);f.end();assert.equal(f.sequence,2);
|
||||
});
|
||||
|
||||
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?'checkpoint':'move',level,id,path.length,{from:path.at(-2)})");
|
||||
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("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)');
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
|
||||
import {boot} from './qa/runtime-harness.mjs';import {solve} from './src/upstream/rules.mjs';import {Haptics} from './src/haptics.mjs';import {FEEDBACK} from './src/feedback-params.mjs';import {feedback} from './src/feedback.mjs';import {renderBrightBoard,landingScale,checkpointScale} from './src/bright-classroom/board-renderer.mjs';
|
||||
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++;}}}
|
||||
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');});
|
||||
test('haptic cadence is approximately 15 percent denser without changing light or medium grades',()=>{let now=0;const calls=[],h=new Haptics({vibrateShort:o=>calls.push(o.type)},{getItem:()=>null},{now:()=>now});h.play('move');now=60;h.play('undo');assert.deepEqual(calls,['light']);now=61;h.play('undo');assert.deepEqual(calls,['light','light']);h.play('checkpoint');assert.equal(calls.at(-1),'medium');assert.ok(Math.abs(70/FEEDBACK.hapticGapMs-1.15)<.01);});
|
||||
test('regular and special scale settle at 90ms with a larger special peak',()=>{assert.equal(FEEDBACK.moveMs,90);assert.equal(FEEDBACK.checkpointMs,90);assert.equal(landingScale(45),1.025);assert.equal(checkpointScale(45),1.065);for(const t of [-10,0,90,1000]){assert.equal(landingScale(t),1);assert.equal(checkpointScale(t),1);}});
|
||||
test('rendered undo target and nonnumeric checkpoint animate immediately without adding ribbons',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,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.025));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.065));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.025);scales.length=0;feedback.now=()=>1100;feedback.emit('undo',level,route[0],0,{});renderBrightBoard(ctx,level,[],375,400,{now:1145});assert.deepEqual(scales,[1.025]);});
|
||||
@@ -1,7 +1,7 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$releaseDir = Join-Path (Split-Path $PSScriptRoot -Parent) 'releases'
|
||||
New-Item -ItemType Directory -Path $releaseDir -Force | Out-Null
|
||||
$releaseInfo = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'release-info.json') -Raw | ConvertFrom-Json
|
||||
$releaseInfo = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'release-info.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$archivePath = Join-Path $releaseDir $releaseInfo.package
|
||||
$assets = @('game.js','game.json','project.config.json','audio','audio-original','music','fonts','release-info.json') | ForEach-Object { Join-Path $PSScriptRoot $_ }
|
||||
Compress-Archive -LiteralPath $assets -DestinationPath $archivePath -Force
|
||||
|
||||
@@ -103,6 +103,10 @@
|
||||
{
|
||||
"type": "file",
|
||||
"value": "music-flow.test.mjs"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "node-feedback.test.mjs"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -110,6 +114,6 @@
|
||||
"appid": "wx7a14d0d70b5dd13d",
|
||||
"editorSetting": {},
|
||||
"projectname": "NumberSwipe_New-main",
|
||||
"description": "正式主版本:分层反馈、音乐与流动指路",
|
||||
"description": "正式主版本:节点音效、90ms跟手缩放与橙紫立体标题",
|
||||
"miniprogramRoot": "./"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@font-face{font-family:NumberSwipePlayful;src:url(../fonts/NumberSwipePlayful.ttf)}
|
||||
*{box-sizing:border-box}body{margin:0;background:#fff6df;color:#43382f;font:15px NumberSwipeSans500,sans-serif}header{padding:28px 32px 20px;border-bottom:1px solid #e5d7ba}h1{margin:0 0 8px;font:25px NumberSwipeSans700}p{margin:8px 0;line-height:1.7;color:#71604f}main{display:flex;gap:32px;padding:28px 32px;align-items:flex-start}.tools{width:270px;position:sticky;top:24px}.tools h2{font-size:17px;margin:24px 0 10px}.tools button,select{font:14px NumberSwipeSans600;cursor:pointer;border:1px solid #d8c4a0;background:#fffbf2;color:#43382f;border-radius:12px;padding:12px;margin:0 6px 8px 0}.tools button:hover{border-color:#6552d9;color:#5140b7}.tools select{width:100%}.phone{border:1px solid #d8c4a0;border-radius:24px;overflow:hidden;box-shadow:0 12px 34px #43382f14;background:#ffe69a}canvas{display:block;touch-action:none}.note{font-size:13px}#status{background:#ece7ff;color:#5140b7;border-radius:12px;padding:12px;margin-top:16px}a{color:#5140b7}@media(max-width:750px){main{padding:16px;flex-direction:column}.tools{width:100%;position:static}.phone{align-self:center}header{padding:20px}}
|
||||
</style>
|
||||
<header><h1>数字一笔 · 分层反馈独立实验版</h1><p>200关实际游戏包的 Canvas 预览。全部画面由程序绘制,字体随包提供。</p></header>
|
||||
<header><h1>数字一笔 · 节点反馈与立体标题验收</h1><p>200关实际游戏包的 Canvas 预览。全部画面由程序绘制,字体随包提供。</p></header>
|
||||
<main><aside class="tools"><label for="size">屏幕尺寸</label><select id="size"><option>375×812</option><option>320×568</option><option>375×667</option><option>390×844</option><option>414×896</option></select><label for="feedback-mode">反馈对照</label><select id="feedback-mode"><option value="enhanced">强化版</option><option value="original">原版</option></select><h2>真实关卡样张</h2><div id="samples"></div><h2>指定关卡</h2><select id="level"></select><button id="partial">画前几步</button><button id="hint">显示指路</button><button id="clear">完成本关</button><button id="select">返回选关</button><p class="note">在右侧直接点击或滑动。音乐、音效与振动开关在游戏选关页的“设置”中。辅助演示按钮仅属于此验收页。</p><div id="status">加载字体与游戏包…</div><p class="note" id="audio-status" role="status">音效:点击游戏后播放</p><p class="note">本页不触发触觉。原版/强化版切换用于音色和表现对照;iPhone、Android触觉与性能仍需真机验收。</p><p><a href="hints.html" target="_blank">运行200关提示像素检查</a></p></aside><div class="phone" id="phone"></div></main>
|
||||
<script type="module">
|
||||
import {solve} from '../src/upstream/rules.mjs';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Browser tab: 10, Title: "指路与玩家填色像素回归", URL: "http://127.0.0.1:17862/qa/hints.html".
|
||||
0 AXWebArea 指路与玩家填色像素回归, URL: 127.0.0.1:17862/qa/hints.html
|
||||
1 heading 指路与玩家填色像素回归, Value: 1
|
||||
2 text 指路与玩家填色像素回归
|
||||
3 text PASS:200/200 关,点击指路前后和撤销重画后,玩家已走格子的实际 Canvas 填色像素一致。左图无提示,右图开启高对比提示。
|
||||
|
||||
The focused UI element is 0 AXWebArea 指路与玩家填色像素回归, URL: 127.0.0.1:17862/qa/hints.html
|
||||
@@ -0,0 +1,104 @@
|
||||
|
||||
> 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 步和六类反馈音 (3.9885ms)
|
||||
✔ 关闭音效停止播放;再次开启会试听并保存偏好;后台中断停止所有播放器 (0.7226ms)
|
||||
✔ 延迟通关音在静音或进入后台时取消,不在返回后突然播放 (43.816ms)
|
||||
✔ 音频错误可诊断,不再静默吞掉 (0.4879ms)
|
||||
✔ 所有音频均为非静音 PCM;原版第13步比第1步高一个八度 (17.705ms)
|
||||
✔ 教学和组合规则用具体动作及奇偶例子解释,没有未解释的术语 (10.1978ms)
|
||||
✔ 过关提示同步使用实际播放起点,非请求时间 (0.67ms)
|
||||
✔ 过关和弦收尾持续衰减,不在后半段重新起音 (2.9896ms)
|
||||
✔ merged irregular outlines retain holes and never draw internal unit edges (47.5019ms)
|
||||
✔ all 200 boards fit four screen budgets with stable safe anchors and real shared entrances (610.3905ms)
|
||||
✔ guides through every irregular block stay inside its occupied shape (29.4299ms)
|
||||
✔ visited, conflicting and hinted category cells retain their material identity (10.7152ms)
|
||||
✔ five font subsets match their manifest and fit the package budget (3.8633ms)
|
||||
✔ upstream adapter fails closed if the inspected render seams change (0.7606ms)
|
||||
✔ procedural cushion pixels preserve holes and light the upper bevel without a border (18.2888ms)
|
||||
✔ visible glyph bounds are centered independently of font ascent and side bearings (1.8222ms)
|
||||
✔ stage with zero depth casts alpha shadows without painting a solid backing (8.1804ms)
|
||||
✔ native width-only TextMetrics still centers the rasterized glyph ink (2.6229ms)
|
||||
✔ fixed channels remain anchored at their block centers after number enlargement (781.7528ms)
|
||||
✔ checkpoint ribbons trigger on correct arrival only and never duplicate per frame (4.1184ms)
|
||||
✔ checkpoint pulse is small, centered in time and settles without looping (0.3466ms)
|
||||
✔ event batches preserve every landing and sequence but commit only highest audio and haptic (2.9218ms)
|
||||
✔ completion waits at most 120ms and ignores late audio callbacks (0.5196ms)
|
||||
✔ light haptics are limited, checkpoint replaces light, and completion adds only one delayed short pulse (0.4533ms)
|
||||
✔ unsupported graded haptic falls back to one short pulse, never long vibration (0.3297ms)
|
||||
✔ haptic disable, preview and lifecycle cancellation suppress delayed or expired work (0.5018ms)
|
||||
✔ late failed haptic requests cannot revive old pulses and ungraded completion falls back once (0.3539ms)
|
||||
✔ rectangles, concave shapes and hexagons require geometric inward distance without treating internal seams as walls (1.3157ms)
|
||||
✔ one high-speed native move records all intermediate cells and completion without duplicate final node (121.2167ms)
|
||||
✔ legal but off-solution moves use public rules, while invalid clues only produce error (60.7207ms)
|
||||
✔ drag near a previous-block boundary cannot undo; deeper entry can, and undo button remains immediate (44.9951ms)
|
||||
✔ tutorial, redraw, hint and restored saved routes do not generate player feedback (78.4011ms)
|
||||
✔ pressure and rebound have bounded amplitude and duration (0.3839ms)
|
||||
✔ interpolation cannot duplicate the same failed-cell event within one native movement (0.2255ms)
|
||||
✔ nodes are driven by explicit events even when multiple arrive before rendering; at most two ribbons survive (18.5497ms)
|
||||
✔ audio fast moves merge to latest pitch and completion stops the whole four-channel pool (1.2874ms)
|
||||
✔ settings persist haptic preference independently and chapter cards always use three columns (75.56ms)
|
||||
✔ original audio is preserved and high pitches lose energy rather than growing sharper (7.6609ms)
|
||||
✔ empty, partial and wrong routes reveal the first incomplete full segment (3.5196ms)
|
||||
✔ completion hides the hint; repeat clicks cannot skip; undo and restart return to earlier segments (0.5284ms)
|
||||
✔ all 200 levels reveal consecutive segments without changing the player path (1330.6071ms)
|
||||
✔ hint draws skip player edges and mask visited cells on their own layer; compositing is restored (0.8237ms)
|
||||
✔ parity is a segment anchor; colour is not; flow moves continuously across cells (0.3012ms)
|
||||
✔ coral teaching ribbon preserves visited masks and offers direction in reduced motion (1.3822ms)
|
||||
✔ hint rounds shared guide junctions and keeps separate runs disconnected (0.2984ms)
|
||||
✔ 实际教学关动画:basic (4.1589ms)
|
||||
✔ 实际教学关动画:walls (0.8719ms)
|
||||
✔ 实际教学关动画:straight (1.9467ms)
|
||||
✔ 实际教学关动画:vertical (2.6791ms)
|
||||
✔ 实际教学关动画:corner (1.1548ms)
|
||||
✔ 实际教学关动画:colour (0.5767ms)
|
||||
✔ 实际教学关动画:parity (0.515ms)
|
||||
✔ 实际教学关动画:rhythm-door (0.704ms)
|
||||
✔ 实际教学关动画:hex (0.6488ms)
|
||||
✔ 实际教学关动画:unknown-start (1.7224ms)
|
||||
✔ 200关的组合规则均能找到对应真实教学关 (91.9189ms)
|
||||
✔ 教学减速,结束停留1秒后自动循环 (4.4562ms)
|
||||
✔ music loops independently, persists mute and cancels in background (1.613ms)
|
||||
✔ old arrows retain distance, rush to endpoint and fresh arrows move at base speed (0.7279ms)
|
||||
✔ settings have exactly three independent preferences, and hint has its own cue (124.8702ms)
|
||||
✔ actual fixed numeric node uses checkpoint pitch, final cell uses complete chord (107.024ms)
|
||||
✔ two original music loops are quiet, nonempty and have a smooth loop seam (1.5419ms)
|
||||
✔ all 200 levels: valid numeric, parity and direction arrivals play checkpoint pitch; final arrival only completes (24773.4619ms)
|
||||
✔ drag back and undo button both vibrate lightly; disabling vibration suppresses both (50.2723ms)
|
||||
✔ haptic cadence is approximately 15 percent denser without changing light or medium grades (1.4843ms)
|
||||
✔ regular and special scale settle at 90ms with a larger special peak (0.3ms)
|
||||
✔ rendered undo target and nonnumeric checkpoint animate immediately without adding ribbons (6.1374ms)
|
||||
✔ quick undo cancels removed node rebound, including last-step removal (1.9203ms)
|
||||
✔ first launch goes directly to gameplay (86.9461ms)
|
||||
✔ resume unfinished route after exit from gameplay (105.339ms)
|
||||
✔ resume unfinished route after exit from selection (89.2179ms)
|
||||
✔ completed level resumes next unfinished, including leaving from cleared screen (75.952ms)
|
||||
✔ replaying a cleared level does not replace the unfinished save (79.5395ms)
|
||||
✔ legacy saves, malformed data and full completion have valid startup targets (56.1729ms)
|
||||
✔ selection pages expose fifteen cards and fit phone safe areas (247.9721ms)
|
||||
✔ hint-only frames reuse static UI, and player input invalidates it (46.1472ms)
|
||||
✔ hint curves are cached and distance lookup is stable at boundaries (1.1599ms)
|
||||
✔ new-mechanic lesson reuses the board while the continuous cursor advances (39.2951ms)
|
||||
ℹ tests 78
|
||||
ℹ suites 0
|
||||
ℹ pass 78
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 25049.2178
|
||||
|
||||
> numberswipe-main@1.0.0 smoke
|
||||
> node smoke.mjs
|
||||
|
||||
PASS 200/200: r2源文件哈希一致、唯一解、微信发布包触摸通关、撤销/重开/指路、存档/音效设置恢复、前后台切换。
|
||||
@@ -1,12 +1,12 @@
|
||||
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));
|
||||
export function boot(storage = new Map(), width = 375, height = 812) {
|
||||
const handlers = {}, frames = new Map(), played=[];
|
||||
const handlers = {}, frames = new Map(), played=[],vibrated=[];
|
||||
let frameId = 0,now=100000;
|
||||
class Clock extends Date {static now(){return now;}}
|
||||
const text = [],images=[];
|
||||
const ctx = new Proxy({ drawImage:(image,x,y,w,h)=>images.push({x,y,w,h}),measureText: s => ({ width: [...String(s)].reduce((n,c)=>n+(/[^\x00-\x7f]/.test(c)?1:.55),0)*(Number(/([\d.]+)px/.exec(ctx.font)?.[1])||16) }), fillText: (s, x, y) => text.push({ s, x, y }) }, { get: (o, k) => o[k] ?? (() => { }), set: (o, k, v) => (o[k] = v, true) });
|
||||
const wx = {setInnerAudioOption:()=>{},createInnerAudioContext:()=>({stop(){},onError(){},play(){assert.ok(fs.existsSync(path.join(root,this.src)),this.src);played.push(this.src);}}), createCanvas: () => ({ getContext: () => ctx }), getWindowInfo: () => ({ windowWidth: width, windowHeight: height, pixelRatio: 2, safeArea: { top: 44, bottom: height - 34 } }), getMenuButtonBoundingClientRect: () => ({ bottom: 80 }), getStorageSync: k => storage.get(k) || '', setStorageSync: (k, v) => storage.set(k, v), removeStorageSync: k => storage.delete(k), getPerformance: () => ({ now: () => Date.now() }), showModal: () => { }, env: { USER_DATA_PATH: '/user' } };
|
||||
const wx = {vibrateShort:o=>vibrated.push({type:o.type,at:now}),setInnerAudioOption:()=>{},createInnerAudioContext:()=>({stop(){},onError(){},play(){assert.ok(fs.existsSync(path.join(root,this.src)),this.src);played.push(this.src);}}), createCanvas: () => ({ getContext: () => ctx }), getWindowInfo: () => ({ windowWidth: width, windowHeight: height, pixelRatio: 2, safeArea: { top: 44, bottom: height - 34 } }), getMenuButtonBoundingClientRect: () => ({ bottom: 80 }), getStorageSync: k => storage.get(k) || '', setStorageSync: (k, v) => storage.set(k, v), removeStorageSync: k => storage.delete(k), getPerformance: () => ({ now: () => Date.now() }), showModal: () => { }, env: { USER_DATA_PATH: '/user' } };
|
||||
wx.loadFont=p=>{assert.ok(fs.existsSync(path.join(root,p)),'字体必须随包提供 '+p);return path.basename(p,'.ttf');};
|
||||
for (const name of ['TouchStart', 'TouchMove', 'TouchEnd', 'TouchCancel', 'Hide', 'Show', 'WindowResize'])
|
||||
wx['on' + name] = cb => handlers[name] = cb;
|
||||
@@ -25,5 +25,5 @@ export function boot(storage = new Map(), width = 375, height = 812) {
|
||||
assert.ok(++steps < 25);
|
||||
} button(api.buttons().some(b => b.label === label) ? label : label + ' ✓'); if (api.buttons().some(b => b.label === '返回游戏'))
|
||||
button('返回游戏'); assert.equal(api.snapshot().level, ordered[i].id); };
|
||||
return { advance(ms){now+=ms;tick();}, api, button, cell, select, tick, handlers, frames, storage, text, played,images };
|
||||
return { advance(ms){now+=ms;tick();}, api, button, cell, select, tick, handlers, frames, storage, text, played,images,vibrated };
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"channel": "main",
|
||||
"version": "2026-10-23-main-layered-feedback",
|
||||
"version": "2026-10-23-responsive-nodes-sticker-header",
|
||||
"date": "2026-10-23",
|
||||
"baseVersion": "2026-10-22-resume-chapter-lessons",
|
||||
"baseMainSHA256": "bb35d5edf8d96eb3253c78052d219b32f9a4241efd9971537f6c9c8896188125",
|
||||
"package": "NumberSwipe-main-20261023.zip",
|
||||
"promotedFrom": "2026-10-22-layered-feedback-music-flow"
|
||||
"package": "NumberSwipe-main-节点跟手与立体标题-20261023.zip",
|
||||
"promotedFrom": "2026-10-22-layered-feedback-music-flow",
|
||||
"previousVersion": "2026-10-23-main-layered-feedback"
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export function renderBrightBoard(ctx,level,path,width,height,{now=Date.now(),pr
|
||||
for(const event of scene.feedbackEvents)if(event.sequence>(scene.feedbackSequence||0)){
|
||||
if(event.kind==='complete')scene.bursts=[];
|
||||
else if(event.kind==='undo')scene.bursts=scene.bursts.filter(b=>b.cell!==event.cell);
|
||||
else if(event.kind==='checkpoint'&&!preferences.reducedMotion){const g=scene.byId.get(event.cell);if(g)scene.bursts.push({cell:event.cell,point:g.center,started:event.time,seed:event.step});}
|
||||
else if(event.kind==='checkpoint'&&event.ribbon!==false&&!preferences.reducedMotion){const g=scene.byId.get(event.cell);if(g)scene.bursts.push({cell:event.cell,point:g.center,started:event.time,seed:event.step});}
|
||||
scene.feedbackSequence=event.sequence;
|
||||
}
|
||||
scene.bursts=scene.bursts.slice(-FEEDBACK.maxRibbons);
|
||||
@@ -232,7 +232,7 @@ export function renderBrightBoard(ctx,level,path,width,height,{now=Date.now(),pr
|
||||
for(let i=0;i<scene.cells.length;i++)drawSymbols(ctx,scene,scene.cells[i],scene.views[i],path);
|
||||
drawCheckpointRibbons(ctx,scene,now);
|
||||
drawBarriers(ctx,scene,path,now);ctx.restore();
|
||||
scene.animating=!preview&&!preferences.reducedMotion&&(scene.bursts.length>0||scene.feedbackEvents.some(e=>now-e.time<(e.kind==='move'?FEEDBACK.moveMs:e.kind==='undo'?FEEDBACK.undoMs:0))||[...scene.doorTransitions.values()].some(d=>d.from!==d.to&&now-d.started<T.motionMs.door));
|
||||
scene.animating=!preview&&!preferences.reducedMotion&&(scene.bursts.length>0||scene.feedbackEvents.some(e=>now-e.time<(e.kind==='checkpoint'||e.kind==='complete'?FEEDBACK.checkpointMs:FEEDBACK.moveMs))||[...scene.doorTransitions.values()].some(d=>d.from!==d.to&&now-d.started<T.motionMs.door));
|
||||
return scene;
|
||||
}
|
||||
|
||||
@@ -272,11 +272,10 @@ export function checkpointScale(elapsed,reducedMotion=false){
|
||||
return 1+(FEEDBACK.checkpointMaxScale-1)*Math.sin(Math.PI*elapsed/FEEDBACK.checkpointMs);
|
||||
}
|
||||
function withCheckpointScale(ctx,scene,g,now,paint){
|
||||
const burst=[...scene.bursts].reverse().find(b=>b.cell===g.cell.id);
|
||||
const landing=feedback.enhanced&&scene.feedbackEvents.findLast(e=>e.cell===g.cell.id&&e.kind==='move');
|
||||
const scale=preferences.reducedMotion?1:burst&&scene.steps.has(g.cell.id)?checkpointScale(now-burst.started):landing&&scene.steps.has(g.cell.id)?landingScale(now-landing.time):1;
|
||||
const landing=feedback.enhanced&&scene.feedbackEvents.findLast(e=>e.cell===g.cell.id||e.kind==='undo'&&e.from===g.cell.id);
|
||||
const scale=preferences.reducedMotion||!landing||landing.kind==='undo'&&landing.from&&landing.from!==g.cell.id?1:landing.kind==='checkpoint'||landing.kind==='complete'?checkpointScale(now-landing.time):landingScale(now-landing.time);
|
||||
if(scale===1){paint();return;}
|
||||
ctx.save();ctx.translate(...g.center);ctx.scale(scale,scale);ctx.translate(-g.center[0],-g.center[1]);paint();ctx.restore();
|
||||
}
|
||||
|
||||
export function landingScale(elapsed){return elapsed<0||elapsed>=FEEDBACK.moveMs?1:1-(1-FEEDBACK.moveMinScale)*Math.sin(Math.PI*elapsed/FEEDBACK.moveMs);}
|
||||
export function landingScale(elapsed){return elapsed<0||elapsed>=FEEDBACK.moveMs?1:1+(FEEDBACK.moveMaxScale-1)*Math.sin(Math.PI*elapsed/FEEDBACK.moveMs);}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const FEEDBACK={moveMs:120,moveMinScale:.985,bridgeMs:100,undoMs:100,checkpointMs:260,checkpointMaxScale:1.035,ribbonMs:1500,maxRibbons:2,hapticGapMs:70,completeHapticDelayMs:90,audioGapMs:50,completeWaitMs:120,errorGapMs:180};
|
||||
export const FEEDBACK={moveMs:90,moveMaxScale:1.025,bridgeMs:70,undoMs:90,checkpointMs:90,checkpointMaxScale:1.065,ribbonMs:1500,maxRibbons:2,hapticGapMs:61,completeHapticDelayMs:90,audioGapMs:50,completeWaitMs:120,errorGapMs:180};
|
||||
export const priority={move:1,undo:0,error:0,checkpoint:2,complete:3};
|
||||
|
||||
@@ -19,7 +19,7 @@ export class Feedback{
|
||||
}
|
||||
flush(){const e=this.pending;this.pending=null;if(!e)return;
|
||||
this.audio?.play(e.kind==='checkpoint'?'checkpoint':e.kind,0,e.step);
|
||||
if(this.enhanced){if(e.kind==='undo'||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();}
|
||||
completionStart(actual,soundEnabled=true){
|
||||
|
||||
@@ -10,7 +10,7 @@ export class Haptics{
|
||||
const fallback=()=>{if(generation!==this.generation||request!==this.request||this.now()-sentAt>FEEDBACK.hapticGapMs||!this.enabled)return;this.graded=false;try{this.api.vibrateShort({fail(){}});}catch{}};
|
||||
try{this.api.vibrateShort(this.graded?{type,fail:fallback}:{fail(){}});}catch{if(this.graded)fallback();}
|
||||
}
|
||||
play(kind){if(!['move','checkpoint','complete'].includes(kind))return;this.pulse(kind==='move'?'light':'medium',kind!=='move');
|
||||
play(kind){if(!['move','undo','checkpoint','complete'].includes(kind))return;this.pulse(kind==='move'||kind==='undo'?'light':'medium',kind!=='move'&&kind!=='undo');
|
||||
if(kind==='complete'&&this.enabled&&this.graded&&!this.preview&&typeof this.api?.vibrateShort==='function'){const generation=this.generation;const timer=this.later(()=>{this.timers.delete(timer);if(generation===this.generation&&this.graded)this.pulse('light',true);},FEEDBACK.completeHapticDelayMs);this.timers.add(timer);}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {C,label,panel,roundRect} from './bright-classroom/theme.mjs';
|
||||
import {C,label,panel,roundRect,textFont,inkMetrics} from './bright-classroom/theme.mjs';
|
||||
|
||||
export function chapterLayout(width,height,top,bottom,count=15){
|
||||
const columns=3,rows=Math.ceil(count/columns),gap=10;
|
||||
@@ -13,3 +13,24 @@ export function levelCard(ctx,labelText,x,y,w,h,{completed,current}){
|
||||
label(ctx,labelText,x+w/2-(completed?8:0),y+(h-3)/2,Math.min(24,h*.4),current&&!completed?C.onPrimary:completed?'#326443':C.ink,700);
|
||||
if(completed){const size=Math.min(24,h*.4),cx=x+w/2+String(labelText).length*size*.28+5,cy=y+(h-3)/2;ctx.save();ctx.strokeStyle='#326443';ctx.lineWidth=2.6;ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();ctx.moveTo(cx-4,cy);ctx.lineTo(cx-1,cy+3);ctx.lineTo(cx+5,cy-4);ctx.stroke();ctx.restore();}
|
||||
}
|
||||
|
||||
// Soft sticker lettering stays sharp on native Canvas at every device density.
|
||||
export function selectionHeader(ctx,width,top,progress,first,last){
|
||||
const size=width<350?36:41,cx=width/2-16,y=top+12,advance=size*1.02;
|
||||
ctx.save();textFont(ctx,size,700);ctx.textAlign='left';ctx.textBaseline='alphabetic';ctx.lineJoin='round';ctx.lineCap='round';
|
||||
for(const [i,char]of [...'数字一笔'].entries()){
|
||||
const m=inkMetrics(ctx,char,size),x=cx+(i-1.5)*advance,baseline=Number.isFinite(m.actualBoundingBoxAscent)?(m.actualBoundingBoxAscent-m.actualBoundingBoxDescent)/2:size*.35;
|
||||
ctx.save();ctx.translate(x,y);ctx.rotate([-.07,.04,-.025,.065][i]);
|
||||
const left=Number.isFinite(m.actualBoundingBoxLeft)?(m.actualBoundingBoxLeft-m.actualBoundingBoxRight)/2:-m.width/2;
|
||||
ctx.lineWidth=10;ctx.strokeStyle='#E8B45D';ctx.strokeText(char,left,baseline+4);
|
||||
ctx.lineWidth=10;ctx.strokeStyle='#FFF6DA';ctx.strokeText(char,left,baseline+1);
|
||||
ctx.lineWidth=7;ctx.strokeStyle='#FFFFFF';ctx.strokeText(char,left,baseline-1);
|
||||
const gradient=ctx.createLinearGradient(0,-size/2,0,size/2),orange=i<2;
|
||||
if(gradient?.addColorStop){gradient.addColorStop(0,orange?'#FF9564':'#9990FF');gradient.addColorStop(.45,orange?'#FF713F':'#6C61F4');gradient.addColorStop(1,orange?'#F2522D':'#4945D8');}
|
||||
ctx.fillStyle=gradient?.addColorStop?gradient:orange?'#FF713F':'#6C61F4';ctx.lineWidth=1.7;ctx.strokeStyle=orange?'#FA6C3C':'#6757EE';ctx.strokeText(char,left,baseline);ctx.fillText(char,left,baseline);ctx.restore();
|
||||
}
|
||||
ctx.restore();
|
||||
const gap=10,pw=132,rw=104,x=(width-pw-rw-gap)/2,py=top+42;
|
||||
panel(ctx,x,py,pw,24,12,C.surface,2);label(ctx,'总进度 '+progress,x+pw/2,py+12,12,C.ink,700);
|
||||
panel(ctx,x+pw+gap,py,rw,24,12,C.coral,2);label(ctx,'第 '+first+'–'+last+' 关',x+pw+gap+rw/2,py+12,12,C.onPrimary,700);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Music} from './music.mjs';
|
||||
import {HintArrows} from './hint-arrows.mjs';
|
||||
import {feedback} from './feedback.mjs';
|
||||
import releaseInfo from '../release-info.json' with {type:'json'};
|
||||
import {chapterLayout,levelCard} from './level-select.mjs';
|
||||
import {chapterLayout,levelCard,selectionHeader} from './level-select.mjs';
|
||||
import {CLEAR_DURATION,clearProgress,drawClearCelebration} from './bright-classroom/celebration.mjs';
|
||||
import {stopAudio,audioDiagnostics} from './audio.mjs';
|
||||
import { mainCanvas, boardCanvas, document, window, PACK, setViewInvalidator } from './platform.mjs';
|
||||
@@ -74,9 +74,9 @@ function draw() {
|
||||
buttons = [];
|
||||
const select = $('screen-play').className.includes('hidden');
|
||||
if (select) {
|
||||
text('数字一笔', width / 2, top + 18, 30,C.primary);
|
||||
|
||||
text('总进度 '+$('progress').textContent, width / 2, top + 42, 12,C.inkSecondary);
|
||||
|
||||
|
||||
button('设置',width-72,top+4,56,34,()=>settingsOpen=true);
|
||||
const levels=levelButtons(),pages=Math.ceil(ordered.length/15);
|
||||
if(!selectWasOpen)page=Math.floor((state.displayOrder-1)/15);
|
||||
@@ -84,7 +84,7 @@ function draw() {
|
||||
const chapterLevels=ordered.slice(page*15,(page+1)*15);
|
||||
const nodes=chapterLevels.map(l=>levels[ordered.indexOf(l)]);
|
||||
const layout=chapterLayout(width,height,top,bottom,15);
|
||||
text('第 '+(page*15+1)+' — '+Math.min((page+1)*15,ordered.length)+' 关',width/2,top+60,16,C.primary);
|
||||
selectionHeader(ctx,width,top,$('progress').textContent,page*15+1,Math.min((page+1)*15,ordered.length));
|
||||
nodes.forEach((node,j)=>{
|
||||
const row=Math.floor(j/layout.columns),column=j%layout.columns,last=nodes.length-row*layout.columns;
|
||||
const offset=0;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"undoRestartHint": true,
|
||||
"storageRestore": true,
|
||||
"foregroundLoop": true,
|
||||
"nativeAudioPlaybackCalls": 2031,
|
||||
"nativeAudioPlaybackCalls": 2243,
|
||||
"plainLanguageRules": true,
|
||||
"pagedRules": true,
|
||||
"clearActionLayout": true,
|
||||
|
||||
Reference in New Issue
Block a user