diff --git a/app/games/fast-piggie/index.js b/app/games/fast-piggie/index.js index 06fed90..f4dcbd2 100644 --- a/app/games/fast-piggie/index.js +++ b/app/games/fast-piggie/index.js @@ -191,6 +191,11 @@ let _feedbackEl = null; let _flashEl = null; let _instructionsEl = null; let _gameAreaEl = null; +let _endPanelEl = null; +let _playAgainBtn = null; +let _returnToMenuBtn = null; +let _finalScoreEl = null; +let _finalHighScoreEl = null; // Game state let _images = null; // [commonImage, outlierImage] @@ -443,6 +448,18 @@ function _getCorrectWedgeIndex(round) { return outlierWedgeIndex; } +/** + * Show the end-game panel with the latest score summary. + * @param {number} score + * @param {number} highScore + */ +function _showEndPanel(score, highScore) { + if (_gameAreaEl) _gameAreaEl.hidden = true; + if (_endPanelEl) _endPanelEl.hidden = false; + if (_finalScoreEl) _finalScoreEl.textContent = String(score); + if (_finalHighScoreEl) _finalHighScoreEl.textContent = String(highScore); +} + /** * Resolves the round after a wedge is selected, updates state and feedback. * @param {number} wedge @@ -537,14 +554,19 @@ export default { init(container) { _instructionsEl = container.querySelector('#fp-instructions'); _gameAreaEl = container.querySelector('#fp-game-area'); + _endPanelEl = container.querySelector('#fp-end-panel'); _startBtn = container.querySelector('#fp-start-btn'); _canvas = container.querySelector('#fp-canvas'); _ctx = _canvas.getContext('2d'); _stopBtn = container.querySelector('#fp-stop-btn'); + _playAgainBtn = container.querySelector('#fp-play-again-btn'); + _returnToMenuBtn = container.querySelector('#fp-return-btn'); _scoreEl = container.querySelector('#fp-score'); _roundEl = container.querySelector('#fp-round-count'); _feedbackEl = container.querySelector('#fp-feedback'); _flashEl = container.querySelector('#fp-flash'); + _finalScoreEl = container.querySelector('#fp-final-score'); + _finalHighScoreEl = container.querySelector('#fp-final-high-score'); // Pre-load images const base = new URL('../fast-piggie/images/', import.meta.url).href; @@ -564,6 +586,15 @@ export default { _canvas.addEventListener('mouseleave', _handleMouseLeave); _canvas.addEventListener('keydown', _handleKeydown); _stopBtn.addEventListener('click', () => this.stop()); + if (_playAgainBtn) { + _playAgainBtn.addEventListener('click', () => { + this.reset(); + this.start(); + }); + } + if (_returnToMenuBtn) { + _returnToMenuBtn.addEventListener('click', () => _returnToMainMenu()); + } }, /** @@ -572,6 +603,7 @@ export default { start() { if (_instructionsEl) _instructionsEl.hidden = true; if (_gameAreaEl) _gameAreaEl.hidden = false; + if (_endPanelEl) _endPanelEl.hidden = true; game.startGame(); _updateStats(); _runRound(); @@ -590,8 +622,7 @@ export default { const result = game.stopGame(); let highScore = result.score; - let previousHigh = 0; - let sessionsPlayed = 1; + let bestStats = game.getBestStats(); // Return a promise for test compatibility return (async () => { try { @@ -617,12 +648,10 @@ export default { } catch { // If load fails, still proceed to save with defaults } - previousHigh = gameEntry.highScore; // Only update highScore if the new score is higher highScore = Math.max(gameEntry.highScore || 0, result.score); - sessionsPlayed = (gameEntry.sessionsPlayed || 0) + 1; // Get best stats from game logic - const bestStats = game.getBestStats(); + bestStats = game.getBestStats(); const updated = { ...existing, games: { @@ -630,7 +659,7 @@ export default { 'fast-piggie': { ...gameEntry, highScore, - sessionsPlayed, + sessionsPlayed: (gameEntry.sessionsPlayed || 0) + 1, lastPlayed: new Date().toISOString(), maxLevel: typeof bestStats.maxScore === 'number' @@ -660,13 +689,8 @@ export default { // Swallow all errors from progress load/save } - // Show accessible summary modal (skip in test env) - if (typeof document !== 'undefined' && - document.body && - !document.body.classList.contains('jest-testing') - ) { - _showSummaryModal(result.score, previousHigh, highScore); - } else if (_feedbackEl) { + _showEndPanel(result.score, highScore); + if (_feedbackEl) { _feedbackEl.textContent = `Game over! Final score: ${result.score} in ${result.roundsPlayed} rounds.`; } _stopBtn.hidden = true; @@ -695,78 +719,14 @@ export default { _stopBtn.hidden = false; if (_instructionsEl) _instructionsEl.hidden = false; if (_gameAreaEl) _gameAreaEl.hidden = true; + if (_endPanelEl) _endPanelEl.hidden = true; }, }; -/** - * Show an accessible summary modal with current and best score, and return button. - * @param {number} score - * @param {number} previousHigh - * @param {number} highScore - */ -function _showSummaryModal(score, previousHigh, highScore) { - // Remove any existing modal - const oldModal = document.getElementById('fp-summary-modal'); - if (oldModal) oldModal.remove(); - - // Get best stats for this session - const bestStats = game.getBestStats(); - - const modal = document.createElement('div'); - modal.id = 'fp-summary-modal'; - modal.className = 'fp-modal'; - modal.setAttribute('role', 'dialog'); - modal.setAttribute('aria-modal', 'true'); - modal.setAttribute('aria-labelledby', 'fp-summary-title'); - modal.setAttribute('tabindex', '-1'); - - modal.innerHTML = ` -
-

Game Over

-

Your score: ${score}

-

Personal best: ${highScore}

-
-

Session Bests

- - -
- `; - - // Trap focus inside modal - modal.addEventListener('keydown', (e) => { - if (e.key === 'Tab') { - const focusable = modal.querySelectorAll('button'); - if (focusable.length) { - e.preventDefault(); - focusable[0].focus(); - } - } - if (e.key === 'Escape') { - _returnToMainMenu(); - } - }); - - // Return button handler - modal.querySelector('#fp-return-btn').addEventListener('click', _returnToMainMenu); - - // Add modal to DOM and focus - document.body.appendChild(modal); - setTimeout(() => { - modal.focus(); - }, 0); -} - /** * Return to the main game selection screen, removing modal and resetting UI. */ function _returnToMainMenu() { - const modal = document.getElementById('fp-summary-modal'); - if (modal) modal.remove(); // Dispatch a custom event to notify the app shell to return to main menu if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('bsx:return-to-main-menu')); diff --git a/app/games/fast-piggie/interface.html b/app/games/fast-piggie/interface.html index 1f20d85..86283f6 100644 --- a/app/games/fast-piggie/interface.html +++ b/app/games/fast-piggie/interface.html @@ -52,4 +52,18 @@

How to Play

- \ No newline at end of file + + + diff --git a/app/games/fast-piggie/style.css b/app/games/fast-piggie/style.css index dd19e12..04b259a 100644 --- a/app/games/fast-piggie/style.css +++ b/app/games/fast-piggie/style.css @@ -5,8 +5,10 @@ align-items: center; gap: 1rem; padding: 1.5rem; - background-color: #f8f9fa; /* matches app body background */ - color: #212529; /* ~14.5:1 contrast on #f8f9fa */ + background-color: #f8f9fa; + /* matches app body background */ + color: #212529; + /* ~14.5:1 contrast on #f8f9fa */ } .fast-piggie h2 { @@ -42,7 +44,8 @@ /* Intrinsic size is 500×500; CSS keeps it responsive */ width: 100%; height: 100%; - border-radius: 50%; /* visual hint that it's a circle game */ + border-radius: 50%; + /* visual hint that it's a circle game */ background-color: #ffffff; cursor: crosshair; } @@ -74,6 +77,7 @@ background-color: #28a745; opacity: 0.55; } + 100% { background-color: #28a745; opacity: 0; @@ -85,6 +89,7 @@ background-color: #dc3545; opacity: 0.55; } + 100% { background-color: #dc3545; opacity: 0; @@ -106,8 +111,10 @@ border: none; border-radius: 4px; cursor: pointer; - background-color: #005fcc; /* primary blue */ - color: #ffffff; /* 7.3:1 contrast on #005fcc */ + background-color: #005fcc; + /* primary blue */ + color: #ffffff; + /* 7.3:1 contrast on #005fcc */ transition: background-color 0.15s ease; } @@ -126,7 +133,8 @@ } .fp-btn--secondary { - background-color: #6c757d; /* muted grey; 4.6:1 contrast on white */ + background-color: #6c757d; + /* muted grey; 4.6:1 contrast on white */ color: #ffffff; } @@ -192,3 +200,36 @@ padding: 0.65rem 1.5rem; font-size: 1.1rem; } + +/* End-game panel */ +.fp-end-panel { + max-width: 360px; + background-color: #ffffff; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 1.5rem 2rem; + text-align: center; +} + +.fp-end-panel h3 { + font-size: 1.5rem; + font-weight: 700; + margin: 0 0 1rem; +} + +.fp-end-panel p { + font-size: 1.1rem; + margin: 0 0 0.5rem; +} + +.fp-end-panel__actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + justify-content: center; + margin-top: 1rem; +} + +.fp-end-panel .fp-btn { + padding: 0.65rem 1.5rem; +} diff --git a/app/games/fast-piggie/tests/index.test.js b/app/games/fast-piggie/tests/index.test.js index 12f09c1..4ea6b4e 100644 --- a/app/games/fast-piggie/tests/index.test.js +++ b/app/games/fast-piggie/tests/index.test.js @@ -145,6 +145,15 @@ function buildContainer() { + `; return div; @@ -364,6 +373,18 @@ describe('stop()', () => { const btn = container.querySelector('#fp-stop-btn'); expect(btn.hidden).toBe(true); }); + + it('shows #fp-end-panel', async () => { + await plugin.stop(); + const endPanel = container.querySelector('#fp-end-panel'); + expect(endPanel.hidden).toBe(false); + }); + + it('writes the final score into #fp-final-score', async () => { + await plugin.stop(); + const finalScore = container.querySelector('#fp-final-score'); + expect(finalScore.textContent).toBe('3'); + }); }); // =========================================================================== @@ -414,6 +435,12 @@ describe('reset()', () => { const gameArea = container.querySelector('#fp-game-area'); expect(gameArea.hidden).toBe(true); }); + + it('hides #fp-end-panel', () => { + plugin.reset(); + const endPanel = container.querySelector('#fp-end-panel'); + expect(endPanel.hidden).toBe(true); + }); }); // ===========================================================================