feat(blackjack): enhance betting UI with prompts and fix button interactions

- Add animated betting prompts with pulsing chip buttons and text overlay
- Implement delayed start for betting animations after 5 seconds
- Refactor chip buttons to use containers for proper hit detection
- Update Button component to ensure interactive events work on all child objects
- Improve betting UI visibility management with proper show/hide states
- Add depth management for betting prompts and chip buttons
This commit is contained in:
Brian Fertig 2026-05-16 16:57:15 -06:00
parent 48f7ade241
commit 1f1897c8fe
2 changed files with 192 additions and 39 deletions

View File

@ -72,6 +72,9 @@ export default class BlackjackGame extends Phaser.Scene {
this.statusBadges = {};
this.nameTxts = {};
this.chipTxts = {};
this.bettingPromptGroup = [];
this.bettingPromptTimer = null;
this.chipPulseTweens = [];
this.add.rectangle(CX, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, COLORS.bg).setDepth(D.bg);
this.buildPlayfield();
@ -199,15 +202,17 @@ export default class BlackjackGame extends Phaser.Scene {
// Chip buttons
CHIP_AMOUNTS.forEach((amt, i) => {
const bx = cx - 120 + i * 80;
const g = this.add.graphics().setDepth(D.ui + 1);
g.fillStyle(CHIP_COLORS[amt], 1);
g.fillCircle(bx, y, 28);
const container = this.add.container(bx, y).setDepth(D.ui + 1);
const g = this.add.graphics();
g.lineStyle(3, 0xffffff, 0.4);
g.strokeCircle(bx, y, 28);
g.setInteractive(new Phaser.Geom.Circle(bx, y, 28), Phaser.Geom.Circle.Contains);
g.on('pointerdown', () => this.onChipClick(amt));
g.on('pointerover', () => g.setAlpha(0.8));
g.on('pointerout', () => g.setAlpha(1));
g.strokeCircle(0, 0, 28);
g.fillStyle(CHIP_COLORS[amt], 1);
g.fillCircle(0, 0, 28);
container.add(g);
container.setInteractive(new Phaser.Geom.Circle(0, 0, 28), Phaser.Geom.Circle.Contains);
container.on('pointerdown', () => this.onChipClick(amt));
container.on('pointerover', () => container.setAlpha(0.8));
container.on('pointerout', () => container.setAlpha(1));
const t = this.add.text(bx, y, `$${amt}`, {
fontFamily: 'system-ui, sans-serif', fontSize: '13px',
@ -238,8 +243,136 @@ export default class BlackjackGame extends Phaser.Scene {
this.hideBettingUI();
}
showBettingUI() { for (const o of this.bettingUIGroup) o.setVisible?.(true) || (o.visible = true); }
hideBettingUI() { for (const o of this.bettingUIGroup) o.setVisible?.(false) || (o.visible = false); }
showBettingUI() {
for (const o of this.bettingUIGroup) o.setVisible?.(true) || (o.visible = true);
this.chipBtnGraphics.forEach(g => g.setDepth(D.ui + 1));
this.startBettingPrompts();
}
hideBettingUI() {
for (const o of this.bettingUIGroup) o.setVisible?.(false) || (o.visible = false);
this.hideBettingPrompts();
}
startBettingPrompts() {
this.hideBettingPrompts();
// Wait 5 seconds before starting any animations
this.bettingPromptTimer = this.time.delayedCall(5000, () => {
if (!this.scene.isActive('BlackjackGame')) return;
if (this.pendingBet > 0) return; // Don't start if already betting
// Start subtle radius pulsing animation
this.chipBtnGraphics.forEach((chip, index) => {
const delay = index * 200;
const tween = this.tweens.add({
targets: chip,
scaleX: 1.25,
scaleY: 1.25,
duration: 1200,
ease: 'Sine.easeInOut',
yoyo: true,
repeat: -1,
delay: delay,
});
this.chipPulseTweens.push(tween);
});
// Show prompt text
this.showBettingPrompt();
});
}
showBettingPrompt() {
if (this.pendingBet > 0) return; // Don't show if already betting
const y = GAME_HEIGHT - 160;
const cx = CX;
// Create semi-transparent background
const bg = this.add.rectangle(cx + 100, y, 420, 60, 0x000000, 0.7)
.setOrigin(0.5, 0.5)
.setDepth(D.ui + 10)
.setAlpha(0);
// Create prompt text
const text = this.add.text(cx + 100, y, 'Choose an amount to bet and click Deal to begin', {
fontFamily: 'system-ui, sans-serif',
fontSize: '18px',
color: '#ffffff',
align: 'center',
})
.setOrigin(0.5, 0.5)
.setDepth(D.ui + 11)
.setAlpha(0);
this.bettingPromptGroup = [bg, text];
// Fade in
this.tweens.add({
targets: bg,
alpha: 0.7,
duration: 300,
ease: 'Power2',
});
this.tweens.add({
targets: text,
alpha: 1,
duration: 300,
ease: 'Power2',
delay: 100,
});
}
hideBettingPrompts() {
// Fade out prompt if visible
if (this.bettingPromptGroup && this.bettingPromptGroup.length > 0) {
this.tweens.add({
targets: this.bettingPromptGroup,
alpha: 0,
duration: 200,
ease: 'Power2',
onComplete: () => {
this.bettingPromptGroup.forEach(obj => obj.destroy());
this.bettingPromptGroup = [];
}
});
} else if (this.bettingPromptGroup.length > 0) {
this.bettingPromptGroup.forEach(obj => obj.destroy());
this.bettingPromptGroup = [];
}
// Stop chip pulsing
this.chipPulseTweens.forEach(tween => tween.destroy());
this.chipPulseTweens = [];
this.chipBtnGraphics.forEach(chip => {
chip.setScale(1, 1);
chip.setAlpha(1);
});
// Clear timer
if (this.bettingPromptTimer) {
this.bettingPromptTimer.remove();
this.bettingPromptTimer = null;
}
}
startBettingPromptsIfNoBet() {
// Only start prompts if no bet has been placed yet
if (this.pendingBet === 0) {
this.startBettingPrompts();
}
}
hideBettingUI() {
for (const o of this.bettingUIGroup) {
o.setVisible?.(false) || (o.visible = false);
}
this.hideBettingPrompts();
// Also hide chip containers from the betting UI
this.chipBtnGraphics.forEach(g => g.setVisible?.(false) || (g.visible = false));
this.chipBtnGraphics.forEach(g => g.setDepth(-100));
}
// ── Chip balance ──────────────────────────────────────────────────────────
async loadPlayerChips() {
@ -474,11 +607,13 @@ export default class BlackjackGame extends Phaser.Scene {
if (this.pendingBet + amount > human.chips) return;
this.pendingBet += amount;
this.updateBetDisplay();
this.hideBettingPrompts();
}
onClearBet() {
this.pendingBet = 0;
this.updateBetDisplay();
this.startBettingPromptsIfNoBet();
}
updateBetDisplay() {
@ -492,6 +627,7 @@ export default class BlackjackGame extends Phaser.Scene {
if (this.animating || this.pendingBet < 5) return;
this.animating = true;
this.hideBettingUI();
this.hideBettingPrompts();
// Commit human bet
this.gs = applyBet(this.gs, 0, this.pendingBet);
@ -900,6 +1036,7 @@ export default class BlackjackGame extends Phaser.Scene {
if (canSplit(p) && p.chips >= p.bet) {
btns.push(new Button(this, CX + 220, by, 'Split', () => this.onSplit(), { width: 120, height: 52, fontSize: 20 }));
}
btns.forEach(btn => btn.setDepth(D.ui + 5));
this.actionBtns = btns;
}

View File

@ -28,7 +28,9 @@ export class Button extends Phaser.GameObjects.Container {
this.add([this.bgRect, this.text]);
this.setSize(width, height);
this.setInteractive({ useHandCursor: true });
this.setInteractive({ useHandCursor: true, hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains });
this.bgRect.setInteractive({ hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains });
this.text.setInteractive({ hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains });
this.on('pointerover', () => this.bgRect.setFillStyle(bgHover, 1));
this.on('pointerout', () => this.bgRect.setFillStyle(bg, variant === 'ghost' ? 0 : 1));
this.on('pointerdown', () => this.bgRect.setScale(0.97));
@ -36,6 +38,20 @@ export class Button extends Phaser.GameObjects.Container {
this.on('pointerupoutside', () => this.bgRect.setScale(1));
if (onClick) this.on('pointerup', onClick);
this.bgRect.on('pointerover', () => this.bgRect.setFillStyle(bgHover, 1));
this.bgRect.on('pointerout', () => this.bgRect.setFillStyle(bg, variant === 'ghost' ? 0 : 1));
this.bgRect.on('pointerdown', () => this.bgRect.setScale(0.97));
this.bgRect.on('pointerup', () => this.bgRect.setScale(1));
this.bgRect.on('pointerupoutside', () => this.bgRect.setScale(1));
if (onClick) this.bgRect.on('pointerup', onClick);
this.text.on('pointerover', () => this.bgRect.setFillStyle(bgHover, 1));
this.text.on('pointerout', () => this.bgRect.setFillStyle(bg, variant === 'ghost' ? 0 : 1));
this.text.on('pointerdown', () => this.bgRect.setScale(0.97));
this.text.on('pointerup', () => this.bgRect.setScale(1));
this.text.on('pointerupoutside', () => this.bgRect.setScale(1));
if (onClick) this.text.on('pointerup', onClick);
scene.add.existing(this);
}