Skip to content

Commit b641983

Browse files
committed
fix: derive press and release edges from fixed-step samples of held states
1 parent b3a51d2 commit b641983

4 files changed

Lines changed: 220 additions & 24 deletions

File tree

src/GameScene.ts

Lines changed: 123 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ const MENU_RIGHT_CODES: readonly string[] = ["ArrowRight"];
153153
const MENU_CONFIRM_CODES: readonly string[] = ["Enter", "NumpadEnter"];
154154
const MENU_CANCEL_CODES: readonly string[] = ["Escape"];
155155
const MENU_CLEAR_CODES: readonly string[] = ["Backspace", "Delete"];
156+
type GameplayButtonAction = "jump" | "dash" | "crouchDash";
157+
const GAMEPLAY_BUTTON_ACTIONS: readonly GameplayButtonAction[] = ["jump", "dash", "crouchDash"];
158+
const GAMEPLAY_BUTTON_BUFFER_TIMES: Record<GameplayButtonAction, number> = {
159+
jump: PLAYER_CONFIG.input.jumpBufferTime,
160+
dash: PLAYER_CONFIG.input.dashBufferTime,
161+
crouchDash: PLAYER_CONFIG.input.dashBufferTime,
162+
};
156163
const ON_OFF_CHOICES: readonly PauseMenuChoice<boolean>[] = [
157164
{ label: "OFF", value: false },
158165
{ label: "ON", value: true },
@@ -178,6 +185,12 @@ export class GameScene extends Phaser.Scene {
178185
private readonly heldKeyCodes = new Set<string>();
179186
private readonly pressedKeyCodes = new Set<string>();
180187
private readonly releasedKeyCodes = new Set<string>();
188+
private readonly sampledGameplayHeldKeyCodes = new Set<string>();
189+
private readonly pendingGameplayPressTimers: Record<GameplayButtonAction, number> = {
190+
jump: 0,
191+
dash: 0,
192+
crouchDash: 0,
193+
};
181194
private gameplayEdgesConsumed = false;
182195
private controls!: PlayerControls;
183196
private captureKeyBindingAction: KeyBindingAction | null = null;
@@ -222,6 +235,7 @@ export class GameScene extends Phaser.Scene {
222235

223236
this.accumulator = 0;
224237
this.simulationTime = 0;
238+
this.resetGameplayInputSampling();
225239
this.confirmBufferTimer = 0;
226240
this.freezeTimer = 0;
227241
this.effectsPaused = false;
@@ -523,6 +537,7 @@ export class GameScene extends Phaser.Scene {
523537
let steps = 0;
524538

525539
while (this.freezeTimer > 0 && this.accumulator >= this.fixedDt && steps < this.maxSteps) {
540+
this.sampleGameplayInput(false);
526541
this.freezeTimer = stepTimer(this.freezeTimer, this.fixedDt);
527542
this.accumulator = subFloat(this.accumulator, this.fixedDt);
528543
steps++;
@@ -543,6 +558,7 @@ export class GameScene extends Phaser.Scene {
543558
let steps = 0;
544559

545560
while (this.roomTransition !== null && this.accumulator >= this.fixedDt && steps < this.maxSteps) {
561+
this.sampleGameplayInput(false);
546562
this.advanceWorldFixedStep();
547563
this.updateRoomTransition(this.fixedDt);
548564
this.accumulator = subFloat(this.accumulator, this.fixedDt);
@@ -559,10 +575,11 @@ export class GameScene extends Phaser.Scene {
559575
let steps = 0;
560576

561577
while (this.deathRespawnSequence !== null && this.accumulator >= this.fixedDt && steps < this.maxSteps) {
578+
this.sampleGameplayInput(false);
562579
this.playerView.advanceDeathRespawn(this.fixedDt);
563580
this.updateDeathRespawnSequence(this.fixedDt);
564581
if (this.deathRespawnSequence !== null && this.deathRespawnSequence.respawnStarted) {
565-
this.advancePlayerOnlyFixedStep(effects);
582+
this.advancePlayerOnlyFixedStep(effects, false);
566583
}
567584
this.accumulator = subFloat(this.accumulator, this.fixedDt);
568585
steps++;
@@ -655,6 +672,7 @@ export class GameScene extends Phaser.Scene {
655672
this.heldKeyCodes.clear();
656673
this.pressedKeyCodes.clear();
657674
this.releasedKeyCodes.clear();
675+
this.resetGameplayInputSampling();
658676
this.captureKeyBindingAction = null;
659677
this.controls?.reset();
660678
this.unpauseRecovery.clear();
@@ -676,12 +694,14 @@ export class GameScene extends Phaser.Scene {
676694
}
677695

678696
private gatherStepInput(): InputState {
679-
if (!this.player.inControl || this.unpauseRecovery.blocksControl) {
697+
const canDeliverGameplayInput = this.player.inControl && !this.unpauseRecovery.blocksControl;
698+
const sample = this.sampleGameplayInput(canDeliverGameplayInput);
699+
700+
if (!canDeliverGameplayInput) {
680701
this.controls.clearTransientState();
681702
return EMPTY_INPUT;
682703
}
683704

684-
const useEdges = !this.gameplayEdgesConsumed;
685705
this.controls.setCheck("left", this.actionHeld("left"));
686706
this.controls.setCheck("right", this.actionHeld("right"));
687707
this.controls.setCheck("up", this.actionHeld("up"));
@@ -691,14 +711,22 @@ export class GameScene extends Phaser.Scene {
691711
this.controls.setCheck("crouchDash", this.actionHeld("crouchDash"));
692712
this.controls.setCheck("grab", this.actionHeld("grab"));
693713

694-
if (useEdges) {
695-
this.queueControlEdges("jump", "jump");
696-
this.queueControlEdges("dash", "dash");
697-
this.queueControlEdges("crouchDash", "crouchDash");
698-
this.gameplayEdgesConsumed = true;
714+
for (const action of GAMEPLAY_BUTTON_ACTIONS) {
715+
this.queueSampledControlEdges(action, sample);
716+
}
717+
718+
const input = this.controls.update(this.fixedDt);
719+
if (input.jumpPressed) {
720+
input.jumpPressBufferTime = sample.pressBufferTimes.jump ?? PLAYER_CONFIG.input.jumpBufferTime;
699721
}
722+
if (input.dashPressed) {
723+
input.dashPressBufferTime = sample.pressBufferTimes.dash ?? PLAYER_CONFIG.input.dashBufferTime;
724+
} else if (input.crouchDashPressed) {
725+
input.dashPressBufferTime = sample.pressBufferTimes.crouchDash ?? PLAYER_CONFIG.input.dashBufferTime;
726+
}
727+
this.gameplayEdgesConsumed = true;
700728

701-
return this.controls.update(this.fixedDt);
729+
return input;
702730
}
703731

704732
private onDebugToggleDown(event: KeyboardEvent): void {
@@ -732,12 +760,70 @@ export class GameScene extends Phaser.Scene {
732760
}
733761
}
734762

735-
private queueControlEdges(action: KeyBindingAction, binding: Parameters<PlayerControls["queuePress"]>[0]): void {
736-
if (this.actionPressed(action)) {
737-
this.controls.queuePress(binding);
763+
private sampleGameplayInput(deliver: boolean): {
764+
pressed: Set<GameplayButtonAction>;
765+
released: Set<GameplayButtonAction>;
766+
pressBufferTimes: Partial<Record<GameplayButtonAction, number>>;
767+
} {
768+
const pressed = new Set<GameplayButtonAction>();
769+
const released = new Set<GameplayButtonAction>();
770+
const pressBufferTimes: Partial<Record<GameplayButtonAction, number>> = {};
771+
772+
for (const action of GAMEPLAY_BUTTON_ACTIONS) {
773+
const bufferTime = GAMEPLAY_BUTTON_BUFFER_TIMES[action];
774+
this.pendingGameplayPressTimers[action] = stepTimer(this.pendingGameplayPressTimers[action], this.fixedDt);
775+
776+
const codes = this.bindingCodes(action);
777+
const currentlyHeld = codes.some((code) => this.heldKeyCodes.has(code));
778+
const justPressed = codes.some((code) =>
779+
this.heldKeyCodes.has(code) && !this.sampledGameplayHeldKeyCodes.has(code)
780+
);
781+
const justReleased = codes.some((code) =>
782+
!this.heldKeyCodes.has(code) && this.sampledGameplayHeldKeyCodes.has(code)
783+
);
784+
785+
if (!currentlyHeld) {
786+
this.pendingGameplayPressTimers[action] = 0;
787+
}
788+
789+
if (justPressed) {
790+
if (deliver) {
791+
pressed.add(action);
792+
pressBufferTimes[action] = bufferTime;
793+
} else {
794+
this.pendingGameplayPressTimers[action] = bufferTime;
795+
}
796+
} else if (deliver && currentlyHeld && this.pendingGameplayPressTimers[action] > 0) {
797+
pressed.add(action);
798+
pressBufferTimes[action] = this.pendingGameplayPressTimers[action];
799+
this.pendingGameplayPressTimers[action] = 0;
800+
}
801+
802+
if (deliver && justReleased) {
803+
released.add(action);
804+
}
805+
}
806+
807+
this.sampledGameplayHeldKeyCodes.clear();
808+
for (const code of this.heldKeyCodes) {
809+
this.sampledGameplayHeldKeyCodes.add(code);
810+
}
811+
812+
return { pressed, released, pressBufferTimes };
813+
}
814+
815+
private queueSampledControlEdges(
816+
action: GameplayButtonAction,
817+
sample: {
818+
pressed: Set<GameplayButtonAction>;
819+
released: Set<GameplayButtonAction>;
820+
},
821+
): void {
822+
if (sample.pressed.has(action)) {
823+
this.controls.queuePress(action);
738824
}
739-
if (this.actionReleased(action)) {
740-
this.controls.queueRelease(binding);
825+
if (sample.released.has(action)) {
826+
this.controls.queueRelease(action);
741827
}
742828
}
743829

@@ -793,6 +879,17 @@ export class GameScene extends Phaser.Scene {
793879
}
794880
}
795881

882+
private resetGameplayInputSampling(): void {
883+
this.sampledGameplayHeldKeyCodes.clear();
884+
for (const code of this.heldKeyCodes) {
885+
this.sampledGameplayHeldKeyCodes.add(code);
886+
}
887+
for (const action of GAMEPLAY_BUTTON_ACTIONS) {
888+
this.pendingGameplayPressTimers[action] = 0;
889+
}
890+
this.gameplayEdgesConsumed = false;
891+
}
892+
796893
private spawnInitialPlayer(): void {
797894
this.startIntroIris();
798895
this.revivePlayerAtCheckpoint("start", this.spawnX, this.spawnY, introIrisTotalDuration());
@@ -805,6 +902,7 @@ export class GameScene extends Phaser.Scene {
805902
}
806903
this.requestDeathShake();
807904
this.controls.clearTransientState();
905+
this.resetGameplayInputSampling();
808906
this.accumulator = 0;
809907
this.freezeTimer = 0;
810908
this.setFreezeEffectsPaused(false);
@@ -833,6 +931,7 @@ export class GameScene extends Phaser.Scene {
833931
this.freezeTimer = 0;
834932
this.setFreezeEffectsPaused(false);
835933
this.controls.clearTransientState();
934+
this.resetGameplayInputSampling();
836935
this.accumulator = 0;
837936
this.startDeathRespawnSequence({
838937
kind: "spike",
@@ -885,6 +984,7 @@ export class GameScene extends Phaser.Scene {
885984
this.world.resetTransientState();
886985
this.syncRefillViews();
887986
this.controls.clearTransientState();
987+
this.resetGameplayInputSampling();
888988
this.playerView.resetDeathRespawn();
889989
this.roomTransition = null;
890990
this.accumulator = 0;
@@ -1070,7 +1170,10 @@ export class GameScene extends Phaser.Scene {
10701170
}
10711171
}
10721172

1073-
private advancePlayerOnlyFixedStep(effects: PlayerEffect[]): void {
1173+
private advancePlayerOnlyFixedStep(effects: PlayerEffect[], sampleInput = true): void {
1174+
if (sampleInput) {
1175+
this.sampleGameplayInput(false);
1176+
}
10741177
this.player.update(this.fixedDt, EMPTY_INPUT);
10751178
const stepSnapshot = this.player.getSnapshot();
10761179
const stepEffects = this.player.consumeEffects();
@@ -1124,6 +1227,7 @@ export class GameScene extends Phaser.Scene {
11241227
toScrollY: target.y,
11251228
};
11261229
this.controls.clearTransientState();
1230+
this.resetGameplayInputSampling();
11271231
this.accumulator = 0;
11281232
this.freezeTimer = 0;
11291233
this.setFreezeEffectsPaused(false);
@@ -1155,6 +1259,7 @@ export class GameScene extends Phaser.Scene {
11551259
}
11561260
this.requestDeathShake();
11571261
this.controls.clearTransientState();
1262+
this.resetGameplayInputSampling();
11581263
this.accumulator = 0;
11591264
this.freezeTimer = 0;
11601265
this.setFreezeEffectsPaused(false);
@@ -1853,6 +1958,7 @@ export class GameScene extends Phaser.Scene {
18531958
private openPauseMenu(): void {
18541959
this.unpauseRecovery.clear();
18551960
this.controls.clearTransientState();
1961+
this.resetGameplayInputSampling();
18561962
this.accumulator = 0;
18571963
this.stopScreenShake();
18581964
this.setPauseEffectsPaused(true);
@@ -2173,6 +2279,7 @@ export class GameScene extends Phaser.Scene {
21732279
this.accumulator = addFloat(this.accumulator, frameDt);
21742280

21752281
while (this.unpauseRecovery.active && this.accumulator >= this.fixedDt) {
2282+
this.resetGameplayInputSampling();
21762283
const result = this.unpauseRecovery.step(this.currentUnpauseRecoveryHeldState());
21772284

21782285
if (result.openPause) {
@@ -2220,6 +2327,7 @@ export class GameScene extends Phaser.Scene {
22202327
private resumeFromPauseMenu(): void {
22212328
this.pauseMenu.close();
22222329
this.accumulator = 0;
2330+
this.resetGameplayInputSampling();
22232331
this.unpauseRecovery.start(this.currentUnpauseRecoveryHeldState());
22242332
}
22252333

src/player/Player.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,18 +222,18 @@ export class Player extends Actor {
222222
}
223223

224224
if (input.jumpPressed) {
225-
this.jumpPressBufferTimer = toFloat(this.cfg.input.jumpBufferTime);
225+
this.jumpPressBufferTimer = toFloat(input.jumpPressBufferTime ?? this.cfg.input.jumpBufferTime);
226226
} else if (!input.jump) {
227227
this.jumpPressBufferTimer = 0;
228228
} else if (this.jumpPressBufferTimer > 0) {
229229
this.jumpPressBufferTimer = stepTimer(this.jumpPressBufferTimer, dt);
230230
}
231231

232232
if (input.crouchDashPressed) {
233-
this.dashPressBufferTimer = toFloat(this.cfg.input.dashBufferTime);
233+
this.dashPressBufferTimer = toFloat(input.dashPressBufferTime ?? this.cfg.input.dashBufferTime);
234234
this.dashPressCrouches = true;
235235
} else if (input.dashPressed) {
236-
this.dashPressBufferTimer = toFloat(this.cfg.input.dashBufferTime);
236+
this.dashPressBufferTimer = toFloat(input.dashPressBufferTime ?? this.cfg.input.dashBufferTime);
237237
this.dashPressCrouches = input.y === 1;
238238
} else if (!input.dash && !input.crouchDash) {
239239
this.dashPressBufferTimer = 0;

src/player/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ export interface InputState {
1111
aimY: number;
1212
jump: boolean;
1313
jumpPressed: boolean;
14+
jumpPressBufferTime?: number;
1415
jumpReleased: boolean;
1516
dash: boolean;
1617
dashPressed: boolean;
18+
dashPressBufferTime?: number;
1719
crouchDash: boolean;
1820
crouchDashPressed: boolean;
1921
grab: boolean;

0 commit comments

Comments
 (0)