// 回归测试: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.location = { search: '', href: 'http://localhost/', pathname: '/', reload: noop }; Object.defineProperty(globalThis, 'navigator', { value: { maxTouchPoints: 0, userAgent: 'node' }, configurable: true }); globalThis.matchMedia = q => ({ matches: false, media: q, addEventListener: noop, removeEventListener: noop }); globalThis.window = { addEventListener: addL, removeEventListener: noop, AudioContext: null, webkitAudioContext: null, innerWidth: 1280, innerHeight: 800, location: globalThis.location, navigator: globalThis.navigator, matchMedia: globalThis.matchMedia, }; 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; const marks0 = L0.deathMarks.length; P.x = L0.spawn.x; P.y = L0.spawn.y; // 先站到出生点,验证能回到存档点而非原地不动 step(5); const beforeX = api.G.player.x; key('keydown', 'KeyR'); step(2); key('keyup', 'KeyR'); console.log(` 活着按 R 后:玩家x=${beforeX.toFixed(0)}→${api.G.player.x.toFixed(0)} alive=${api.G.player.alive} dead=${api.G.player.dead} 死亡数=${deaths0}→${api.G.deaths} 红叉数=${marks0}→${L0.deathMarks.length} 粒子=${L0.particles.list.length} 冲击环=${L0.deathFX.length}`); if (api.G.player.dead || !api.G.player.alive) err('活着按 R 没有当帧重生'); if (api.G.deaths !== deaths0) err('主动重生被计入了死亡数(主动重生不是死亡)'); if (L0.deathMarks.length !== marks0) err('主动重生留下了红叉(不应留血迹)'); if (Math.abs(api.G.player.x - cp.x) > 4) err(`按 R 没有回到存档点(x=${api.G.player.x.toFixed(0)} 期望 ${cp.x.toFixed(0)})`); if (!(L0.particles.list.length > 0)) err('死亡演出粒子缺失(应留在死亡位置播完)'); if (!L0.deathFX.length) err('死亡演出冲击环缺失(应留在死亡位置播完)'); // ---- 死亡中按 R = 手动重生回存档点 ---- api.G.player.kill(); step(3); if (!api.G.player.dead) err('kill 后未进入死亡定格状态'); key('keydown', 'KeyR'); step(2); key('keyup', 'KeyR'); step(10); const survivedLevel = api.G.level === L0; console.log(` 死亡中按 R 重生后:关卡对象未被重建=${survivedLevel} 玩家x=${api.G.player.x.toFixed(0)} alive=${api.G.player.alive} 残余粒子=${L0.particles.list.length}`); if (!survivedLevel) err('R 键把整关重开了,存档进度丢失'); if (api.G.player.dead || !api.G.player.alive) err('死亡中按 R 未手动重生'); if (Math.abs(api.G.player.x - cp.x) > 4) err(`重生后没有回到存档点(x=${api.G.player.x.toFixed(0)} 期望 ${cp.x.toFixed(0)})`); if (Math.abs(api.G.player.x - L0.spawn.x) < 4) err('重生后被送回了关卡起点'); if (!(L0.particles.list.length > 0)) 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);