feat(splendor): improve reserve card hover preview, dynamic control bar, and sound effects
- Add 500ms hover-intent preview for reserve cards, temporarily hiding player portraits - Dynamically calculate control bar panel height based on player count - Adjust reserve button positioning and increase control bar background opacity - Reposition turn label and remove redundant win condition subtitle - Integrate audio feedback for card interactions (ui-attach) and achievement animations (firework)
This commit is contained in:
parent
6abcbfa32b
commit
aa6fce0f6c
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -64,6 +64,7 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
this.animatingReserve = null; // { seat, cardId } suppresses that pill during animation
|
this.animatingReserve = null; // { seat, cardId } suppresses that pill during animation
|
||||||
this.animatingBuy = null; // { seat, cardId } suppresses thumbnail during animation
|
this.animatingBuy = null; // { seat, cardId } suppresses thumbnail during animation
|
||||||
this.boughtCards = {}; // seat → card[] — persists purchased cards for display
|
this.boughtCards = {}; // seat → card[] — persists purchased cards for display
|
||||||
|
this._thumbHoverTimer = null; // pending 500 ms intent timer for thumbnail preview
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -147,6 +148,7 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
|
|
||||||
clearDyn() {
|
clearDyn() {
|
||||||
this.clearPreview();
|
this.clearPreview();
|
||||||
|
if (this._thumbHoverTimer) { this._thumbHoverTimer.remove(); this._thumbHoverTimer = null; }
|
||||||
for (const o of this.dyn) { try { o.destroy(); } catch { /* noop */ } }
|
for (const o of this.dyn) { try { o.destroy(); } catch { /* noop */ } }
|
||||||
this.dyn = [];
|
this.dyn = [];
|
||||||
}
|
}
|
||||||
|
|
@ -538,6 +540,22 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
color: bc.bonus === 'white' ? '#333' : '#fff',
|
color: bc.bonus === 'white' ? '#333' : '#fff',
|
||||||
}).setDepth(DEPTH.ui + 4));
|
}).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
|
// reserved cards
|
||||||
|
|
@ -575,9 +593,13 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
|
|
||||||
// ── bottom control bar ──────────────────────────────────────────────────────
|
// ── bottom control bar ──────────────────────────────────────────────────────
|
||||||
drawControlBar() {
|
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));
|
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);
|
g.lineStyle(1, COLORS.accent, 0.4).strokeRoundedRect(x, y, w, h, 12);
|
||||||
|
|
||||||
if (isGameOver(this.gs)) return;
|
if (isGameOver(this.gs)) return;
|
||||||
|
|
@ -617,10 +639,10 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
bx += 170;
|
bx += 170;
|
||||||
}
|
}
|
||||||
if (source === 'board' && human.reserved.length < MAX_RESERVED) {
|
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 }),
|
() => this.applyHuman({ type: 'reserve', cardId: card.id, tier: card.tier }),
|
||||||
{ width: 230, height: 46, variant: 'ghost' });
|
{ width: 230, height: 46, variant: 'ghost' });
|
||||||
bx += 250;
|
bx += 285;
|
||||||
}
|
}
|
||||||
this.addButton(bx + 70, y + 70, 'Cancel', () => { this.selectedCard = null; this.render(); },
|
this.addButton(bx + 70, y + 70, 'Cancel', () => { this.selectedCard = null; this.render(); },
|
||||||
{ width: 130, height: 46, variant: 'ghost' });
|
{ width: 130, height: 46, variant: 'ghost' });
|
||||||
|
|
@ -659,12 +681,9 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
|
|
||||||
drawTurnLabel() {
|
drawTurnLabel() {
|
||||||
const txt = isGameOver(this.gs) ? 'Game over' : `${this.pname(this.gs.current)}'s turn`;
|
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,
|
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.textHex,
|
||||||
}).setOrigin(0, 0.5).setDepth(DEPTH.ui + 1));
|
}).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 ─────────────────────────────────────────────────────────────────
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -850,6 +869,8 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
this.tweens.add({ targets: overlay, alpha: 0, duration: 300,
|
this.tweens.add({ targets: overlay, alpha: 0, duration: 300,
|
||||||
onComplete: () => { try { overlay.destroy(); } catch { /* */ } } });
|
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({
|
this.tweens.add({
|
||||||
targets: container,
|
targets: container,
|
||||||
x: pillCX, y: pillCY,
|
x: pillCX, y: pillCY,
|
||||||
|
|
@ -896,6 +917,8 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
const sparkCount = 16 + Math.floor(Math.random() * 8);
|
const sparkCount = 16 + Math.floor(Math.random() * 8);
|
||||||
|
|
||||||
this.time.delayedCall(delay, () => {
|
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
|
// Central flash — expands and fades
|
||||||
const flash = this.add.circle(bx, by, 7, palette[0], 1).setDepth(DEPTH.popup + 2);
|
const flash = this.add.circle(bx, by, 7, palette[0], 1).setDepth(DEPTH.popup + 2);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
|
|
@ -958,6 +981,8 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
this.tweens.add({ targets: overlay, alpha: 0, duration: 300,
|
this.tweens.add({ targets: overlay, alpha: 0, duration: 300,
|
||||||
onComplete: () => { try { overlay.destroy(); } catch { /* */ } } });
|
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({
|
this.tweens.add({
|
||||||
targets: container,
|
targets: container,
|
||||||
x: thumbCX, y: thumbCY,
|
x: thumbCX, y: thumbCY,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue