fea: 优化死亡特效
This commit is contained in:
1 parent
afe2f64b9b
commit
e7200989ab
9 files changed
+373
-94
No files matched your search
+38
-4
@@ -87,7 +87,7 @@ export class Level {
|
||||
/** 完整重置(死亡 / 重开) */
|
||||
reset(full = false) {
|
||||
// tiles 状态(vanish 等)
|
||||
if (full) this.deathMarks = []; // 死亡血迹只在整关重开时清空,复活后保留
|
||||
if (full) { this.deathMarks = []; this.deathFX = []; } // 血迹/死亡特效只在整关重开时清空,复活后保留
|
||||
this.vanish = new Map();
|
||||
this.bullets = [];
|
||||
this.ebullets = [];
|
||||
@@ -215,14 +215,15 @@ export class Level {
|
||||
this.totalSaves = saveNo;
|
||||
}
|
||||
|
||||
/** 死亡后的软重置:保留存档点与已拾取道具,其余复位 */
|
||||
/** 死亡后的软重置:保留存档点与已拾取道具,其余复位。
|
||||
* 粒子与震屏是"演出层":不清场——无论主动按 R 还是被动死亡后秒按 R,
|
||||
* 死亡特效都留在死亡位置自然播完(粒子 34 帧 / 冲击环 14 帧 / 震屏衰减),
|
||||
* 重生只重置关卡逻辑状态,不掐断演出。整关重开(reset(true))才会清空 */
|
||||
softReset() {
|
||||
this.vanish.clear();
|
||||
if (this.vanishT) this.vanishT.clear();
|
||||
this.bullets.length = 0;
|
||||
this.ebullets.length = 0;
|
||||
this.particles?.clear();
|
||||
this.shake = 0;
|
||||
for (const m of this.movers) {
|
||||
const a = m.path[0];
|
||||
if (a) { m.x = a.x; m.y = a.y; m.i = 0; }
|
||||
@@ -253,6 +254,31 @@ export class Level {
|
||||
this.deathMarks.push({ x, y });
|
||||
}
|
||||
|
||||
// 死亡特效(扩散冲击环):记录到关卡层而非玩家——主动重生时玩家当帧就复活了,
|
||||
// 环要留在死亡位置播完。t 由 update 推进,14 帧扩散淡出后移除
|
||||
addDeathFX(x, y) {
|
||||
this.deathFX.push({ x, y, t: 0 });
|
||||
}
|
||||
|
||||
drawDeathRing(ctx, fx) {
|
||||
const t = fx.t;
|
||||
if (t > 14) return;
|
||||
const r = 4 + t * 1.8, a = Math.max(0, 1 - t / 14);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = a * 0.85;
|
||||
ctx.strokeStyle = '#ff8fa3';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(fx.x, fx.y, r, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = a * 0.3;
|
||||
ctx.fillStyle = '#ff5c7a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(fx.x, fx.y, r * 0.55, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
drawDeathMark(ctx, dm) {
|
||||
const s = 4; // 红叉半边长(< kid 的 11×21,整体约 8px)
|
||||
ctx.lineCap = 'round';
|
||||
@@ -370,6 +396,11 @@ export class Level {
|
||||
this.time += dt;
|
||||
const t = this.time;
|
||||
|
||||
// 死亡特效(冲击环)推进:14 帧后移除;粒子由 particles.update 自理
|
||||
for (let i = this.deathFX.length - 1; i >= 0; i--) {
|
||||
if (++this.deathFX[i].t > 14) this.deathFX.splice(i, 1);
|
||||
}
|
||||
|
||||
// 移动平台
|
||||
for (const m of this.movers) {
|
||||
if (!m.path.length) continue;
|
||||
@@ -623,6 +654,9 @@ export class Level {
|
||||
// 死亡血迹(IWBTG 风格,常驻于死亡地点)
|
||||
for (const dm of this.deathMarks) this.drawDeathMark(ctx, dm);
|
||||
|
||||
// 死亡特效(扩散冲击环,14 帧淡出)
|
||||
for (const fx of this.deathFX) this.drawDeathRing(ctx, fx);
|
||||
|
||||
this.particles.draw(ctx);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
+21
-3
@@ -136,10 +136,13 @@ function restart() {
|
||||
G.deaths = d; // 死亡数不重置
|
||||
}
|
||||
|
||||
/** 自杀(IWBT 的 R 键):死后自动回到最近的存档点 */
|
||||
/** R 键 / 触屏重生键:直接重生回最近的存档点——死亡中按 = 立即复活;活着按 = 原地播死亡动画后当帧重生 */
|
||||
function suicide() {
|
||||
if (!G.player || G.state !== 'playing') return;
|
||||
G.player.kill();
|
||||
if (G.player.dead) G.player.respawn();
|
||||
// 活着按:原地播死亡动画(粒子+轻震+冲击环),但不留红叉、不计死亡数,当帧就回存档点——
|
||||
// 不等待,特效由 Level 层留在死亡位置自然播完(重生不清粒子/震屏)
|
||||
else { G.player.deathBurst(); G.player.respawn(); }
|
||||
}
|
||||
|
||||
/** 触屏射击:仅在持枪、未死亡且游戏进行中时发射(受玩家自身冷却限制) */
|
||||
@@ -191,7 +194,7 @@ function resume() { if (G.state === 'paused') { G.state = 'playing'; UI.showGame
|
||||
// ---------------- 更新 ----------------
|
||||
function update() {
|
||||
const P = G.player;
|
||||
// R = 自杀回存档点;Backspace = 整关重开
|
||||
// R = 重生回存档点;Backspace = 整关重开
|
||||
if (Input.hit('suicide')) { suicide(); return; }
|
||||
if (Input.hit('restart')) { restart(); return; }
|
||||
if (Input.hit('pause')) { pause(); return; }
|
||||
@@ -240,6 +243,21 @@ function render() {
|
||||
ctx.restore();
|
||||
L.drawOverlay(ctx, C, t);
|
||||
UI.toast(L.toast || '');
|
||||
// 死亡中:提示手动重生(呼吸闪烁,不抢死亡动画镜头)
|
||||
if (G.state === 'playing' && P.dead) {
|
||||
const blink = Math.sin(t / 12) > -0.3;
|
||||
ctx.save();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.globalAlpha = blink ? 1 : 0.3;
|
||||
ctx.fillStyle = '#ff8fa3';
|
||||
ctx.font = 'bold 20px monospace';
|
||||
ctx.fillText(useTouch ? '按右下角重生键' : '按 R 重生', W / 2, H / 2 - 36);
|
||||
ctx.globalAlpha = 0.7;
|
||||
ctx.fillStyle = '#a8a8c0';
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText('Backspace 整关重来', W / 2, H / 2 - 12);
|
||||
ctx.restore();
|
||||
}
|
||||
} else {
|
||||
drawTitleBg(t);
|
||||
}
|
||||
|
||||
+37
-14
@@ -50,6 +50,7 @@ export class Player {
|
||||
this.alive = true;
|
||||
this.dead = false;
|
||||
this.deadT = 0;
|
||||
this.deathX = 0; this.deathY = 0; // 死亡动画圆心(掉坑死亡时 clamp 到关卡内)
|
||||
this.inWater = false;
|
||||
this.animT = 0;
|
||||
this.shootCool = 0;
|
||||
@@ -67,15 +68,36 @@ export class Player {
|
||||
get cx() { return this.x + this.w / 2; }
|
||||
get cy() { return this.y + this.h / 2; }
|
||||
|
||||
// 死亡:不播任何死亡动画(无等待、无镜头演出),只留音效、死亡计数,
|
||||
// 以及在死亡地点落一处 IWBTG 风格的血迹(由 Level 常驻渲染,复活后仍在)。
|
||||
kill() {
|
||||
// 纯死亡演出(不进死亡状态):粒子爆开 + 屏幕轻震 + 死亡点扩散冲击环(Level 层,见 addDeathFX)。
|
||||
// 主动按重生键时直接调它再当帧 respawn——特效留在死亡位置自然播完。
|
||||
// 圆心 clamp 到关卡内:掉坑死亡时玩家已在屏幕外,保证红叉/粒子/圆环可见
|
||||
deathBurst() {
|
||||
const L = this.level;
|
||||
const dx = clamp(this.cx, 10, Math.max(10, L.pxW - 10));
|
||||
const dy = clamp(this.cy, 10, Math.max(10, L.pxH - 10));
|
||||
sfx.death();
|
||||
L.particles.spawn(dx, dy, 16, {
|
||||
colors: ['#ff5c7a', '#ff8095', '#fff', '#ffc63a'], speed: 3.4, life: 34, size: 3, grav: 0.16,
|
||||
});
|
||||
L.shake = Math.max(L.shake || 0, 6);
|
||||
L.addDeathFX(dx, dy);
|
||||
return { dx, dy };
|
||||
}
|
||||
|
||||
// 死亡:定格在原地,等玩家手动按重生键(R / 触屏重生键)。
|
||||
// 视觉:① 死亡地点留下常驻红叉(IWBTG 血迹,复活后仍在;opts.mark=false 时不留)
|
||||
// ② deathBurst() 的"小"死亡动画(粒子+轻震+冲击环)
|
||||
// opts:
|
||||
// mark=false → 不留红叉
|
||||
// count=false → 不触发 onDeath 计数(不计入死亡数)
|
||||
kill(opts = {}) {
|
||||
if (this.dead || !this.alive) return;
|
||||
this.dead = true; this.alive = false; this.deadT = 0;
|
||||
this.vx = 0; this.vy = 0; this.sliding = false;
|
||||
sfx.death();
|
||||
this.level.addDeathMark(this.cx, this.cy);
|
||||
if (this.onDeath) this.onDeath();
|
||||
const { dx, dy } = this.deathBurst();
|
||||
this.deathX = dx; this.deathY = dy;
|
||||
if (opts.mark !== false) this.level.addDeathMark(dx, dy);
|
||||
if (opts.count !== false && this.onDeath) this.onDeath();
|
||||
}
|
||||
|
||||
respawn() {
|
||||
@@ -90,7 +112,7 @@ export class Player {
|
||||
// 安全兜底:万一重生点被地形/消失块堵住,向上顶出,别一复活就卡死
|
||||
let guard = 0;
|
||||
while (L.solidRect(this.x, this.y, this.w, this.h) && guard++ < 48) this.y -= 2;
|
||||
L.softReset();
|
||||
L.softReset(); // 不清粒子/震屏:死亡特效留在死亡位置自然播完
|
||||
this.justRespawned = true; // 供 main.js 把镜头直接吸附到重生点
|
||||
}
|
||||
|
||||
@@ -100,8 +122,9 @@ export class Player {
|
||||
this.animT++;
|
||||
if (this.shootCool > 0) this.shootCool--;
|
||||
|
||||
// 上一帧/外部(R 键自杀、假存档)触发的死亡:立刻复活,不留死亡动画
|
||||
if (this.dead) this.respawn();
|
||||
// 死亡:定格在原地等玩家手动重生(R 键 / 触屏重生键),
|
||||
// 世界继续运转(敌人/子弹/粒子照走),但玩家不响应任何输入
|
||||
if (this.dead) { this.deadT++; return; }
|
||||
|
||||
const inWater = L.waterRect(this.x + 2, this.y + 4, this.w - 4, this.h - 4);
|
||||
if (inWater && !this.inWater) sfx.splash();
|
||||
@@ -252,11 +275,11 @@ export class Player {
|
||||
|
||||
// ---- 边界 ----
|
||||
this.x = clamp(this.x, 0, L.pxW - this.w);
|
||||
if (this.y > L.pxH + 200) { this.kill(); if (this.dead) { this.respawn(); return; } }
|
||||
if (this.y > L.pxH + 200) { this.kill(); return; }
|
||||
if (this.y < -300) { this.y = -300; this.vy = 0; }
|
||||
|
||||
// ---- 致死:同帧立刻复活,画面上不留任何死亡状态 ----
|
||||
if (L.deadlyRect(this.x, this.y, this.w, this.h)) { this.kill(); if (this.dead) { this.respawn(); return; } }
|
||||
if (L.deadlyRect(this.x, this.y, this.w, this.h)) { this.kill(); return; }
|
||||
|
||||
// ---- 射击 ----
|
||||
if (this.hasGun && Input.hit('shoot') && this.shootCool === 0 && L.bullets.length < 5) {
|
||||
@@ -278,8 +301,6 @@ export class Player {
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:本帧内任何环节致死(如假存档)都立刻复活
|
||||
if (this.dead) this.respawn();
|
||||
}
|
||||
|
||||
shoot() {
|
||||
@@ -314,7 +335,7 @@ export class Player {
|
||||
L.particles.spawn(e.x + e.w / 2, e.y + e.h / 2, 18,
|
||||
{ colors: ['#b06bff', '#fff', '#e5d0ff'], speed: 2.6, life: 30 });
|
||||
// 假存档:立刻复活回上一个存档点,不播死亡动画
|
||||
if (e.fake) { L.showToast('假的。', 90); this.kill(); if (this.dead) { this.respawn(); return; } }
|
||||
if (e.fake) { L.showToast('假的。', 90); this.kill(); return; }
|
||||
}
|
||||
break;
|
||||
case 'goal':
|
||||
@@ -368,6 +389,8 @@ export class Player {
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
// 死亡中:kid 已爆开,不画本体(粒子/冲击环由 Level 层渲染,重生后仍能播完)
|
||||
if (this.dead) return;
|
||||
S.drawKid(ctx, this, this.animT);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user