// 单三角形刺:视觉与碰撞形状完全对应的单元测试 // 验证 spikeTri 顶点、triRectOverlap 的正确性,以及 deadlyRect 对刺的判定。 const 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; } }); 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: noop, removeEventListener: noop, AudioContext: null, webkitAudioContext: null, innerWidth: 1280, innerHeight: 800, location: globalThis.location, navigator: globalThis.navigator, matchMedia: globalThis.matchMedia }; globalThis.document = { getElementById: () => ({ getContext: () => ctx2d, style: {}, classList: { add: noop, remove: noop, toggle: noop }, addEventListener: noop, appendChild: noop, querySelector: () => ({ getContext: () => ctx2d }), querySelectorAll: () => [] }), querySelector: () => ({ getContext: () => ctx2d }), querySelectorAll: () => [], createElement: () => ({ getContext: () => ctx2d, style: {}, classList: { add: noop, remove: noop } }), addEventListener: noop, body: {} }; 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.confirm = () => true; globalThis.alert = noop; const { Level, T, spikeTri, triRectOverlap } = await import('../js/level.js'); const { TILE } = await import('../js/util.js'); let fail = 0; const ok = (c, m) => { console.log((c ? ' ✔ ' : ' ✗ ') + m); if (!c) fail++; }; const near = (a, b, e = 1e-6) => Math.abs(a - b) < e; console.log('=== spikeTri 顶点(与 drawSpike 一致)==='); const s = TILE; { const tr = spikeTri(T.SPIKE_U, 0, 0); ok(near(tr.ax, s / 2) && near(tr.ay, 0) && near(tr.bx, 0) && near(tr.by, s) && near(tr.cx, s) && near(tr.cy, s), 'SPIKE_U:尖在顶中,底在底部'); const tr2 = spikeTri(T.SPIKE_D, 0, 0); ok(near(tr2.ax, s / 2) && near(tr2.ay, s) && near(tr2.bx, 0) && near(tr2.by, 0), 'SPIKE_D:尖在底中,底在顶部'); const tr3 = spikeTri(T.SPIKE_L, 0, 0); ok(near(tr3.ax, 0) && near(tr3.ay, s / 2) && near(tr3.bx, s) && near(tr3.by, 0) && near(tr3.cx, s) && near(tr3.cy, s), 'SPIKE_L:尖在左中,底在右侧'); const tr4 = spikeTri(T.SPIKE_R, 0, 0); ok(near(tr4.ax, s) && near(tr4.ay, s / 2) && near(tr4.bx, 0) && near(tr4.by, 0), 'SPIKE_R:尖在右中,底在左侧'); } console.log('=== triRectOverlap 基本几何 ==='); { // 上刺三角形:A(16,0) B(0,32) C(32,32)(TILE=32) const A = spikeTri(T.SPIKE_U, 0, 0); // 完全在三角形内部的矩形 ok(triRectOverlap(A.ax, A.ay, A.bx, A.by, A.cx, A.cy, 14, 20, 4, 4), '三角形内部矩形 → 相交'); // 在三角形“上方窄尖之外”的矩形(顶部左侧,AABB 内但三角外) ok(!triRectOverlap(A.ax, A.ay, A.bx, A.by, A.cx, A.cy, 1, 1, 4, 4), '尖顶外侧矩形(AABB 内、三角外)→ 不相交'); // 完全在三角形之外(远离) ok(!triRectOverlap(A.ax, A.ay, A.bx, A.by, A.cx, A.cy, 100, 100, 4, 4), '远处矩形 → 不相交'); // AABB 都不重叠 ok(!triRectOverlap(A.ax, A.ay, A.bx, A.by, A.cx, A.cy, 50, 0, 4, 4), 'AABB 不重叠 → 不相交'); // 底边左角(上刺底部很宽)——应当相交 ok(triRectOverlap(A.ax, A.ay, A.bx, A.by, A.cx, A.cy, 1, 28, 4, 4), '上刺底左角矩形 → 相交(形状精确:底宽尖窄)'); } console.log('=== deadlyRect 集成(上刺占一格)==='); { const data = { name: 't', width: 3, grid: ['###', '#^#', '###'] }; const L = new Level(data); ok(L.tiles[1][1] === T.SPIKE_U, '网格解析:中心格子为 SPIKE_U'); const cx = 1 * s, cy = 1 * s; // 刺格像素 [32,32]-[64,64] // 顶部左侧(尖窄处,三角外)→ 不致死 ok(!L.deadlyRect(cx + 1, cy + 1, 4, 4), '上刺尖顶外侧的碰撞盒 → 不致死'); // 底部中央(基底宽处)→ 致死 ok(L.deadlyRect(cx + 13, cy + 26, 8, 4), '上刺底部中央的碰撞盒 → 致死'); // 底左角 → 致死 ok(L.deadlyRect(cx + 1, cy + 26, 6, 4), '上刺底左角的碰撞盒 → 致死(形状精确)'); // 相邻空格(格外)→ 不致死 ok(!L.deadlyRect(0, 0, 11, 21), '完全在空地上的碰撞盒 → 不致死'); } console.log('=== 形状精确性对比:旧矩形近似会把底左角判为安全,现在判为致死 ==='); { // 旧 spikeBox 对上刺 x+6..x+10, y+8.. :底左角 (cx+1,cy+26) 落在 x+6 之外,旧逻辑安全; // 新三角在底部很宽,应致死。下面断言新逻辑与“旧近似矩形”在该点结论相反(证明形状更精确)。 const data = { name: 't', width: 3, grid: ['###', '#^#', '###'] }; const L = new Level(data); const cx = s, cy = s; const oldSafe = (cx + 1 < cx + 6); // 旧矩形左界 x+6,该点在其左 → 旧逻辑“安全” const newDeadly = L.deadlyRect(cx + 1, cy + 26, 3, 3); ok(oldSafe && newDeadly, '底左角:旧矩形近似判安全,新三角判致死(符合“底宽尖窄”的真实形状)'); } console.log(fail === 0 ? '\n✅ 单三角形刺(视觉=碰撞)全部断言通过' : `\n❌ 失败 ${fail} 项`); process.exit(fail === 0 ? 0 : 1);