diff --git a/public/assets/fx/casino-lose.mp3 b/public/assets/fx/casino-lose.mp3 new file mode 100644 index 0000000..1b69267 Binary files /dev/null and b/public/assets/fx/casino-lose.mp3 differ diff --git a/public/assets/fx/pencil-write.mp3 b/public/assets/fx/pencil-write.mp3 new file mode 100644 index 0000000..1ea1eee Binary files /dev/null and b/public/assets/fx/pencil-write.mp3 differ diff --git a/public/assets/fx/piece-click.mp3 b/public/assets/fx/piece-click.mp3 new file mode 100644 index 0000000..3869088 Binary files /dev/null and b/public/assets/fx/piece-click.mp3 differ diff --git a/public/assets/images/background-casino.png b/public/assets/images/background-casino.png new file mode 100644 index 0000000..85921cb Binary files /dev/null and b/public/assets/images/background-casino.png differ diff --git a/public/assets/images/background-room.png b/public/assets/images/background-room.png new file mode 100644 index 0000000..4fea47d Binary files /dev/null and b/public/assets/images/background-room.png differ diff --git a/public/src/games/backgammon/BackgammonGame.js b/public/src/games/backgammon/BackgammonGame.js index 5e7307a..7a58043 100644 --- a/public/src/games/backgammon/BackgammonGame.js +++ b/public/src/games/backgammon/BackgammonGame.js @@ -8,6 +8,7 @@ import { applyMove, endTurn, hasAnyMove, computePipCount, } from './BackgammonLogic.js'; import { chooseMoves } from './BackgammonAI.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; // ── Layout constants ────────────────────────────────────────────────────────── const BX = 300; // board outer left @@ -656,6 +657,7 @@ export default class BackgammonGame extends Phaser.Scene { } animateDiceRoll(finalValues, onComplete) { + playSound(this, SFX.DICE_ROLL); this.diceContainers.forEach((c) => c.setAlpha(1)); let elapsed = 0; const totalMs = 700; diff --git a/public/src/games/blackjack/BlackjackGame.js b/public/src/games/blackjack/BlackjackGame.js index a78ef07..7cdcd5f 100644 --- a/public/src/games/blackjack/BlackjackGame.js +++ b/public/src/games/blackjack/BlackjackGame.js @@ -4,6 +4,7 @@ import { Button } from '../../ui/Button.js'; import { Modal } from '../../ui/Modal.js'; import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js'; import { api } from '../../services/api.js'; +import { playSound, playChipBet, SFX } from '../../ui/Sounds.js'; import { buildShoe, handValue, isBlackjack, isBust, canDouble, canSplit, createInitialState, prepareRound, applyBet, dealAllCards, @@ -392,6 +393,7 @@ export default class BlackjackGame extends Phaser.Scene { startNewRound() { if (this.shoe.length < 52) this.shoe = buildShoe(); + playSound(this, SFX.CARD_SHUFFLE); this.pendingBet = 0; this.bustedSeats = new Set(); this.gs = prepareRound(this.gs); @@ -656,6 +658,7 @@ export default class BlackjackGame extends Phaser.Scene { // ── Animations ──────────────────────────────────────────────────────────── animateBets(onComplete) { this.renderBetAreas(); + playChipBet(this); let done = 0; const active = this.gs.players.filter(p => p.active); if (active.length === 0) { onComplete(); return; } @@ -724,6 +727,7 @@ export default class BlackjackGame extends Phaser.Scene { targets: flying, x: tx, y: ty, duration: 180, ease: 'Power2', onComplete: () => { flying.destroy(); + playSound(this, SFX.CARD_DEAL); // Reveal the card at destination if (entry.type === 'dealer') { const cont = this.dealerCardGraphics[entry.cardIdx]; @@ -742,6 +746,7 @@ export default class BlackjackGame extends Phaser.Scene { animateDealerReveal(onComplete) { const holeCard = this.dealerCardGraphics[1]; if (!holeCard) { this.gs.dealer.revealed = true; this.renderAll(); onComplete(); return; } + playSound(this, SFX.CARD_SHOW); // Flip: scale X 1→0, redraw face-up, scale X 0→1 this.tweens.add({ @@ -778,6 +783,7 @@ export default class BlackjackGame extends Phaser.Scene { targets: flying, x: tx, y: ty, duration: 200, ease: 'Power2', onComplete: () => { flying.destroy(); + playSound(this, SFX.CARD_PLACE); // Reveal the card face-up at the destination const cardConts = this.cardGraphics[seat] ?? []; const targetCont = cardConts[cardIdx]; @@ -800,6 +806,10 @@ export default class BlackjackGame extends Phaser.Scene { const isWin = result === 'win' || result === 'blackjack'; const isLose = result === 'lose' || result === 'bust'; + if (result === 'blackjack') playSound(this, SFX.CASINO_BLACKJACK); + else if (result === 'win') playSound(this, SFX.CASINO_WIN); + else if (result === 'lose' || result === 'bust') playSound(this, SFX.CASINO_LOSE); + if (seat > 0 && this.portraits[seat]) { this.portraits[seat].playEmotion?.(isWin ? 'happy' : 'upset'); } diff --git a/public/src/games/holdem/HoldemGame.js b/public/src/games/holdem/HoldemGame.js index c0e3660..c3957ef 100644 --- a/public/src/games/holdem/HoldemGame.js +++ b/public/src/games/holdem/HoldemGame.js @@ -10,6 +10,7 @@ import { } from './HoldemLogic.js'; import { chooseAction } from './HoldemAI.js'; import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js'; +import { playSound, playChipBet, SFX } from '../../ui/Sounds.js'; // ── Layout constants ────────────────────────────────────────────────────────── const CX = GAME_WIDTH / 2; // 960 @@ -416,6 +417,7 @@ export default class HoldemGame extends Phaser.Scene { } this.foldedCardDisplays = {}; this.gs = startHand(this.gs); + playSound(this, SFX.CARD_SHUFFLE); if (this.gs.phase === 'game_over') { this.showGameOver(); @@ -538,6 +540,7 @@ export default class HoldemGame extends Phaser.Scene { } animateFold(seat, onComplete) { + playSound(this, SFX.CARD_PLACE); const sc = this.seatContainers[seat]; const { x: sx, y: sy } = SEAT_POS[seat]; const { x: tx, y: ty } = this.betZone(seat); @@ -588,6 +591,7 @@ export default class HoldemGame extends Phaser.Scene { const { x: sx, y: sy } = SEAT_POS[seat]; const { x: tx, y: ty } = this.betZone(seat); + playChipBet(this); const CHIP_COLORS = [0xf0e8d0, 0x4a90d9, 0xe05c5c, 0x5cb85c, 0xf0a830]; const allObjs = []; let done = 0; @@ -788,6 +792,7 @@ export default class HoldemGame extends Phaser.Scene { ease: 'Power2.easeOut', onComplete: () => { cardCont.destroy(); + playSound(this, SFX.CARD_DEAL); done++; if (done === sequence.length) onComplete(); }, @@ -841,6 +846,7 @@ export default class HoldemGame extends Phaser.Scene { this.renderCard(slot.cardObj, card, true); } slot.cardObj.setVisible(true).setScale(0, 1); + playSound(this, SFX.CARD_SHOW); this.tweens.add({ targets: slot.cardObj, @@ -1031,6 +1037,7 @@ export default class HoldemGame extends Phaser.Scene { // Compute winnings (resolveShowdown handles the fold-win case correctly) const winnings = resolveShowdown(players, community); + if ((winnings.get(0) ?? 0) > 0) playSound(this, SFX.CASINO_WIN); // Hide game-world portraits so their DOM videos don't bleed through the overlay for (const p of this.portraits) p.hide?.(); @@ -1082,7 +1089,7 @@ export default class HoldemGame extends Phaser.Scene { const continueBtn = new Button(this, CX, 1042, 'Continue to Next Hand', () => { this.cleanupSummary(); this.dealNextHand(); - }, { width: 320 }).setDepth(D.modal + 2).setAlpha(0); + }, { width: 420, fontSize: 24 }).setDepth(D.modal + 2).setAlpha(0); S.push(continueBtn); this.tweens.add({ targets: continueBtn, alpha: 1, duration: 300, delay: 1100 }); } diff --git a/public/src/games/parchisi/ParchisiGame.js b/public/src/games/parchisi/ParchisiGame.js index 99ab079..7404064 100644 --- a/public/src/games/parchisi/ParchisiGame.js +++ b/public/src/games/parchisi/ParchisiGame.js @@ -9,6 +9,7 @@ import { COLORS as PCOLORS, ENTRY, HOME_ENTRY, HOME_COL_LEN, } from './ParchisiLogic.js'; import { chooseMoves } from './ParchisiAI.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; // ── Layout constants ──────────────────────────────────────────────────────── const CELL = 50; @@ -327,6 +328,7 @@ export default class ParchisiGame extends Phaser.Scene { } animateDiceRoll(finalValues, onComplete) { + playSound(this, SFX.DICE_ROLL); this.diceContainers.forEach((c) => c.setAlpha(1)); let elapsed = 0; const totalMs = 650; @@ -881,6 +883,7 @@ export default class ParchisiGame extends Phaser.Scene { // ── Move execution + animation ──────────────────────────────────────────── executeMove(move, onComplete) { + playSound(this, SFX.PIECE_CLICK); const obj = this.pawnObjs[move.player][move.pawnIdx]; const from = this.locWorld(move.from, move.player); const to = this.locWorld(move.to, move.player); diff --git a/public/src/games/phase10/Phase10Game.js b/public/src/games/phase10/Phase10Game.js index 9a062cd..8913acc 100644 --- a/public/src/games/phase10/Phase10Game.js +++ b/public/src/games/phase10/Phase10Game.js @@ -4,6 +4,7 @@ import { Button } from '../../ui/Button.js'; import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js'; import { auth } from '../../services/auth.js'; import { api } from '../../services/api.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; import { PHASES, getPhase } from './PhaseSpec.js'; import { applyDrawFromDeck, @@ -433,6 +434,7 @@ export default class Phase10Game extends Phaser.Scene { const playerCount = this.slotForSeat.length; this.gs = createInitialState({ playerCount }); + playSound(this, SFX.CARD_SHUFFLE); this.renderAll(); this.setStatus('Your turn — draw from deck or discard.'); this.maybeStartAITurn(); @@ -855,6 +857,7 @@ export default class Phase10Game extends Phaser.Scene { } const next = applyDiscard(this.gs, handIdx); if (next === this.gs) { this.setStatus("Can't discard right now."); return; } + playSound(this, SFX.CARD_PLACE); this.gs = next; this.renderAll(); this.afterTurnTransition(); @@ -914,6 +917,7 @@ export default class Phase10Game extends Phaser.Scene { this.clearHighlights(); const next = applyHit(this.gs, handIdx, target.targetSeat, target.groupIdx, target.position); if (next === this.gs) { this.setStatus("Can't hit there."); return; } + playSound(this, SFX.CARD_PLACE); this.gs = next; this.renderAll(); if (this.gs.roundPhase === 'roundOver' || this.gs.roundPhase === 'gameOver') { @@ -1047,6 +1051,7 @@ export default class Phase10Game extends Phaser.Scene { for (const o of items) o.destroy(); btn.destroy(); this.gs = startNextRound(this.gs); + playSound(this, SFX.CARD_SHUFFLE); this.renderAll(); this.maybeStartAITurn(); }, { width: 240, height: 48, fontSize: 20 }).setDepth(D.modal); diff --git a/public/src/games/skipbo/SkipBoGame.js b/public/src/games/skipbo/SkipBoGame.js index 9014aa0..f281128 100644 --- a/public/src/games/skipbo/SkipBoGame.js +++ b/public/src/games/skipbo/SkipBoGame.js @@ -4,6 +4,7 @@ import { Button } from '../../ui/Button.js'; import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js'; import { auth } from '../../services/auth.js'; import { api } from '../../services/api.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; import { BUILD_PILE_COUNT, DISCARD_PILE_COUNT, @@ -312,6 +313,7 @@ export default class SkipBoGame extends Phaser.Scene { const playerCount = this.slotForSeat.length; this.gs = createInitialState({ playerCount }); + playSound(this, SFX.CARD_SHUFFLE); this.renderAll(); this.setStatus('Your turn'); @@ -706,6 +708,7 @@ export default class SkipBoGame extends Phaser.Scene { const target = this.cardObjs.get(fromKey); const dest = buildPilePos(action.buildIdx); if (!target) { done(); return; } + playSound(this, SFX.CARD_PLACE); this.tweens.add({ targets: target, x: dest.x, y: dest.y, scale: 1, @@ -721,6 +724,7 @@ export default class SkipBoGame extends Phaser.Scene { const layout = slotLayout(this.slotForSeat[before.currentPlayer]); const dest = layout.discards[discardIdx]; if (!target) { done(); return; } + playSound(this, SFX.CARD_PLACE); this.tweens.add({ targets: target, x: dest.x, y: dest.y, duration: 240, ease: 'Cubic.easeOut', diff --git a/public/src/games/yatzi/YatziGame.js b/public/src/games/yatzi/YatziGame.js index e8e0aed..6d67357 100644 --- a/public/src/games/yatzi/YatziGame.js +++ b/public/src/games/yatzi/YatziGame.js @@ -11,6 +11,7 @@ import { getWinners, } from './YatziLogic.js'; import { chooseDiceToHold, chooseCategory, shouldKeepRolling } from './YatziAI.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; // ─── Layout ─────────────────────────────────────────────────────────────── const LEFT_CX = 480; @@ -361,6 +362,7 @@ export default class YatziGame extends Phaser.Scene { } async animateRoll(targetDice) { + playSound(this, SFX.DICE_ROLL); this.animating = true; return new Promise((resolve) => { this.tweens.addCounter({ @@ -507,6 +509,7 @@ export default class YatziGame extends Phaser.Scene { const before = this.gs; const after = commitScore(before, cat); if (after === before) return; + playSound(this, SFX.PENCIL_WRITE); this.gs = after; if (after.lastBonusGained) this.showBonusToast(); this.cellHoverGfx?.clear(); diff --git a/public/src/scenes/OpponentSelectScene.js b/public/src/scenes/OpponentSelectScene.js index c43f19c..319abd5 100644 --- a/public/src/scenes/OpponentSelectScene.js +++ b/public/src/scenes/OpponentSelectScene.js @@ -34,17 +34,25 @@ export default class OpponentSelectScene extends Phaser.Scene { async create() { const cx = GAME_WIDTH / 2; - this.add.text(cx, 60, this.gameDef.name, { + const bgKey = this.gameDef.category === 'casino' ? 'bg-casino' : 'bg-room'; + this.add.image(cx, GAME_HEIGHT / 2, bgKey).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(-1); + this.add.rectangle(cx, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.45).setDepth(-1); + + const titleText = this.add.text(cx, 60, this.gameDef.name, { fontFamily: 'Righteous', fontSize: '52px', color: COLORS.textHex, }).setOrigin(0.5); + const titlePill = this.add.rectangle(cx, 60, titleText.width + 48, titleText.height + 20, 0x000000, 0.72); + this.children.moveBelow(titlePill, titleText); - this.add.text(cx, 122, 'Choose your opponent', { + const subtitleText = this.add.text(cx, 122, 'Choose your opponent', { fontFamily: 'Righteous', fontSize: '36px', color: COLORS.mutedHex, }).setOrigin(0.5); + const subtitlePill = this.add.rectangle(cx, 122, subtitleText.width + 48, subtitleText.height + 20, 0x000000, 0.72); + this.children.moveBelow(subtitlePill, subtitleText); let opponents = []; try { @@ -59,20 +67,29 @@ export default class OpponentSelectScene extends Phaser.Scene { } const min = this.gameDef.minOpponents ?? 1; - this.startBtn = new Button(this, cx, 1048, 'Start Game', () => this.startGame(), { width: 280 }); - this.startBtn.setEnabled(min === 0); - - new Button(this, cx, 978, 'Back', () => this.scene.start('GameMenu'), { + new Button(this, cx - 150, 1013, 'Back', () => this.scene.start('GameMenu'), { variant: 'ghost', width: 280, }); + this.startBtn = new Button(this, cx + 150, 1013, 'Start Game', () => this.startGame(), { + width: 280, + bg: COLORS.accent, + bgHover: COLORS.gold, + textColor: COLORS.textDarkHex, + textHoverColor: COLORS.textDarkHex, + }); + this.startBtn.setEnabled(min === 0); + for (let i = opponents.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [opponents[i], opponents[j]] = [opponents[j], opponents[i]]; } this.buildOpponentGrid(opponents); + const max = this.gameDef.maxOpponents ?? 1; + this.cards.slice(0, max).forEach(({ opp, el }) => this.toggleOpponent(opp, el)); + this.buildOptionSection('Playfield', 630, this.cache.json.get('playfields')?.playfields ?? [], 'selectedPlayfield', 'playfieldTiles', (pf) => this.selectPlayfield(pf)); @@ -82,12 +99,16 @@ export default class OpponentSelectScene extends Phaser.Scene { CARD_TILE_W, CARD_TILE_H, CARD_TILE_GAP); } - // Apply defaults + // Apply playfield default; card back is chosen randomly const pfd = this.cache.json.get('playfields') ?? {}; this.applyDefault('playfields', pfd.default, 'selectedPlayfield', 'playfieldTiles'); if (this.gameDef.cardGame) { - const cbd = this.cache.json.get('card-backs') ?? {}; - this.applyDefault('card-backs', cbd.default, 'selectedCardBack', 'cardBackTiles'); + const cardBacks = this.cache.json.get('card-backs')?.cardBacks ?? []; + if (cardBacks.length > 0) { + const randomCb = cardBacks[Math.floor(Math.random() * cardBacks.length)]; + this.selectedCardBack = randomCb; + this.highlightTile('cardBackTiles', randomCb.id); + } } } @@ -153,7 +174,7 @@ export default class OpponentSelectScene extends Phaser.Scene { 'cursor:pointer', 'box-sizing:border-box', 'user-select:none', - 'transition:border-color 0.12s', + 'transition:border 0.12s,background 0.15s,box-shadow 0.18s,color 0.12s', ].join(';'); // Portrait wrapper — fixed size so canvas/video can be stacked inside @@ -241,6 +262,7 @@ export default class OpponentSelectScene extends Phaser.Scene { info.appendChild(bio); el.appendChild(portraitWrap); el.appendChild(info); + el._nameEl = name; el.addEventListener('click', () => this.toggleOpponent(opp, el)); @@ -254,7 +276,7 @@ export default class OpponentSelectScene extends Phaser.Scene { if (this.selected.has(opp.id)) { this.selected.delete(opp.id); - el.style.borderColor = COLORS.mutedHex; + this.applyOpponentStyle(el, false); const card = this.cards.find((c) => c.opp.id === opp.id); if (card) { card.video.pause(); @@ -268,14 +290,14 @@ export default class OpponentSelectScene extends Phaser.Scene { this.selected.delete(oldestId); const oldCard = this.cards.find((c) => c.opp.id === oldestId); if (oldCard) { - oldCard.el.style.borderColor = COLORS.mutedHex; + this.applyOpponentStyle(oldCard.el, false); oldCard.video.pause(); oldCard.video.style.display = 'none'; oldCard.canvas.style.display = 'block'; } } this.selected.add(opp.id); - el.style.borderColor = COLORS.accentHex; + this.applyOpponentStyle(el, true); const card = this.cards.find((c) => c.opp.id === opp.id); if (card) { card.canvas.style.display = 'none'; @@ -293,11 +315,13 @@ export default class OpponentSelectScene extends Phaser.Scene { buildOptionSection(label, labelY, items, selectedProp, tilesProp, onSelect, tileW = TILE_W, tileH = TILE_H, tileGap = TILE_GAP) { const cx = GAME_WIDTH / 2; - this.add.text(cx, labelY, label, { + const labelText = this.add.text(cx, labelY, label, { fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.mutedHex, }).setOrigin(0.5); + const labelPill = this.add.rectangle(cx, labelY, labelText.width + 40, labelText.height + 18, 0x000000, 0.72); + this.children.moveBelow(labelPill, labelText); const tileY = labelY + Math.max(82, Math.round(tileH / 2) + 18); const totalW = items.length * tileW + (items.length - 1) * tileGap; @@ -317,6 +341,8 @@ export default class OpponentSelectScene extends Phaser.Scene { .setStrokeStyle(3, isSelected ? COLORS.accent : COLORS.muted) .setInteractive({ useHandCursor: true }); + const overlay = this.add.rectangle(0, 0, tileW, tileH, COLORS.accent, 0); + const thumbW = tileW - 16; const thumbH = tileH - 30; let thumb; @@ -338,13 +364,11 @@ export default class OpponentSelectScene extends Phaser.Scene { color: COLORS.textHex, }).setOrigin(0.5); - container.add([bg, thumb, nameText]); + container.add([bg, overlay, thumb, nameText]); bg.on('pointerup', () => { this[selectedProp] = item; - for (const t of this[tilesProp]) { - t.bg.setStrokeStyle(3, t.item.id === item.id ? COLORS.accent : COLORS.muted); - } + this.highlightTile(tilesProp, item.id); onSelect(item); }); bg.on('pointerover', () => { @@ -354,7 +378,7 @@ export default class OpponentSelectScene extends Phaser.Scene { if (this[selectedProp]?.id !== item.id) bg.setStrokeStyle(3, COLORS.muted); }); - this[tilesProp].push({ item, bg }); + this[tilesProp].push({ item, bg, overlay, nameText }); } applyDefault(cacheKey, defaultId, selectedProp, tilesProp) { @@ -363,8 +387,29 @@ export default class OpponentSelectScene extends Phaser.Scene { const def = items.find((i) => i.id === defaultId) ?? items[0] ?? null; if (!def) return; this[selectedProp] = def; + this.highlightTile(tilesProp, def.id); + } + + applyOpponentStyle(el, selected) { + if (selected) { + el.style.border = `3px solid ${COLORS.accentHex}`; + el.style.background = `linear-gradient(rgba(200,168,75,0.22),rgba(200,168,75,0.22)),${COLORS.panelHex}`; + el.style.boxShadow = `0 0 0 2px rgba(200,168,75,0.22), 0 0 24px rgba(200,168,75,0.42)`; + if (el._nameEl) el._nameEl.style.color = COLORS.accentHex; + } else { + el.style.border = `2px solid ${COLORS.mutedHex}`; + el.style.background = COLORS.panelHex; + el.style.boxShadow = 'none'; + if (el._nameEl) el._nameEl.style.color = COLORS.textHex; + } + } + + highlightTile(tilesProp, selectedId) { for (const t of this[tilesProp]) { - t.bg.setStrokeStyle(3, t.item.id === def.id ? COLORS.accent : COLORS.muted); + const sel = t.item.id === selectedId; + t.overlay.setFillStyle(COLORS.accent, sel ? 0.22 : 0); + t.bg.setStrokeStyle(sel ? 4 : 3, sel ? COLORS.accent : COLORS.muted); + t.nameText.setColor(sel ? COLORS.accentHex : COLORS.textHex); } } diff --git a/public/src/scenes/PreloadScene.js b/public/src/scenes/PreloadScene.js index 385daec..0e3f8aa 100644 --- a/public/src/scenes/PreloadScene.js +++ b/public/src/scenes/PreloadScene.js @@ -31,9 +31,23 @@ export default class PreloadScene extends Phaser.Scene { frameHeight: 420, }); this.load.image('bg-menu', '/assets/images/background-menu.png'); + this.load.image('bg-room', '/assets/images/background-room.png'); + this.load.image('bg-casino', '/assets/images/background-casino.png'); this.load.image('main-title', '/assets/images/main-title.png'); this.load.json('playfields', '/data/playfields.json'); this.load.json('card-backs', '/data/card-backs.json'); + + this.load.audio('sfx-card-deal', '/assets/fx/card-deal.mp3'); + this.load.audio('sfx-card-place', '/assets/fx/card-place.mp3'); + this.load.audio('sfx-card-show', '/assets/fx/card-show.mp3'); + this.load.audio('sfx-card-shuffle', '/assets/fx/card-shuffle.mp3'); + this.load.audio('sfx-casino-blackjack', '/assets/fx/casino-blackjack.mp3'); + this.load.audio('sfx-casino-lose', '/assets/fx/casino-lose.mp3'); + this.load.audio('sfx-casino-win', '/assets/fx/casino-win.mp3'); + this.load.audio('sfx-chip-bet', '/assets/fx/chip-bet.mp3'); + this.load.audio('sfx-dice-roll', '/assets/fx/dice-roll.mp3'); + this.load.audio('sfx-pencil-write', '/assets/fx/pencil-write.mp3'); + this.load.audio('sfx-piece-click', '/assets/fx/piece-click.mp3'); } async create() { diff --git a/public/src/ui/Sounds.js b/public/src/ui/Sounds.js new file mode 100644 index 0000000..0c9f3c1 --- /dev/null +++ b/public/src/ui/Sounds.js @@ -0,0 +1,27 @@ +// Sound effect keys — must match the keys loaded in PreloadScene. +export const SFX = { + CARD_DEAL: 'sfx-card-deal', + CARD_PLACE: 'sfx-card-place', + CARD_SHOW: 'sfx-card-show', + CARD_SHUFFLE: 'sfx-card-shuffle', + CASINO_BLACKJACK: 'sfx-casino-blackjack', + CASINO_LOSE: 'sfx-casino-lose', + CASINO_WIN: 'sfx-casino-win', + CHIP_BET: 'sfx-chip-bet', + DICE_ROLL: 'sfx-dice-roll', + PENCIL_WRITE: 'sfx-pencil-write', + PIECE_CLICK: 'sfx-piece-click', +}; + +export function playSound(scene, key) { + try { scene.sound.play(key); } catch (_) {} +} + +// chip-bet is limited to one simultaneous instance. +let _chipBetSound = null; +export function playChipBet(scene) { + if (_chipBetSound?.isPlaying) return; + _chipBetSound?.destroy(); + _chipBetSound = scene.sound.add(SFX.CHIP_BET); + _chipBetSound.play(); +}