feat: overhaul player turn order and add action callouts
- Replace left-to-right seat processing with clockwise PLAY_ORDER starting top-right - Animate deal sequence to follow the new play order - Add pulsing highlight ring for active player's portrait - Show large animated action callouts (Hit/Stand/Double/Split) between player and cards - Position action buttons dynamically relative to the active player's portrait - Update result reveal order to match clockwise play order - Move betting UI panel and adjust button depths for proper layering - Remove unused `nextActiveSeat` import in favor of `nextPlaySeat` method
This commit is contained in:
parent
349d2736c9
commit
02d10faa2d
|
|
@ -10,7 +10,7 @@ import {
|
|||
buildShoe, handValue, isBlackjack, isBust, canDouble, canSplit,
|
||||
createInitialState, prepareRound, applyBet, dealAllCards,
|
||||
applyInsurance, resolveInsurance, applyHit, applyStand, applyDouble, applySplit,
|
||||
runDealerTurn, resolveRound, nextActiveSeat, isPlayerDone,
|
||||
runDealerTurn, resolveRound, isPlayerDone,
|
||||
} from './BlackjackLogic.js';
|
||||
import { chooseBet, chooseAction, chooseInsurance } from './BlackjackAI.js';
|
||||
|
||||
|
|
@ -28,15 +28,22 @@ const DEALER_Y = 420; // ~1/3 down the table ellipse (top ≈190, bottom ≈890)
|
|||
// 6 opponents fanned 3-per-side (filled outward-alternating so the table stays
|
||||
// balanced when fewer than 6 opponents are chosen).
|
||||
const SEAT_POS = [
|
||||
{ x: CX, y: 860, portraitR: 72, portraitX: CX - 230, portraitY: 860, betX: 1110, betY: 770 }, // 0 Human (bottom centre)
|
||||
{ x: 1490, y: 745, portraitR: 58, portraitX: 1600, portraitY: 800 }, // 1 right-bottom
|
||||
{ x: 430, y: 745, portraitR: 58, portraitX: 320, portraitY: 800 }, // 2 left-bottom
|
||||
{ x: 1670, y: 595, portraitR: 58, portraitX: 1805, portraitY: 610 }, // 3 right-mid
|
||||
{ x: 250, y: 595, portraitR: 58, portraitX: 115, portraitY: 610 }, // 4 left-mid
|
||||
{ x: 1585, y: 375, portraitR: 58, portraitX: 1705, portraitY: 335, labelDX: 10 }, // 5 right-top
|
||||
{ x: 335, y: 375, portraitR: 58, portraitX: 215, portraitY: 335, labelDX: -10 }, // 6 left-top
|
||||
{ x: CX, y: 782, portraitR: 72, portraitX: CX - 230, portraitY: 860, betX: 1110, betY: 770 }, // 0 Human (bottom centre)
|
||||
{ x: 1380, y: 710, portraitR: 58, portraitX: 1600, portraitY: 800 }, // 1 right-bottom
|
||||
{ x: 540, y: 710, portraitR: 58, portraitX: 320, portraitY: 800 }, // 2 left-bottom
|
||||
{ x: 1560, y: 588, portraitR: 58, portraitX: 1805, portraitY: 610 }, // 3 right-mid
|
||||
{ x: 360, y: 588, portraitR: 58, portraitX: 115, portraitY: 610 }, // 4 left-mid
|
||||
{ x: 1470, y: 410, portraitR: 58, portraitX: 1705, portraitY: 335, labelDX: 10 }, // 5 right-top
|
||||
{ x: 450, y: 410, portraitR: 58, portraitX: 215, portraitY: 335, labelDX: -10 }, // 6 left-top
|
||||
];
|
||||
|
||||
// Turn order: start at the top-right seat and proceed clockwise around the table.
|
||||
const PLAY_ORDER = [5, 3, 1, 0, 2, 4, 6];
|
||||
|
||||
// Large per-action callout shown between a player and their cards.
|
||||
const ACTION_LABELS = { hit: 'Hit', stand: 'Stand', double: 'Double', split: 'Split' };
|
||||
const ACTION_COLORS = { hit: '#3cc6c0', stand: '#f5d020', double: '#ff8c00', split: '#b07cd6' };
|
||||
|
||||
const CHIP_COLORS = { 5: 0xe05c5c, 25: 0x5cb85c, 50: 0x4a90d9, 100: 0x2c2c2c };
|
||||
const CHIP_TEXT_COLORS = { 5: '#ffffff', 25: '#ffffff', 50: '#ffffff', 100: '#ffffff' };
|
||||
const CHIP_AMOUNTS = [5, 25, 50, 100];
|
||||
|
|
@ -253,6 +260,14 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
const y = GAME_HEIGHT - 80;
|
||||
const cx = CX + 60; // shift right slightly to avoid portrait
|
||||
|
||||
// Solid window behind all the betting controls (shown between rounds)
|
||||
const panel = this.add.graphics().setDepth(D.ui);
|
||||
panel.fillStyle(0x000000, 1);
|
||||
panel.fillRoundedRect(cx - 174, y - 69, 726, 116, 18);
|
||||
panel.lineStyle(3, COLORS.accent, 1);
|
||||
panel.strokeRoundedRect(cx - 174, y - 69, 726, 116, 18);
|
||||
this.bettingUIGroup.push(panel);
|
||||
|
||||
// Chip buttons
|
||||
CHIP_AMOUNTS.forEach((amt, i) => {
|
||||
const bx = cx - 120 + i * 80;
|
||||
|
|
@ -292,6 +307,9 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
width: 110, height: 50, fontSize: 20,
|
||||
});
|
||||
this.dealBtn.setEnabled(false);
|
||||
// Keep the buttons above the betting window panel
|
||||
clearBtn.setDepth(D.ui + 1);
|
||||
this.dealBtn.setDepth(D.ui + 1);
|
||||
|
||||
this.bettingUIGroup.push(this.betDisplayText, this.balanceText, clearBtn, this.dealBtn);
|
||||
this.hideBettingUI();
|
||||
|
|
@ -451,6 +469,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
this.bustedSeats = new Set();
|
||||
this.gs = prepareRound(this.gs);
|
||||
this.clearCardGraphics();
|
||||
this.clearActiveHighlight();
|
||||
this.renderAll();
|
||||
this.showBettingUI();
|
||||
this.updateBetDisplay();
|
||||
|
|
@ -735,12 +754,13 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
animateDeal(onComplete) {
|
||||
// Build deal order: player0-c1, p1-c1, ..., dealer-upcard, p0-c2, p1-c2, ..., dealer-hole
|
||||
const activePlayers = this.gs.players.filter(p => p.active);
|
||||
// Deal in play order (top-right, clockwise): each player card 1, dealer upcard,
|
||||
// each player card 2, dealer hole.
|
||||
const seats = PLAY_ORDER.filter(seat => this.gs.players[seat]?.active);
|
||||
const dealSeq = [];
|
||||
for (const p of activePlayers) dealSeq.push({ type: 'player', seat: p.seat, cardIdx: 0 });
|
||||
for (const seat of seats) dealSeq.push({ type: 'player', seat, cardIdx: 0 });
|
||||
dealSeq.push({ type: 'dealer', cardIdx: 0 }); // upcard
|
||||
for (const p of activePlayers) dealSeq.push({ type: 'player', seat: p.seat, cardIdx: 1 });
|
||||
for (const seat of seats) dealSeq.push({ type: 'player', seat, cardIdx: 1 });
|
||||
dealSeq.push({ type: 'dealer', cardIdx: 1 }); // hole card
|
||||
|
||||
// Hide all cards initially
|
||||
|
|
@ -829,8 +849,8 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
const tx = this.cardX(pos.x, cardIdx, total);
|
||||
const ty = pos.y;
|
||||
|
||||
// Flying card-back
|
||||
const flying = this.add.container(CX + 200, 50).setDepth(D.cards + 10);
|
||||
// Flying card-back dealt from the dealer's area
|
||||
const flying = this.add.container(DEALER_X, DEALER_Y).setDepth(D.cards + 10);
|
||||
this.addCardBackToContainer(flying);
|
||||
|
||||
this.tweens.add({
|
||||
|
|
@ -850,6 +870,67 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
});
|
||||
}
|
||||
|
||||
// ── Per-turn action callout ──────────────────────────────────────────────
|
||||
// Large colored word (Hit/Stand/Double/Split) shown between the player and
|
||||
// their cards; lingers >= 1s before fading. Fire-and-forget (no flow gating).
|
||||
animateActionText(seat, action) {
|
||||
const label = ACTION_LABELS[action];
|
||||
if (!label) return;
|
||||
const pos = SEAT_POS[seat];
|
||||
const px = pos.portraitX ?? pos.x;
|
||||
const py = pos.portraitY ?? pos.y;
|
||||
const tx = (px + pos.x) / 2;
|
||||
const ty = (py + pos.y) / 2;
|
||||
|
||||
const txt = this.add.text(tx, ty, label, {
|
||||
fontFamily: '"Julius Sans One"',
|
||||
fontSize: '44px',
|
||||
color: ACTION_COLORS[action] ?? '#ffffff',
|
||||
fontStyle: 'bold',
|
||||
stroke: '#000000', strokeThickness: 5,
|
||||
}).setOrigin(0.5).setAlpha(0).setScale(1.4).setDepth(D.modal);
|
||||
|
||||
this.tweens.add({
|
||||
targets: txt, alpha: 1, scaleX: 1, scaleY: 1,
|
||||
duration: 180, ease: 'Back.Out',
|
||||
});
|
||||
this.time.delayedCall(1000, () => {
|
||||
this.tweens.add({
|
||||
targets: txt, alpha: 0, y: ty - 26,
|
||||
duration: 350, ease: 'Power2',
|
||||
onComplete: () => txt.destroy(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Active-player turn highlight (pulsing ring around the portrait) ────────
|
||||
highlightActiveSeat(seat) {
|
||||
const pos = SEAT_POS[seat];
|
||||
const px = pos.portraitX ?? pos.x;
|
||||
const py = pos.portraitY ?? pos.y;
|
||||
|
||||
if (!this.activeRing) {
|
||||
this.activeRing = this.add.graphics().setDepth(D.ui - 1);
|
||||
}
|
||||
this.activeRing.clear();
|
||||
this.activeRing.lineStyle(5, COLORS.accent, 0.9);
|
||||
this.activeRing.strokeCircle(0, 0, pos.portraitR + 8);
|
||||
this.activeRing.setPosition(px, py).setVisible(true).setScale(1).setAlpha(1);
|
||||
|
||||
this.activeRingTween?.stop();
|
||||
this.activeRingTween = this.tweens.add({
|
||||
targets: this.activeRing,
|
||||
scaleX: 1.12, scaleY: 1.12, alpha: 0.45,
|
||||
duration: 650, ease: 'Sine.easeInOut', yoyo: true, repeat: -1,
|
||||
});
|
||||
}
|
||||
|
||||
clearActiveHighlight() {
|
||||
this.activeRingTween?.stop();
|
||||
this.activeRingTween = null;
|
||||
if (this.activeRing) this.activeRing.setVisible(false);
|
||||
}
|
||||
|
||||
// ── Per-seat result animation (text + chips + fireworks) ─────────────────
|
||||
animateSeatResult(seat, onComplete) {
|
||||
const p = this.gs.players[seat];
|
||||
|
|
@ -1054,14 +1135,29 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
// ── Player turns ──────────────────────────────────────────────────────────
|
||||
// First seat to act, walking PLAY_ORDER (clockwise from top-right).
|
||||
firstPlaySeat() {
|
||||
for (const seat of PLAY_ORDER) {
|
||||
if (this.gs.players[seat]?.active) return seat;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Next seat in PLAY_ORDER after the current one that still needs to act.
|
||||
nextPlaySeat() {
|
||||
const start = PLAY_ORDER.indexOf(this.gs.currentSeat);
|
||||
for (let i = start + 1; i < PLAY_ORDER.length; i++) {
|
||||
const p = this.gs.players[PLAY_ORDER[i]];
|
||||
if (p?.active && (p.status === 'playing' || p.status === 'waiting')) return PLAY_ORDER[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
startPlayerTurns() {
|
||||
this.gs = { ...this.gs, phase: 'player_turn' };
|
||||
// Mark first active player as 'playing'
|
||||
const p = this.gs.players.find(pl => pl.active);
|
||||
if (p) {
|
||||
this.gs = { ...this.gs, currentSeat: p.seat };
|
||||
}
|
||||
this.advanceToPlayer(p?.seat ?? 0);
|
||||
const seat = this.firstPlaySeat();
|
||||
this.gs = { ...this.gs, currentSeat: seat };
|
||||
this.advanceToPlayer(seat);
|
||||
}
|
||||
|
||||
advanceToPlayer(seat) {
|
||||
|
|
@ -1071,13 +1167,14 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
|
||||
// Skip if already determined (blackjack on deal)
|
||||
if (isPlayerDone(p)) {
|
||||
const next = nextActiveSeat(this.gs);
|
||||
const next = this.nextPlaySeat();
|
||||
if (next === null) { this.startDealerTurn(); return; }
|
||||
this.gs = { ...this.gs, currentSeat: next };
|
||||
this.advanceToPlayer(next);
|
||||
return;
|
||||
}
|
||||
|
||||
this.highlightActiveSeat(seat);
|
||||
if (p.isHuman) {
|
||||
this.showActionButtons();
|
||||
} else {
|
||||
|
|
@ -1090,17 +1187,21 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
const seat = this.gs.currentSeat;
|
||||
const p = this.gs.players[seat];
|
||||
const pos = SEAT_POS[seat];
|
||||
const by = GAME_HEIGHT - 80;
|
||||
const by = 918; // just under the table felt
|
||||
let bx = pos.portraitX + pos.portraitR + 80; // first button, just right of the portrait
|
||||
|
||||
const btns = [
|
||||
new Button(this, CX - 180, by, 'Hit', () => this.onHit(), { width: 120, height: 52, fontSize: 20 }),
|
||||
new Button(this, CX - 50, by, 'Stand', () => this.onStand(), { width: 120, height: 52, fontSize: 20 }),
|
||||
new Button(this, bx, by, 'Hit', () => this.onHit(), { width: 120, height: 52, fontSize: 20 }),
|
||||
];
|
||||
bx += 130;
|
||||
btns.push(new Button(this, bx, by, 'Stand', () => this.onStand(), { width: 120, height: 52, fontSize: 20 }));
|
||||
if (canDouble(p)) {
|
||||
btns.push(new Button(this, CX + 80, by, 'Double', () => this.onDouble(), { width: 130, height: 52, fontSize: 20 }));
|
||||
bx += 130;
|
||||
btns.push(new Button(this, bx, by, 'Double', () => this.onDouble(), { width: 130, height: 52, fontSize: 20 }));
|
||||
}
|
||||
if (canSplit(p) && p.chips >= p.bet) {
|
||||
btns.push(new Button(this, CX + 220, by, 'Split', () => this.onSplit(), { width: 120, height: 52, fontSize: 20 }));
|
||||
bx += 140;
|
||||
btns.push(new Button(this, bx, by, 'Split', () => this.onSplit(), { width: 120, height: 52, fontSize: 20 }));
|
||||
}
|
||||
btns.forEach(btn => btn.setDepth(D.ui + 5));
|
||||
this.actionBtns = btns;
|
||||
|
|
@ -1127,6 +1228,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
|
||||
onHit() {
|
||||
this.hideActionButtons();
|
||||
this.animateActionText(this.gs.currentSeat, 'hit');
|
||||
this.animating = true;
|
||||
this.gs = applyHit(this.gs, this.gs.currentSeat, this.shoe);
|
||||
const p = this.gs.players[this.gs.currentSeat];
|
||||
|
|
@ -1151,6 +1253,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
onStand() {
|
||||
this.hideActionButtons();
|
||||
const seat = this.gs.currentSeat;
|
||||
this.animateActionText(seat, 'stand');
|
||||
this.gs = applyStand(this.gs, seat);
|
||||
const p = this.gs.players[seat];
|
||||
// If split and now playing hand 2, stay on same seat
|
||||
|
|
@ -1163,6 +1266,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
|
||||
onDouble() {
|
||||
this.hideActionButtons();
|
||||
this.animateActionText(this.gs.currentSeat, 'double');
|
||||
this.animating = true;
|
||||
this.gs = applyDouble(this.gs, this.gs.currentSeat, this.shoe);
|
||||
this.renderSeatCards(this.gs.currentSeat);
|
||||
|
|
@ -1181,6 +1285,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
|
||||
onSplit() {
|
||||
this.hideActionButtons();
|
||||
this.animateActionText(this.gs.currentSeat, 'split');
|
||||
this.animating = true;
|
||||
this.gs = applySplit(this.gs, this.gs.currentSeat, this.shoe);
|
||||
this.renderSeatCards(this.gs.currentSeat);
|
||||
|
|
@ -1205,7 +1310,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
this.showActionButtons();
|
||||
return;
|
||||
}
|
||||
const next = nextActiveSeat(this.gs);
|
||||
const next = this.nextPlaySeat();
|
||||
if (next === null) {
|
||||
this.startDealerTurn();
|
||||
} else {
|
||||
|
|
@ -1223,6 +1328,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
const action = chooseAction(p, this.gs.dealer.hand[0]);
|
||||
this.animateActionText(seat, action);
|
||||
this.animating = true;
|
||||
|
||||
const doAction = () => {
|
||||
|
|
@ -1293,6 +1399,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
|
||||
// ── Dealer turn ───────────────────────────────────────────────────────────
|
||||
startDealerTurn() {
|
||||
this.clearActiveHighlight();
|
||||
this.gs = { ...this.gs, phase: 'dealer_turn', currentSeat: -1 };
|
||||
this.renderAll();
|
||||
this.time.delayedCall(400, () => {
|
||||
|
|
@ -1325,17 +1432,15 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
showAllResults(onComplete) {
|
||||
const active = this.gs.players.filter(p => p.active);
|
||||
if (active.length === 0) { onComplete(); return; }
|
||||
// Reveal top-right then clockwise around the table (PLAY_ORDER).
|
||||
const seats = PLAY_ORDER.filter(seat => this.gs.players[seat]?.active);
|
||||
if (seats.length === 0) { onComplete(); return; }
|
||||
|
||||
// Reveal right-to-left (descending seat x position)
|
||||
const sorted = [...active].sort((a, b) => SEAT_POS[b.seat].x - SEAT_POS[a.seat].x);
|
||||
|
||||
let pending = sorted.length;
|
||||
let pending = seats.length;
|
||||
const done = () => { if (--pending <= 0) onComplete(); };
|
||||
|
||||
sorted.forEach((p, i) => {
|
||||
this.time.delayedCall(i * 480, () => this.animateSeatResult(p.seat, done));
|
||||
seats.forEach((seat, i) => {
|
||||
this.time.delayedCall(i * 480, () => this.animateSeatResult(seat, done));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1360,7 +1465,7 @@ export default class BlackjackGame extends Phaser.Scene {
|
|||
return;
|
||||
}
|
||||
|
||||
const btn = new Button(this, CX, GAME_HEIGHT / 2 + 40, 'Next Round', () => {
|
||||
const btn = new Button(this, CX, 925, 'Next Round', () => {
|
||||
btn.destroy();
|
||||
this.startNewRound();
|
||||
}, { width: 220, height: 60, fontSize: 24 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue