Skip to content

Commit 4a52e8b

Browse files
committed
feat: replace simple respawn flash with a particle-based reconstruction animation and adjust player intro state offsets
1 parent a5a5ae5 commit 4a52e8b

4 files changed

Lines changed: 101 additions & 27 deletions

File tree

src/GameScene.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,6 @@ export class GameScene extends Phaser.Scene {
610610
}
611611
if (transition.kind === "spike") {
612612
this.cameras.main.shake(120, 0.0026);
613-
this.cameras.main.flash(180, 0, 0, 0, false);
614613
}
615614
transition.exploded = true;
616615
}

src/player/intro.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export const PLAYER_INTRO_TIMING = {
3232
respawnClampPad: 40,
3333
} as const;
3434

35+
export const PLAYER_RESPAWN_VISUALS = {
36+
useSourceLerp: false,
37+
} as const;
38+
3539
export function introDuration(type: Exclude<PlayerIntroType, "none">): number {
3640
switch (type) {
3741
case "start":
@@ -86,8 +90,12 @@ export function samplePlayerIntroState(
8690
};
8791
}
8892

89-
const fromOffsetX = (sourceX ?? currentCenterX) - currentCenterX;
90-
const fromOffsetY = (sourceY ?? currentCenterY) - currentCenterY;
93+
const fromOffsetX = PLAYER_RESPAWN_VISUALS.useSourceLerp
94+
? (sourceX ?? currentCenterX) - currentCenterX
95+
: 0;
96+
const fromOffsetY = PLAYER_RESPAWN_VISUALS.useSourceLerp
97+
? (sourceY ?? currentCenterY) - currentCenterY
98+
: 0;
9199
return {
92100
type,
93101
progress: t,

src/view/PlayerView.ts

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ interface DeathRecoilState {
9595
scale: number;
9696
}
9797

98+
const RESPAWN_RECONSTRUCTION_VISUALS = {
99+
burstSpeed: 22,
100+
burstSpread: 14,
101+
startRadiusXScale: 0.9,
102+
startRadiusYScale: 0.8,
103+
endRadiusXScale: 0.24,
104+
endRadiusYScale: 0.2,
105+
minRadiusX: 10,
106+
minRadiusY: 8,
107+
minEndRadius: 2,
108+
startVelocityMult: 3.2,
109+
endVelocityMult: 2.2,
110+
velocityJitter: 0.14,
111+
} as const;
112+
98113
export class PlayerView {
99114
private scene: Phaser.Scene;
100115
private playerSprite: GlyphSprite;
@@ -540,29 +555,24 @@ export class PlayerView {
540555
const introType = snapshot.intro?.type ?? null;
541556
if (introType !== this.activeIntroType) {
542557
if (this.activeIntroType !== null) {
543-
this.respawnEmitter.setParticleTint(snapshot.hairColor);
544-
this.respawnEmitter.emitParticleAt(
545-
snapshot.x,
546-
snapshot.y,
558+
this.emitRespawnBurst(
559+
snapshot.centerX,
560+
snapshot.centerY,
547561
PLAYER_VISUALS.respawnSparkCount,
562+
snapshot.hairColor,
548563
);
549564
}
550565
this.activeIntroType = introType;
551566
this.introSparkTimer = 0;
552567
if (introType === "start") {
553-
this.respawnEmitter.setParticleTint(snapshot.hairColor);
554-
this.respawnEmitter.emitParticleAt(
555-
snapshot.x,
556-
snapshot.y,
568+
this.emitRespawnBurst(
569+
snapshot.centerX,
570+
snapshot.centerY,
557571
Math.max(3, Math.round(PLAYER_VISUALS.respawnSparkCount * 0.4)),
572+
snapshot.hairColor,
558573
);
559574
} else if (introType === "respawn" && snapshot.intro !== null) {
560-
this.respawnEmitter.setParticleTint(snapshot.hairColor);
561-
this.respawnEmitter.emitParticleAt(
562-
snapshot.centerX + snapshot.intro.offsetX,
563-
snapshot.centerY + snapshot.intro.offsetY,
564-
2,
565-
);
575+
this.emitRespawnReconstructionParticles(snapshot, snapshot.intro, 4);
566576
}
567577
}
568578

@@ -578,21 +588,16 @@ export class PlayerView {
578588
this.introSparkTimer = PLAYER_VISUALS.respawnSparkInterval;
579589
if (snapshot.intro.type === "start") {
580590
const sample = sampleStartIntro(snapshot.intro.progress);
581-
this.respawnEmitter.setParticleTint(snapshot.hairColor);
582-
this.respawnEmitter.emitParticleAt(
583-
snapshot.x,
591+
this.emitRespawnBurst(
592+
snapshot.centerX,
584593
snapshot.y - snapshot.drawH * (1 - sample.ghostScaleY) * 0.18,
585594
1,
595+
snapshot.hairColor,
586596
);
587597
return;
588598
}
589599

590-
this.respawnEmitter.setParticleTint(snapshot.hairColor);
591-
this.respawnEmitter.emitParticleAt(
592-
snapshot.centerX + snapshot.intro.offsetX,
593-
snapshot.centerY + snapshot.intro.offsetY,
594-
1,
595-
);
600+
this.emitRespawnReconstructionParticles(snapshot, snapshot.intro, 1);
596601
}
597602

598603
private renderIntro(snapshot: PlayerSnapshot): void {
@@ -874,6 +879,67 @@ export class PlayerView {
874879
this.deathEmitter.emitParticleAt(cx, cy, Math.floor(count * 0.2));
875880
}
876881

882+
private emitRespawnBurst(x: number, y: number, count: number, tint: number): void {
883+
const speed = RESPAWN_RECONSTRUCTION_VISUALS.burstSpeed;
884+
const spread = RESPAWN_RECONSTRUCTION_VISUALS.burstSpread;
885+
this.respawnEmitter.setParticleTint(tint);
886+
this.respawnEmitter.speedX = { min: -speed - spread, max: speed + spread };
887+
this.respawnEmitter.speedY = { min: -speed - spread, max: speed + spread };
888+
this.respawnEmitter.emitParticleAt(x, y, count);
889+
}
890+
891+
private emitRespawnReconstructionParticles(
892+
snapshot: PlayerSnapshot,
893+
intro: PlayerIntroStateSnapshot,
894+
count: number,
895+
): void {
896+
const targetX = snapshot.centerX + intro.offsetX;
897+
const targetY = snapshot.centerY + intro.offsetY;
898+
const startRadiusX = Math.max(
899+
snapshot.drawW * RESPAWN_RECONSTRUCTION_VISUALS.startRadiusXScale,
900+
RESPAWN_RECONSTRUCTION_VISUALS.minRadiusX,
901+
);
902+
const startRadiusY = Math.max(
903+
snapshot.drawH * RESPAWN_RECONSTRUCTION_VISUALS.startRadiusYScale,
904+
RESPAWN_RECONSTRUCTION_VISUALS.minRadiusY,
905+
);
906+
const endRadiusX = Math.max(
907+
snapshot.drawW * RESPAWN_RECONSTRUCTION_VISUALS.endRadiusXScale,
908+
RESPAWN_RECONSTRUCTION_VISUALS.minEndRadius,
909+
);
910+
const endRadiusY = Math.max(
911+
snapshot.drawH * RESPAWN_RECONSTRUCTION_VISUALS.endRadiusYScale,
912+
RESPAWN_RECONSTRUCTION_VISUALS.minEndRadius,
913+
);
914+
const radiusX = Phaser.Math.Linear(startRadiusX, endRadiusX, intro.progress);
915+
const radiusY = Phaser.Math.Linear(startRadiusY, endRadiusY, intro.progress);
916+
const velocityMult = Phaser.Math.Linear(
917+
RESPAWN_RECONSTRUCTION_VISUALS.startVelocityMult,
918+
RESPAWN_RECONSTRUCTION_VISUALS.endVelocityMult,
919+
intro.progress,
920+
);
921+
922+
this.respawnEmitter.setParticleTint(snapshot.hairColor);
923+
for (let i = 0; i < count; i++) {
924+
const angle = Phaser.Math.FloatBetween(0, Math.PI * 2);
925+
const px = targetX + Math.cos(angle) * radiusX;
926+
const py = targetY + Math.sin(angle) * radiusY;
927+
const baseVx = (targetX - px) * velocityMult;
928+
const baseVy = (targetY - py) * velocityMult;
929+
const vx = baseVx * Phaser.Math.FloatBetween(
930+
1 - RESPAWN_RECONSTRUCTION_VISUALS.velocityJitter,
931+
1 + RESPAWN_RECONSTRUCTION_VISUALS.velocityJitter,
932+
);
933+
const vy = baseVy * Phaser.Math.FloatBetween(
934+
1 - RESPAWN_RECONSTRUCTION_VISUALS.velocityJitter,
935+
1 + RESPAWN_RECONSTRUCTION_VISUALS.velocityJitter,
936+
);
937+
this.respawnEmitter.speedX = { min: vx, max: vx };
938+
this.respawnEmitter.speedY = { min: vy, max: vy };
939+
this.respawnEmitter.emitParticleAt(px, py, 1);
940+
}
941+
}
942+
877943
private clearAfterimages(): void {
878944
this.dashParticleTimer = 0;
879945
this.dashSlashTimer = 0;

tests/model/player-intro.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ describe("Player intro lifecycle", () => {
2121
expect(start.justRespawned).toBe(true);
2222
expect(start.facing).toBe(-1);
2323
expect(start.intro?.type).toBe("respawn");
24-
expect(start.intro?.offsetX).not.toBe(0);
24+
expect(start.intro?.offsetX).toBe(0);
25+
expect(start.intro?.offsetY).toBe(0);
2526

2627
const frames = step(player, {
2728
x: 0,

0 commit comments

Comments
 (0)