feat: add sound effects, backgrounds, and enhance OpponentSelect UI
- Introduce `Sounds.js` utility and load new audio assets (dice, cards, chips, casino). - Play appropriate sound effects in Backgammon, Blackjack, Holdem, Parchisi, Phase10, SkipBo, and Yatzi. - Add `background-casino.png` and `background-room.png` to PreloadScene. - Update OpponentSelectScene with new backgrounds, styled title/subtitle pills, and improved Start/Back buttons. - Enhance opponent selection with gradient backgrounds and shadows via `applyOpponentStyle`. - Implement `highlightTile` for better visual feedback in option selectors. - Randomly select a card back for card games in OpponentSelectScene.
This commit is contained in:
parent
792fd35fd4
commit
fcfff62a15
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Reference in New Issue