diff --git a/public/assets/fx/firework.mp3 b/public/assets/fx/firework.mp3 new file mode 100644 index 0000000..b6579a0 Binary files /dev/null and b/public/assets/fx/firework.mp3 differ diff --git a/public/assets/fx/gem-01.mp3 b/public/assets/fx/gem-01.mp3 new file mode 100644 index 0000000..58f1975 Binary files /dev/null and b/public/assets/fx/gem-01.mp3 differ diff --git a/public/assets/fx/gem-02.mp3 b/public/assets/fx/gem-02.mp3 new file mode 100644 index 0000000..c2daf17 Binary files /dev/null and b/public/assets/fx/gem-02.mp3 differ diff --git a/public/assets/fx/ui-attach.mp3 b/public/assets/fx/ui-attach.mp3 new file mode 100644 index 0000000..517c6c7 Binary files /dev/null and b/public/assets/fx/ui-attach.mp3 differ diff --git a/public/src/games/splendor/SplendorGame.js b/public/src/games/splendor/SplendorGame.js index 1722426..dbf608e 100644 --- a/public/src/games/splendor/SplendorGame.js +++ b/public/src/games/splendor/SplendorGame.js @@ -64,6 +64,7 @@ export default class SplendorGame extends Phaser.Scene { this.animatingReserve = null; // { seat, cardId } suppresses that pill during animation this.animatingBuy = null; // { seat, cardId } suppresses thumbnail during animation this.boughtCards = {}; // seat → card[] — persists purchased cards for display + this._thumbHoverTimer = null; // pending 500 ms intent timer for thumbnail preview } create() { @@ -147,6 +148,7 @@ export default class SplendorGame extends Phaser.Scene { clearDyn() { this.clearPreview(); + if (this._thumbHoverTimer) { this._thumbHoverTimer.remove(); this._thumbHoverTimer = null; } for (const o of this.dyn) { try { o.destroy(); } catch { /* noop */ } } this.dyn = []; } @@ -538,6 +540,22 @@ export default class SplendorGame extends Phaser.Scene { color: bc.bonus === 'white' ? '#333' : '#fff', }).setDepth(DEPTH.ui + 4)); } + // 500 ms hover-intent preview + const tz = this.reg( + this.add.zone(tx + THUMB_W / 2, ty + THUMB_H / 2, THUMB_W, THUMB_H) + .setInteractive().setDepth(DEPTH.ui + 5) + ); + tz.on('pointerover', () => { + this._thumbHoverTimer = this.time.delayedCall(500, () => { + for (const p of this.portraits) p?.hide(); + this.showCardPreview(bc, tx + THUMB_W / 2, ty + THUMB_H); + }); + }); + tz.on('pointerout', () => { + if (this._thumbHoverTimer) { this._thumbHoverTimer.remove(); this._thumbHoverTimer = null; } + this.clearPreview(); + for (const p of this.portraits) p?.show(); + }); }); // reserved cards @@ -575,9 +593,13 @@ export default class SplendorGame extends Phaser.Scene { // ── bottom control bar ────────────────────────────────────────────────────── drawControlBar() { - const x = MARKET_X, y = CONTROL_Y, w = 920, h = GAME_HEIGHT - y - 24; + const n = this.gs?.players?.length ?? 4; + const panelGap = 16; + const panelH = Math.min(220, Math.floor((GAME_HEIGHT - 90 - panelGap * (n - 1)) / n)); + const lastPanelBottom = 76 + (n - 1) * (panelH + panelGap) + panelH; + const x = MARKET_X, y = CONTROL_Y, w = 920, h = Math.max(120, lastPanelBottom - y); const g = this.reg(this.add.graphics().setDepth(DEPTH.ui)); - g.fillStyle(0x000000, 0.3).fillRoundedRect(x, y, w, h, 12); + g.fillStyle(0x000000, 0.55).fillRoundedRect(x, y, w, h, 12); g.lineStyle(1, COLORS.accent, 0.4).strokeRoundedRect(x, y, w, h, 12); if (isGameOver(this.gs)) return; @@ -617,10 +639,10 @@ export default class SplendorGame extends Phaser.Scene { bx += 170; } if (source === 'board' && human.reserved.length < MAX_RESERVED) { - this.addButton(bx + 80, y + 70, 'Reserve (+1 gold)', + this.addButton(bx + 115, y + 70, 'Reserve (+1 gold)', () => this.applyHuman({ type: 'reserve', cardId: card.id, tier: card.tier }), { width: 230, height: 46, variant: 'ghost' }); - bx += 250; + bx += 285; } this.addButton(bx + 70, y + 70, 'Cancel', () => { this.selectedCard = null; this.render(); }, { width: 130, height: 46, variant: 'ghost' }); @@ -659,12 +681,9 @@ export default class SplendorGame extends Phaser.Scene { drawTurnLabel() { const txt = isGameOver(this.gs) ? 'Game over' : `${this.pname(this.gs.current)}'s turn`; - this.reg(this.add.text(BANK_X, GAME_HEIGHT - 70, txt, { + this.reg(this.add.text(BANK_X, GAME_HEIGHT - 48, txt, { fontFamily: 'Righteous', fontSize: '22px', color: COLORS.textHex, }).setOrigin(0, 0.5).setDepth(DEPTH.ui + 1)); - this.reg(this.add.text(BANK_X, GAME_HEIGHT - 40, `First to ${WIN_POINTS} prestige wins`, { - fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex, - }).setOrigin(0, 0.5).setDepth(DEPTH.ui + 1)); } // ── helpers ───────────────────────────────────────────────────────────────── @@ -850,6 +869,8 @@ export default class SplendorGame extends Phaser.Scene { this.tweens.add({ targets: overlay, alpha: 0, duration: 300, onComplete: () => { try { overlay.destroy(); } catch { /* */ } } }); + try { const a = new Audio('/assets/fx/ui-attach.mp3'); a.volume = 0.8; a.play(); } catch { /* */ } + this.tweens.add({ targets: container, x: pillCX, y: pillCY, @@ -896,6 +917,8 @@ export default class SplendorGame extends Phaser.Scene { const sparkCount = 16 + Math.floor(Math.random() * 8); this.time.delayedCall(delay, () => { + try { const fw = new Audio('/assets/fx/firework.mp3'); fw.volume = 0.7; fw.play(); } catch { /* */ } + // Central flash — expands and fades const flash = this.add.circle(bx, by, 7, palette[0], 1).setDepth(DEPTH.popup + 2); this.tweens.add({ @@ -958,6 +981,8 @@ export default class SplendorGame extends Phaser.Scene { this.tweens.add({ targets: overlay, alpha: 0, duration: 300, onComplete: () => { try { overlay.destroy(); } catch { /* */ } } }); + try { const a = new Audio('/assets/fx/ui-attach.mp3'); a.volume = 0.8; a.play(); } catch { /* */ } + this.tweens.add({ targets: container, x: thumbCX, y: thumbCY,