Skip to content

Commit ec87d3d

Browse files
committed
refactor: extract respawn landing effect suppression into a dedicated gate utility
1 parent 7e472fd commit ec87d3d

4 files changed

Lines changed: 96 additions & 19 deletions

File tree

src/GameScene.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ import {
2424
transitionTimings,
2525
SPAWN_WIPE_VISUALS,
2626
} from "./view/deathRespawn";
27+
import {
28+
armRespawnEffectGate,
29+
createRespawnEffectGate,
30+
filterRespawnEffects,
31+
resetRespawnEffectGate,
32+
} from "./view/respawnEffectGate";
2733
import { LightingSource, LightingSystem } from "./lighting/LightingSystem";
2834

2935
interface RefillView {
@@ -113,6 +119,7 @@ export class GameScene extends Phaser.Scene {
113119
private refillEmitter!: Phaser.GameObjects.Particles.ParticleEmitter;
114120
private lighting!: LightingSystem;
115121
private spawnTransition: SpawnTransitionState | null = null;
122+
private readonly respawnEffectGate = createRespawnEffectGate();
116123
private forceCameraUpdate = false;
117124
private forceCameraSnapNextFrame = true;
118125

@@ -276,6 +283,7 @@ export class GameScene extends Phaser.Scene {
276283

277284
let stepEffects = this.player.consumeEffects();
278285
const stepSnapshot = this.player.getSnapshot();
286+
stepEffects = filterRespawnEffects(this.respawnEffectGate, stepSnapshot, stepEffects);
279287
const spike = this.world.collidesWithSpike(
280288
this.player.getHurtboxBounds(),
281289
stepSnapshot.vx,
@@ -525,8 +533,9 @@ export class GameScene extends Phaser.Scene {
525533
}
526534

527535
private snapPlayerToCheckpoint(): void {
528-
this.player.hardRespawn(this.spawnX, this.spawnY);
529-
this.player.finalizeRespawnState();
536+
this.player.resetStateAt(this.spawnX, this.spawnY);
537+
this.player.syncStateAfterExternalMove();
538+
armRespawnEffectGate(this.respawnEffectGate, this.player.getSnapshot());
530539
this.world.resetTransientState();
531540
this.syncRefillViews();
532541
this.controls.clearTransientState();
@@ -541,6 +550,7 @@ export class GameScene extends Phaser.Scene {
541550
private startSpawnTransition(
542551
state: Omit<SpawnTransitionState, "elapsed" | "totalDuration">,
543552
): void {
553+
resetRespawnEffectGate(this.respawnEffectGate);
544554
const totalDuration = baseTransitionDuration(state.kind);
545555
this.spawnTransition = {
546556
...state,

src/player/Player.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ export class Player extends Actor {
113113
private liftTimer = 0;
114114

115115
private wasOnGround = false;
116-
private suppressGroundedLandingEffects = false;
117116
private effects: PlayerEffect[] = [];
118117

119118
private cfg: PlayerConfig;
@@ -164,9 +163,6 @@ export class Player extends Actor {
164163
this.frameDt = dt;
165164
this.input = input;
166165
this.refreshEnvironment();
167-
if (this.suppressGroundedLandingEffects && !this.onGround) {
168-
this.suppressGroundedLandingEffects = false;
169-
}
170166
this.wallDustDir = 0;
171167

172168
if (this.forceMoveXTimer > 0) {
@@ -284,9 +280,6 @@ export class Player extends Actor {
284280
this.moveV(mulFloat(this.vy, dt));
285281

286282
this.refreshEnvironment();
287-
if (this.suppressGroundedLandingEffects && !this.onGround) {
288-
this.suppressGroundedLandingEffects = false;
289-
}
290283
this.updateHairState(dt);
291284

292285
this.wasOnGround = this.onGround;
@@ -372,7 +365,7 @@ export class Player extends Actor {
372365
this.liftTimer = toFloat(this.cfg.lift.momentumStoreTime);
373366
}
374367

375-
hardRespawn(x: number, y: number): void {
368+
resetStateAt(x: number, y: number): void {
376369
this.x = x;
377370
this.y = y;
378371
this.vx = 0;
@@ -388,7 +381,6 @@ export class Player extends Actor {
388381
this.wallDir = 0;
389382
this.wallDustDir = 0;
390383
this.wasOnGround = false;
391-
this.suppressGroundedLandingEffects = false;
392384

393385
this.moveXInput = 0;
394386
this.jumpGraceTimer = 0;
@@ -441,10 +433,11 @@ export class Player extends Actor {
441433
this.stateMachine.forceState("normal");
442434
}
443435

444-
finalizeRespawnState(): void {
436+
// Scene-managed teleports bypass the normal update loop, so collision-derived
437+
// state needs an explicit refresh before normal simulation resumes.
438+
syncStateAfterExternalMove(): void {
445439
this.refreshEnvironment();
446440
this.wasOnGround = this.onGround;
447-
this.suppressGroundedLandingEffects = this.onGround;
448441
if (this.onGround) {
449442
this.jumpGraceTimer = toFloat(this.cfg.jump.graceTime);
450443
}
@@ -1610,12 +1603,7 @@ export class Player extends Actor {
16101603
this.applyDashSlide(!this.dashStartedOnGround);
16111604
}
16121605

1613-
if (
1614-
step > 0 &&
1615-
this.vy > 0 &&
1616-
this.stateMachine.state !== "climb" &&
1617-
!this.suppressGroundedLandingEffects
1618-
) {
1606+
if (step > 0 && this.vy > 0 && this.stateMachine.state !== "climb") {
16191607
const impact = clamp01Float(this.vy / this.cfg.gravity.fastMaxFall);
16201608
this.emit({ type: "land", impact });
16211609
}

src/view/respawnEffectGate.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { type PlayerEffect, type PlayerSnapshot } from "../player/types";
2+
3+
export interface RespawnEffectGate {
4+
suppressGroundedLandEffects: boolean;
5+
}
6+
7+
export function createRespawnEffectGate(): RespawnEffectGate {
8+
return {
9+
suppressGroundedLandEffects: false,
10+
};
11+
}
12+
13+
export function resetRespawnEffectGate(gate: RespawnEffectGate): void {
14+
gate.suppressGroundedLandEffects = false;
15+
}
16+
17+
export function armRespawnEffectGate(
18+
gate: RespawnEffectGate,
19+
snapshot: Pick<PlayerSnapshot, "onGround">,
20+
): void {
21+
gate.suppressGroundedLandEffects = snapshot.onGround;
22+
}
23+
24+
export function filterRespawnEffects(
25+
gate: RespawnEffectGate,
26+
snapshot: Pick<PlayerSnapshot, "onGround">,
27+
effects: PlayerEffect[],
28+
): PlayerEffect[] {
29+
if (!gate.suppressGroundedLandEffects) {
30+
return effects;
31+
}
32+
33+
if (!snapshot.onGround) {
34+
gate.suppressGroundedLandEffects = false;
35+
return effects;
36+
}
37+
38+
return effects.filter((effect) => effect.type !== "land");
39+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, test } from "bun:test";
2+
import {
3+
armRespawnEffectGate,
4+
createRespawnEffectGate,
5+
filterRespawnEffects,
6+
} from "../../src/view/respawnEffectGate.ts";
7+
8+
describe("Respawn effect gate", () => {
9+
test("suppresses grounded landing effects until the player leaves the ground once", () => {
10+
const gate = createRespawnEffectGate();
11+
armRespawnEffectGate(gate, { onGround: true });
12+
13+
const groundedEffects = filterRespawnEffects(gate, { onGround: true }, [
14+
{ type: "land", impact: 1 },
15+
{ type: "jump" },
16+
]);
17+
18+
expect(groundedEffects).toEqual([{ type: "jump" }]);
19+
expect(gate.suppressGroundedLandEffects).toBe(true);
20+
21+
const airborneEffects = filterRespawnEffects(gate, { onGround: false }, [
22+
{ type: "land", impact: 0.6 },
23+
]);
24+
25+
expect(airborneEffects).toEqual([{ type: "land", impact: 0.6 }]);
26+
expect(gate.suppressGroundedLandEffects).toBe(false);
27+
});
28+
29+
test("does not arm suppression when the player respawns airborne", () => {
30+
const gate = createRespawnEffectGate();
31+
armRespawnEffectGate(gate, { onGround: false });
32+
33+
const effects = filterRespawnEffects(gate, { onGround: true }, [
34+
{ type: "land", impact: 0.5 },
35+
]);
36+
37+
expect(effects).toEqual([{ type: "land", impact: 0.5 }]);
38+
expect(gate.suppressGroundedLandEffects).toBe(false);
39+
});
40+
});

0 commit comments

Comments
 (0)