feat(bingo): add countdown timer, opponent match flash, and fullscreen fix
- Add a 10-second countdown timer between ball draws with a dedicated UI overlay - Implement visual flash effect on opponent panels when their cards match the called number - Play happy emotion animation on opponent portraits during matches - Replace CASINO_WIN/LOSE sounds with CASINO_BLACKJACK for win/loss events - Fix fullscreen toggle to properly handle exitFullscreen and requestFullscreen - Add new video assets for opponent emotions (happy, idle, upset)
This commit is contained in:
parent
3022e0eb23
commit
e46cd1cc2f
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -75,6 +75,13 @@ export default class BingoGame extends Phaser.Scene {
|
|||
this.pendingAIClaim = null;
|
||||
this._bingoPulse = null;
|
||||
this.MatterBody = null;
|
||||
|
||||
// Countdown timer
|
||||
this.countdownActive = false;
|
||||
this.countdownValue = 10;
|
||||
this.countdownEvent = null;
|
||||
this.countdownContainer = null;
|
||||
this.countdownDisplay = null;
|
||||
}
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
||||
|
|
@ -92,6 +99,7 @@ export default class BingoGame extends Phaser.Scene {
|
|||
this.buildCalledBoard();
|
||||
this.buildRevealSlot();
|
||||
this.buildButtons();
|
||||
this.buildCountdownWindow();
|
||||
this.showBuyInModal();
|
||||
|
||||
this.events.once('shutdown', () => this.cleanup());
|
||||
|
|
@ -306,7 +314,7 @@ export default class BingoGame extends Phaser.Scene {
|
|||
const px = OPP_COLX[col], py = OPP_ROWY[row];
|
||||
const opp = this.activeOpponents[i];
|
||||
|
||||
this.add.rectangle(px, py, PW, PH, 0x000000, 0.6).setStrokeStyle(1, 0x8a7050).setDepth(D.panel);
|
||||
const bgRect = this.add.rectangle(px, py, PW, PH, 0x000000, 0.6).setStrokeStyle(1, 0x8a7050).setDepth(D.panel);
|
||||
|
||||
// Cream white background behind the mini bingo card
|
||||
const miniW = 90, miniH = 90;
|
||||
|
|
@ -317,10 +325,11 @@ export default class BingoGame extends Phaser.Scene {
|
|||
fontFamily: '"Julius Sans One"', fontSize: '17px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(D.panel + 4);
|
||||
|
||||
this.oppPortraits.push(createOpponentPortrait(this, opp, px - 54, py + 6, 28, D.panel + 1));
|
||||
const portrait = createOpponentPortrait(this, opp, px - 54, py + 6, 28, D.panel + 1);
|
||||
this.oppPortraits.push(portrait);
|
||||
|
||||
const miniG = this.add.graphics().setDepth(D.panel + 4);
|
||||
this.oppPanels.push({ seat: i + 1, miniG, mx: px - 8, my: py - 28 });
|
||||
this.oppPanels.push({ seat: i + 1, miniG, mx: px - 8, my: py - 28, bgRect, portrait });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -522,6 +531,7 @@ export default class BingoGame extends Phaser.Scene {
|
|||
// ── Draw flow ────────────────────────────────────────────────────────────────
|
||||
|
||||
onNextBall() {
|
||||
this.stopCountdown();
|
||||
if (!this.gs || this.gs.phase !== 'playing' || this.animating) return;
|
||||
|
||||
// Forfeit gate: advancing while an opponent has an unclaimed line hands them the win.
|
||||
|
|
@ -570,9 +580,11 @@ export default class BingoGame extends Phaser.Scene {
|
|||
this.refreshHumanMarkable();
|
||||
this.refreshClaimButton();
|
||||
this.armAISuspense();
|
||||
this.flashOpponentMatches(n);
|
||||
this.setSpin(0, 1500, 'Quad.easeOut'); // wind the drum back down to idle
|
||||
this.animating = false;
|
||||
if (this.gs.phase === 'playing') this.nextBtn.setEnabled(true);
|
||||
this.startCountdown();
|
||||
};
|
||||
|
||||
if (this.drumBalls.length === 0) { reveal(); return; }
|
||||
|
|
@ -598,6 +610,117 @@ export default class BingoGame extends Phaser.Scene {
|
|||
});
|
||||
}
|
||||
|
||||
// ── Opponent match flash ─────────────────────────────────────────────────────
|
||||
|
||||
flashOpponentMatches(n) {
|
||||
if (!this.gs || this.gs.phase !== 'playing') return;
|
||||
|
||||
const letter = letterForNumber(n);
|
||||
const col = LETTERS.indexOf(letter);
|
||||
const matches = [];
|
||||
|
||||
for (let i = 0; i < this.gs.players.length; i++) {
|
||||
const p = this.gs.players[i];
|
||||
if (p.isHuman) continue;
|
||||
for (let row = 0; row < 5; row++) {
|
||||
if (p.card[col][row] === n) {
|
||||
matches.push({ seat: i });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.length === 0) return;
|
||||
|
||||
const FLASH_DURATION = 2000;
|
||||
const GAP = 200;
|
||||
|
||||
matches.forEach((match, index) => {
|
||||
const panel = this.oppPanels.find(p => p.seat === match.seat);
|
||||
if (!panel || !panel.bgRect) return;
|
||||
|
||||
const startDelay = index * (FLASH_DURATION + GAP);
|
||||
|
||||
// Flash to green + play happy video
|
||||
this.time.delayedCall(startDelay, () => {
|
||||
panel.bgRect.setFillStyle(0x2d7a4e, 1);
|
||||
playSound(this, SFX.CASINO_LOSE);
|
||||
panel.portrait?.playEmotion?.('happy');
|
||||
});
|
||||
|
||||
// Flash back to black
|
||||
this.time.delayedCall(startDelay + FLASH_DURATION, () => {
|
||||
panel.bgRect.setFillStyle(0x000000, 0.6);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Countdown timer ──────────────────────────────────────────────────────────
|
||||
|
||||
buildCountdownWindow() {
|
||||
const winW = 340, winH = 120;
|
||||
const winX = DRUM_X, winY = 804;
|
||||
|
||||
this.countdownContainer = this.add.container(winX, winY).setDepth(D.ui);
|
||||
|
||||
// Black rounded background with white border
|
||||
const bg = this.add.graphics();
|
||||
bg.fillStyle(0x000000, 0.9);
|
||||
bg.fillRoundedRect(-winW / 2, -winH / 2, winW, winH, 10);
|
||||
bg.lineStyle(2, 0xffffff, 0.5);
|
||||
bg.strokeRoundedRect(-winW / 2, -winH / 2, winW, winH, 10);
|
||||
this.countdownContainer.add(bg);
|
||||
|
||||
// Header text
|
||||
const header = this.add.text(0, -25, 'Next Ball Countdown', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5);
|
||||
this.countdownContainer.add(header);
|
||||
|
||||
// Timer number display
|
||||
this.countdownDisplay = this.add.text(0, 25, '05', {
|
||||
fontFamily: 'Righteous', fontSize: '48px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5);
|
||||
this.countdownContainer.add(this.countdownDisplay);
|
||||
|
||||
this.countdownContainer.setVisible(false);
|
||||
}
|
||||
|
||||
startCountdown() {
|
||||
if (this.countdownActive) return;
|
||||
if (!this.gs || this.gs.phase !== 'playing') return;
|
||||
if (this.gs.bag.length === 0) return;
|
||||
|
||||
this.countdownActive = true;
|
||||
this.countdownValue = 10;
|
||||
this.countdownContainer.setVisible(true);
|
||||
this.countdownDisplay.setText('05');
|
||||
|
||||
this.countdownEvent = this.time.addEvent({
|
||||
delay: 1000,
|
||||
callback: () => {
|
||||
if (!this.countdownActive) return;
|
||||
this.countdownValue--;
|
||||
this.countdownDisplay.setText(String(this.countdownValue).padStart(2, '0'));
|
||||
|
||||
if (this.countdownValue <= 0) {
|
||||
this.countdownActive = false;
|
||||
this.countdownContainer.setVisible(false);
|
||||
this.onNextBall();
|
||||
}
|
||||
},
|
||||
repeat: 9,
|
||||
});
|
||||
}
|
||||
|
||||
stopCountdown() {
|
||||
if (this.countdownEvent) {
|
||||
this.countdownEvent.remove();
|
||||
this.countdownEvent = null;
|
||||
}
|
||||
this.countdownActive = false;
|
||||
this.countdownContainer?.setVisible(false);
|
||||
}
|
||||
|
||||
// ── Claim race ───────────────────────────────────────────────────────────────
|
||||
|
||||
armAISuspense() {
|
||||
|
|
@ -615,6 +738,7 @@ export default class BingoGame extends Phaser.Scene {
|
|||
onHumanClaim() {
|
||||
if (!this.gs || this.gs.phase !== 'playing') return;
|
||||
if (!hasCompletedLine(this.gs.players[0])) return;
|
||||
this.stopCountdown();
|
||||
if (this.pendingAIClaim) { this.pendingAIClaim.remove(false); this.pendingAIClaim = null; }
|
||||
|
||||
this.gs = resolveClaim(this.gs, 0);
|
||||
|
|
@ -623,12 +747,13 @@ export default class BingoGame extends Phaser.Scene {
|
|||
this.reactOpponents(0);
|
||||
this.showBanner('BINGO!');
|
||||
this.screenFireworks();
|
||||
playSound(this, SFX.CASINO_WIN);
|
||||
playSound(this, SFX.CASINO_BLACKJACK);
|
||||
this.time.delayedCall(1500, () => this.showGameOver(true));
|
||||
}
|
||||
|
||||
onAIWin(seat) {
|
||||
if (!this.gs || this.gs.phase !== 'playing') return;
|
||||
this.stopCountdown();
|
||||
if (this.pendingAIClaim) { this.pendingAIClaim.remove(false); this.pendingAIClaim = null; }
|
||||
|
||||
this.gs = resolveClaim(this.gs, seat);
|
||||
|
|
@ -637,7 +762,7 @@ export default class BingoGame extends Phaser.Scene {
|
|||
const name = this.gs.players[seat]?.name ?? 'Opponent';
|
||||
this.reactOpponents(seat);
|
||||
this.showBanner(`${name} shouts BINGO!`);
|
||||
playSound(this, SFX.CASINO_LOSE);
|
||||
playSound(this, SFX.CASINO_BLACKJACK);
|
||||
this.time.delayedCall(1600, () => this.showGameOver(false));
|
||||
}
|
||||
|
||||
|
|
@ -686,6 +811,8 @@ export default class BingoGame extends Phaser.Scene {
|
|||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5).setDepth(D.modal);
|
||||
|
||||
this.stopCountdown();
|
||||
|
||||
new Button(this, CX - 110, GAME_HEIGHT / 2 + 100, 'Play Again', () => this.scene.restart(), {
|
||||
width: 200,
|
||||
}).setDepth(D.modal);
|
||||
|
|
@ -697,6 +824,7 @@ export default class BingoGame extends Phaser.Scene {
|
|||
// ── Cleanup ──────────────────────────────────────────────────────────────────
|
||||
|
||||
cleanup() {
|
||||
this.stopCountdown();
|
||||
if (this.pendingAIClaim) { this.pendingAIClaim.remove(false); this.pendingAIClaim = null; }
|
||||
if (this.spinTween) { this.spinTween.stop(); this.spinTween = null; }
|
||||
if (this.spinHoldTimer) { this.spinHoldTimer.remove(false); this.spinHoldTimer = null; }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ export function addFullscreenButton(scene) {
|
|||
GAME_WIDTH - width / 2 - 30,
|
||||
52,
|
||||
'Toggle Fullscreen',
|
||||
() => scene.scale.toggleFullscreen(),
|
||||
() => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
document.documentElement.requestFullscreen();
|
||||
}
|
||||
},
|
||||
{ width, height: 52, fontSize: 22, variant: 'ghost' },
|
||||
).setDepth(100);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue