@@ -153,6 +153,13 @@ const MENU_RIGHT_CODES: readonly string[] = ["ArrowRight"];
153153const MENU_CONFIRM_CODES : readonly string [ ] = [ "Enter" , "NumpadEnter" ] ;
154154const MENU_CANCEL_CODES : readonly string [ ] = [ "Escape" ] ;
155155const 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+ } ;
156163const 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
0 commit comments