// 回归测试:R 键必须"自杀回存档点",不能把整关重开(否则存档点白碰) const noop = () => { }; const els = new Map(); function mkEl(id = '') { return { id, tagName: 'DIV', value: '', textContent: '', innerHTML: '', style: {}, dataset: {}, children: [], width: 800, height: 608, classList: { add: noop, remove: noop, toggle: noop, contains: () => false }, addEventListener: noop, removeEventListener: noop, appendChild: noop, querySelector: () => mkEl(), querySelectorAll: () => [], getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 608 }), getContext: () => ctx2d, focus: noop, click: noop, }; } const ctx2d = new Proxy({ canvas: { width: 800, height: 608 }, measureText: () => ({ width: 12 }), createLinearGradient: () => ({ addColorStop: noop }), createRadialGradient: () => ({ addColorStop: noop }), getImageData: () => ({ data: [] }), }, { get(t, k) { return k in t ? t[k] : noop; }, set(t, k, v) { t[k] = v; return true; } }); const handlers = {}; const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); }; const key = (t, c) => (handlers[t] || []).forEach(fn => fn({ code: c, preventDefault() { }, repeat: false })); let clock = 1000; globalThis.performance = { now: () => clock }; let rafCb = null; globalThis.requestAnimationFrame = fn => { rafCb = fn; return 1; }; globalThis.window = { addEventListener: addL, removeEventListener: noop, AudioContext: null, webkitAudioContext: null, innerWidth: 1280, innerHeight: 800, }; globalThis.addEventListener = addL; globalThis.removeEventListener = noop; globalThis.confirm = () => true; globalThis.alert = noop; globalThis.localStorage = { _d: {}, getItem(k) { return this._d[k] ?? null; }, setItem(k, v) { this._d[k] = String(v); }, removeItem(k) { delete this._d[k]; }, }; globalThis.document = { getElementById(id) { if (!els.has(id)) els.set(id, mkEl(id)); return els.get(id); }, querySelector: s => mkEl(s), querySelectorAll: () => [], createElement: t => mkEl(), addEventListener: addL, body: mkEl('body'), }; await import('../js/main.js'); const { LEVELS } = await import('../js/levels/index.js'); const api = globalThis.window.__IW; let fail = 0; const err = m => { console.log(' ✗ ' + m); fail++; }; const step = n => { for (let i = 0; i < n; i++) { clock += 17; const f = rafCb; rafCb = null; if (f) f(clock); } }; const d = LEVELS.find(l => l.id === 'j1'); api.startLevel(d, 0); step(10); const L0 = api.G.level, P = api.G.player; // 手动吃一个存档点(模拟玩家走到那里) const s = L0.entities.filter(e => e.type === 'save')[1]; P.x = s.x + s.w / 2 - P.w / 2; P.y = s.y + s.h - P.h; step(3); const cp = P.savePoint; console.log(` 存档点激活=${!!cp} 位置=(${cp?.x.toFixed(0)},${cp?.y.toFixed(0)}) 关卡出生点x=${L0.spawn.x.toFixed(0)}`); if (!cp) err('存档点未激活'); // ---- 按 R ---- const deaths0 = api.G.deaths; key('keydown', 'KeyR'); step(2); key('keyup', 'KeyR'); step(60); const survivedLevel = api.G.level === L0; console.log(` 按 R 后:关卡对象未被重建=${survivedLevel} 玩家x=${api.G.player.x.toFixed(0)} 死亡=${deaths0}→${api.G.deaths}`); if (!survivedLevel) err('R 键把整关重开了,存档进度丢失'); if (Math.abs(api.G.player.x - cp.x) > 4) err(`R 键后没有回到存档点(x=${api.G.player.x.toFixed(0)} 期望 ${cp.x.toFixed(0)})`); if (api.G.deaths !== deaths0 + 1) err('R 键没有计入死亡数'); if (Math.abs(api.G.player.x - L0.spawn.x) < 4) err('R 键后被送回了关卡起点'); // ---- 按 Backspace = 整关重来 ---- key('keydown', 'Backspace'); step(2); key('keyup', 'Backspace'); step(10); console.log(` 按 Backspace 后:关卡对象被重建=${api.G.level !== L0} 玩家x=${api.G.player.x.toFixed(0)}`); if (api.G.level === L0) err('Backspace 没有整关重开'); if (Math.abs(api.G.player.x - api.G.level.spawn.x) > 4) err('Backspace 后玩家不在出生点'); console.log(fail ? `\n❌ ${fail} 项失败` : '\n✅ 按键语义正确:R=回存档点,Backspace=整关重来'); process.exit(fail ? 1 : 0);