// 回归测试:多面地图的跨面传送门 + 存档点带面 + 内置关卡兼容 const handlers = {}; const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); }; globalThis.addEventListener = addL; globalThis.window = { addEventListener: addL }; const key = (type, code) => (handlers[type] || []).forEach(fn => fn({ code, preventDefault() { }, repeat: false })); const { Level } = await import('../js/level.js'); const { Player } = await import('../js/player.js'); const { Input } = await import('../js/input.js'); const W = 20, TILE = 32; let fail = 0; const err = m => { console.log(' ✗ ' + m); fail++; }; const ok = m => console.log(' ✓ ' + m); // 两面临门地图:A 面门 col5 指向 B 面门;B 面门 col10 指回 A 面门 const gridA = ['#'.repeat(W), '#' + 'S' + '.'.repeat(W - 3) + '#', '#' + '.'.repeat(4) + 'W' + '.'.repeat(W - 6) + '#', '#'.repeat(W), '#X' + '.'.repeat(W - 3) + '#', '#'.repeat(W), '#'.repeat(W), '#'.repeat(W), '#'.repeat(W), '#'.repeat(W)]; const gridB = ['#'.repeat(W), '#' + 'S' + '.'.repeat(W - 3) + '#', '#' + '.'.repeat(9) + 'W' + '.'.repeat(W - 11) + '#', '#'.repeat(W), '#X' + '.'.repeat(W - 3) + '#', '#'.repeat(W), '#'.repeat(W), '#'.repeat(W), '#'.repeat(W), '#'.repeat(W)]; const doorA = { id: 1, tx: 5, ty: 2, to: { face: 1, door: 1 } }; const doorB = { id: 1, tx: 10, ty: 2, to: { face: 0, door: 1 } }; const L0 = new Level({ name: 'A', type: 'custom', width: W, grid: gridA, doors: [doorA] }); L0.faceIndex = 0; const L1 = new Level({ name: 'B', type: 'custom', width: W, grid: gridB, doors: [doorB] }); L1.faceIndex = 1; // 1) 门目标正确挂接 { const w0 = L0.entities.find(e => e.type === 'warp'); const w1 = L1.entities.find(e => e.type === 'warp'); if (!w0 || !w1) err('门实体未生成'); else { if (w0.to && w0.to.face === 1 && w0.to.door === 1 && w0.doorId === 1) ok('A 面门挂上跨面目标 {face:1,door:1}'); else err('A 面门目标错误: ' + JSON.stringify(w0.to)); if (w1.to && w1.to.face === 0 && w1.to.door === 1 && w1.doorId === 1) ok('B 面门挂上跨面目标 {face:0,door:1}'); else err('B 面门目标错误: ' + JSON.stringify(w1.to)); } } // 2) 踩门触发 onPortal 钩子(main 据此切面) { const P = new Player(L0); let captured = null; P.onPortal = (e) => { captured = e; }; const w0 = L0.entities.find(e => e.type === 'warp'); P.x = w0.x; P.y = w0.y; P.vx = 0; P.vy = 0; L0.update(1, P, null); P.update(1); Input.endFrame(); if (captured && captured.to && captured.to.face === 1) ok('踩到门触发 onPortal,目标面=1'); else err('踩门未触发跨面 onPortal'); } // 3) 存档点带 faceIndex { const gridC = ['#'.repeat(W), '#' + 'S' + '.'.repeat(W - 3) + '#', '#' + 'G' + '.'.repeat(W - 3) + '#', '#X' + '.'.repeat(W - 3) + '#']; const LC = new Level({ name: 'C', type: 'custom', width: W, grid: gridC, doors: [] }); LC.faceIndex = 2; const P = new Player(LC); const g = LC.entities.find(e => e.type === 'save'); P.x = g.x; P.y = g.y; LC.update(1, P, null); P.update(1); Input.endFrame(); if (P.savePoint && P.savePoint.face === 2) ok('存档点记录 face=2'); else err('存档点未带 face,得到 ' + (P.savePoint && P.savePoint.face)); } // 4) 兼容:无 doors 的内置风格关卡,门仍走原有 pair 逻辑(to 为 null) { const gridD = ['#'.repeat(W), '#' + 'S' + '.'.repeat(W - 3) + '#', '#' + '.'.repeat(4) + 'W' + '.'.repeat(W - 6) + '#', '#'.repeat(W), '#' + '.'.repeat(4) + 'W' + '.'.repeat(W - 6) + '#', '#X' + '.'.repeat(W - 3) + '#']; const LD = new Level({ name: 'D', type: 'jump', width: W, grid: gridD }); const ws = LD.entities.filter(e => e.type === 'warp'); if (ws.length === 2 && ws[0].to === null && ws[1].to === null) ok('无 doors 时门 to=null(走原有 pair 逻辑,不退化)'); else err('无 doors 时门目标异常: ' + JSON.stringify(ws.map(w => w.to))); } console.log(fail ? `\n❌ ${fail} 项失败` : '\n✅ 多面地图/传送门 全部通过'); process.exit(fail ? 1 : 0);