fix: 优化跳跃和下落手感

This commit is contained in:
cancerai committed 2026-10-24 01:54:39 +08:00
1 parent ada64fe4be
commit afe2f64b9b
25 files changed
+1197 -188

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
// 单三角形刺:视觉与碰撞形状完全对应的单元测试
// 验证 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);