// 二段跳时机回归(tools/dj-test.mjs) // 历史 bug 链: // ① 全额抵消(旧 `vy = max(vy,0) - DJUMP`):满力一段到顶点后晚几帧接二段,二段塌到接近 0(用户报"二段非常短") // ② 部分抵消(DJ_INERTIA 0.5→0.35):仍有"好沉+高度短"——起跳初速被下落速度从 6.7 吃到 3~4, // 起跳那一下没劲才是"沉"的根源,与上升重力无关 // ③ 现行(DJ_INERTIA = 0,同 IWBT 原版):任何时机接二段都满力起跳,初速恒 -DJUMP // 本测试用真实引擎测量:不同时机接二段时,二段自身的提升高度(djRise)。 // 通过标准: // · 任意时机接二段都 ≈ 满力(≈60px ≈ 1.9 格)——INERTIA=0 语义,曲线应基本持平 // · 满力一段 ≈ 84px(≈2.6 格);顶点接二段总高 ≈ 4.5 格 const handlers = {}; const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); }; globalThis.addEventListener = addL; globalThis.window = { addEventListener: addL }; const key = (t, c) => (handlers[t] || []).forEach(fn => fn({ code: c, preventDefault() { }, repeat: false })); const { Level } = await import('../js/level.js'); const { Player } = await import('../js/player.js'); const { Input } = await import('../js/input.js'); // 与 jumpdist.mjs 同一块平地:row17 是实心平台 const flat = { id: 'test', name: 'T', type: 'jump', width: 44, grid: (() => { const rows = []; rows.push('#'.repeat(44)); for (let i = 0; i < 16; i++) rows.push('#' + '.'.repeat(42) + '#'); rows.push('#' + '#'.repeat(42) + '#'); rows.push('#'.repeat(44)); return rows; })(), }; // 满力一段跳(起跳后一直按住);第 djFrame 帧松键、djFrame+1 帧接二段(之后保持按住) // 返回:firstRise 一段顶点提升 / djRise 二段自身提升(接二段位置→二段后最高点)/ total 二段顶点离平台高度 function measure(djFrame) { const L = new Level(flat); const P = new Player(L); P.x = 8 * 32; P.y = 17 * 32 - P.h; // 站在 row17 实心平台上 P.vx = 0; P.vy = 0; P.grounded = true; P.jumps = 0; const y0 = P.y; key('keydown', 'ShiftLeft'); // 起跳并按住 = 满力一段 let apex1 = y0, apex2 = y0, djDone = false, vyAtDj = null, yAtDj = y0; for (let i = 0; i < 240; i++) { const f = i + 1; if (f === djFrame) key('keyup', 'ShiftLeft'); if (f === djFrame + 1) { key('keydown', 'ShiftLeft'); vyAtDj = P.vy; yAtDj = P.y; } L.update(1, P, null); P.update(1); Input.endFrame(); if (djFrame && f === djFrame + 1 && P.jumps === 2) djDone = true; if (djDone) apex2 = Math.min(apex2, P.y); else apex1 = Math.min(apex1, P.y); if (i > 2 && P.grounded) break; if (P.dead) break; } key('keyup', 'ShiftLeft'); Input.endFrame(); return { firstRise: y0 - apex1, djRise: djDone ? yAtDj - apex2 : 0, total: y0 - apex2, vyAtDj, djDone }; } // 满力一段顶点≈第 27 帧;落地≈第 50 帧。取 27~46 帧覆盖"顶点 → 落地前"整个二段窗口 const FRAMES = [27, 30, 34, 38, 42, 46]; const T = 32; let fail = 0; const rows = []; console.log('满力一段跳后,不同时机接二段:二段自身提升高度(真实引擎实测)'); console.log(' 时机 接二段时vy 一段顶点 二段自身提升 二段顶点离平台'); for (const f of FRAMES) { const r = measure(f); rows.push({ f, ...r }); const dj = r.djDone ? `${r.djRise.toFixed(1).padStart(6)}px = ${(r.djRise / T).toFixed(2)} 格` : ' —— 已落地,二段窗口已过 ——'; console.log(` 第${String(f).padStart(2)}帧 ${(r.vyAtDj ?? 0).toFixed(2).padStart(5)} ${r.firstRise.toFixed(1).padStart(6)}px ${dj} ${(r.total / T).toFixed(2)} 格`); } const at = f => rows.find(r => r.f === f); const need = (cond, msg) => { if (!cond) { fail++; console.log(' ❌ ' + msg); } }; for (const r of rows) need(r.djDone, `第 ${r.f} 帧应还在二段窗口内(未落地)`); const apex = at(27); need(apex.firstRise > 79 && apex.firstRise < 90, `满力一段高度应 ≈84px(2.6格),实测 ${apex.firstRise.toFixed(1)}`); need(apex.djRise > 52 && apex.djRise < 68, `顶点接二段自身提升应 ≈59px(1.85格),实测 ${apex.djRise.toFixed(1)}`); need(apex.total > 138 && apex.total < 150, `顶点接二段总高应 ≈4.5格,实测 ${(apex.total / T).toFixed(2)}`); // INERTIA=0:任何时机接二段都满力起跳,曲线应基本持平(与顶点差异 ≤3px) for (const r of rows) { if (!r.f) continue; need(r.djRise >= 55, `第 ${r.f} 帧接二段自身提升应 ≈满力(≥55px),实测 ${r.djRise.toFixed(1)}`); need(Math.abs(r.djRise - apex.djRise) <= 3, `第 ${r.f} 帧与顶点接二段差异应 ≤3px(INERTIA=0 曲线持平),实测差 ${Math.abs(r.djRise - apex.djRise).toFixed(1)}`); } need(at(46).djRise >= 55, `即使摔得快二段也不应衰减(INERTIA=0),实测 ${at(46).djRise.toFixed(1)}`); if (fail) { console.log(`\n❌ dj-test 失败 ${fail} 项`); process.exit(1); } console.log('\n✅ dj-test 通过:任意时机接二段均满力起跳(INERTIA=0),顶点总高 ≈4.5 格');