501 lines
19 KiB
JavaScript
501 lines
19 KiB
JavaScript
import * as Phaser from 'phaser';
|
||
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
|
||
import { Button } from '../../ui/Button.js';
|
||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||
import { api } from '../../services/api.js';
|
||
import {
|
||
createGame, setBet, deal, toggleHold, drawReplacements,
|
||
PAY_TABLE, MIN_BET, MAX_BET, HAND_SIZE,
|
||
} from './VideoPokerLogic.js';
|
||
|
||
// ── Layout constants ──────────────────────────────────────────────────────────
|
||
const D = { bg: 0, cabinet: 5, screen: 10, scan: 14, glow: 16, card: 20, ui: 40, overlay: 60 };
|
||
|
||
// CRT monitor frame (the plastic cabinet) and the inset screen inside it.
|
||
const MON_X = 110, MON_Y = 150, MON_W = 1180, MON_H = 720;
|
||
const SCR_PAD = 56; // bezel thickness
|
||
const SCR_X = MON_X + SCR_PAD, SCR_Y = MON_Y + SCR_PAD + 6;
|
||
const SCR_W = MON_W - SCR_PAD * 2, SCR_H = MON_H - SCR_PAD * 2 - 6;
|
||
|
||
// Cards drawn inside the screen.
|
||
const CARD_W = 168, CARD_H = 236, CARD_R = 12, CARD_GAP = 24;
|
||
|
||
// Retro CRT palette (independent of the global gold theme).
|
||
const CRT = {
|
||
cabinet: 0x2a2d33,
|
||
cabinetHi: 0x444851,
|
||
cabinetLo: 0x14161a,
|
||
screenBg: 0x0a1456, // classic video-poker royal blue (lower)
|
||
screenTop: 0x16278f, // royal blue (upper)
|
||
screenEdge: 0x4a6cff, // bright blue screen-edge glow
|
||
phosphor: 0x39ff9e, // green phosphor glow
|
||
phosphorHex: '#39ff9e',
|
||
amber: 0xffcf4a,
|
||
amberHex: '#ffcf4a',
|
||
held: 0xff4d6d,
|
||
heldHex: '#ff4d6d',
|
||
scan: 0x39ff9e,
|
||
};
|
||
|
||
const SUIT_RED = '#d9263c';
|
||
const SUIT_BLACK = '#10131a';
|
||
|
||
export default class VideoPokerGame extends Phaser.Scene {
|
||
constructor() { super('VideoPokerGame'); }
|
||
|
||
init(data) {
|
||
this.gameDef = data.game ?? { slug: 'videopoker', name: 'Video Poker' };
|
||
this.state = createGame(MIN_BET);
|
||
this.credits = 2000; // synced from server in create()
|
||
this.sessionNet = 0; // net chips delta pending persistence
|
||
this.handsPlayed = 0;
|
||
this.busy = false; // true during deal/draw animation
|
||
this.cardObjs = []; // per-slot container of graphics for each card
|
||
this.heldTags = []; // per-slot "HELD" label
|
||
this.payRows = []; // per-row { cells: text[], nameText }
|
||
}
|
||
|
||
async create() {
|
||
this.buildBackground();
|
||
this.buildCabinet();
|
||
this.buildScreen();
|
||
this.buildPayTable();
|
||
this.buildControls();
|
||
this.buildMeters();
|
||
|
||
await this.loadPlayerChips();
|
||
this.refreshMeters();
|
||
this.highlightBetColumn();
|
||
this.showIdleScreen();
|
||
}
|
||
|
||
// ── Bankroll ────────────────────────────────────────────────────────────────
|
||
async loadPlayerChips() {
|
||
try {
|
||
const { profile } = await api.get('/profile');
|
||
this.credits = profile.chips ?? 2000;
|
||
} catch {
|
||
this.credits = 2000;
|
||
}
|
||
}
|
||
|
||
// Persist the running net delta to the shared casino bankroll (best effort).
|
||
async flushChips() {
|
||
const delta = this.sessionNet;
|
||
if (delta === 0) return;
|
||
this.sessionNet = 0;
|
||
try { await api.post('/profile/chips/adjust', { delta }); }
|
||
catch { this.sessionNet += delta; /* retry on next flush */ }
|
||
}
|
||
|
||
async recordHistory(result) {
|
||
try {
|
||
await api.post('/history/single-player', {
|
||
slug: 'videopoker',
|
||
score: this.credits,
|
||
opponentScores: [],
|
||
result,
|
||
});
|
||
} catch { /* ignore */ }
|
||
}
|
||
|
||
// ── Background & cabinet ──────────────────────────────────────────────────────
|
||
buildBackground() {
|
||
const g = this.add.graphics().setDepth(D.bg);
|
||
const top = Phaser.Display.Color.ValueToColor(0x10131a);
|
||
const bot = Phaser.Display.Color.ValueToColor(0x05070b);
|
||
for (let i = 0; i < GAME_HEIGHT; i += 4) {
|
||
const c = Phaser.Display.Color.Interpolate.ColorWithColor(top, bot, 100, Math.floor((i / GAME_HEIGHT) * 100));
|
||
g.fillStyle(Phaser.Display.Color.GetColor(c.r, c.g, c.b), 1);
|
||
g.fillRect(0, i, GAME_WIDTH, 4);
|
||
}
|
||
// Faint floor ambience under the cabinet glow.
|
||
g.fillStyle(CRT.phosphor, 0.04);
|
||
g.fillEllipse(MON_X + MON_W / 2, MON_Y + MON_H + 60, MON_W + 120, 160);
|
||
|
||
this.add.text(GAME_WIDTH / 2, 64, 'VIDEO POKER', {
|
||
fontFamily: 'Righteous', fontSize: '46px', color: CRT.amberHex,
|
||
}).setOrigin(0.5).setDepth(D.ui)
|
||
.setShadow(0, 0, 'rgba(255,207,74,0.85)', 18);
|
||
this.add.text(GAME_WIDTH / 2, 108, 'JACKS OR BETTER', {
|
||
fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.mutedHex,
|
||
}).setOrigin(0.5).setDepth(D.ui);
|
||
}
|
||
|
||
buildCabinet() {
|
||
const g = this.add.graphics().setDepth(D.cabinet);
|
||
// Outer plastic shell with a rounded bezel and bevel highlights.
|
||
g.fillStyle(CRT.cabinetLo, 1);
|
||
g.fillRoundedRect(MON_X - 14, MON_Y - 14, MON_W + 28, MON_H + 28, 38);
|
||
g.fillStyle(CRT.cabinet, 1);
|
||
g.fillRoundedRect(MON_X, MON_Y, MON_W, MON_H, 30);
|
||
// Top bevel highlight.
|
||
g.fillStyle(CRT.cabinetHi, 0.55);
|
||
g.fillRoundedRect(MON_X + 10, MON_Y + 10, MON_W - 20, 26, 12);
|
||
// Inner shadow lip framing the screen.
|
||
g.fillStyle(0x000000, 0.85);
|
||
g.fillRoundedRect(SCR_X - 12, SCR_Y - 12, SCR_W + 24, SCR_H + 24, 22);
|
||
// Two decorative cabinet screws.
|
||
for (const sx of [MON_X + 24, MON_X + MON_W - 24]) {
|
||
g.fillStyle(CRT.cabinetLo, 1); g.fillCircle(sx, MON_Y + MON_H - 22, 8);
|
||
g.fillStyle(CRT.cabinetHi, 0.6); g.fillCircle(sx - 2, MON_Y + MON_H - 24, 3);
|
||
}
|
||
}
|
||
|
||
buildScreen() {
|
||
// Inset screen with a vertical phosphor gradient.
|
||
const g = this.add.graphics().setDepth(D.screen);
|
||
const top = Phaser.Display.Color.ValueToColor(CRT.screenTop);
|
||
const bot = Phaser.Display.Color.ValueToColor(CRT.screenBg);
|
||
for (let i = 0; i < SCR_H; i += 3) {
|
||
const c = Phaser.Display.Color.Interpolate.ColorWithColor(top, bot, 100, Math.floor((i / SCR_H) * 100));
|
||
g.fillStyle(Phaser.Display.Color.GetColor(c.r, c.g, c.b), 1);
|
||
g.fillRect(SCR_X, SCR_Y + i, SCR_W, 3);
|
||
}
|
||
|
||
// Geometry mask so cards + scanlines never spill past the screen.
|
||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||
maskG.fillStyle(0xffffff);
|
||
maskG.fillRoundedRect(SCR_X, SCR_Y, SCR_W, SCR_H, 16);
|
||
this.screenMask = maskG.createGeometryMask();
|
||
|
||
// CRT scanlines: dark horizontal lines tiled across the screen. An 8px strip
|
||
// with a 4px dark line + 4px gap gives thick, clearly visible scanlines.
|
||
// Drawn above the cards (D.card + 2) so the lines sweep over them too.
|
||
const strip = this.make.graphics({ x: 0, y: 0, add: false });
|
||
strip.fillStyle(0x000000, 0.45);
|
||
strip.fillRect(0, 0, SCR_W, 4);
|
||
strip.generateTexture('vpScan', SCR_W, 8);
|
||
const scan = this.add.tileSprite(SCR_X, SCR_Y, SCR_W, SCR_H, 'vpScan')
|
||
.setOrigin(0, 0).setDepth(D.card + 2).setAlpha(0.55).setMask(this.screenMask);
|
||
// Gentle vertical drift for a live-CRT shimmer.
|
||
this.tweens.add({ targets: scan, tilePositionY: SCR_H, duration: 9000, repeat: -1, ease: 'Linear' });
|
||
|
||
// Curved-corner vignette + blue screen edge glow.
|
||
const vg = this.add.graphics().setDepth(D.glow);
|
||
vg.lineStyle(3, CRT.screenEdge, 0.25);
|
||
vg.strokeRoundedRect(SCR_X + 2, SCR_Y + 2, SCR_W - 4, SCR_H - 4, 14);
|
||
|
||
// Status line that shows messages on the screen (e.g. GAME OVER / payout).
|
||
this.statusText = this.add.text(SCR_X + SCR_W / 2, SCR_Y + 40, '', {
|
||
fontFamily: 'Righteous', fontSize: '30px', color: CRT.amberHex, align: 'center',
|
||
}).setOrigin(0.5).setDepth(D.card).setMask(this.screenMask)
|
||
.setShadow(0, 0, 'rgba(255,207,74,0.8)', 14);
|
||
|
||
// Card slot anchors centered in the lower portion of the screen.
|
||
const totalW = HAND_SIZE * CARD_W + (HAND_SIZE - 1) * CARD_GAP;
|
||
const startX = SCR_X + (SCR_W - totalW) / 2 + CARD_W / 2;
|
||
const cy = SCR_Y + SCR_H - CARD_H / 2 - 56;
|
||
this.slotX = [];
|
||
this.slotY = cy;
|
||
for (let i = 0; i < HAND_SIZE; i++) {
|
||
const cx = startX + i * (CARD_W + CARD_GAP);
|
||
this.slotX.push(cx);
|
||
|
||
const cont = this.add.container(cx, cy).setDepth(D.card).setMask(this.screenMask);
|
||
this.cardObjs.push(cont);
|
||
|
||
const tag = this.add.text(cx, cy - CARD_H / 2 - 26, 'HELD', {
|
||
fontFamily: 'Righteous', fontSize: '24px', color: CRT.heldHex,
|
||
}).setOrigin(0.5).setDepth(D.card).setVisible(false).setMask(this.screenMask)
|
||
.setShadow(0, 0, 'rgba(255,77,109,0.9)', 12);
|
||
this.heldTags.push(tag);
|
||
|
||
// Click-to-hold hit zone over each slot.
|
||
const zone = this.add.zone(cx, cy, CARD_W + CARD_GAP, CARD_H + 40)
|
||
.setInteractive({ useHandCursor: true }).setDepth(D.card);
|
||
zone.on('pointerup', () => this.onSlotClicked(i));
|
||
}
|
||
}
|
||
|
||
// Draw a single card face (or back) into its slot container.
|
||
renderCard(i, card, faceUp) {
|
||
const cont = this.cardObjs[i];
|
||
cont.removeAll(true);
|
||
const hw = CARD_W / 2, hh = CARD_H / 2;
|
||
const g = this.add.graphics();
|
||
|
||
if (!faceUp || !card) {
|
||
g.fillStyle(0x0b1f3a, 1);
|
||
g.fillRoundedRect(-hw, -hh, CARD_W, CARD_H, CARD_R);
|
||
g.lineStyle(3, CRT.phosphor, 0.5);
|
||
g.strokeRoundedRect(-hw, -hh, CARD_W, CARD_H, CARD_R);
|
||
g.lineStyle(2, 0x2a6cff, 0.6);
|
||
g.strokeRoundedRect(-hw + 14, -hh + 14, CARD_W - 28, CARD_H - 28, 8);
|
||
cont.add(g);
|
||
return;
|
||
}
|
||
|
||
const col = card.isRed ? SUIT_RED : SUIT_BLACK;
|
||
g.fillStyle(0xf7f4ec, 1);
|
||
g.fillRoundedRect(-hw, -hh, CARD_W, CARD_H, CARD_R);
|
||
g.lineStyle(2, 0x000000, 0.25);
|
||
g.strokeRoundedRect(-hw, -hh, CARD_W, CARD_H, CARD_R);
|
||
cont.add(g);
|
||
|
||
const corner = (x, y, originX, originY) => {
|
||
const t = this.add.text(x, y, `${card.label}\n${card.suitSymbol}`, {
|
||
fontFamily: '"Julius Sans One"', fontSize: '34px', color: col, align: 'center', lineSpacing: -6,
|
||
}).setOrigin(originX, originY);
|
||
cont.add(t);
|
||
};
|
||
corner(-hw + 16, -hh + 12, 0, 0);
|
||
const br = this.add.text(hw - 16, hh - 12, `${card.label}\n${card.suitSymbol}`, {
|
||
fontFamily: '"Julius Sans One"', fontSize: '34px', color: col, align: 'center', lineSpacing: -6,
|
||
}).setOrigin(0, 0).setAngle(180);
|
||
cont.add(br);
|
||
|
||
const center = this.add.text(0, 0, card.suitSymbol, {
|
||
fontFamily: '"Julius Sans One"', fontSize: '92px', color: col,
|
||
}).setOrigin(0.5);
|
||
cont.add(center);
|
||
}
|
||
|
||
showIdleScreen() {
|
||
for (let i = 0; i < HAND_SIZE; i++) this.renderCard(i, null, false);
|
||
this.setStatus('PLACE YOUR BET • PRESS DEAL', CRT.amberHex);
|
||
}
|
||
|
||
setStatus(msg, colorHex = CRT.amberHex) {
|
||
this.statusText.setText(msg).setColor(colorHex);
|
||
}
|
||
|
||
// ── Pay table panel ──────────────────────────────────────────────────────────
|
||
buildPayTable() {
|
||
const px = 1320, py = MON_Y - 60, pw = GAME_WIDTH - px - 20, ph = MON_H;
|
||
const g = this.add.graphics().setDepth(D.ui);
|
||
g.fillStyle(0x07140f, 0.92);
|
||
g.fillRoundedRect(px, py, pw, ph, 18);
|
||
g.lineStyle(3, CRT.phosphor, 0.65);
|
||
g.strokeRoundedRect(px, py, pw, ph, 18);
|
||
|
||
this.add.text(px + pw / 2, py + 30, 'WINNINGS', {
|
||
fontFamily: 'Righteous', fontSize: '30px', color: CRT.phosphorHex,
|
||
}).setOrigin(0.5).setDepth(D.ui).setShadow(0, 0, 'rgba(57,255,158,0.8)', 12);
|
||
|
||
const nameX = px + 24;
|
||
const colW = 78;
|
||
const colsRight = px + pw - 20;
|
||
const col5X = colsRight;
|
||
const col1X = col5X - 4 * colW;
|
||
this.payColX = [col1X, col1X + colW, col1X + 2 * colW, col1X + 3 * colW, col1X + 4 * colW - 10];
|
||
|
||
// Coin column headers (1–5).
|
||
this.payHeaderY = py + 70;
|
||
for (let c = 0; c < 5; c++) {
|
||
this.add.text(this.payColX[c], this.payHeaderY, String(c + 1), {
|
||
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.mutedHex,
|
||
}).setOrigin(0.5).setDepth(D.ui);
|
||
}
|
||
|
||
// Column highlight bar (moves with the current bet).
|
||
this.payColHi = this.add.graphics().setDepth(D.ui - 0.5);
|
||
|
||
const rowTop = py + 96;
|
||
const rowH = (ph - 96 - 24) / PAY_TABLE.length;
|
||
this.payRowGeo = { rowTop, rowH, px, pw };
|
||
PAY_TABLE.forEach((row, r) => {
|
||
const ry = rowTop + r * rowH + rowH / 2;
|
||
const nameText = this.add.text(nameX, ry, row.name, {
|
||
fontFamily: '"Julius Sans One"', fontSize: '21px', color: CRT.amberHex,
|
||
}).setOrigin(0, 0.5).setDepth(D.ui);
|
||
const cells = [];
|
||
for (let c = 0; c < 5; c++) {
|
||
const t = this.add.text(this.payColX[c], ry, String(row.payouts[c]), {
|
||
fontFamily: '"Julius Sans One"', fontSize: '21px', color: COLORS.textHex,
|
||
}).setOrigin(0.5).setDepth(D.ui);
|
||
cells.push(t);
|
||
}
|
||
this.payRows.push({ nameText, cells, key: row.key });
|
||
});
|
||
|
||
// Per-row highlight (winning hand flash).
|
||
this.payRowHi = this.add.graphics().setDepth(D.ui - 0.5);
|
||
}
|
||
|
||
highlightBetColumn() {
|
||
const { rowTop, rowH } = this.payRowGeo;
|
||
const x = this.payColX[this.state.bet - 1];
|
||
this.payColHi.clear();
|
||
this.payColHi.fillStyle(CRT.amber, 0.14);
|
||
this.payColHi.fillRoundedRect(x - 34, this.payHeaderY - 16, 68, (rowH * PAY_TABLE.length) + 40, 8);
|
||
// Bold the active column header & cells.
|
||
this.payRows.forEach((row) => {
|
||
row.cells.forEach((cell, c) => {
|
||
cell.setColor(c === this.state.bet - 1 ? CRT.amberHex : COLORS.textHex);
|
||
});
|
||
});
|
||
}
|
||
|
||
highlightWinningRow(key) {
|
||
this.payRowHi.clear();
|
||
if (!key) return;
|
||
const idx = PAY_TABLE.findIndex((p) => p.key === key);
|
||
if (idx < 0) return;
|
||
const { rowTop, rowH, px, pw } = this.payRowGeo;
|
||
const ry = rowTop + idx * rowH;
|
||
this.payRowHi.fillStyle(CRT.phosphor, 0.22);
|
||
this.payRowHi.fillRoundedRect(px + 8, ry, pw - 16, rowH, 6);
|
||
const row = this.payRows[idx];
|
||
this.tweens.add({
|
||
targets: [row.nameText, ...row.cells],
|
||
alpha: { from: 0.35, to: 1 }, duration: 220, yoyo: true, repeat: 3,
|
||
});
|
||
}
|
||
|
||
// ── Controls ─────────────────────────────────────────────────────────────────
|
||
buildControls() {
|
||
const y = MON_Y + MON_H + 70;
|
||
const btnOpts = { width: 220, height: 70, fontSize: 24 };
|
||
|
||
this.betOneBtn = new Button(this, MON_X + 130, y, 'BET ONE', () => this.onBetOne(), btnOpts);
|
||
this.betMaxBtn = new Button(this, MON_X + 370, y, 'BET MAX', () => this.onBetMax(), btnOpts);
|
||
this.dealBtn = new Button(this, MON_X + 660, y, 'DEAL', () => this.onDealOrDraw(),
|
||
{ ...btnOpts, width: 260, bg: COLORS.gold, variant: 'solid' });
|
||
this.leaveBtn = new Button(this, MON_X + 940, y, 'Leave', () => this.leave(),
|
||
{ ...btnOpts, variant: 'ghost' });
|
||
|
||
[this.betOneBtn, this.betMaxBtn, this.dealBtn, this.leaveBtn].forEach((b) => b.setDepth(D.ui));
|
||
}
|
||
|
||
buildMeters() {
|
||
const y = MON_Y + MON_H + 18;
|
||
const mk = (x, label, color) => {
|
||
this.add.text(x, y - 14, label, {
|
||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex,
|
||
}).setOrigin(0, 0.5).setDepth(D.ui);
|
||
return this.add.text(x, y + 8, '0', {
|
||
fontFamily: 'Righteous', fontSize: '28px', color,
|
||
}).setOrigin(0, 0.5).setDepth(D.ui).setShadow(0, 0, 'rgba(57,255,158,0.7)', 10);
|
||
};
|
||
this.creditText = mk(1360, 'CREDITS', CRT.phosphorHex);
|
||
this.betText = mk(1560, 'BET', CRT.amberHex);
|
||
this.winText = mk(1700, 'WIN', CRT.amberHex);
|
||
}
|
||
|
||
refreshMeters() {
|
||
this.creditText.setText(this.credits.toLocaleString());
|
||
this.betText.setText(String(this.state.bet));
|
||
const win = this.state.lastWin?.coinsWon ?? 0;
|
||
this.winText.setText(String(win));
|
||
}
|
||
|
||
// ── Input handlers ───────────────────────────────────────────────────────────
|
||
onBetOne() {
|
||
if (this.busy || this.state.phase === 'draw') return;
|
||
const next = this.state.bet >= MAX_BET ? MIN_BET : this.state.bet + 1;
|
||
setBet(this.state, next);
|
||
playSound(this, SFX.CHIP_BET);
|
||
this.highlightBetColumn();
|
||
this.refreshMeters();
|
||
}
|
||
|
||
onBetMax() {
|
||
if (this.busy || this.state.phase === 'draw') return;
|
||
setBet(this.state, MAX_BET);
|
||
playSound(this, SFX.CHIP_BET);
|
||
this.highlightBetColumn();
|
||
this.refreshMeters();
|
||
// Bet Max also deals immediately (classic machine behaviour) when idle.
|
||
if (this.state.phase !== 'draw') this.onDealOrDraw();
|
||
}
|
||
|
||
onDealOrDraw() {
|
||
if (this.busy) return;
|
||
if (this.state.phase === 'draw') this.doDraw();
|
||
else this.doDeal();
|
||
}
|
||
|
||
onSlotClicked(i) {
|
||
if (this.busy || this.state.phase !== 'draw') return;
|
||
toggleHold(this.state, i);
|
||
playSound(this, SFX.CARD_SHOW);
|
||
this.heldTags[i].setVisible(this.state.held[i]);
|
||
// Subtle lift to show the hold.
|
||
this.tweens.add({ targets: this.cardObjs[i], y: this.state.held[i] ? this.slotY - 14 : this.slotY, duration: 120 });
|
||
}
|
||
|
||
// ── Deal / Draw flow ─────────────────────────────────────────────────────────
|
||
doDeal() {
|
||
if (this.credits < this.state.bet) {
|
||
this.setStatus('NOT ENOUGH CREDITS', CRT.heldHex);
|
||
return;
|
||
}
|
||
this.busy = true;
|
||
this.credits -= this.state.bet;
|
||
this.sessionNet -= this.state.bet;
|
||
this.payRowHi.clear();
|
||
this.heldTags.forEach((t) => t.setVisible(false));
|
||
this.cardObjs.forEach((c) => { c.y = this.slotY; });
|
||
|
||
deal(this.state);
|
||
this.refreshMeters();
|
||
this.setStatus('HOLD CARDS • PRESS DRAW', CRT.phosphorHex);
|
||
this.dealBtn.setLabel('DRAW');
|
||
|
||
playSound(this, SFX.CARD_SHUFFLE);
|
||
this.dealSequence(this.state.hand.map((_, i) => i), () => { this.busy = false; });
|
||
}
|
||
|
||
doDraw() {
|
||
this.busy = true;
|
||
const { replaced, win } = drawReplacements(this.state);
|
||
this.setStatus('', CRT.amberHex);
|
||
|
||
const finish = () => {
|
||
this.resolveWin(win);
|
||
this.busy = false;
|
||
};
|
||
if (replaced.length === 0) { finish(); return; }
|
||
playSound(this, SFX.CARD_DEAL);
|
||
this.dealSequence(replaced, finish);
|
||
}
|
||
|
||
// Animate face-up reveals for the given slot indices, one after another.
|
||
dealSequence(indices, done) {
|
||
let n = 0;
|
||
const next = () => {
|
||
if (n >= indices.length) { done && done(); return; }
|
||
const i = indices[n++];
|
||
const card = this.state.hand[i];
|
||
const cont = this.cardObjs[i];
|
||
cont.setScale(1, 0);
|
||
this.renderCard(i, card, true);
|
||
playSound(this, SFX.CARD_DEAL);
|
||
this.tweens.add({
|
||
targets: cont, scaleY: 1, duration: 110, ease: 'Quad.out',
|
||
onComplete: () => this.time.delayedCall(40, next),
|
||
});
|
||
};
|
||
next();
|
||
}
|
||
|
||
resolveWin(win) {
|
||
this.refreshMeters();
|
||
this.highlightWinningRow(win.key);
|
||
this.dealBtn.setLabel('DEAL');
|
||
this.state.phase = 'bet';
|
||
this.handsPlayed += 1;
|
||
|
||
if (win.coinsWon > 0) {
|
||
this.credits += win.coinsWon;
|
||
this.sessionNet += win.coinsWon;
|
||
this.refreshMeters();
|
||
this.setStatus(`${win.name.toUpperCase()} — WIN ${win.coinsWon}`, CRT.phosphorHex);
|
||
playSound(this, win.coinsWon >= 50 ? SFX.COINS : SFX.CASINO_WIN);
|
||
this.recordHistory('win');
|
||
} else {
|
||
this.setStatus('GAME OVER — NO WIN', CRT.heldHex);
|
||
playSound(this, SFX.CASINO_LOSE);
|
||
this.recordHistory('loss');
|
||
}
|
||
// Persist the net change to the shared bankroll after each hand.
|
||
this.flushChips();
|
||
}
|
||
|
||
async leave() {
|
||
await this.flushChips();
|
||
this.scene.start('GameMenu');
|
||
}
|
||
}
|