diff --git a/public/assets/fx/countdown-01.mp3 b/public/assets/fx/countdown-01.mp3 new file mode 100644 index 0000000..9f7e39b Binary files /dev/null and b/public/assets/fx/countdown-01.mp3 differ diff --git a/public/assets/fx/countdown-02.mp3 b/public/assets/fx/countdown-02.mp3 new file mode 100644 index 0000000..e9239a5 Binary files /dev/null and b/public/assets/fx/countdown-02.mp3 differ diff --git a/public/assets/music/track14.mp3 b/public/assets/music/track14.mp3 new file mode 100644 index 0000000..2ce8bcc Binary files /dev/null and b/public/assets/music/track14.mp3 differ diff --git a/public/assets/music/track15.mp3 b/public/assets/music/track15.mp3 new file mode 100644 index 0000000..876ddd7 Binary files /dev/null and b/public/assets/music/track15.mp3 differ diff --git a/public/assets/music/track16.mp3 b/public/assets/music/track16.mp3 new file mode 100644 index 0000000..7060d7b Binary files /dev/null and b/public/assets/music/track16.mp3 differ diff --git a/public/data/music.json b/public/data/music.json index 519193e..aa56a3b 100644 --- a/public/data/music.json +++ b/public/data/music.json @@ -64,6 +64,21 @@ "file": "track13.mp3", "artist": "Dance All Night", "title": "Michael Jackson" + }, + { + "file": "track14.mp3", + "artist": "Growin' Up in the Heartland", + "title": "Bryan Adams" + }, + { + "file": "track15.mp3", + "artist": "Synthetic Highway", + "title": "The Midnight" + }, + { + "file": "track16.mp3", + "artist": "Surf Sundown", + "title": "Khruangbin" } ] } \ No newline at end of file diff --git a/public/src/games/boggle/BoggleGame.js b/public/src/games/boggle/BoggleGame.js index 7dfbb1e..6ff6a29 100644 --- a/public/src/games/boggle/BoggleGame.js +++ b/public/src/games/boggle/BoggleGame.js @@ -538,7 +538,7 @@ export default class BoggleGame extends Phaser.Scene { } buildControls() { - const y = ORIGIN_Y + SPAN + 150; + const y = ORIGIN_Y + SPAN + 110; const submit = new Button(this, TRAY_CX + 120, y, 'Submit Word', () => this.submitWord(), { width: 220, height: 64, fontSize: 26 }); submit.setDepth(D.ui); diff --git a/public/src/games/geniussquare/GeniusSquareGame.js b/public/src/games/geniussquare/GeniusSquareGame.js index 333c79d..f143efa 100644 --- a/public/src/games/geniussquare/GeniusSquareGame.js +++ b/public/src/games/geniussquare/GeniusSquareGame.js @@ -69,6 +69,10 @@ export default class GeniusSquareGame extends Phaser.Scene { this.skill = data.opponents?.[0]?.skill ?? 3; this.playfield = data.playfield ?? null; + this.humanWins = data.humanWins ?? 0; + this.aiWins = data.aiWins ?? 0; + this.roundNum = data.roundNum ?? 1; + this.blockers = null; this.humanBoard = null; this.aiBoard = null; @@ -113,6 +117,7 @@ export default class GeniusSquareGame extends Phaser.Scene { this._buildGridGraphics(); this._buildLabels(); this._buildPortraits(); + this._buildRoundDisplay(); this._buildTray(); this._buildHints(); @@ -250,6 +255,35 @@ export default class GeniusSquareGame extends Phaser.Scene { } } + _buildRoundDisplay() { + const cx = GAME_WIDTH / 2; + const py = 74; + + this.add.text(cx, py, `ROUND ${this.roundNum} OF 3`, { + fontFamily: '"Julius Sans One"', fontSize: '17px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.ui); + + // Human win pips (left of center) + for (let i = 0; i < 2; i++) { + const filled = i < this.humanWins; + const gfx = this.add.graphics().setDepth(D.ui); + gfx.fillStyle(filled ? 0x45d17a : 0x2a2a4a, 1); + gfx.fillCircle(700 + i * 24, py, 8); + gfx.lineStyle(2, filled ? 0x45d17a : 0x444466, 1); + gfx.strokeCircle(700 + i * 24, py, 8); + } + + // AI win pips (right of center) + for (let i = 0; i < 2; i++) { + const filled = i < this.aiWins; + const gfx = this.add.graphics().setDepth(D.ui); + gfx.fillStyle(filled ? COLORS.danger : 0x2a2a4a, 1); + gfx.fillCircle(1196 + i * 24, py, 8); + gfx.lineStyle(2, filled ? COLORS.danger : 0x444466, 1); + gfx.strokeCircle(1196 + i * 24, py, 8); + } + } + _buildTray() { const trayLeft = HCX - (SLOT_W * 1.5); // left edge of 3-column tray @@ -675,6 +709,7 @@ export default class GeniusSquareGame extends Phaser.Scene { } label.setText(steps[i]).setScale(1.6).setAlpha(1); + playSound(this, steps[i] === 'GO!' ? SFX.COUNTDOWN_GO : SFX.COUNTDOWN_TICK); this.tweens.add({ targets: label, scaleX: 1, scaleY: 1, @@ -732,59 +767,116 @@ export default class GeniusSquareGame extends Phaser.Scene { this.gameOver = true; this._stopAI(); this.opponentPortrait?.playEmotion('upset'); - playSound(this, SFX.UI_CHIME); - playSound(this, SFX.VICTORY_SHORT); - this._recordResult('win'); - this._showOverlay(true); + this._endRound(true); } _onAIWin() { if (this.gameOver) return; this.gameOver = true; + this._stopAI(); this.opponentPortrait?.playEmotion('happy'); - this._recordResult('loss'); - this._showOverlay(false); + this._endRound(false); } - _recordResult(result) { - api.post('/history/single-player', { - slug: 'geniussquare', score: result === 'win' ? 1 : 0, result, - }).catch(() => {}); + _endRound(humanWon) { + if (humanWon) this.humanWins++; + else this.aiWins++; + + const matchOver = this.humanWins >= 2 || this.aiWins >= 2; + if (matchOver) { + const playerWon = this.humanWins > this.aiWins; + playSound(this, playerWon ? SFX.VICTORY_SHORT : SFX.SCIFI_RISER); + api.post('/history/single-player', { + slug: 'geniussquare', score: playerWon ? 1 : 0, result: playerWon ? 'win' : 'loss', + }).catch(() => {}); + this._showMatchOverlay(playerWon); + } else { + playSound(this, humanWon ? SFX.CASINO_WIN : SFX.CASINO_LOSE); + this._showInterRoundOverlay(humanWon); + } } - _showOverlay(humanWon) { + _showInterRoundOverlay(humanWon) { const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2; const borderColor = humanWon ? 0x45d17a : COLORS.danger; - const headline = humanWon ? 'You Win!' : 'Opponent Wins!'; + const headline = humanWon ? 'Round Win!' : 'Round Loss!'; const headColor = humanWon ? '#45d17a' : COLORS.dangerHex; const subline = humanWon ? 'You filled the grid first!' : 'The opponent finished first.'; - // Dim background this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6) .setDepth(D.overlay).setInteractive(); - // Panel const panel = this.add.graphics().setDepth(D.overlay + 1); panel.fillStyle(0x1a1828, 0.97); - panel.fillRoundedRect(cx - 340, cy - 190, 680, 380, 18); + panel.fillRoundedRect(cx - 340, cy - 220, 680, 440, 18); panel.lineStyle(3, borderColor, 1); - panel.strokeRoundedRect(cx - 340, cy - 190, 680, 380, 18); + panel.strokeRoundedRect(cx - 340, cy - 220, 680, 440, 18); - this.add.text(cx, cy - 110, headline, { + this.add.text(cx, cy - 150, headline, { fontFamily: 'Righteous', fontSize: '68px', color: headColor, }).setOrigin(0.5).setDepth(D.overlayUI); - this.add.text(cx, cy - 26, subline, { + this.add.text(cx, cy - 70, subline, { fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.mutedHex, }).setOrigin(0.5).setDepth(D.overlayUI); - const data = { game: this.gameDef, opponents: this.opponents, playfield: this.playfield }; - new Button(this, cx - 170, cy + 100, 'Play Again', - () => this.scene.restart(data), + this.add.text(cx, cy - 12, `${this.humanWins} — ${this.aiWins}`, { + fontFamily: 'Righteous', fontSize: '56px', color: COLORS.textHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + + this.add.text(cx, cy + 50, `ROUND ${this.roundNum + 1} NEXT`, { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + + const nextData = { + game: this.gameDef, opponents: this.opponents, playfield: this.playfield, + humanWins: this.humanWins, aiWins: this.aiWins, roundNum: this.roundNum + 1, + }; + new Button(this, cx, cy + 130, 'Next Round', + () => this.scene.restart(nextData), + { width: 280, height: 58, fontSize: 22 } + ).setDepth(D.overlayUI); + } + + _showMatchOverlay(playerWon) { + const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2; + const borderColor = playerWon ? 0x45d17a : COLORS.danger; + const headline = playerWon ? 'You Win!' : 'Opponent Wins!'; + const headColor = playerWon ? '#45d17a' : COLORS.dangerHex; + const subline = playerWon ? 'You won the best of 3!' : 'The opponent won the best of 3.'; + + this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6) + .setDepth(D.overlay).setInteractive(); + + const panel = this.add.graphics().setDepth(D.overlay + 1); + panel.fillStyle(0x1a1828, 0.97); + panel.fillRoundedRect(cx - 340, cy - 220, 680, 440, 18); + panel.lineStyle(3, borderColor, 1); + panel.strokeRoundedRect(cx - 340, cy - 220, 680, 440, 18); + + this.add.text(cx, cy - 150, headline, { + fontFamily: 'Righteous', fontSize: '68px', color: headColor, + }).setOrigin(0.5).setDepth(D.overlayUI); + + this.add.text(cx, cy - 70, subline, { + fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + + this.add.text(cx, cy - 12, `${this.humanWins} — ${this.aiWins}`, { + fontFamily: 'Righteous', fontSize: '56px', color: COLORS.textHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + + this.add.text(cx, cy + 50, 'BEST OF 3', { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + + const freshData = { game: this.gameDef, opponents: this.opponents, playfield: this.playfield }; + new Button(this, cx - 170, cy + 130, 'Play Again', + () => this.scene.restart(freshData), { width: 280, height: 58, fontSize: 22 } ).setDepth(D.overlayUI); - new Button(this, cx + 170, cy + 100, 'Menu', + new Button(this, cx + 170, cy + 130, 'Menu', () => this.scene.start('GameMenu'), { variant: 'ghost', width: 280, height: 58, fontSize: 22 } ).setDepth(D.overlayUI); diff --git a/public/src/games/wordle/WordleAI.js b/public/src/games/wordle/WordleAI.js index de87e1a..0653df9 100644 --- a/public/src/games/wordle/WordleAI.js +++ b/public/src/games/wordle/WordleAI.js @@ -17,11 +17,11 @@ import { evaluateGuess } from './WordleLogic.js'; const SKILL_PROFILES = { - 1: { openerTier: 0, useFilter: true, blunder: 0.85, maxCandidates: 0, delay: [3800, 6500] }, - 2: { openerTier: 1, useFilter: true, blunder: 0.52, maxCandidates: 0, delay: [3000, 5000] }, - 3: { openerTier: 2, useFilter: true, blunder: 0.22, maxCandidates: 10, delay: [2000, 3600] }, - 4: { openerTier: 3, useFilter: true, blunder: 0.08, maxCandidates: 35, delay: [1100, 2000] }, - 5: { openerTier: 4, useFilter: true, blunder: 0.03, maxCandidates: 150, delay: [450, 950] }, + 1: { openerTier: 0, useFilter: true, blunder: 0.85, maxCandidates: 0, delay: [5500, 9000] }, + 2: { openerTier: 1, useFilter: true, blunder: 0.52, maxCandidates: 0, delay: [4500, 7000] }, + 3: { openerTier: 2, useFilter: true, blunder: 0.22, maxCandidates: 10, delay: [3200, 5500] }, + 4: { openerTier: 3, useFilter: true, blunder: 0.08, maxCandidates: 35, delay: [2000, 3500] }, + 5: { openerTier: 4, useFilter: true, blunder: 0.03, maxCandidates: 150, delay: [1200, 2200] }, }; // Opener tiers — ordered by expected information gain. diff --git a/public/src/games/wordle/WordleGame.js b/public/src/games/wordle/WordleGame.js index 23581df..3aa28f6 100644 --- a/public/src/games/wordle/WordleGame.js +++ b/public/src/games/wordle/WordleGame.js @@ -110,9 +110,8 @@ export default class WordleGame extends Phaser.Scene { this.buildStatusText(); this.buildScoreDisplay(); this.buildControls(); - this.setupInput(); - this.scheduleAITurn(); + this._startCountdown(); } // ── Build ────────────────────────────────────────────────────────────────── @@ -279,6 +278,53 @@ export default class WordleGame extends Phaser.Scene { }).setDepth(DEPTH.ui); } + // ── Countdown ───────────────────────────────────────────────────────────── + + _startCountdown() { + const steps = ['3', '2', '1', 'GO!']; + const cx = GAME_WIDTH / 2; + const cy = GAME_HEIGHT / 2; + const OVERLAY_D = DEPTH.ui + 5; + + const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.50) + .setDepth(OVERLAY_D); + + const label = this.add.text(cx, cy, '', { + fontFamily: 'Righteous', + fontSize: '240px', + color: '#f7c948', + stroke: '#b08800', + strokeThickness: 8, + }).setOrigin(0.5).setDepth(OVERLAY_D + 1).setAlpha(0); + + const runStep = (i) => { + if (i >= steps.length) { + dim.destroy(); + label.destroy(); + this.setupInput(); + this.scheduleAITurn(); + return; + } + + label.setText(steps[i]).setScale(1.6).setAlpha(1); + playSound(this, steps[i] === 'GO!' ? SFX.COUNTDOWN_GO : SFX.COUNTDOWN_TICK); + + this.tweens.add({ + targets: label, scaleX: 1, scaleY: 1, + duration: 260, ease: 'Back.easeOut', + }); + + this.time.delayedCall(steps[i] === 'GO!' ? 560 : 740, () => { + this.tweens.add({ + targets: label, alpha: 0, duration: 180, + onComplete: () => runStep(i + 1), + }); + }); + }; + + runStep(0); + } + // ── Input ────────────────────────────────────────────────────────────────── setupInput() { @@ -336,7 +382,7 @@ export default class WordleGame extends Phaser.Scene { await this.animateTileFlip(this.playerTiles[row], word, this.gs.guesses[row].evaluation); this.updateKeyboardColors(this.gs); - playSound(this, SFX.PENCIL_WRITE); + playSound(this, SFX.UI_ACTIVATE); const { over } = isGameOver(this.gs); if (over) { @@ -513,8 +559,11 @@ export default class WordleGame extends Phaser.Scene { this.time.delayedCall(700, () => { if (this.playerWins >= 3 || this.aiWins >= 3) { - this.showVictoryScreen(this.playerWins >= 3); + const playerWon = this.playerWins >= 3; + playSound(this, playerWon ? SFX.VICTORY_SHORT : SFX.SCIFI_RISER); + this.showVictoryScreen(playerWon); } else { + if (!isDraw) playSound(this, roundWon ? SFX.CASINO_WIN : SFX.CASINO_LOSE); this.showRoundResult(roundWon, isDraw); } }); diff --git a/public/src/scenes/PreloadScene.js b/public/src/scenes/PreloadScene.js index 2266690..e8d5889 100644 --- a/public/src/scenes/PreloadScene.js +++ b/public/src/scenes/PreloadScene.js @@ -125,6 +125,8 @@ export default class PreloadScene extends Phaser.Scene { this.load.audio('sfx-woosh', '/assets/fx/woosh.mp3'); this.load.spritesheet('jello-items', '/assets/images/jello-items.png', { frameWidth: 132, frameHeight: 132 }); this.load.audio('sfx-jello-monsters','/assets/fx/jello-monsters.mp3'); + this.load.audio('sfx-countdown-tick', '/assets/fx/countdown-01.mp3'); + this.load.audio('sfx-countdown-go', '/assets/fx/countdown-02.mp3'); this.load.audio('sfx-ui-pick', '/assets/fx/ui-pick.mp3'); this.load.audio('sfx-ui-activate', '/assets/fx/ui-activate.mp3'); this.load.audio('sfx-ui-flip', '/assets/fx/ui-flip.mp3'); diff --git a/public/src/ui/Sounds.js b/public/src/ui/Sounds.js index 8bc2dd8..12d6b1f 100644 --- a/public/src/ui/Sounds.js +++ b/public/src/ui/Sounds.js @@ -48,6 +48,8 @@ export const SFX = { SQUASH: 'sfx-squash', WOOSH: 'sfx-woosh', JELLO_MONSTERS: 'sfx-jello-monsters', + COUNTDOWN_TICK: 'sfx-countdown-tick', + COUNTDOWN_GO: 'sfx-countdown-go', UI_PICK: 'sfx-ui-pick', UI_ACTIVATE: 'sfx-ui-activate', UI_FLIP: 'sfx-ui-flip',