Files
kidgo/tools/death-test.mjs
T

136 lines
5.8 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.
// 回归测试:kid 死亡后必须立刻复活,且不留下任何死亡动画(无粒子、无震屏、无延迟)
const handlers = {};
const addL = (t, fn) => { (handlers[t] = handlers[t] || []).push(fn); };
globalThis.addEventListener = addL;
globalThis.window = { addEventListener: addL };
const key = (type, code) => (handlers[type] || []).forEach(fn => fn({ code, preventDefault() { }, repeat: false }));
const { Level } = await import('../js/level.js');
const { Player } = await import('../js/player.js');
const { Input } = await import('../js/input.js');
const W = 20, H = 8, TILE = 32;
// row2 放一枚上刺(col 5),row3 是地板,spawn 在 col1
const grid = [
'#'.repeat(W),
'#' + 'S' + '.'.repeat(W - 3) + '#',
'#' + '.'.repeat(4) + '^' + '.'.repeat(4) + '^' + '.'.repeat(9) + '#',
'#'.repeat(W),
'#'.repeat(W),
'#'.repeat(W),
'#'.repeat(W),
'#'.repeat(W),
];
let fail = 0;
const err = m => { console.log(' ✗ ' + m); fail++; };
const ok = m => console.log(' ✓ ' + m);
function fresh() {
const L = new Level({ id: 'dt', name: 'DT', type: 'jump', width: W, grid });
const P = new Player(L);
let deaths = 0;
P.onDeath = () => deaths++;
for (let i = 0; i < 12; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
return { L, P, deaths: () => deaths };
}
// 1) 踩到刺:同一帧内就必须已经复活(不是等 34 帧)
{
const { L, P, deaths } = fresh();
const sx = L.spawn.x;
P.x = 5 * TILE; P.y = 2 * TILE + 11; // 直接压在上刺上
L.update(1, P, null); P.update(1); Input.endFrame();
if (!P.alive || P.dead) err('踩刺后本帧仍处于死亡状态(复活有延迟)');
else ok('踩刺后同帧即复活,无死亡等待');
if (deaths() !== 1) err(`死亡计数应为 1,实际 ${deaths()}`);
else ok('死亡计数 = 1(正常累加)');
if (Math.abs(P.x - sx) > 1) err(`复活位置不对:x=${P.x.toFixed(1)} 期望 ${sx}`);
else ok('复活位置 = 最近存档点/出生点');
if (L.particles.list.length) err(`死亡仍产生粒子(${L.particles.list.length} 个)`);
else ok('无屏幕级死亡粒子(血迹走常驻层,非瞬态粒子)');
if (L.shake > 0) err(`死亡仍触发震屏 shake=${L.shake}`);
else ok('无死亡震屏');
if (!L.deathMarks.length) err('死亡未在死亡地点留下 IWBT 血迹效果');
else {
const dm = L.deathMarks[0];
const ex = 5 * TILE + 5.5, ey = 2 * TILE + 21.5;
if (Math.abs(dm.x - ex) > 30 || Math.abs(dm.y - ey) > 30) err(`血迹偏离死亡点 (${dm.x.toFixed(0)},${dm.y.toFixed(0)}) 期望约 (${ex},${ey})`);
else ok('在死亡地点留下红色死亡叉(X)');
}
if (P.justRespawned !== true) err('未标记 justRespawned,镜头会慢慢飘回去');
else ok('justRespawned 已置位(镜头瞬移)');
}
// 2) R 键自杀路径:kill() 后下一帧必须复活
{
const { L, P, deaths } = fresh();
P.kill();
if (deaths() !== 1) err('kill() 未触发死亡回调');
L.update(1, P, null); P.update(1); Input.endFrame();
if (!P.alive || P.dead) err('自杀后未立刻复活');
else ok('自杀(R 键)后下一帧即复活');
// 立刻复活后应能马上操作:按住右键能走
const x0 = P.x;
key('keydown', 'ArrowRight');
for (let i = 0; i < 6; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
if (P.x <= x0) err('复活后当帧无法操作');
else ok(`复活后立即可操作(6 帧走了 ${(P.x - x0).toFixed(1)}px)`);
key('keyup', 'ArrowRight');
}
// 3) 连续死亡:不能卡死在死亡状态
{
const { L, P, deaths } = fresh();
for (let k = 0; k < 5; k++) {
P.x = 5 * TILE; P.y = 2 * TILE + 11;
L.update(1, P, null); P.update(1); Input.endFrame();
if (!P.alive) { err(`第 ${k + 1} 次死亡后卡在死亡状态`); break; }
}
if (deaths() !== 5) err(`连续 5 次死亡计数为 ${deaths()}`);
else ok('连续 5 次死亡均即时复活,计数 = 5');
}
// 4) 死亡后不应残留任何死亡视觉:跑 30 帧确认没有死亡粒子产生
{
const { L, P } = fresh();
P.x = 5 * TILE; P.y = 2 * TILE + 11;
let maxParticles = 0;
for (let i = 0; i < 30; i++) {
L.update(1, P, null); P.update(1); Input.endFrame();
maxParticles = Math.max(maxParticles, L.particles.list.length);
}
if (maxParticles > 0) err(`死亡后仍出现全局粒子(峰值 ${maxParticles})`);
else ok('30 帧内无屏幕级死亡粒子(血迹由常驻层渲染)');
if (!L.deathMarks.length) err('死亡血迹未常驻保留');
else ok(`死亡红叉常驻保留(共 ${L.deathMarks.length} 处,30 帧后仍在)`);
}
// 5) 多次死亡:只显示上一次死亡的血迹(不累积)
{
const { L, P } = fresh();
for (let k = 0; k < 2; k++) { P.x = 5 * TILE; P.y = 2 * TILE + 11; L.update(1, P, null); P.update(1); Input.endFrame(); }
for (let k = 0; k < 2; k++) { P.x = 10 * TILE; P.y = 2 * TILE + 11; L.update(1, P, null); P.update(1); Input.endFrame(); }
if (L.deathMarks.length !== 1) err(`多次死亡应只显示最后一次血迹,实际 ${L.deathMarks.length} 处`);
else {
const dm = L.deathMarks[0];
const ex = 10 * TILE + 5.5, ey = 2 * TILE + 21.5;
if (Math.abs(dm.x - ex) > 30) err(`应只保留最后一次(col10)血迹,实际在 x=${dm.x.toFixed(0)}`);
else ok('多次死亡只保留上一次死亡地点的血迹');
}
}
// 6) 整关重开:血迹清空
{
const { L, P } = fresh();
P.x = 5 * TILE; P.y = 2 * TILE + 11;
L.update(1, P, null); P.update(1); Input.endFrame();
if (!L.deathMarks.length) err('死亡未生成血迹');
L.reset(true);
if (L.deathMarks.length) err(`整关重开后血迹应清空,实际 ${L.deathMarks.length}`);
else ok('整关重开(reset(true))后血迹清空');
}
console.log(fail ? `\n❌ ${fail} 项失败` : '\n✅ 死亡即时复活,全部通过');
process.exit(fail ? 1 : 0);