fea: 优化死亡特效
This commit is contained in:
1 parent
afe2f64b9b
commit
e7200989ab
9 files changed
+373
-94
No files matched your search
+199
-62
@@ -1,19 +1,26 @@
|
||||
// 回归测试:kid 死亡后必须立刻复活,且不留下任何死亡动画(无粒子、无震屏、无延迟)
|
||||
// 回归测试:死亡流程(2026-10-24 新语义,推翻旧的"即时复活、无动画"需求)
|
||||
// 新需求(用户原话):
|
||||
// ① "当角色死亡后不要立马重生,需要用户手动按重生键重生"
|
||||
// ② "死亡时除了原先的红叉,还要附加一个小的死亡动画"
|
||||
// 本测试覆盖:死亡定格等待手动重生 / 死亡动画要素(粒子+震动+冲击环圆心)/
|
||||
// 手动 respawn 回存档点 / 连续死亡 / 红叉常驻与去重 / 掉坑死亡位置 clamp
|
||||
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 down = c => key('keydown', c);
|
||||
const up = c => key('keyup', c);
|
||||
|
||||
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
|
||||
// row1 有存档点 'G'(col1);row2 放两枚上刺(col5/col10);row3 实心地板
|
||||
const grid = [
|
||||
'#'.repeat(W),
|
||||
'#' + 'S' + '.'.repeat(W - 3) + '#',
|
||||
'#' + 'G' + '.'.repeat(W - 3) + '#',
|
||||
'#' + '.'.repeat(4) + '^' + '.'.repeat(4) + '^' + '.'.repeat(9) + '#',
|
||||
'#'.repeat(W),
|
||||
'#'.repeat(W),
|
||||
@@ -25,111 +32,241 @@ const grid = [
|
||||
let fail = 0;
|
||||
const err = m => { console.log(' ✗ ' + m); fail++; };
|
||||
const ok = m => console.log(' ✓ ' + m);
|
||||
const step = (L, P, n) => { for (let i = 0; i < n; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); } };
|
||||
|
||||
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(); }
|
||||
step(L, P, 12);
|
||||
return { L, P, deaths: () => deaths };
|
||||
}
|
||||
|
||||
// 1) 踩到刺:同一帧内就必须已经复活(不是等 34 帧)
|
||||
// 1) 踩刺:当帧进入死亡状态并定格(不自动复活),动画要素齐全
|
||||
{
|
||||
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 (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 (P.vx !== 0 || P.vy !== 0) err('死亡时速度未清零');
|
||||
if (!(L.particles.list.length > 0)) err('死亡没有触发粒子爆发(小死亡动画缺失)');
|
||||
else ok(`死亡动画:粒子爆发 ${L.particles.list.length} 个`);
|
||||
if (!(L.shake > 0)) err('死亡没有触发屏幕轻震');
|
||||
else ok(`死亡动画:屏幕轻震 shake=${L.shake.toFixed(1)}`);
|
||||
if (Math.abs(P.deathX - (5 * TILE + 5.5)) > 12 || Math.abs(P.deathY - (2 * TILE + 21.5)) > 12)
|
||||
err(`死亡动画圆心偏离死亡点 (${P.deathX.toFixed(0)},${P.deathY.toFixed(0)})`);
|
||||
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})`);
|
||||
if (Math.abs(dm.x - ex) > 30 || Math.abs(dm.y - ey) > 30) err(`血迹偏离死亡点 (${dm.x.toFixed(0)},${dm.y.toFixed(0)})`);
|
||||
else ok('在死亡地点留下红色死亡叉(X)');
|
||||
}
|
||||
if (P.justRespawned !== true) err('未标记 justRespawned,镜头会慢慢飘回去');
|
||||
else ok('justRespawned 已置位(镜头瞬移)');
|
||||
if (P.x !== 5 * TILE) err('死亡后位置被改动(应定格在原处)');
|
||||
else ok('死亡后定格在死亡位置');
|
||||
}
|
||||
|
||||
// 2) R 键自杀路径:kill() 后下一帧必须复活
|
||||
// 2) 死亡后 60 帧:仍然 dead、不自动重生、输入完全无效
|
||||
{
|
||||
const { L, P, deaths } = fresh();
|
||||
P.kill();
|
||||
if (deaths() !== 1) err('kill() 未触发死亡回调');
|
||||
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('自杀(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');
|
||||
down('ArrowRight'); down('ShiftLeft');
|
||||
step(L, P, 60);
|
||||
up('ArrowRight'); up('ShiftLeft');
|
||||
if (P.alive) err('死亡 60 帧后自动复活了(违反"手动重生"需求)');
|
||||
else ok('死亡后 60 帧仍定格等待手动重生');
|
||||
if (deaths() !== 1) err(`死亡计数被重复触发(${deaths()})`);
|
||||
if (P.deadT < 60) err(`deadT 未随死亡时间递增(${P.deadT})`);
|
||||
else ok(`deadT 随时间递增(${P.deadT})`);
|
||||
if (P.x !== x0) err('死亡后按方向键仍能移动');
|
||||
else ok('死亡中输入无效(方向/跳跃均不响应)');
|
||||
}
|
||||
|
||||
// 3) 连续死亡:不能卡死在死亡状态
|
||||
// 3) 手动 respawn:回到最近存档点、justRespawned 置位、立即可操作
|
||||
{
|
||||
const { L, P } = fresh();
|
||||
// 先碰存档点(col1)
|
||||
const s = L.entities.find(e => e.type === 'save');
|
||||
P.x = s.x + s.w / 2 - P.w / 2; P.y = s.y + s.h - P.h;
|
||||
step(L, P, 3);
|
||||
const cp = P.savePoint;
|
||||
if (!cp) { err('存档点未激活(前置失败)'); }
|
||||
else {
|
||||
P.x = 5 * TILE; P.y = 2 * TILE + 11;
|
||||
L.update(1, P, null); P.update(1); Input.endFrame();
|
||||
if (!P.dead) err('踩刺未死');
|
||||
P.respawn(); // ← 模拟玩家按 R 手动重生
|
||||
if (!P.alive || P.dead) err('手动 respawn 后未复活');
|
||||
else ok('手动 respawn 成功复活');
|
||||
if (Math.abs(P.x - cp.x) > 1) err(`复活位置不对:x=${P.x.toFixed(1)} 期望 ${cp.x.toFixed(1)}`);
|
||||
else ok('复活位置 = 最近存档点');
|
||||
if (P.justRespawned !== true) err('未标记 justRespawned,镜头会慢慢飘回去');
|
||||
else ok('justRespawned 已置位(镜头瞬移)');
|
||||
const x0 = P.x;
|
||||
down('ArrowRight');
|
||||
step(L, P, 6);
|
||||
up('ArrowRight');
|
||||
if (P.x <= x0) err('重生后当帧无法操作');
|
||||
else ok(`重生后立即可操作(6 帧走了 ${(P.x - x0).toFixed(1)}px)`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 连续死亡 5 次:每次都须手动重生才能继续,计数准确
|
||||
{
|
||||
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 (!P.dead) { err(`第 ${k + 1} 次踩刺未死`); break; }
|
||||
step(L, P, 10); // 死亡中跑 10 帧:不许自动复活
|
||||
if (P.alive) { err(`第 ${k + 1} 次死亡后自动复活`); break; }
|
||||
P.respawn(); // 手动重生后才有下一次
|
||||
}
|
||||
if (deaths() !== 5) err(`连续 5 次死亡计数为 ${deaths()}`);
|
||||
else ok('连续 5 次死亡均即时复活,计数 = 5');
|
||||
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) 整关重开:血迹清空
|
||||
// 5) 红叉常驻保留;整关重开清空
|
||||
{
|
||||
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('死亡未生成血迹');
|
||||
P.respawn();
|
||||
step(L, P, 30);
|
||||
if (!L.deathMarks.length) err('重生后红叉被清掉了(应常驻保留)');
|
||||
else ok(`死亡红叉常驻保留(${L.deathMarks.length} 处,重生 30 帧后仍在)`);
|
||||
L.reset(true);
|
||||
if (L.deathMarks.length) err(`整关重开后血迹应清空,实际 ${L.deathMarks.length}`);
|
||||
else ok('整关重开(reset(true))后血迹清空');
|
||||
}
|
||||
|
||||
console.log(fail ? `\n❌ ${fail} 项失败` : '\n✅ 死亡即时复活,全部通过');
|
||||
// 6) 多次死亡:只显示最后一次死亡的红叉(不累积)
|
||||
{
|
||||
const { L, P } = fresh();
|
||||
P.x = 5 * TILE; P.y = 2 * TILE + 11;
|
||||
L.update(1, P, null); P.update(1); Input.endFrame();
|
||||
P.respawn();
|
||||
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];
|
||||
if (Math.abs(dm.x - (10 * TILE + 5.5)) > 30) err(`应只保留最后一次(col10)血迹,实际在 x=${dm.x.toFixed(0)}`);
|
||||
else ok('多次死亡只保留上一次死亡地点的血迹');
|
||||
}
|
||||
}
|
||||
|
||||
// 7) 掉坑死亡:动画圆心/红叉 clamp 到关卡内(玩家已在屏幕外也要可见)
|
||||
{
|
||||
const { L, P } = fresh();
|
||||
P.y = L.pxH + 300; // 深坑底(屏幕外)
|
||||
L.update(1, P, null); P.update(1); Input.endFrame();
|
||||
if (!P.dead) err('掉坑未触发死亡');
|
||||
else ok('掉坑触发死亡并定格');
|
||||
if (P.deathY > L.pxH - 10) err(`掉坑死亡的动画圆心未 clamp 到关卡内(deathY=${P.deathY.toFixed(0)} > ${L.pxH - 10})`);
|
||||
else ok(`掉坑死亡圆心 clamp 到关卡底缘(deathY=${P.deathY.toFixed(0)})`);
|
||||
if (P.deathX < 10 || P.deathX > L.pxW - 10) err('动画圆心 X 未 clamp 到关卡内');
|
||||
if (!L.deathMarks.length) err('掉坑死亡未留红叉');
|
||||
else if (L.deathMarks[0].y > L.pxH) err('掉坑死亡的红叉画到了关卡外');
|
||||
else ok('掉坑死亡的红叉在关卡内可见');
|
||||
}
|
||||
|
||||
// 8) 死亡动画渲染冒烟:冲击环(Level 层)+ 粒子逐帧 draw 不抛错(canvas 用代理桩)
|
||||
{
|
||||
const { L } = fresh();
|
||||
const ctx2d = new Proxy({ canvas: { width: 800, height: 608 } },
|
||||
{ get(t, k) { return k in t ? t[k] : (() => { }); }, set(t, k, v) { t[k] = v; return true; } });
|
||||
try {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
L.drawDeathRing(ctx2d, { x: 100, y: 100, t: i });
|
||||
L.particles.spawn(100, 100, 4, { life: 20 });
|
||||
L.particles.draw(ctx2d);
|
||||
}
|
||||
ok('死亡动画(冲击环+粒子)20 帧渲染无异常');
|
||||
} catch (e) { err(`死亡动画渲染抛错:${e.message}`); }
|
||||
}
|
||||
|
||||
// 9) 主动按重生键(main.js suicide):当帧重生 + 演出留在死亡位置播完(不清粒子/震屏)
|
||||
{
|
||||
const { L, P, deaths } = fresh();
|
||||
const s = L.entities.find(e => e.type === 'save');
|
||||
P.x = s.x + s.w / 2 - P.w / 2; P.y = s.y + s.h - P.h;
|
||||
step(L, P, 3);
|
||||
const cp = P.savePoint;
|
||||
if (!cp) { err('存档点未激活(前置失败)'); }
|
||||
else {
|
||||
P.x = 12 * TILE; P.y = 2 * TILE - P.h; // 走到右侧空地(col5/col10 有刺,避开)
|
||||
step(L, P, 5);
|
||||
const marks0 = L.deathMarks.length;
|
||||
const burstX = P.cx, burstY = P.cy;
|
||||
P.deathBurst(); // ← 模拟活着按 R 的演出部分(不进死亡状态)
|
||||
if (P.dead || !P.alive) err('死亡演出把玩家带进了死亡状态(应纯演出)');
|
||||
else ok('死亡演出为纯演出(不进死亡状态,可当帧重生)');
|
||||
if (deaths() !== 0) err(`主动重生键不应计死亡,实际 ${deaths()}`);
|
||||
else ok('主动重生键不计死亡数');
|
||||
if (L.deathMarks.length !== marks0) err('主动重生键不应留红叉');
|
||||
else ok('主动重生键不留红叉');
|
||||
if (!(L.particles.list.length > 0) || !(L.shake > 0)) err('主动重生键的死亡动画缺失(粒子/震屏)');
|
||||
else ok('主动重生键播死亡动画(粒子+轻震+冲击环)');
|
||||
if (!L.deathFX.length) err('冲击环未记录到关卡层');
|
||||
else if (Math.abs(L.deathFX[0].x - burstX) > 12) err('冲击环圆心偏离死亡点');
|
||||
else ok('冲击环记录在死亡位置(Level 层)');
|
||||
// 当帧重生:不等动画
|
||||
P.respawn();
|
||||
if (!P.alive || P.dead) err('未当帧重生');
|
||||
else ok('当帧重生(不等待动画)');
|
||||
if (Math.abs(P.x - cp.x) > 1) err(`重生位置不对:x=${P.x.toFixed(1)} 期望 ${cp.x.toFixed(1)}`);
|
||||
else ok('重生回最近存档点');
|
||||
if (P.justRespawned !== true) err('未标记 justRespawned,镜头不会瞬移');
|
||||
// 动画在死亡位置继续播完:粒子未被清、冲击环在推进
|
||||
if (!(L.particles.list.length > 0)) err('重生把演出粒子清掉了(应留在死亡位置播完)');
|
||||
else ok('重生不清场:粒子留在死亡位置继续播');
|
||||
step(L, P, 5);
|
||||
if (!L.deathFX.length) err('冲击环被提前清掉了');
|
||||
step(L, P, 12); // 累计 17 帧 > 14
|
||||
if (L.deathFX.length) err('冲击环 14 帧后未移除');
|
||||
else ok('冲击环 14 帧扩散淡出后自动移除');
|
||||
step(L, P, 25); // 粒子 life 34,累计 42 帧应自然播完
|
||||
if (L.particles.list.length) err(`粒子未自然播完(残余 ${L.particles.list.length} 个)`);
|
||||
else ok('粒子自然播完(34 帧寿命)');
|
||||
if (!(L.shake < 0.6)) err(`震屏未自然衰减(${L.shake.toFixed(2)})`);
|
||||
else ok('震屏自然衰减归零');
|
||||
}
|
||||
}
|
||||
|
||||
// 10) 真死亡(被动)后玩家很快按 R:特效不被重生清场,留在死亡位置自然播完
|
||||
{
|
||||
const { L, P, deaths } = fresh();
|
||||
P.x = 5 * TILE; P.y = 2 * TILE + 11;
|
||||
L.update(1, P, null); P.update(1); Input.endFrame(); // 踩刺真死
|
||||
if (!P.dead) err('踩刺未死(前置失败)');
|
||||
else {
|
||||
step(L, P, 3); // 玩家只等了 3 帧就按 R
|
||||
if (deaths() !== 1) err(`死亡计数应为 1,实际 ${deaths()}`);
|
||||
P.respawn();
|
||||
if (!P.alive || P.dead) err('快速重生失败');
|
||||
else ok('真死亡后 3 帧即按 R:成功重生');
|
||||
if (!(L.particles.list.length > 0)) err('快速重生把死亡粒子清掉了(应留在死亡位置播完)');
|
||||
else ok('快速重生不清场:粒子留在死亡位置继续播');
|
||||
if (!L.deathFX.length) err('快速重生把冲击环清掉了');
|
||||
else ok('快速重生不清场:冲击环继续扩散');
|
||||
if (!(L.shake > 0)) err('快速重生把震屏清掉了');
|
||||
else ok('快速重生不清场:震屏继续衰减');
|
||||
step(L, P, 50); // 粒子 34 / 环 14 / 震屏衰减,全部应自然结束
|
||||
if (L.particles.list.length) err(`粒子未自然播完(残余 ${L.particles.list.length})`);
|
||||
else if (L.deathFX.length) err('冲击环未自然播完');
|
||||
else if (L.shake > 0.01) err(`震屏未自然衰减(${L.shake.toFixed(3)})`);
|
||||
else ok('特效全部自然播完(粒子 34 帧 / 冲击环 14 帧 / 震屏衰减)');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(fail ? `\n❌ ${fail} 项失败` : '\n✅ 死亡流程全部通过:定格等待手动重生 + 小红死亡动画 + 主动重生键当帧重生');
|
||||
process.exit(fail ? 1 : 0);
|
||||
+27
-7
@@ -1,4 +1,4 @@
|
||||
// 回归测试:R 键必须"自杀回存档点",不能把整关重开(否则存档点白碰)
|
||||
// 回归测试:R 键必须"重生回存档点",不能把整关重开(否则存档点白碰)
|
||||
const noop = () => { };
|
||||
const els = new Map();
|
||||
function mkEl(id = '') {
|
||||
@@ -73,19 +73,39 @@ const cp = P.savePoint;
|
||||
console.log(` 存档点激活=${!!cp} 位置=(${cp?.x.toFixed(0)},${cp?.y.toFixed(0)}) 关卡出生点x=${L0.spawn.x.toFixed(0)}`);
|
||||
if (!cp) err('存档点未激活');
|
||||
|
||||
// ---- 按 R ----
|
||||
// ---- 按 R(活着)= 原地播死亡动画后当帧重生回存档点(不计死亡、不留红叉、不等动画)----
|
||||
const deaths0 = api.G.deaths;
|
||||
const marks0 = L0.deathMarks.length;
|
||||
P.x = L0.spawn.x; P.y = L0.spawn.y; // 先站到出生点,验证能回到存档点而非原地不动
|
||||
step(5);
|
||||
const beforeX = api.G.player.x;
|
||||
key('keydown', 'KeyR');
|
||||
step(2);
|
||||
key('keyup', 'KeyR');
|
||||
step(60);
|
||||
console.log(` 活着按 R 后:玩家x=${beforeX.toFixed(0)}→${api.G.player.x.toFixed(0)} alive=${api.G.player.alive} dead=${api.G.player.dead} 死亡数=${deaths0}→${api.G.deaths} 红叉数=${marks0}→${L0.deathMarks.length} 粒子=${L0.particles.list.length} 冲击环=${L0.deathFX.length}`);
|
||||
if (api.G.player.dead || !api.G.player.alive) err('活着按 R 没有当帧重生');
|
||||
if (api.G.deaths !== deaths0) err('主动重生被计入了死亡数(主动重生不是死亡)');
|
||||
if (L0.deathMarks.length !== marks0) err('主动重生留下了红叉(不应留血迹)');
|
||||
if (Math.abs(api.G.player.x - cp.x) > 4) err(`按 R 没有回到存档点(x=${api.G.player.x.toFixed(0)} 期望 ${cp.x.toFixed(0)})`);
|
||||
if (!(L0.particles.list.length > 0)) err('死亡演出粒子缺失(应留在死亡位置播完)');
|
||||
if (!L0.deathFX.length) err('死亡演出冲击环缺失(应留在死亡位置播完)');
|
||||
|
||||
// ---- 死亡中按 R = 手动重生回存档点 ----
|
||||
api.G.player.kill();
|
||||
step(3);
|
||||
if (!api.G.player.dead) err('kill 后未进入死亡定格状态');
|
||||
key('keydown', 'KeyR');
|
||||
step(2);
|
||||
key('keyup', 'KeyR');
|
||||
step(10);
|
||||
|
||||
const survivedLevel = api.G.level === L0;
|
||||
console.log(` 按 R 后:关卡对象未被重建=${survivedLevel} 玩家x=${api.G.player.x.toFixed(0)} 死亡=${deaths0}→${api.G.deaths}`);
|
||||
console.log(` 死亡中按 R 重生后:关卡对象未被重建=${survivedLevel} 玩家x=${api.G.player.x.toFixed(0)} alive=${api.G.player.alive} 残余粒子=${L0.particles.list.length}`);
|
||||
if (!survivedLevel) err('R 键把整关重开了,存档进度丢失');
|
||||
if (Math.abs(api.G.player.x - cp.x) > 4) err(`R 键后没有回到存档点(x=${api.G.player.x.toFixed(0)} 期望 ${cp.x.toFixed(0)})`);
|
||||
if (api.G.deaths !== deaths0 + 1) err('R 键没有计入死亡数');
|
||||
if (Math.abs(api.G.player.x - L0.spawn.x) < 4) err('R 键后被送回了关卡起点');
|
||||
if (api.G.player.dead || !api.G.player.alive) err('死亡中按 R 未手动重生');
|
||||
if (Math.abs(api.G.player.x - cp.x) > 4) err(`重生后没有回到存档点(x=${api.G.player.x.toFixed(0)} 期望 ${cp.x.toFixed(0)})`);
|
||||
if (Math.abs(api.G.player.x - L0.spawn.x) < 4) err('重生后被送回了关卡起点');
|
||||
if (!(L0.particles.list.length > 0)) err('死亡中按 R 重生把特效清掉了(应留在死亡位置播完)');
|
||||
|
||||
// ---- 按 Backspace = 整关重来 ----
|
||||
key('keydown', 'Backspace');
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ for (const d of LEVELS) {
|
||||
const last = saves[saves.length - 1];
|
||||
const want = { x: last.x + last.w / 2 - P.w / 2, y: last.y + last.h - P.h };
|
||||
const spawn = L.spawn;
|
||||
P.kill();
|
||||
P.kill(); // 死亡后不再自动重生(新语义:定格等待手动重生)
|
||||
P.respawn(); // 模拟玩家按 R 手动重生,应回到最后一个存档点
|
||||
for (let i = 0; i < 45; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
const dx = Math.abs(P.x - want.x), dy = Math.abs(P.y - want.y);
|
||||
const backToStart = Math.abs(P.x - spawn.x) < 3 && Math.abs(P.y - spawn.y) < 3;
|
||||
|
||||
+8
-3
@@ -51,7 +51,7 @@ for (const d of LEVELS) {
|
||||
}
|
||||
function G_DEATHS() { return ''; }
|
||||
|
||||
console.log('\n=== 死亡与重生 ===');
|
||||
console.log('\n=== 死亡与手动重生 ===');
|
||||
{
|
||||
const L = new Level(LEVELS.find(l => l.id === 'j1'));
|
||||
const P = new Player(L);
|
||||
@@ -59,9 +59,14 @@ console.log('\n=== 死亡与重生 ===');
|
||||
P.onDeath = () => deaths++;
|
||||
for (let i = 0; i < 30; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
P.kill();
|
||||
// 新语义(2026-10-24):死亡后不自动重生,定格等待玩家手动按重生键
|
||||
for (let i = 0; i < 80; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
console.log(` 死亡计数=${deaths} 重生后 alive=${P.alive} 位置=(${P.x.toFixed(0)},${P.y.toFixed(0)})`);
|
||||
if (!P.alive) err('死亡后未复活');
|
||||
if (P.alive) err('死亡后未等待手动重生(自动复活了)');
|
||||
else console.log(` 死亡计数=${deaths} 80帧后仍定格等待手动重生 ✓(deadT=${P.deadT})`);
|
||||
P.respawn();
|
||||
for (let i = 0; i < 10; i++) { L.update(1, P, null); P.update(1); Input.endFrame(); }
|
||||
if (!P.alive) err('手动重生失败');
|
||||
else console.log(` 手动重生后 alive ✓ 位置=(${P.x.toFixed(0)},${P.y.toFixed(0)})`);
|
||||
if (deaths !== 1) err('死亡回调未触发');
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user