@@ -21,6 +21,7 @@ import {
2121 type PauseMenuOption ,
2222 type PauseOptionsMenu ,
2323} from "./pause/menu" ;
24+ import { UnpauseRecovery } from "./pause/unpauseRecovery" ;
2425import { clampRespawnSource , type PlayerIntroType } from "./player/intro" ;
2526import { addFloat , approach , maxFloat , stepTimer , subFloat , toFloat } from "./player/math" ;
2627import { Player } from "./player/Player" ;
@@ -129,6 +130,7 @@ export class GameScene extends Phaser.Scene {
129130 private controls ! : PlayerControls ;
130131 private confirmBufferedFrames = 0 ;
131132 private readonly pauseMenu = new PauseMenuController ( ) ;
133+ private readonly unpauseRecovery = new UnpauseRecovery ( ) ;
132134 private pauseOverlay ! : PauseOverlay ;
133135 private gameOptions : GameOptions = loadGameOptions ( ) ;
134136
@@ -271,6 +273,15 @@ export class GameScene extends Phaser.Scene {
271273
272274 this . pauseOverlay . hide ( ) ;
273275
276+ let accumulatorPrimed = false ;
277+ if ( this . unpauseRecovery . active ) {
278+ accumulatorPrimed = true ;
279+ if ( this . advanceUnpauseRecovery ( rawFrameDt ) ) {
280+ this . renderPassiveFrame ( ) ;
281+ return ;
282+ }
283+ }
284+
274285 if ( this . keys . restart . isDown && this . deathRespawnSequence === null && this . player . canRetry ) {
275286 this . beginNormalRespawn ( ) ;
276287 }
@@ -331,7 +342,9 @@ export class GameScene extends Phaser.Scene {
331342 return ;
332343 }
333344
334- this . accumulator = addFloat ( this . accumulator , rawFrameDt ) ;
345+ if ( ! accumulatorPrimed ) {
346+ this . accumulator = addFloat ( this . accumulator , rawFrameDt ) ;
347+ }
335348 let steps = 0 ;
336349
337350 while ( this . accumulator >= this . fixedDt && steps < this . maxSteps ) {
@@ -464,6 +477,7 @@ export class GameScene extends Phaser.Scene {
464477 this . keys . pause ?. off ( "down" , this . onPauseDown , this ) ;
465478 }
466479 this . controls ?. reset ( ) ;
480+ this . unpauseRecovery . clear ( ) ;
467481 this . playerView ?. destroy ( ) ;
468482 this . pauseOverlay ?. destroy ( ) ;
469483 this . spawnWipe ?. destroy ( ) ;
@@ -477,7 +491,7 @@ export class GameScene extends Phaser.Scene {
477491 }
478492
479493 private gatherStepInput ( ) : InputState {
480- if ( ! this . player . inControl ) {
494+ if ( ! this . player . inControl || this . unpauseRecovery . blocksControl ) {
481495 this . controls . clearTransientState ( ) ;
482496 return EMPTY_INPUT ;
483497 }
@@ -500,6 +514,9 @@ export class GameScene extends Phaser.Scene {
500514 this . afterPauseMenuInteraction ( ) ;
501515 return ;
502516 }
517+ if ( this . unpauseRecovery . blocksControl ) {
518+ return ;
519+ }
503520 this . confirmBufferedFrames = 2 ;
504521 if ( this . deathRespawnSequence !== null ) {
505522 this . requestDeathRespawnSkip ( ) ;
@@ -522,6 +539,9 @@ export class GameScene extends Phaser.Scene {
522539 this . afterPauseMenuInteraction ( ) ;
523540 return ;
524541 }
542+ if ( this . unpauseRecovery . blocksControl ) {
543+ return ;
544+ }
525545 if ( ! this . player . inControl ) {
526546 return ;
527547 }
@@ -535,6 +555,9 @@ export class GameScene extends Phaser.Scene {
535555 this . afterPauseMenuInteraction ( ) ;
536556 return ;
537557 }
558+ if ( this . unpauseRecovery . active ) {
559+ return ;
560+ }
538561
539562 this . openPauseMenu ( ) ;
540563 }
@@ -1313,6 +1336,7 @@ export class GameScene extends Phaser.Scene {
13131336 }
13141337
13151338 private openPauseMenu ( ) : void {
1339+ this . unpauseRecovery . clear ( ) ;
13161340 this . controls . clearTransientState ( ) ;
13171341 this . stopScreenShake ( ) ;
13181342 this . refillEmitter . pause ( ) ;
@@ -1325,20 +1349,20 @@ export class GameScene extends Phaser.Scene {
13251349 kind : "action" ,
13261350 title : "PAUSED" ,
13271351 selectedIndex : 0 ,
1328- onCancel : ( controller ) => {
1329- controller . close ( ) ;
1352+ onCancel : ( ) => {
1353+ this . resumeFromPauseMenu ( ) ;
13301354 } ,
13311355 items : [
13321356 {
13331357 label : "Resume" ,
1334- activate : ( controller ) => {
1335- controller . close ( ) ;
1358+ activate : ( ) => {
1359+ this . resumeFromPauseMenu ( ) ;
13361360 } ,
13371361 } ,
13381362 {
13391363 label : "Retry" ,
1340- activate : ( controller ) => {
1341- controller . close ( ) ;
1364+ activate : ( ) => {
1365+ this . closePauseMenuImmediately ( ) ;
13421366 this . retryFromPause ( ) ;
13431367 } ,
13441368 } ,
@@ -1397,8 +1421,9 @@ export class GameScene extends Phaser.Scene {
13971421 private afterPauseMenuInteraction ( ) : void {
13981422 this . controls . clearTransientState ( ) ;
13991423 if ( ! this . pauseMenu . isOpen ) {
1400- this . refillEmitter . resume ( ) ;
1401- this . playerView . resumeEffects ( ) ;
1424+ if ( ! this . unpauseRecovery . active ) {
1425+ this . resumePauseManagedEffects ( ) ;
1426+ }
14021427 this . pauseOverlay . hide ( ) ;
14031428 }
14041429 }
@@ -1414,4 +1439,69 @@ export class GameScene extends Phaser.Scene {
14141439 private stopScreenShake ( ) : void {
14151440 this . cameras . main . resetFX ( ) ;
14161441 }
1442+
1443+ private renderPassiveFrame ( ) : void {
1444+ const snapshot = this . player . getSnapshot ( ) ;
1445+ this . playerView . render ( snapshot ) ;
1446+ this . renderLighting ( snapshot ) ;
1447+ this . updateHUD ( snapshot , [ ] ) ;
1448+ if ( this . deathRespawnSequence !== null ) {
1449+ this . renderSpawnWipe ( ) ;
1450+ } else {
1451+ this . clearSpawnWipe ( ) ;
1452+ }
1453+ }
1454+
1455+ private advanceUnpauseRecovery ( rawFrameDt : number ) : boolean {
1456+ this . accumulator = addFloat ( this . accumulator , rawFrameDt ) ;
1457+
1458+ while ( this . unpauseRecovery . active && this . accumulator >= this . fixedDt ) {
1459+ const result = this . unpauseRecovery . step ( this . currentUnpauseRecoveryHeldState ( ) ) ;
1460+
1461+ if ( result . openPause ) {
1462+ this . accumulator = 0 ;
1463+ this . openPauseMenu ( ) ;
1464+ return true ;
1465+ }
1466+
1467+ if ( result . blockGameplay ) {
1468+ this . accumulator = subFloat ( this . accumulator , this . fixedDt ) ;
1469+ continue ;
1470+ }
1471+
1472+ if ( result . queueJump ) {
1473+ this . controls . queuePress ( "jump" ) ;
1474+ }
1475+ if ( result . queueDash ) {
1476+ this . controls . queuePress ( "dash" ) ;
1477+ }
1478+ this . resumePauseManagedEffects ( ) ;
1479+ return false ;
1480+ }
1481+
1482+ return this . unpauseRecovery . active ;
1483+ }
1484+
1485+ private currentUnpauseRecoveryHeldState ( ) : { pause : boolean ; jump : boolean ; dash : boolean } {
1486+ return {
1487+ pause : this . keys . pause . isDown ,
1488+ jump : this . keys . jump . isDown ,
1489+ dash : this . keys . dash . isDown ,
1490+ } ;
1491+ }
1492+
1493+ private resumeFromPauseMenu ( ) : void {
1494+ this . pauseMenu . close ( ) ;
1495+ this . unpauseRecovery . start ( this . currentUnpauseRecoveryHeldState ( ) ) ;
1496+ }
1497+
1498+ private closePauseMenuImmediately ( ) : void {
1499+ this . pauseMenu . close ( ) ;
1500+ this . unpauseRecovery . clear ( ) ;
1501+ }
1502+
1503+ private resumePauseManagedEffects ( ) : void {
1504+ this . refillEmitter . resume ( ) ;
1505+ this . playerView . resumeEffects ( ) ;
1506+ }
14171507}
0 commit comments