用参考图标题美术替换字体描边模拟
This commit is contained in:
@@ -28,7 +28,7 @@ function boot(){
|
||||
const later=(callback,delay)=>{const timer=setTimeout(()=>{timers.delete(timer);callback();},delay);timers.add(timer);return timer;};
|
||||
const cancel=timer=>{clearTimeout(timer);timers.delete(timer);};
|
||||
class Clock extends Date{static now(){return now;}}
|
||||
const wx={__feedbackPreview:true,__feedbackMode:typeof feedbackMode==='undefined'?'enhanced':feedbackMode,createCanvas(){const c=document.createElement('canvas');if(!canvases.length)canvases.push(c);return c;},loadFont:p=>p.split('/').at(-1).replace('.ttf',''),getWindowInfo:()=>({windowWidth:width,windowHeight:height,pixelRatio:2,safeArea:{top:24,bottom:height-24}}),getMenuButtonBoundingClientRect:()=>({bottom:42}),getPerformance:()=>({now:()=>now}),getStorageSync:k=>store.get(k)||'',setStorageSync:(k,v)=>store.set(k,v),removeStorageSync:k=>store.delete(k),setInnerAudioOption:sound.setInnerAudioOption,createInnerAudioContext:sound.createInnerAudioContext,showModal(){},env:{USER_DATA_PATH:'/preview'}};
|
||||
const wx={createImage(){const i=new Image();return {get width(){return i.naturalWidth;},get height(){return i.naturalHeight;},set onload(fn){i.onload=fn;},set onerror(fn){i.onerror=fn;},set src(v){i.src=new URL('../'+v,location.href).href;},_native:i};},__feedbackPreview:true,__feedbackMode:typeof feedbackMode==='undefined'?'enhanced':feedbackMode,createCanvas(){const c=document.createElement('canvas');const p=c.getContext('2d'),draw=p.drawImage.bind(p);p.drawImage=(i,...args)=>draw(i?._native||i,...args);if(!canvases.length)canvases.push(c);return c;},loadFont:p=>p.split('/').at(-1).replace('.ttf',''),getWindowInfo:()=>({windowWidth:width,windowHeight:height,pixelRatio:2,safeArea:{top:24,bottom:height-24}}),getMenuButtonBoundingClientRect:()=>({bottom:42}),getPerformance:()=>({now:()=>now}),getStorageSync:k=>store.get(k)||'',setStorageSync:(k,v)=>store.set(k,v),removeStorageSync:k=>store.delete(k),setInnerAudioOption:sound.setInnerAudioOption,createInnerAudioContext:sound.createInnerAudioContext,showModal(){},env:{USER_DATA_PATH:'/preview'}};
|
||||
for(const n of ['TouchStart','TouchMove','TouchEnd','TouchCancel','Hide','Show','WindowResize'])wx['on'+n]=cb=>handlers[n]=cb;
|
||||
const global={};new Function('wx','GameGlobal','requestAnimationFrame','cancelAnimationFrame','setTimeout','clearTimeout','Date',source)(wx,global,fn=>{frames.set(++frame,fn);return frame;},id=>frames.delete(id),later,cancel,Clock);
|
||||
const api=global.numberSwipeR2,canvas=canvases[0];canvas.style.width=width+'px';canvas.style.height=height+'px';phone.style.width=width+'px';phone.replaceChildren(canvas);
|
||||
|
||||
@@ -16,9 +16,9 @@ const source=await(await fetch('../game.js')).text();
|
||||
const levels=pack.playOrder.map(id=>pack.levels.find(l=>l.id===id));
|
||||
const canvases=[],handlers={},frames=new Map(),storage=new Map();let frameId=0,now=100000;
|
||||
class Clock extends Date{static now(){return now;}}
|
||||
const wx={__feedbackPreview:true,__feedbackMode:typeof feedbackMode==='undefined'?'enhanced':feedbackMode,
|
||||
const wx={createImage(){const i=new Image();return {get width(){return i.naturalWidth;},get height(){return i.naturalHeight;},set onload(fn){i.onload=fn;},set onerror(fn){i.onerror=fn;},set src(v){i.src=new URL('../'+v,location.href).href;},_native:i};},__feedbackPreview:true,__feedbackMode:typeof feedbackMode==='undefined'?'enhanced':feedbackMode,
|
||||
loadFont:p=>p.split('/').at(-1).replace('.ttf',''),
|
||||
createCanvas(){const c=document.createElement('canvas');if(!canvases.length)canvases.push(c);return c;},
|
||||
createCanvas(){const c=document.createElement('canvas');const p=c.getContext('2d'),draw=p.drawImage.bind(p);p.drawImage=(i,...args)=>draw(i?._native||i,...args);if(!canvases.length)canvases.push(c);return c;},
|
||||
getWindowInfo:()=>({windowWidth:375,windowHeight:812,pixelRatio:1,safeArea:{top:44,bottom:778}}),
|
||||
getMenuButtonBoundingClientRect:()=>({bottom:80}),getPerformance:()=>({now:()=>now}),
|
||||
getStorageSync:k=>storage.get(k)||'',setStorageSync:(k,v)=>storage.set(k,v),removeStorageSync:k=>storage.delete(k),
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,109 @@
|
||||
|
||||
> 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.0119ms)
|
||||
✔ 关闭音效停止播放;再次开启会试听并保存偏好;后台中断停止所有播放器 (0.9311ms)
|
||||
✔ 延迟通关音在静音或进入后台时取消,不在返回后突然播放 (47.2484ms)
|
||||
✔ 音频错误可诊断,不再静默吞掉 (0.5926ms)
|
||||
✔ 所有音频均为非静音 PCM;原版第13步比第1步高一个八度 (77.8758ms)
|
||||
✔ 教学和组合规则用具体动作及奇偶例子解释,没有未解释的术语 (18.6892ms)
|
||||
✔ 过关提示同步使用实际播放起点,非请求时间 (0.6079ms)
|
||||
✔ 过关和弦收尾持续衰减,不在后半段重新起音 (3.0069ms)
|
||||
✔ merged irregular outlines retain holes and never draw internal unit edges (129.6431ms)
|
||||
✔ all 200 boards fit four screen budgets with stable safe anchors and real shared entrances (923.4894ms)
|
||||
✔ guides through every irregular block stay inside its occupied shape (59.9688ms)
|
||||
✔ visited, conflicting and hinted category cells retain their material identity (19.912ms)
|
||||
✔ five font subsets match their manifest and fit the package budget (6.5877ms)
|
||||
✔ upstream adapter fails closed if the inspected render seams change (1.9187ms)
|
||||
✔ procedural cushion pixels preserve holes and light the upper bevel without a border (30.7559ms)
|
||||
✔ visible glyph bounds are centered independently of font ascent and side bearings (2.2727ms)
|
||||
✔ stage with zero depth casts alpha shadows without painting a solid backing (7.147ms)
|
||||
✔ native width-only TextMetrics still centers the rasterized glyph ink (3.0655ms)
|
||||
✔ fixed channels remain anchored at their block centers after number enlargement (1269.7132ms)
|
||||
✔ checkpoint ribbons trigger on correct arrival only and never duplicate per frame (4.1897ms)
|
||||
✔ checkpoint pulse is small, centered in time and settles without looping (0.4945ms)
|
||||
✔ event batches preserve every landing and sequence but commit only highest audio and haptic (6.0019ms)
|
||||
✔ completion waits at most 120ms and ignores late audio callbacks (1.2469ms)
|
||||
✔ light haptics are limited, checkpoint replaces light, and completion adds only one delayed short pulse (1.9119ms)
|
||||
✔ unsupported graded haptic falls back to one short pulse, never long vibration (1.3278ms)
|
||||
✔ haptic disable, preview and lifecycle cancellation suppress delayed or expired work (0.7516ms)
|
||||
✔ late failed haptic requests cannot revive old pulses and ungraded completion falls back once (0.4369ms)
|
||||
✔ rectangles, concave shapes and hexagons require geometric inward distance without treating internal seams as walls (15.9339ms)
|
||||
✔ one high-speed native move records all intermediate cells and completion without duplicate final node (267.092ms)
|
||||
✔ legal but off-solution moves use public rules, while invalid clues only produce error (97.6956ms)
|
||||
✔ drag near a previous-block boundary cannot undo; deeper entry can, and undo button remains immediate (76.8074ms)
|
||||
✔ tutorial, redraw, hint and restored saved routes do not generate player feedback (166.166ms)
|
||||
✔ pressure and rebound have bounded amplitude and duration (0.4204ms)
|
||||
✔ interpolation cannot duplicate the same failed-cell event within one native movement (0.4006ms)
|
||||
✔ nodes are driven by explicit events even when multiple arrive before rendering; at most two ribbons survive (35.9257ms)
|
||||
✔ audio fast moves merge to latest pitch and completion stops the whole four-channel pool (3.0347ms)
|
||||
✔ settings persist haptic preference independently and chapter cards always use three columns (81.7164ms)
|
||||
✔ original audio is preserved and high pitches lose energy rather than growing sharper (9.9401ms)
|
||||
✔ empty, partial and wrong routes reveal the first incomplete full segment (3.6375ms)
|
||||
✔ completion hides the hint; repeat clicks cannot skip; undo and restart return to earlier segments (0.9455ms)
|
||||
✔ all 200 levels reveal consecutive segments without changing the player path (2293.2505ms)
|
||||
✔ hint draws skip player edges and mask visited cells on their own layer; compositing is restored (1.0414ms)
|
||||
✔ parity is a segment anchor; colour is not; flow moves continuously across cells (0.4072ms)
|
||||
✔ coral teaching ribbon preserves visited masks and offers direction in reduced motion (1.7181ms)
|
||||
✔ hint rounds shared guide junctions and keeps separate runs disconnected (0.4092ms)
|
||||
✔ 实际教学关动画:basic (5.1841ms)
|
||||
✔ 实际教学关动画:walls (1.1257ms)
|
||||
✔ 实际教学关动画:straight (2.8009ms)
|
||||
✔ 实际教学关动画:vertical (6.6101ms)
|
||||
✔ 实际教学关动画:corner (2.6063ms)
|
||||
✔ 实际教学关动画:colour (2.0917ms)
|
||||
✔ 实际教学关动画:parity (0.8465ms)
|
||||
✔ 实际教学关动画:rhythm-door (1.214ms)
|
||||
✔ 实际教学关动画:hex (1.4503ms)
|
||||
✔ 实际教学关动画:unknown-start (4.5831ms)
|
||||
✔ 200关的组合规则均能找到对应真实教学关 (216.7609ms)
|
||||
✔ 教学减速,结束停留1秒后自动循环 (8.4881ms)
|
||||
✔ music loops independently, persists mute and cancels in background (4.1939ms)
|
||||
✔ old arrows retain distance, rush to endpoint and fresh arrows move at base speed (1.1354ms)
|
||||
✔ settings have exactly three independent preferences, and hint has its own cue (242.1052ms)
|
||||
✔ actual fixed numeric node uses checkpoint pitch, final cell uses complete chord (161.9424ms)
|
||||
✔ two original music loops are quiet, nonempty and have a smooth loop seam (7.5788ms)
|
||||
✔ warm gameplay ambient is 32 seconds, softly leveled and free from percussive envelope jumps (31.2164ms)
|
||||
✔ gameplay ambient uses lower scene volume and keeps independent mute and resume behavior (0.8613ms)
|
||||
✔ all 200 levels: valid numeric, parity and direction arrivals play checkpoint pitch; final arrival only completes (29378.3694ms)
|
||||
✔ drag back and undo button both vibrate lightly; disabling vibration suppresses both (40.0552ms)
|
||||
✔ haptic cadence is approximately 15 percent denser without changing light or medium grades (1.0956ms)
|
||||
✔ regular and special scale settle at 90ms with a larger special peak (0.1883ms)
|
||||
✔ rendered undo target and nonnumeric checkpoint animate immediately without adding ribbons (6.2144ms)
|
||||
✔ quick undo cancels removed node rebound, including last-step removal (1.9216ms)
|
||||
✔ first launch goes directly to gameplay (136.5207ms)
|
||||
✔ resume unfinished route after exit from gameplay (169.5016ms)
|
||||
✔ resume unfinished route after exit from selection (204.0092ms)
|
||||
✔ completed level resumes next unfinished, including leaving from cleared screen (102.4366ms)
|
||||
✔ replaying a cleared level does not replace the unfinished save (95.3532ms)
|
||||
✔ legacy saves, malformed data and full completion have valid startup targets (82.9585ms)
|
||||
✔ selection pages expose fifteen cards and fit phone safe areas (433.5342ms)
|
||||
✔ hint-only frames reuse static UI, and player input invalidates it (63.2693ms)
|
||||
✔ hint curves are cached and distance lookup is stable at boundaries (1.0996ms)
|
||||
✔ new-mechanic lesson reuses the board while the continuous cursor advances (46.259ms)
|
||||
✔ title artwork is a transparent, high resolution PNG packaged locally (3.9438ms)
|
||||
✔ asynchronous native image load requests a repaint and draws the artwork without a font substitute (1.4977ms)
|
||||
✔ artwork stays above progress pills and clear of settings; fifteen cards still fit all phone sizes (414.5903ms)
|
||||
ℹ tests 83
|
||||
ℹ suites 0
|
||||
ℹ pass 83
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 29896.8672
|
||||
|
||||
> numberswipe-main@1.0.0 smoke
|
||||
> node smoke.mjs
|
||||
|
||||
PASS 200/200: r2源文件哈希一致、唯一解、微信发布包触摸通关、撤销/重开/指路、存档/音效设置恢复、前后台切换。
|
||||
@@ -5,8 +5,8 @@ export function boot(storage = new Map(), width = 375, height = 812) {
|
||||
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 = {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' } };
|
||||
const ctx = new Proxy({ drawImage:(image,x,y,w,h)=>images.push({source:image.source||null,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 = {createImage:()=>({set src(value){const b=fs.readFileSync(path.join(root,value));this.width=b.readUInt32BE(16);this.height=b.readUInt32BE(20);this.source=value;this.onload?.();}}),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;
|
||||
|
||||
@@ -6,5 +6,5 @@ const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
||||
http.createServer((req,res)=>{
|
||||
const file=path.resolve(root,'.'+decodeURIComponent(new URL(req.url,'http://localhost').pathname));
|
||||
if(!file.startsWith(root+path.sep)){res.writeHead(403).end();return;}
|
||||
try{res.setHeader('Content-Type',file.endsWith('.html')?'text/html; charset=utf-8':file.endsWith('.wav')?'audio/wav':file.endsWith('.json')?'application/json':'text/javascript; charset=utf-8');res.end(fs.readFileSync(file));}catch{res.writeHead(404).end();}
|
||||
try{res.setHeader('Content-Type',file.endsWith('.html')?'text/html; charset=utf-8':file.endsWith('.png')?'image/png':file.endsWith('.wav')?'audio/wav':file.endsWith('.json')?'application/json':'text/javascript; charset=utf-8');res.end(fs.readFileSync(file));}catch{res.writeHead(404).end();}
|
||||
}).listen(Number(process.env.NUMBERSWIPE_QA_PORT||17859),'127.0.0.1',()=>console.log('Canvas QA: http://127.0.0.1:17859/qa/hints.html'));
|
||||
|
||||
Reference in New Issue
Block a user