Files
kidgo/tools/spike-test.mjs
T

87 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 单三角形刺:视觉与碰撞形状完全对应的单元测试
// 验证 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);