Skip to content

Commit 99f9db1

Browse files
committed
feat: implement room fallout behavior with invincibility bottom bounce mechanic
1 parent e32e04b commit 99f9db1

3 files changed

Lines changed: 78 additions & 3 deletions

File tree

src/GameScene.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ export class GameScene extends Phaser.Scene {
261261

262262
if (this.pauseMenu.isOpen) {
263263
this.updatePauseInput();
264-
const snapshot = this.player.getSnapshot();
264+
let snapshot = this.player.getSnapshot();
265265
this.playerView.render(snapshot);
266266
this.renderLighting(snapshot);
267267
this.updateHUD(snapshot, []);
@@ -295,7 +295,7 @@ export class GameScene extends Phaser.Scene {
295295

296296
if (this.roomTransition) {
297297
this.updateRoomTransition(rawFrameDt);
298-
const snapshot = this.player.getSnapshot();
298+
let snapshot = this.player.getSnapshot();
299299
this.playerView.render(snapshot);
300300
this.renderLighting(snapshot);
301301
this.updateHUD(snapshot, effects);
@@ -380,13 +380,17 @@ export class GameScene extends Phaser.Scene {
380380
}
381381
}
382382

383-
const snapshot = this.player.getSnapshot();
383+
let snapshot = this.player.getSnapshot();
384384
this.playerView.tick(snapshot, stepEffects, this.fixedDt);
385385
effects.push(...stepEffects);
386386

387387
if (this.tryStartRoomTransition(snapshot)) {
388388
break;
389389
}
390+
if (this.tryHandleBottomFallout(snapshot)) {
391+
break;
392+
}
393+
snapshot = this.player.getSnapshot();
390394

391395
this.updateCamera(snapshot, this.fixedDt);
392396
this.accumulator = subFloat(this.accumulator, this.fixedDt);
@@ -871,6 +875,47 @@ export class GameScene extends Phaser.Scene {
871875
return true;
872876
}
873877

878+
private tryHandleBottomFallout(snapshot: ReturnType<Player["getSnapshot"]>): boolean {
879+
const bounds = this.currentRoom.bounds;
880+
if (snapshot.top <= bounds.y + bounds.h) {
881+
return false;
882+
}
883+
884+
const roomBelow = findAdjacentRoom(this.rooms, this.currentRoom, "down", snapshot.centerX);
885+
if (roomBelow !== null) {
886+
return false;
887+
}
888+
889+
if (this.beginBottomFalloutRespawn(snapshot)) {
890+
return true;
891+
}
892+
893+
this.player.bounceFromBottom(bounds.y + bounds.h, this.keys.jump.isDown);
894+
return false;
895+
}
896+
897+
private beginBottomFalloutRespawn(snapshot: ReturnType<Player["getSnapshot"]>): boolean {
898+
if (!this.player.die({ x: 0, y: 0 })) {
899+
return false;
900+
}
901+
this.requestDeathShake();
902+
this.controls.clearTransientState();
903+
this.accumulator = 0;
904+
this.freezeTimer = 0;
905+
this.roomTransition = null;
906+
this.startDeathRespawnSequence({
907+
kind: "normal",
908+
exploded: false,
909+
revealStarted: false,
910+
respawnStarted: false,
911+
respawnSourceX: snapshot.centerX,
912+
respawnSourceY: snapshot.centerY,
913+
knockback: null,
914+
});
915+
this.startTransitionExplosion(snapshot);
916+
return true;
917+
}
918+
874919
private enforceCurrentRoomTopLimit(): void {
875920
const snapshot = this.player.getSnapshot();
876921
const roomAbove = findAdjacentRoom(this.rooms, this.currentRoom, "up", snapshot.centerX);

src/player/Player.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const USED_HAIR_LERP_RATE = 6;
3838
const BOUNCE_AUTO_JUMP_TIME = 0.1;
3939
const BOUNCE_VAR_JUMP_TIME = 0.2;
4040
const BOUNCE_SPEED = -140;
41+
const BOTTOM_BOUNCE_JUMP_HELD_SPEED = -180;
4142
const SWEAT_JUMP_HOLD_TIME = 0.12;
4243
const ZERO_DIRECTION = { x: 0, y: 0 };
4344
const EMPTY_INPUT: InputState = {
@@ -636,6 +637,21 @@ export class Player extends Actor {
636637
this.emit({ type: "bounce", dirY: -1 });
637638
}
638639

640+
bounceFromBottom(bottomLimit: number, jumpHeld: boolean): void {
641+
const hitbox = this.requireBodyHitbox();
642+
if (this.getHitboxBottom() > bottomLimit) {
643+
this.y = this.entityYForFoot(hitbox, bottomLimit);
644+
}
645+
646+
this.bounce();
647+
if (jumpHeld) {
648+
this.varJumpTimer = BOUNCE_VAR_JUMP_TIME;
649+
this.autoJump = true;
650+
this.autoJumpTimer = BOUNCE_AUTO_JUMP_TIME;
651+
this.varJumpSpeed = this.vy = BOTTOM_BOUNCE_JUMP_HELD_SPEED;
652+
}
653+
}
654+
639655
enforceTopLimit(minTop: number): void {
640656
const bounds = this.getHitboxBounds();
641657
if (bounds.y >= minTop) {

tests/gameplay/transition-behavior.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,18 @@ describe("Transition behavior", () => {
7171
const result = stepOnce(player, makeInput());
7272
expect(result.snapshot.vy).toBe(-140);
7373
});
74+
75+
test("bottom bounce clamps to the room edge and jumps higher while jump is held", () => {
76+
const world = buildWorld([]);
77+
const player = createPlayer(world, 100, 100);
78+
79+
player.bounceFromBottom(92, true);
80+
81+
const bounced = player.getSnapshot();
82+
expect(bounced.bottom).toBe(92);
83+
expect(bounced.vy).toBe(-180);
84+
85+
const result = stepOnce(player, makeInput({ jump: true }));
86+
expect(result.snapshot.vy).toBe(-180);
87+
});
7488
});

0 commit comments

Comments
 (0)