@@ -191,6 +191,11 @@ let _feedbackEl = null;
191191let _flashEl = null ;
192192let _instructionsEl = null ;
193193let _gameAreaEl = null ;
194+ let _endPanelEl = null ;
195+ let _playAgainBtn = null ;
196+ let _returnToMenuBtn = null ;
197+ let _finalScoreEl = null ;
198+ let _finalHighScoreEl = null ;
194199
195200// Game state
196201let _images = null ; // [commonImage, outlierImage]
@@ -443,6 +448,18 @@ function _getCorrectWedgeIndex(round) {
443448 return outlierWedgeIndex ;
444449}
445450
451+ /**
452+ * Show the end-game panel with the latest score summary.
453+ * @param {number } score
454+ * @param {number } highScore
455+ */
456+ function _showEndPanel ( score , highScore ) {
457+ if ( _gameAreaEl ) _gameAreaEl . hidden = true ;
458+ if ( _endPanelEl ) _endPanelEl . hidden = false ;
459+ if ( _finalScoreEl ) _finalScoreEl . textContent = String ( score ) ;
460+ if ( _finalHighScoreEl ) _finalHighScoreEl . textContent = String ( highScore ) ;
461+ }
462+
446463/**
447464 * Resolves the round after a wedge is selected, updates state and feedback.
448465 * @param {number } wedge
@@ -537,14 +554,19 @@ export default {
537554 init ( container ) {
538555 _instructionsEl = container . querySelector ( '#fp-instructions' ) ;
539556 _gameAreaEl = container . querySelector ( '#fp-game-area' ) ;
557+ _endPanelEl = container . querySelector ( '#fp-end-panel' ) ;
540558 _startBtn = container . querySelector ( '#fp-start-btn' ) ;
541559 _canvas = container . querySelector ( '#fp-canvas' ) ;
542560 _ctx = _canvas . getContext ( '2d' ) ;
543561 _stopBtn = container . querySelector ( '#fp-stop-btn' ) ;
562+ _playAgainBtn = container . querySelector ( '#fp-play-again-btn' ) ;
563+ _returnToMenuBtn = container . querySelector ( '#fp-return-btn' ) ;
544564 _scoreEl = container . querySelector ( '#fp-score' ) ;
545565 _roundEl = container . querySelector ( '#fp-round-count' ) ;
546566 _feedbackEl = container . querySelector ( '#fp-feedback' ) ;
547567 _flashEl = container . querySelector ( '#fp-flash' ) ;
568+ _finalScoreEl = container . querySelector ( '#fp-final-score' ) ;
569+ _finalHighScoreEl = container . querySelector ( '#fp-final-high-score' ) ;
548570
549571 // Pre-load images
550572 const base = new URL ( '../fast-piggie/images/' , import . meta. url ) . href ;
@@ -564,6 +586,15 @@ export default {
564586 _canvas . addEventListener ( 'mouseleave' , _handleMouseLeave ) ;
565587 _canvas . addEventListener ( 'keydown' , _handleKeydown ) ;
566588 _stopBtn . addEventListener ( 'click' , ( ) => this . stop ( ) ) ;
589+ if ( _playAgainBtn ) {
590+ _playAgainBtn . addEventListener ( 'click' , ( ) => {
591+ this . reset ( ) ;
592+ this . start ( ) ;
593+ } ) ;
594+ }
595+ if ( _returnToMenuBtn ) {
596+ _returnToMenuBtn . addEventListener ( 'click' , ( ) => _returnToMainMenu ( ) ) ;
597+ }
567598 } ,
568599
569600 /**
@@ -572,6 +603,7 @@ export default {
572603 start ( ) {
573604 if ( _instructionsEl ) _instructionsEl . hidden = true ;
574605 if ( _gameAreaEl ) _gameAreaEl . hidden = false ;
606+ if ( _endPanelEl ) _endPanelEl . hidden = true ;
575607 game . startGame ( ) ;
576608 _updateStats ( ) ;
577609 _runRound ( ) ;
@@ -590,8 +622,7 @@ export default {
590622 const result = game . stopGame ( ) ;
591623
592624 let highScore = result . score ;
593- let previousHigh = 0 ;
594- let sessionsPlayed = 1 ;
625+ let bestStats = game . getBestStats ( ) ;
595626 // Return a promise for test compatibility
596627 return ( async ( ) => {
597628 try {
@@ -617,20 +648,18 @@ export default {
617648 } catch {
618649 // If load fails, still proceed to save with defaults
619650 }
620- previousHigh = gameEntry . highScore ;
621651 // Only update highScore if the new score is higher
622652 highScore = Math . max ( gameEntry . highScore || 0 , result . score ) ;
623- sessionsPlayed = ( gameEntry . sessionsPlayed || 0 ) + 1 ;
624653 // Get best stats from game logic
625- const bestStats = game . getBestStats ( ) ;
654+ bestStats = game . getBestStats ( ) ;
626655 const updated = {
627656 ...existing ,
628657 games : {
629658 ...existing . games ,
630659 'fast-piggie' : {
631660 ...gameEntry ,
632661 highScore,
633- sessionsPlayed,
662+ sessionsPlayed : ( gameEntry . sessionsPlayed || 0 ) + 1 ,
634663 lastPlayed : new Date ( ) . toISOString ( ) ,
635664 maxLevel :
636665 typeof bestStats . maxScore === 'number'
@@ -660,13 +689,8 @@ export default {
660689 // Swallow all errors from progress load/save
661690 }
662691
663- // Show accessible summary modal (skip in test env)
664- if ( typeof document !== 'undefined' &&
665- document . body &&
666- ! document . body . classList . contains ( 'jest-testing' )
667- ) {
668- _showSummaryModal ( result . score , previousHigh , highScore ) ;
669- } else if ( _feedbackEl ) {
692+ _showEndPanel ( result . score , highScore ) ;
693+ if ( _feedbackEl ) {
670694 _feedbackEl . textContent = `Game over! Final score: ${ result . score } in ${ result . roundsPlayed } rounds.` ;
671695 }
672696 _stopBtn . hidden = true ;
@@ -695,78 +719,14 @@ export default {
695719 _stopBtn . hidden = false ;
696720 if ( _instructionsEl ) _instructionsEl . hidden = false ;
697721 if ( _gameAreaEl ) _gameAreaEl . hidden = true ;
722+ if ( _endPanelEl ) _endPanelEl . hidden = true ;
698723 } ,
699724} ;
700725
701- /**
702- * Show an accessible summary modal with current and best score, and return button.
703- * @param {number } score
704- * @param {number } previousHigh
705- * @param {number } highScore
706- */
707- function _showSummaryModal ( score , previousHigh , highScore ) {
708- // Remove any existing modal
709- const oldModal = document . getElementById ( 'fp-summary-modal' ) ;
710- if ( oldModal ) oldModal . remove ( ) ;
711-
712- // Get best stats for this session
713- const bestStats = game . getBestStats ( ) ;
714-
715- const modal = document . createElement ( 'div' ) ;
716- modal . id = 'fp-summary-modal' ;
717- modal . className = 'fp-modal' ;
718- modal . setAttribute ( 'role' , 'dialog' ) ;
719- modal . setAttribute ( 'aria-modal' , 'true' ) ;
720- modal . setAttribute ( 'aria-labelledby' , 'fp-summary-title' ) ;
721- modal . setAttribute ( 'tabindex' , '-1' ) ;
722-
723- modal . innerHTML = `
724- <div class="fp-modal-content">
725- <h2 id="fp-summary-title">Game Over</h2>
726- <p>Your score: <strong>${ score } </strong></p>
727- <p>Personal best: <strong>${ highScore } </strong></p>
728- <hr />
729- <h3>Session Bests</h3>
730- <ul>
731- <li>Max score: <strong>${ bestStats . maxScore } </strong></li>
732- <li>Most rounds: <strong>${ bestStats . mostRounds } </strong></li>
733- <li>Most guinea pigs in a round: <strong>${ bestStats . mostGuineaPigs } </strong></li>
734- <li>Top speed (ms): <strong>${ bestStats . topSpeedMs !== null ? bestStats . topSpeedMs : '—' } </strong></li>
735- </ul>
736- <button id="fp-return-btn" class="fp-btn fp-btn--primary">Return to Main Menu</button>
737- </div>
738- ` ;
739-
740- // Trap focus inside modal
741- modal . addEventListener ( 'keydown' , ( e ) => {
742- if ( e . key === 'Tab' ) {
743- const focusable = modal . querySelectorAll ( 'button' ) ;
744- if ( focusable . length ) {
745- e . preventDefault ( ) ;
746- focusable [ 0 ] . focus ( ) ;
747- }
748- }
749- if ( e . key === 'Escape' ) {
750- _returnToMainMenu ( ) ;
751- }
752- } ) ;
753-
754- // Return button handler
755- modal . querySelector ( '#fp-return-btn' ) . addEventListener ( 'click' , _returnToMainMenu ) ;
756-
757- // Add modal to DOM and focus
758- document . body . appendChild ( modal ) ;
759- setTimeout ( ( ) => {
760- modal . focus ( ) ;
761- } , 0 ) ;
762- }
763-
764726/**
765727 * Return to the main game selection screen, removing modal and resetting UI.
766728 */
767729function _returnToMainMenu ( ) {
768- const modal = document . getElementById ( 'fp-summary-modal' ) ;
769- if ( modal ) modal . remove ( ) ;
770730 // Dispatch a custom event to notify the app shell to return to main menu
771731 if ( typeof window !== 'undefined' ) {
772732 window . dispatchEvent ( new CustomEvent ( 'bsx:return-to-main-menu' ) ) ;
0 commit comments