@@ -12,10 +12,20 @@ import {
1212 type RoomDirection ,
1313} from "./level" ;
1414import { PlayerControls } from "./input/PlayerControls" ;
15+ import { loadGameOptions , saveGameOptions , type GameOptions } from "./options" ;
16+ import {
17+ currentPauseOptionValue ,
18+ PauseMenuChoice ,
19+ PauseMenuController ,
20+ type PauseActionMenu ,
21+ type PauseMenuOption ,
22+ type PauseOptionsMenu ,
23+ } from "./pause/menu" ;
1524import { clampRespawnSource , type PlayerIntroType } from "./player/intro" ;
1625import { addFloat , approach , maxFloat , stepTimer , subFloat , toFloat } from "./player/math" ;
1726import { Player } from "./player/Player" ;
1827import { InputState , PlayerEffect } from "./player/types" ;
28+ import { PauseOverlay } from "./view/PauseOverlay" ;
1929import { PlayerView } from "./view/PlayerView" ;
2030import {
2131 baseTransitionDuration ,
@@ -99,6 +109,10 @@ const EMPTY_INPUT: InputState = {
99109 dashPressed : false ,
100110 grab : false ,
101111} ;
112+ const ON_OFF_CHOICES : readonly PauseMenuChoice < boolean > [ ] = [
113+ { label : "OFF" , value : false } ,
114+ { label : "ON" , value : true } ,
115+ ] ;
102116
103117export class GameScene extends Phaser . Scene {
104118 private player ! : Player ;
@@ -114,6 +128,9 @@ export class GameScene extends Phaser.Scene {
114128 private keys ! : Record < string , Phaser . Input . Keyboard . Key > ;
115129 private controls ! : PlayerControls ;
116130 private confirmBufferedFrames = 0 ;
131+ private readonly pauseMenu = new PauseMenuController ( ) ;
132+ private pauseOverlay ! : PauseOverlay ;
133+ private gameOptions : GameOptions = loadGameOptions ( ) ;
117134
118135 private accumulator = 0 ;
119136 private readonly fixedDt = toFloat ( 1 / 60 ) ;
@@ -190,6 +207,8 @@ export class GameScene extends Phaser.Scene {
190207 this . keys . jump . on ( "down" , this . onJumpDown , this ) ;
191208 this . keys . jump . on ( "up" , this . onJumpUp , this ) ;
192209 this . keys . dash . on ( "down" , this . onDashDown , this ) ;
210+ this . keys . pause = kb . addKey ( Phaser . Input . Keyboard . KeyCodes . ESC ) ;
211+ this . keys . pause . on ( "down" , this . onPauseDown , this ) ;
193212
194213 this . hudText = this . add
195214 . text ( 8 , 8 , "" , {
@@ -206,7 +225,7 @@ export class GameScene extends Phaser.Scene {
206225 . text (
207226 VIEWPORT . width - 8 ,
208227 VIEWPORT . height - 8 ,
209- "Move: arrow keys\nC jump X dash Z grab R reset" ,
228+ "Move: arrow keys\nC jump X dash Z grab R reset Esc pause " ,
210229 {
211230 fontFamily : "monospace" ,
212231 fontSize : "9px" ,
@@ -220,6 +239,7 @@ export class GameScene extends Phaser.Scene {
220239 . setDepth ( 10 )
221240 . setScrollFactor ( 0 ) ;
222241
242+ this . pauseOverlay = new PauseOverlay ( this ) ;
223243 this . spawnInitialPlayer ( ) ;
224244 const snapshot = this . player . getSnapshot ( ) ;
225245 this . playerView . render ( snapshot ) ;
@@ -233,6 +253,24 @@ export class GameScene extends Phaser.Scene {
233253 this . confirmBufferedFrames -- ;
234254 }
235255
256+ if ( this . pauseMenu . isOpen ) {
257+ this . updatePauseInput ( ) ;
258+ const snapshot = this . player . getSnapshot ( ) ;
259+ this . playerView . render ( snapshot ) ;
260+ this . renderLighting ( snapshot ) ;
261+ this . updateHUD ( snapshot , [ ] ) ;
262+ this . renderSpawnWipe ( ) ;
263+ const current = this . pauseMenu . current ;
264+ if ( current ) {
265+ this . pauseOverlay . render ( current ) ;
266+ } else {
267+ this . pauseOverlay . hide ( ) ;
268+ }
269+ return ;
270+ }
271+
272+ this . pauseOverlay . hide ( ) ;
273+
236274 if ( this . keys . restart . isDown && this . deathRespawnSequence === null && this . player . canRetry ) {
237275 this . beginNormalRespawn ( ) ;
238276 }
@@ -407,17 +445,27 @@ export class GameScene extends Phaser.Scene {
407445 addCameraImpulse ( x : number , y : number ) : void {
408446 if ( x === 0 && y === 0 ) return ;
409447 const intensity = Phaser . Math . Clamp ( Math . hypot ( x , y ) * 0.00045 , 0.0006 , 0.0018 ) ;
410- this . cameras . main . shake ( 45 , intensity ) ;
448+ this . requestScreenShake ( 45 , intensity ) ;
449+ }
450+
451+ requestScreenShake ( durationMs : number , intensity : number ) : void {
452+ if ( ! this . gameOptions . screenShakeEffects ) {
453+ return ;
454+ }
455+
456+ this . cameras . main . shake ( durationMs , intensity ) ;
411457 }
412458
413459 shutdown ( ) : void {
414460 if ( this . keys ) {
415461 this . keys . jump . off ( "down" , this . onJumpDown , this ) ;
416462 this . keys . jump . off ( "up" , this . onJumpUp , this ) ;
417463 this . keys . dash . off ( "down" , this . onDashDown , this ) ;
464+ this . keys . pause ?. off ( "down" , this . onPauseDown , this ) ;
418465 }
419466 this . controls ?. reset ( ) ;
420467 this . playerView ?. destroy ( ) ;
468+ this . pauseOverlay ?. destroy ( ) ;
421469 this . spawnWipe ?. destroy ( ) ;
422470 this . refillEmitter ?. destroy ( ) ;
423471 this . lighting ?. destroy ( ) ;
@@ -447,6 +495,11 @@ export class GameScene extends Phaser.Scene {
447495
448496 private onJumpDown ( event : KeyboardEvent ) : void {
449497 if ( event . repeat ) return ;
498+ if ( this . pauseMenu . isOpen ) {
499+ this . pauseMenu . confirm ( ) ;
500+ this . afterPauseMenuInteraction ( ) ;
501+ return ;
502+ }
450503 this . confirmBufferedFrames = 2 ;
451504 if ( this . deathRespawnSequence !== null ) {
452505 this . requestDeathRespawnSkip ( ) ;
@@ -464,12 +517,28 @@ export class GameScene extends Phaser.Scene {
464517
465518 private onDashDown ( event : KeyboardEvent ) : void {
466519 if ( event . repeat ) return ;
520+ if ( this . pauseMenu . isOpen ) {
521+ this . pauseMenu . cancel ( ) ;
522+ this . afterPauseMenuInteraction ( ) ;
523+ return ;
524+ }
467525 if ( ! this . player . inControl ) {
468526 return ;
469527 }
470528 this . controls . queuePress ( "dash" ) ;
471529 }
472530
531+ private onPauseDown ( event : KeyboardEvent ) : void {
532+ if ( event . repeat ) return ;
533+ if ( this . pauseMenu . isOpen ) {
534+ this . pauseMenu . cancel ( ) ;
535+ this . afterPauseMenuInteraction ( ) ;
536+ return ;
537+ }
538+
539+ this . openPauseMenu ( ) ;
540+ }
541+
473542 private spawnInitialPlayer ( ) : void {
474543 this . revivePlayerAtCheckpoint ( "none" ) ;
475544 }
@@ -609,7 +678,7 @@ export class GameScene extends Phaser.Scene {
609678 this . playerView . startDeath ( snapshot ) ;
610679 }
611680 if ( transition . kind === "spike" ) {
612- this . cameras . main . shake ( 120 , 0.0026 ) ;
681+ this . requestScreenShake ( 120 , 0.0026 ) ;
613682 }
614683 transition . exploded = true ;
615684 }
@@ -1167,7 +1236,7 @@ export class GameScene extends Phaser.Scene {
11671236 for ( const refill of consumed ) {
11681237 this . refillEmitter . setParticleTint ( this . refillColor ( refill . type ) ) ;
11691238 this . refillEmitter . emitParticleAt ( refill . x , refill . visualY , 7 ) ;
1170- this . cameras . main . shake ( 40 , 0.0012 ) ;
1239+ this . requestScreenShake ( 40 , 0.0012 ) ;
11711240 }
11721241
11731242 this . syncRefillViews ( ) ;
@@ -1242,4 +1311,107 @@ export class GameScene extends Phaser.Scene {
12421311
12431312 this . hudText . setText ( lines . join ( "\n" ) ) ;
12441313 }
1314+
1315+ private openPauseMenu ( ) : void {
1316+ this . controls . clearTransientState ( ) ;
1317+ this . stopScreenShake ( ) ;
1318+ this . refillEmitter . pause ( ) ;
1319+ this . playerView . pauseEffects ( ) ;
1320+ this . pauseMenu . open ( this . createPauseRootMenu ( ) ) ;
1321+ }
1322+
1323+ private createPauseRootMenu ( ) : PauseActionMenu {
1324+ return {
1325+ kind : "action" ,
1326+ title : "PAUSED" ,
1327+ selectedIndex : 0 ,
1328+ onCancel : ( controller ) => {
1329+ controller . close ( ) ;
1330+ } ,
1331+ items : [
1332+ {
1333+ label : "Resume" ,
1334+ activate : ( controller ) => {
1335+ controller . close ( ) ;
1336+ } ,
1337+ } ,
1338+ {
1339+ label : "Retry" ,
1340+ activate : ( controller ) => {
1341+ controller . close ( ) ;
1342+ this . retryFromPause ( ) ;
1343+ } ,
1344+ } ,
1345+ {
1346+ label : "Options" ,
1347+ activate : ( controller ) => {
1348+ controller . push ( this . createOptionsMenu ( ) ) ;
1349+ } ,
1350+ } ,
1351+ ] ,
1352+ } ;
1353+ }
1354+
1355+ private createOptionsMenu ( ) : PauseOptionsMenu {
1356+ const screenShakeOption : PauseMenuOption < boolean > = {
1357+ label : "Screen Shake Effects" ,
1358+ values : ON_OFF_CHOICES ,
1359+ valueIndex : this . gameOptions . screenShakeEffects ? 1 : 0 ,
1360+ } ;
1361+
1362+ const draft : PauseOptionsMenu = {
1363+ kind : "options" ,
1364+ title : "OPTIONS" ,
1365+ selectedIndex : 0 ,
1366+ onCancel : ( controller ) => {
1367+ const screenShakeEffects = currentPauseOptionValue ( screenShakeOption ) ;
1368+ this . gameOptions = saveGameOptions ( {
1369+ ...this . gameOptions ,
1370+ screenShakeEffects : screenShakeEffects ?? this . gameOptions . screenShakeEffects ,
1371+ } ) ;
1372+ if ( ! this . gameOptions . screenShakeEffects ) {
1373+ this . stopScreenShake ( ) ;
1374+ }
1375+ controller . pop ( ) ;
1376+ } ,
1377+ items : [ screenShakeOption ] ,
1378+ } ;
1379+ return draft ;
1380+ }
1381+
1382+ private updatePauseInput ( ) : void {
1383+ if ( Phaser . Input . Keyboard . JustDown ( this . keys . up ) ) {
1384+ this . pauseMenu . moveVertical ( - 1 ) ;
1385+ }
1386+ if ( Phaser . Input . Keyboard . JustDown ( this . keys . down ) ) {
1387+ this . pauseMenu . moveVertical ( 1 ) ;
1388+ }
1389+ if ( Phaser . Input . Keyboard . JustDown ( this . keys . left ) ) {
1390+ this . pauseMenu . moveHorizontal ( - 1 ) ;
1391+ }
1392+ if ( Phaser . Input . Keyboard . JustDown ( this . keys . right ) ) {
1393+ this . pauseMenu . moveHorizontal ( 1 ) ;
1394+ }
1395+ }
1396+
1397+ private afterPauseMenuInteraction ( ) : void {
1398+ this . controls . clearTransientState ( ) ;
1399+ if ( ! this . pauseMenu . isOpen ) {
1400+ this . refillEmitter . resume ( ) ;
1401+ this . playerView . resumeEffects ( ) ;
1402+ this . pauseOverlay . hide ( ) ;
1403+ }
1404+ }
1405+
1406+ private retryFromPause ( ) : void {
1407+ if ( this . deathRespawnSequence !== null || ! this . player . canRetry ) {
1408+ return ;
1409+ }
1410+
1411+ this . beginNormalRespawn ( ) ;
1412+ }
1413+
1414+ private stopScreenShake ( ) : void {
1415+ this . cameras . main . resetFX ( ) ;
1416+ }
12451417}
0 commit comments