87 lines
5.6 KiB
JavaScript
87 lines
5.6 KiB
JavaScript
// 单三角形刺:视觉与碰撞形状完全对应的单元测试
|
||
// 验证 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);
|