feat: overhaul visual theme to vintage gold and update UI components

- Update color palette to warm vintage tones (gold, cream, dark brown) in config.js
- Apply 'Righteous' and 'Julius Sans One' fonts across all game scenes and UI components
- Redesign Parchisi game logic: switch to counter-clockwise movement, update entry/home positions, and add bonus chip indicator
- Improve Parchisi UI: reposition dice and buttons, add turn indicator movement with callbacks, and fix AI turn timing
- Enhance LandingScene with animated logo, avatar support, and improved layout
- Update Button component with rounded corners, shadow effects, and hover states
- Add background image and main title asset loading in PreloadScene
- Extend auth service to include displayName and avatarPath in session user data
- Update CSS to match new theme and load custom fonts
This commit is contained in:
Brian Fertig 2026-05-16 21:04:16 -06:00
parent d206cf6e5b
commit 56f1cdd752
21 changed files with 350 additions and 195 deletions

View File

@ -2,18 +2,22 @@ export const GAME_WIDTH = 1920;
export const GAME_HEIGHT = 1080; export const GAME_HEIGHT = 1080;
export const COLORS = { export const COLORS = {
bg: 0x0a0e14, bg: 0x0f0d0a,
bgHex: '#0a0e14', bgHex: '#0f0d0a',
panel: 0x111923, panel: 0x1e1a12,
panelHex: '#111923', panelHex: '#1e1a12',
accent: 0x5aa9e6, accent: 0xc8a84b,
accentHex: '#5aa9e6', accentHex: '#c8a84b',
text: 0xe6edf3, text: 0xf2ead8,
textHex: '#e6edf3', textHex: '#f2ead8',
muted: 0x8a94a6, muted: 0x9e9080,
mutedHex: '#8a94a6', mutedHex: '#9e9080',
danger: 0xe06c75, danger: 0xe06c75,
dangerHex: '#e06c75', dangerHex: '#e06c75',
gold: 0xd4a017,
goldHex: '#d4a017',
textDark: 0x1a1208,
textDarkHex: '#1a1208',
}; };
export const API_BASE = '/api'; export const API_BASE = '/api';

View File

@ -151,7 +151,7 @@ export default class BackgammonGame extends Phaser.Scene {
const isBottom = idx < 12; const isBottom = idx < 12;
const ly = isBottom ? FY + FH + 14 : FY - 14; const ly = isBottom ? FY + FH + 14 : FY - 14;
this.add.text(cx, ly, label, { this.add.text(cx, ly, label, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '18px', fontSize: '18px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.board); }).setOrigin(0.5).setDepth(DEPTH.board);
@ -181,20 +181,20 @@ export default class BackgammonGame extends Phaser.Scene {
// Labels // Labels
this.add.text(BEAR_X + BEAR_W / 2, FY + 18, 'OFF', { this.add.text(BEAR_X + BEAR_W / 2, FY + 18, 'OFF', {
fontFamily: 'system-ui, sans-serif', fontSize: '16px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.board); }).setOrigin(0.5).setDepth(DEPTH.board);
this.add.text(BEAR_X + BEAR_W / 2, FY + FH - 18, 'OFF', { this.add.text(BEAR_X + BEAR_W / 2, FY + FH - 18, 'OFF', {
fontFamily: 'system-ui, sans-serif', fontSize: '16px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.board); }).setOrigin(0.5).setDepth(DEPTH.board);
// Pip count labels // Pip count labels
this.pipBlackText = this.add.text(BEAR_X + BEAR_W / 2, FY + FH / 4, '0', { this.pipBlackText = this.add.text(BEAR_X + BEAR_W / 2, FY + FH / 4, '0', {
fontFamily: 'system-ui, sans-serif', fontSize: '28px', color: '#3a3a4e', fontFamily: '"Julius Sans One"', fontSize: '28px', color: '#3a3a4e',
}).setOrigin(0.5).setDepth(DEPTH.ui); }).setOrigin(0.5).setDepth(DEPTH.ui);
this.pipWhiteText = this.add.text(BEAR_X + BEAR_W / 2, FY + 3 * FH / 4, '0', { this.pipWhiteText = this.add.text(BEAR_X + BEAR_W / 2, FY + 3 * FH / 4, '0', {
fontFamily: 'system-ui, sans-serif', fontSize: '28px', color: '#d4c5a0', fontFamily: '"Julius Sans One"', fontSize: '28px', color: '#d4c5a0',
}).setOrigin(0.5).setDepth(DEPTH.ui); }).setOrigin(0.5).setDepth(DEPTH.ui);
} }
@ -246,13 +246,13 @@ export default class BackgammonGame extends Phaser.Scene {
// Status message (also serves as turn label at bottom) // Status message (also serves as turn label at bottom)
this.turnText = this.add.text(cx, BY + BH + 18, '', { this.turnText = this.add.text(cx, BY + BH + 18, '', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '24px', fontSize: '24px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.ui); }).setOrigin(0.5).setDepth(DEPTH.ui);
this.statusText = this.add.text(cx, BY + BH + 46, '', { this.statusText = this.add.text(cx, BY + BH + 46, '', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '22px', fontSize: '22px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.ui); }).setOrigin(0.5).setDepth(DEPTH.ui);
@ -289,7 +289,7 @@ export default class BackgammonGame extends Phaser.Scene {
this.add.circle(avatarX, oppAY, r + 5, C.barWood).setDepth(depth); this.add.circle(avatarX, oppAY, r + 5, C.barWood).setDepth(depth);
this.opponentPortrait = createOpponentPortrait(this, opp, avatarX, oppAY, r, depth + 1); this.opponentPortrait = createOpponentPortrait(this, opp, avatarX, oppAY, r, depth + 1);
this.add.text(avatarX, oppAY + r + 14, opp?.name ?? 'CPU', { this.add.text(avatarX, oppAY + r + 14, opp?.name ?? 'CPU', {
fontFamily: 'system-ui, sans-serif', fontSize: '18px', fontFamily: '"Julius Sans One"', fontSize: '18px',
color: COLORS.textHex, wordWrap: { width: 240 }, align: 'center', color: COLORS.textHex, wordWrap: { width: 240 }, align: 'center',
}).setOrigin(0.5, 0).setDepth(depth + 2); }).setOrigin(0.5, 0).setDepth(depth + 2);
@ -298,7 +298,7 @@ export default class BackgammonGame extends Phaser.Scene {
this.add.circle(avatarX, plrAY, r + 5, COLORS.accent, 0.5).setDepth(depth); this.add.circle(avatarX, plrAY, r + 5, COLORS.accent, 0.5).setDepth(depth);
createPlayerPortrait(this, avatarX, plrAY, r, depth + 1, 'Backgammon'); createPlayerPortrait(this, avatarX, plrAY, r, depth + 1, 'Backgammon');
this.add.text(avatarX, plrAY - r - 14, auth.user?.username ?? 'You', { this.add.text(avatarX, plrAY - r - 14, auth.user?.username ?? 'You', {
fontFamily: 'system-ui, sans-serif', fontSize: '18px', fontFamily: '"Julius Sans One"', fontSize: '18px',
color: COLORS.textHex, wordWrap: { width: 240 }, align: 'center', color: COLORS.textHex, wordWrap: { width: 240 }, align: 'center',
}).setOrigin(0.5, 1).setDepth(depth + 2); }).setOrigin(0.5, 1).setDepth(depth + 2);
} }
@ -352,7 +352,7 @@ export default class BackgammonGame extends Phaser.Scene {
if (pt.count > 5) { if (pt.count > 5) {
const pos = this.checkerScreenPos(idx, 4); const pos = this.checkerScreenPos(idx, 4);
this.add.text(pos.x, pos.y, String(pt.count), { this.add.text(pos.x, pos.y, String(pt.count), {
fontFamily: 'system-ui, sans-serif', fontSize: '22px', fontFamily: '"Julius Sans One"', fontSize: '22px',
color: pt.color === 'white' ? '#2c1a0e' : '#f0e8d0', color: pt.color === 'white' ? '#2c1a0e' : '#f0e8d0',
}).setOrigin(0.5).setDepth(DEPTH.checker + 1); }).setOrigin(0.5).setDepth(DEPTH.checker + 1);
} }
@ -377,7 +377,7 @@ export default class BackgammonGame extends Phaser.Scene {
if (count > 4) { if (count > 4) {
const dy = color === 'white' ? 3 * (CR * 2 + 2) : -(3 * (CR * 2 + 2)); const dy = color === 'white' ? 3 * (CR * 2 + 2) : -(3 * (CR * 2 + 2));
this.add.text(barCX, baseY + dy, `×${count}`, { this.add.text(barCX, baseY + dy, `×${count}`, {
fontFamily: 'system-ui, sans-serif', fontSize: '20px', fontFamily: '"Julius Sans One"', fontSize: '20px',
color: color === 'white' ? COLORS.textHex : COLORS.mutedHex, color: color === 'white' ? COLORS.textHex : COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.checker + 1); }).setOrigin(0.5).setDepth(DEPTH.checker + 1);
} }
@ -817,7 +817,7 @@ export default class BackgammonGame extends Phaser.Scene {
showTurnBanner(text) { showTurnBanner(text) {
const cx = BX + BW / 2; const cx = BX + BW / 2;
const banner = this.add.text(cx, BY - 80, text, { const banner = this.add.text(cx, BY - 80, text, {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '36px', fontSize: '36px',
color: COLORS.textHex, color: COLORS.textHex,
backgroundColor: '#111923ee', backgroundColor: '#111923ee',
@ -888,7 +888,7 @@ export default class BackgammonGame extends Phaser.Scene {
.setStrokeStyle(3, COLORS.accent) .setStrokeStyle(3, COLORS.accent)
.setDepth(DEPTH.banner); .setDepth(DEPTH.banner);
const txt = this.add.text(BX + BW / 2, BY + BH / 2 - 40, msg, { const txt = this.add.text(BX + BW / 2, BY + BH / 2 - 40, msg, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '32px', fontSize: '32px',
color: isHuman ? '#ffd700' : COLORS.textHex, color: isHuman ? '#ffd700' : COLORS.textHex,
align: 'center', align: 'center',

View File

@ -116,15 +116,15 @@ export default class BlackjackGame extends Phaser.Scene {
// ── Dealer area ─────────────────────────────────────────────────────────── // ── Dealer area ───────────────────────────────────────────────────────────
buildDealerArea() { buildDealerArea() {
this.add.text(CX, 60, 'Blackjack', { this.add.text(CX, 60, 'Blackjack', {
fontFamily: 'system-ui, sans-serif', fontSize: '52px', color: COLORS.textHex, fontFamily: 'Righteous', fontSize: '52px', color: COLORS.textHex,
}).setOrigin(0.5).setDepth(D.ui); }).setOrigin(0.5).setDepth(D.ui);
this.add.text(CX, 110, 'Dealer stands on all 17s · Blackjack pays 3:2', { this.add.text(CX, 110, 'Dealer stands on all 17s · Blackjack pays 3:2', {
fontFamily: 'system-ui, sans-serif', fontSize: '18px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(D.ui); }).setOrigin(0.5).setDepth(D.ui);
this.dealerScoreTxt = this.add.text(CX, DEALER_Y - CARD_H / 2 - 22, '', { this.dealerScoreTxt = this.add.text(CX, DEALER_Y - CARD_H / 2 - 22, '', {
fontFamily: 'system-ui, sans-serif', fontSize: '22px', color: COLORS.textHex, fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex,
}).setOrigin(0.5).setDepth(D.ui); }).setOrigin(0.5).setDepth(D.ui);
} }
@ -159,11 +159,11 @@ export default class BlackjackGame extends Phaser.Scene {
} }
this.nameTxts[seat] = this.add.text(nameX, nameY, player.name, { this.nameTxts[seat] = this.add.text(nameX, nameY, player.name, {
fontFamily: 'system-ui, sans-serif', fontSize: '20px', color: COLORS.textHex, fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex,
}).setOrigin(labelAnchorX, 0.5).setDepth(D.ui); }).setOrigin(labelAnchorX, 0.5).setDepth(D.ui);
this.chipTxts[seat] = this.add.text(chipX, chipY, '', { this.chipTxts[seat] = this.add.text(chipX, chipY, '', {
fontFamily: 'system-ui, sans-serif', fontSize: '18px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
}).setOrigin(labelAnchorX, 0.5).setDepth(D.ui); }).setOrigin(labelAnchorX, 0.5).setDepth(D.ui);
// Semi-transparent backing behind both text lines, depth below portrait images/videos // Semi-transparent backing behind both text lines, depth below portrait images/videos
@ -180,7 +180,7 @@ export default class BlackjackGame extends Phaser.Scene {
} }
this.scoreTxts[seat] = this.add.text(pos.x, pos.y - CARD_H / 2 - 11, '', { this.scoreTxts[seat] = this.add.text(pos.x, pos.y - CARD_H / 2 - 11, '', {
fontFamily: 'system-ui, sans-serif', fontSize: '20px', color: COLORS.textHex, fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex,
}).setOrigin(0.5).setDepth(D.ui); }).setOrigin(0.5).setDepth(D.ui);
// Portraits // Portraits
@ -215,7 +215,7 @@ export default class BlackjackGame extends Phaser.Scene {
container.on('pointerout', () => container.setAlpha(1)); container.on('pointerout', () => container.setAlpha(1));
const t = this.add.text(bx, y, `$${amt}`, { const t = this.add.text(bx, y, `$${amt}`, {
fontFamily: 'system-ui, sans-serif', fontSize: '13px', fontFamily: '"Julius Sans One"', fontSize: '13px',
color: CHIP_TEXT_COLORS[amt], fontStyle: 'bold', color: CHIP_TEXT_COLORS[amt], fontStyle: 'bold',
}).setOrigin(0.5).setDepth(D.ui + 2); }).setOrigin(0.5).setDepth(D.ui + 2);
@ -224,11 +224,11 @@ export default class BlackjackGame extends Phaser.Scene {
}); });
this.betDisplayText = this.add.text(cx + 170, y, 'Bet: $0', { this.betDisplayText = this.add.text(cx + 170, y, 'Bet: $0', {
fontFamily: 'system-ui, sans-serif', fontSize: '22px', color: COLORS.textHex, fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex,
}).setOrigin(0, 0.5).setDepth(D.ui + 1); }).setOrigin(0, 0.5).setDepth(D.ui + 1);
this.balanceText = this.add.text(cx + 170, y - 32, '', { this.balanceText = this.add.text(cx + 170, y - 32, '', {
fontFamily: 'system-ui, sans-serif', fontSize: '18px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
}).setOrigin(0, 0.5).setDepth(D.ui + 1); }).setOrigin(0, 0.5).setDepth(D.ui + 1);
const clearBtn = new Button(this, cx + 360, y, 'Clear', () => this.onClearBet(), { const clearBtn = new Button(this, cx + 360, y, 'Clear', () => this.onClearBet(), {
@ -296,7 +296,7 @@ export default class BlackjackGame extends Phaser.Scene {
// Create prompt text // Create prompt text
const text = this.add.text(cx + 100, y, 'Choose an amount to bet and click Deal to begin', { const text = this.add.text(cx + 100, y, 'Choose an amount to bet and click Deal to begin', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '18px', fontSize: '18px',
color: '#ffffff', color: '#ffffff',
align: 'center', align: 'center',
@ -512,7 +512,7 @@ export default class BlackjackGame extends Phaser.Scene {
circle.lineStyle(2, 0xf0e8d0, 0.8); circle.lineStyle(2, 0xf0e8d0, 0.8);
circle.strokeCircle(0, 0, 28); circle.strokeCircle(0, 0, 28);
const txt = this.add.text(0, 0, `$${p.bet}`, { const txt = this.add.text(0, 0, `$${p.bet}`, {
fontFamily: 'system-ui, sans-serif', fontSize: '14px', color: '#f0e8d0', fontFamily: '"Julius Sans One"', fontSize: '14px', color: '#f0e8d0',
}).setOrigin(0.5); }).setOrigin(0.5);
cont.add([circle, txt]); cont.add([circle, txt]);
this.betGraphics[seat] = cont; this.betGraphics[seat] = cont;
@ -575,7 +575,7 @@ export default class BlackjackGame extends Phaser.Scene {
container.add(g); container.add(g);
const color = card.isRed ? '#c0392b' : '#1a1a2e'; const color = card.isRed ? '#c0392b' : '#1a1a2e';
const style = (sz, bold = false) => ({ const style = (sz, bold = false) => ({
fontFamily: 'system-ui, sans-serif', fontSize: `${sz}px`, color, fontFamily: '"Julius Sans One"', fontSize: `${sz}px`, color,
...(bold ? { fontStyle: 'bold' } : {}), ...(bold ? { fontStyle: 'bold' } : {}),
}); });
container.add(this.add.text(x + 7, y + 5, card.label, style(17, true))); container.add(this.add.text(x + 7, y + 5, card.label, style(17, true)));
@ -811,7 +811,7 @@ export default class BlackjackGame extends Phaser.Scene {
const textY = pos.y - CARD_H / 2 - 60; const textY = pos.y - CARD_H / 2 - 60;
const badge = this.add.text(pos.x, textY, LABELS[result] ?? result, { const badge = this.add.text(pos.x, textY, LABELS[result] ?? result, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: SIZES[result] ?? '48px', fontSize: SIZES[result] ?? '48px',
color: COLORS[result] ?? '#ffffff', color: COLORS[result] ?? '#ffffff',
fontStyle: 'bold', fontStyle: 'bold',
@ -953,7 +953,7 @@ export default class BlackjackGame extends Phaser.Scene {
const modal = this.add.container(CX, GAME_HEIGHT / 2).setDepth(D.modal); const modal = this.add.container(CX, GAME_HEIGHT / 2).setDepth(D.modal);
const bg = this.add.rectangle(0, 0, 560, 200, COLORS.panel).setStrokeStyle(2, COLORS.accent); const bg = this.add.rectangle(0, 0, 560, 200, COLORS.panel).setStrokeStyle(2, COLORS.accent);
const txt = this.add.text(0, -55, 'Dealer shows Ace\nTake insurance? (pays 2:1)', { const txt = this.add.text(0, -55, 'Dealer shows Ace\nTake insurance? (pays 2:1)', {
fontFamily: 'system-ui, sans-serif', fontSize: '24px', color: COLORS.textHex, align: 'center', fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex, align: 'center',
}).setOrigin(0.5); }).setOrigin(0.5);
modal.add([bg, txt]); modal.add([bg, txt]);

View File

@ -150,7 +150,7 @@ export default class HoldemGame extends Phaser.Scene {
buildPotDisplay() { buildPotDisplay() {
this.potText = this.add.text(CX, CY - CARD_H / 2 - 36, 'Pot: $0', { this.potText = this.add.text(CX, CY - CARD_H / 2 - 36, 'Pot: $0', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '26px', fontSize: '26px',
color: '#f0e8d0', color: '#f0e8d0',
}).setOrigin(0.5).setDepth(D.ui); }).setOrigin(0.5).setDepth(D.ui);
@ -158,13 +158,13 @@ export default class HoldemGame extends Phaser.Scene {
buildBlindDisplay() { buildBlindDisplay() {
this.blindText = this.add.text(24, 24, 'Blinds $5/$10', { this.blindText = this.add.text(24, 24, 'Blinds $5/$10', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '20px', fontSize: '20px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0, 0).setDepth(D.ui); }).setOrigin(0, 0).setDepth(D.ui);
this.timerText = this.add.text(24, 50, '', { this.timerText = this.add.text(24, 50, '', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '16px', fontSize: '16px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0, 0).setDepth(D.ui); }).setOrigin(0, 0).setDepth(D.ui);
@ -181,7 +181,7 @@ export default class HoldemGame extends Phaser.Scene {
// Name label // Name label
const nameY = isHuman ? -CARD_H / 2 - 48 : CARD_H / 2 + 28; const nameY = isHuman ? -CARD_H / 2 - 48 : CARD_H / 2 + 28;
const name = this.add.text(0, nameY, '—', { const name = this.add.text(0, nameY, '—', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '18px', fontSize: '18px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);
@ -189,7 +189,7 @@ export default class HoldemGame extends Phaser.Scene {
// Chip count // Chip count
const chipY = isHuman ? -CARD_H / 2 - 24 : CARD_H / 2 + 52; const chipY = isHuman ? -CARD_H / 2 - 24 : CARD_H / 2 + 52;
const chipTxt = this.add.text(0, chipY, '$0', { const chipTxt = this.add.text(0, chipY, '$0', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '20px', fontSize: '20px',
color: '#f0e8d0', color: '#f0e8d0',
}).setOrigin(0.5); }).setOrigin(0.5);
@ -197,7 +197,7 @@ export default class HoldemGame extends Phaser.Scene {
// Bet display (shown at table edge between seat and center) // Bet display (shown at table edge between seat and center)
const betY = isHuman ? -CARD_H / 2 - 80 : (seat === 2 ? CARD_H / 2 + 80 : (y > CY ? -CARD_H / 2 - 80 : CARD_H / 2 + 80)); const betY = isHuman ? -CARD_H / 2 - 80 : (seat === 2 ? CARD_H / 2 + 80 : (y > CY ? -CARD_H / 2 - 80 : CARD_H / 2 + 80));
const betTxt = this.add.text(0, betY, '', { const betTxt = this.add.text(0, betY, '', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '18px', fontSize: '18px',
color: '#ffd700', color: '#ffd700',
}).setOrigin(0.5); }).setOrigin(0.5);
@ -205,7 +205,7 @@ export default class HoldemGame extends Phaser.Scene {
// Dealer chip // Dealer chip
const dealerChip = this.add.circle(CARD_W / 2 + 16, -CARD_H / 2 + 16, 14, 0xf0e8d0).setVisible(false); const dealerChip = this.add.circle(CARD_W / 2 + 16, -CARD_H / 2 + 16, 14, 0xf0e8d0).setVisible(false);
const dealerTxt = this.add.text(CARD_W / 2 + 16, -CARD_H / 2 + 16, 'D', { const dealerTxt = this.add.text(CARD_W / 2 + 16, -CARD_H / 2 + 16, 'D', {
fontFamily: 'system-ui, sans-serif', fontSize: '14px', color: '#1a1a1a', fontFamily: '"Julius Sans One"', fontSize: '14px', color: '#1a1a1a',
}).setOrigin(0.5).setVisible(false); }).setOrigin(0.5).setVisible(false);
// Card containers — each holds graphics + text children // Card containers — each holds graphics + text children
@ -271,7 +271,7 @@ export default class HoldemGame extends Phaser.Scene {
const bg = this.add.rectangle(0, 0, 560, 68, 0x111923, 0.95) const bg = this.add.rectangle(0, 0, 560, 68, 0x111923, 0.95)
.setStrokeStyle(1, COLORS.accent); .setStrokeStyle(1, COLORS.accent);
this.raiseAmtText = this.add.text(0, -10, '$0', { this.raiseAmtText = this.add.text(0, -10, '$0', {
fontFamily: 'system-ui, sans-serif', fontSize: '22px', color: '#f0e8d0', fontFamily: '"Julius Sans One"', fontSize: '22px', color: '#f0e8d0',
}).setOrigin(0.5); }).setOrigin(0.5);
const minus = new Button(this, -200, 0, '', () => this.adjustRaise(-this.raiseStep()), { width: 50, height: 40, fontSize: 22 }); const minus = new Button(this, -200, 0, '', () => this.adjustRaise(-this.raiseStep()), { width: 50, height: 40, fontSize: 22 });
@ -365,22 +365,22 @@ export default class HoldemGame extends Phaser.Scene {
.setStrokeStyle(2, COLORS.accent).setDepth(D.modal); .setStrokeStyle(2, COLORS.accent).setDepth(D.modal);
const title = this.add.text(CX, CY - 120, "Texas Hold 'Em", { const title = this.add.text(CX, CY - 120, "Texas Hold 'Em", {
fontFamily: 'system-ui, sans-serif', fontSize: '36px', color: COLORS.textHex, fontFamily: 'Righteous', fontSize: '36px', color: COLORS.textHex,
}).setOrigin(0.5).setDepth(D.modal); }).setOrigin(0.5).setDepth(D.modal);
const balanceTxt = this.add.text(CX, CY - 60, `Your balance: $${this.globalChips.toLocaleString()}`, { const balanceTxt = this.add.text(CX, CY - 60, `Your balance: $${this.globalChips.toLocaleString()}`, {
fontFamily: 'system-ui, sans-serif', fontSize: '22px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(D.modal); }).setOrigin(0.5).setDepth(D.modal);
const buyInTxt = this.add.text(CX, CY - 20, `Buy-in: $${this.buyIn}`, { const buyInTxt = this.add.text(CX, CY - 20, `Buy-in: $${this.buyIn}`, {
fontFamily: 'system-ui, sans-serif', fontSize: '28px', color: '#ffd700', fontFamily: '"Julius Sans One"', fontSize: '28px', color: '#ffd700',
}).setOrigin(0.5).setDepth(D.modal); }).setOrigin(0.5).setDepth(D.modal);
const modalItems = [overlay, panel, title, balanceTxt, buyInTxt]; const modalItems = [overlay, panel, title, balanceTxt, buyInTxt];
if (this.globalChips <= 0) { if (this.globalChips <= 0) {
this.add.text(CX, CY + 40, 'You have no chips! Return to the lobby.', { this.add.text(CX, CY + 40, 'You have no chips! Return to the lobby.', {
fontFamily: 'system-ui, sans-serif', fontSize: '20px', color: COLORS.dangerHex, fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.dangerHex,
}).setOrigin(0.5).setDepth(D.modal); }).setOrigin(0.5).setDepth(D.modal);
return; return;
} }
@ -742,7 +742,7 @@ export default class HoldemGame extends Phaser.Scene {
} }
const label = this.add.text(0, 22, `$${p.bet}`, { const label = this.add.text(0, 22, `$${p.bet}`, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '15px', fontSize: '15px',
color: '#f0e8d0', color: '#f0e8d0',
stroke: '#000000', stroke: '#000000',
@ -951,7 +951,7 @@ export default class HoldemGame extends Phaser.Scene {
const color = card.isRed ? '#c0392b' : '#1a1a2e'; const color = card.isRed ? '#c0392b' : '#1a1a2e';
const sym = card.suitSymbol; const sym = card.suitSymbol;
const style = (size, bold = false) => ({ const style = (size, bold = false) => ({
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: `${size}px`, fontSize: `${size}px`,
color, color,
...(bold ? { fontStyle: 'bold' } : {}), ...(bold ? { fontStyle: 'bold' } : {}),
@ -995,10 +995,10 @@ export default class HoldemGame extends Phaser.Scene {
const isHuman = seat === 0; const isHuman = seat === 0;
const badgeY = isHuman ? pos.y - CARD_H / 2 - 110 : pos.y + CARD_H / 2 + 70; const badgeY = isHuman ? pos.y - CARD_H / 2 - 110 : pos.y + CARD_H / 2 + 70;
const labels = { fold: 'FOLD', check: 'CHECK', call: 'CALL', raise: `RAISE $${action.amount ?? ''}`, allin: 'ALL IN' }; const labels = { fold: 'FOLD', check: 'CHECK', call: 'CALL', raise: `RAISE $${action.amount ?? ''}`, allin: 'ALL IN' };
const colors = { fold: '#e06c75', check: '#8a94a6', call: '#5aa9e6', raise: '#ffd700', allin: '#ff6b35' }; const colors = { fold: '#e06c75', check: '#9e9080', call: COLORS.accentHex, raise: '#ffd700', allin: '#ff6b35' };
const badge = this.add.text(pos.x, badgeY, labels[action.type] ?? action.type.toUpperCase(), { const badge = this.add.text(pos.x, badgeY, labels[action.type] ?? action.type.toUpperCase(), {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '22px', fontSize: '22px',
color: colors[action.type] ?? COLORS.textHex, color: colors[action.type] ?? COLORS.textHex,
backgroundColor: '#111923cc', backgroundColor: '#111923cc',
@ -1043,7 +1043,7 @@ export default class HoldemGame extends Phaser.Scene {
// Title // Title
const title = this.add.text(CX, 38, 'Hand Summary', { const title = this.add.text(CX, 38, 'Hand Summary', {
fontFamily: 'system-ui, sans-serif', fontSize: '26px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(D.modal + 1).setAlpha(0); }).setOrigin(0.5).setDepth(D.modal + 1).setAlpha(0);
S.push(title); S.push(title);
this.tweens.add({ targets: title, alpha: 1, duration: 300, delay: 200 }); this.tweens.add({ targets: title, alpha: 1, duration: 300, delay: 200 });
@ -1143,7 +1143,7 @@ export default class HoldemGame extends Phaser.Scene {
} else { } else {
const initial = (auth.user?.username ?? 'You').charAt(0).toUpperCase(); const initial = (auth.user?.username ?? 'You').charAt(0).toUpperCase();
const initTxt = this.add.text(portX, rowY, initial, { const initTxt = this.add.text(portX, rowY, initial, {
fontFamily: 'system-ui, sans-serif', fontSize: '46px', color: COLORS.accentHex, fontFamily: 'Righteous', fontSize: '46px', color: COLORS.accentHex,
}).setOrigin(0.5).setDepth(D.modal + 2).setAlpha(0); }).setOrigin(0.5).setDepth(D.modal + 2).setAlpha(0);
S.push(initTxt); S.push(initTxt);
this.tweens.add({ targets: initTxt, alpha: 1, duration: 350, delay }); this.tweens.add({ targets: initTxt, alpha: 1, duration: 350, delay });
@ -1152,7 +1152,7 @@ export default class HoldemGame extends Phaser.Scene {
// Player name // Player name
const nameX = 250; const nameX = 250;
const nameTxt = this.add.text(nameX, rowY - 28, player.name ?? '—', { const nameTxt = this.add.text(nameX, rowY - 28, player.name ?? '—', {
fontFamily: 'system-ui, sans-serif', fontSize: '22px', color: COLORS.textHex, fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.textHex,
}).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0); }).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0);
S.push(nameTxt); S.push(nameTxt);
this.tweens.add({ targets: nameTxt, alpha: 1, duration: 350, delay }); this.tweens.add({ targets: nameTxt, alpha: 1, duration: 350, delay });
@ -1172,7 +1172,7 @@ export default class HoldemGame extends Phaser.Scene {
} }
if (statusStr) { if (statusStr) {
const statusTxt = this.add.text(nameX, rowY + 4, statusStr, { const statusTxt = this.add.text(nameX, rowY + 4, statusStr, {
fontFamily: 'system-ui, sans-serif', fontSize: '18px', color: statusColor, fontFamily: '"Julius Sans One"', fontSize: '18px', color: statusColor,
}).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0); }).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0);
S.push(statusTxt); S.push(statusTxt);
this.tweens.add({ targets: statusTxt, alpha: 1, duration: 350, delay }); this.tweens.add({ targets: statusTxt, alpha: 1, duration: 350, delay });
@ -1202,7 +1202,7 @@ export default class HoldemGame extends Phaser.Scene {
const won = winnings.get(player.seat) ?? 0; const won = winnings.get(player.seat) ?? 0;
if (won > 0) { if (won > 0) {
const wonTxt = this.add.text(790, rowY, `+$${won}`, { const wonTxt = this.add.text(790, rowY, `+$${won}`, {
fontFamily: 'system-ui, sans-serif', fontSize: '28px', fontFamily: '"Julius Sans One"', fontSize: '28px',
color: '#ffd700', fontStyle: 'bold', color: '#ffd700', fontStyle: 'bold',
}).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0); }).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0);
S.push(wonTxt); S.push(wonTxt);
@ -1212,7 +1212,7 @@ export default class HoldemGame extends Phaser.Scene {
// Chip count // Chip count
const chipStr = player.eliminated ? 'Eliminated' : `$${player.chips} chips`; const chipStr = player.eliminated ? 'Eliminated' : `$${player.chips} chips`;
const chipTxt = this.add.text(1050, rowY, chipStr, { const chipTxt = this.add.text(1050, rowY, chipStr, {
fontFamily: 'system-ui, sans-serif', fontSize: '18px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
}).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0); }).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0);
S.push(chipTxt); S.push(chipTxt);
this.tweens.add({ targets: chipTxt, alpha: 1, duration: 350, delay }); this.tweens.add({ targets: chipTxt, alpha: 1, duration: 350, delay });
@ -1246,19 +1246,19 @@ export default class HoldemGame extends Phaser.Scene {
: '🎉 You win!'; : '🎉 You win!';
this.add.text(CX, CY - 140, headline, { this.add.text(CX, CY - 140, headline, {
fontFamily: 'system-ui, sans-serif', fontSize: '40px', fontFamily: 'Righteous', fontSize: '40px',
color: won ? '#ffd700' : COLORS.textHex, color: won ? '#ffd700' : COLORS.textHex,
}).setOrigin(0.5).setDepth(D.modal); }).setOrigin(0.5).setDepth(D.modal);
this.add.text(CX, CY - 70, won this.add.text(CX, CY - 70, won
? `+$${net} profit` ? `+$${net} profit`
: net === 0 ? 'Broke even' : `-$${Math.abs(net)} loss`, { : net === 0 ? 'Broke even' : `-$${Math.abs(net)} loss`, {
fontFamily: 'system-ui, sans-serif', fontSize: '28px', fontFamily: '"Julius Sans One"', fontSize: '28px',
color: won ? '#5aa9e6' : COLORS.dangerHex, color: won ? COLORS.accentHex : COLORS.dangerHex,
}).setOrigin(0.5).setDepth(D.modal); }).setOrigin(0.5).setDepth(D.modal);
this.add.text(CX, CY - 20, `Your balance: $${this.globalChips.toLocaleString()}`, { this.add.text(CX, CY - 20, `Your balance: $${this.globalChips.toLocaleString()}`, {
fontFamily: 'system-ui, sans-serif', fontSize: '22px', color: COLORS.mutedHex, fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(D.modal); }).setOrigin(0.5).setDepth(D.modal);
new Button(this, CX - 110, CY + 100, 'Play Again', () => { new Button(this, CX - 110, CY + 100, 'Play Again', () => {

View File

@ -52,19 +52,19 @@ function trackXY(idx) {
} }
function homeXY(color, idx) { function homeXY(color, idx) {
if (color === 'red') return { col: 9, row: 17 - idx }; if (color === 'red') return { col: 1 + idx, row: 9 }; // west spoke
if (color === 'blue') return { col: 17 - idx, row: 9 }; if (color === 'blue') return { col: 9, row: 17 - idx }; // south spoke
if (color === 'yellow') return { col: 9, row: 1 + idx }; if (color === 'yellow') return { col: 17 - idx, row: 9 }; // east spoke
if (color === 'green') return { col: 1 + idx, row: 9 }; if (color === 'green') return { col: 9, row: 1 + idx }; // north spoke
throw new Error(`bad color ${color}`); throw new Error(`bad color ${color}`);
} }
// Final "home" cell at the center boundary per color. // Final "home" cell at the center boundary per color.
function homeFinalXY(color) { function homeFinalXY(color) {
if (color === 'red') return { col: 9, row: 10 }; if (color === 'red') return { col: 8, row: 9 }; // west spoke
if (color === 'blue') return { col: 10, row: 9 }; if (color === 'blue') return { col: 9, row: 10 }; // south spoke
if (color === 'yellow') return { col: 9, row: 8 }; if (color === 'yellow') return { col: 10, row: 9 }; // east spoke
if (color === 'green') return { col: 8, row: 9 }; if (color === 'green') return { col: 9, row: 8 }; // north spoke
} }
function cellWorld(col, row) { function cellWorld(col, row) {
@ -146,6 +146,9 @@ export default class ParchisiGame extends Phaser.Scene {
this.turnIndicator = null; this.turnIndicator = null;
this.turnIndicatorGfx = null; this.turnIndicatorGfx = null;
this.turnIndicatorPulseTween = null; this.turnIndicatorPulseTween = null;
this.turnIndicatorMoveTween = null;
this.bonusChip20 = null;
this._bonusChip20Visible = false;
} }
create() { create() {
@ -153,6 +156,7 @@ export default class ParchisiGame extends Phaser.Scene {
this.buildBoard(); this.buildBoard();
this.buildDice(); this.buildDice();
this.buildUI(); this.buildUI();
this.buildBonusChip();
this.buildPlayerCards(); this.buildPlayerCards();
this.buildTurnIndicator(); this.buildTurnIndicator();
this.buildPawns(); this.buildPawns();
@ -229,6 +233,7 @@ export default class ParchisiGame extends Phaser.Scene {
if (isSafeTrack(idx) && !entryColor) { if (isSafeTrack(idx) && !entryColor) {
this.drawStar(g, wp.x, wp.y, 5, 10, 5, 0x2b5d80, 0.7); this.drawStar(g, wp.x, wp.y, 5, 10, 5, 0x2b5d80, 0.7);
} }
} }
drawHomeColumn(color) { drawHomeColumn(color) {
@ -263,7 +268,7 @@ export default class ParchisiGame extends Phaser.Scene {
g.lineStyle(2, TRACK_STROKE, 0.9); g.lineStyle(2, TRACK_STROKE, 0.9);
g.strokeCircle(cx, cy, r); g.strokeCircle(cx, cy, r);
this.add.text(cx, cy, 'HOME', { this.add.text(cx, cy, 'HOME', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '16px', fontSize: '16px',
color: '#3a2010', color: '#3a2010',
fontStyle: 'bold', fontStyle: 'bold',
@ -287,7 +292,7 @@ export default class ParchisiGame extends Phaser.Scene {
// ── Dice ────────────────────────────────────────────────────────────────── // ── Dice ──────────────────────────────────────────────────────────────────
buildDice() { buildDice() {
const baseX = ORIGIN_X - 80; const baseX = ORIGIN_X - 80;
const baseY = GAME_HEIGHT / 2 - 150; const baseY = GAME_HEIGHT / 2 - 80;
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
const g = this.add.graphics(); const g = this.add.graphics();
const container = this.add.container(baseX, baseY + i * 80).setDepth(DEPTH.dice); const container = this.add.container(baseX, baseY + i * 80).setDepth(DEPTH.dice);
@ -344,6 +349,7 @@ export default class ParchisiGame extends Phaser.Scene {
} }
updateDiceDisplay() { updateDiceDisplay() {
this.updateBonusChip();
if (!this.gs.dice) { if (!this.gs.dice) {
this.diceContainers.forEach((c) => c.setAlpha(0.25)); this.diceContainers.forEach((c) => c.setAlpha(0.25));
return; return;
@ -364,27 +370,66 @@ export default class ParchisiGame extends Phaser.Scene {
// ── UI / Portraits ──────────────────────────────────────────────────────── // ── UI / Portraits ────────────────────────────────────────────────────────
buildUI() { buildUI() {
const xLeft = ORIGIN_X - 80; const xLeft = ORIGIN_X - 80;
const yRoll = GAME_HEIGHT / 2 + 10; const yRoll = GAME_HEIGHT / 2 + 80;
this.rollBtn = new Button(this, xLeft, yRoll, 'Roll', () => this.onRollClick(), { this.rollBtn = new Button(this, xLeft, yRoll, 'Roll', () => this.onRollClick(), {
width: 110, height: 44, fontSize: 22, width: 110, height: 44, fontSize: 22,
}); });
this.rollBtn.setDepth(DEPTH.ui); this.rollBtn.setDepth(DEPTH.ui);
new Button(this, xLeft, yRoll + 70, 'New', () => this.initGame(), { new Button(this, 80, GAME_HEIGHT - 70, 'New', () => this.initGame(), {
variant: 'ghost', width: 110, height: 40, fontSize: 18, variant: 'ghost', width: 110, height: 40, fontSize: 18,
}).setDepth(DEPTH.ui); }).setDepth(DEPTH.ui);
new Button(this, xLeft, yRoll + 120, 'Leave', () => this.scene.start('GameMenu'), { new Button(this, 80, GAME_HEIGHT - 25, 'Leave', () => this.scene.start('GameMenu'), {
variant: 'ghost', width: 110, height: 40, fontSize: 18, variant: 'ghost', width: 110, height: 40, fontSize: 18,
}).setDepth(DEPTH.ui); }).setDepth(DEPTH.ui);
this.statusText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 30, '', { this.statusText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 30, '', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '22px', fontSize: '22px',
color: UI.textHex, color: UI.textHex,
}).setOrigin(0.5).setDepth(DEPTH.ui); }).setOrigin(0.5).setDepth(DEPTH.ui);
} }
buildBonusChip() {
const x = ORIGIN_X - 155;
const y = GAME_HEIGHT / 2 - 40;
const R = 26;
const gfx = this.add.graphics();
gfx.fillStyle(0xffffff, 1);
gfx.fillCircle(0, 0, R);
gfx.lineStyle(3, 0x222222, 1);
gfx.strokeCircle(0, 0, R);
const label = this.add.text(0, 0, '20', {
fontFamily: '"Julius Sans One"',
fontSize: '20px',
fontStyle: 'bold',
color: '#111111',
}).setOrigin(0.5);
this.bonusChip20 = this.add.container(x, y, [gfx, label])
.setDepth(DEPTH.ui + 3)
.setAlpha(0)
.setScale(0.3);
}
updateBonusChip() {
const should = !!(this.gs?.movesLeft?.includes(20));
if (should === this._bonusChip20Visible) return;
this._bonusChip20Visible = should;
this.tweens.killTweensOf(this.bonusChip20);
if (should) {
this.tweens.add({
targets: this.bonusChip20, alpha: 1, scale: 1,
duration: 250, ease: 'Back.easeOut',
});
} else {
this.tweens.add({
targets: this.bonusChip20, alpha: 0, scale: 0.3,
duration: 180, ease: 'Cubic.easeIn',
});
}
}
buildPlayerCards() { buildPlayerCards() {
const portraitR = 56; const portraitR = 56;
// Left nests (red, green): col0=0, so left edge is ORIGIN_X // Left nests (red, green): col0=0, so left edge is ORIGIN_X
@ -397,7 +442,7 @@ export default class ParchisiGame extends Phaser.Scene {
this.add.circle(xLeft, plY, portraitR + 5, COLOR_HEX.red.fill, 0.6).setDepth(DEPTH.ui); this.add.circle(xLeft, plY, portraitR + 5, COLOR_HEX.red.fill, 0.6).setDepth(DEPTH.ui);
createPlayerPortrait(this, xLeft, plY, portraitR, DEPTH.ui + 1, 'ParchisiGame'); createPlayerPortrait(this, xLeft, plY, portraitR, DEPTH.ui + 1, 'ParchisiGame');
this.add.text(xLeft, plY + portraitR + 14, auth.user?.username ?? 'You', { this.add.text(xLeft, plY + portraitR + 14, auth.user?.username ?? 'You', {
fontFamily: 'system-ui, sans-serif', fontSize: '16px', color: UI.textHex, fontFamily: '"Julius Sans One"', fontSize: '16px', color: UI.textHex,
}).setOrigin(0.5, 0).setDepth(DEPTH.ui + 2); }).setOrigin(0.5, 0).setDepth(DEPTH.ui + 2);
// AI opponents: blue (right/bottom), yellow (right/top), green (left/top) // AI opponents: blue (right/bottom), yellow (right/top), green (left/top)
@ -410,7 +455,7 @@ export default class ParchisiGame extends Phaser.Scene {
this.add.circle(x, y, portraitR + 5, COLOR_HEX[color].fill, 0.6).setDepth(DEPTH.ui); this.add.circle(x, y, portraitR + 5, COLOR_HEX[color].fill, 0.6).setDepth(DEPTH.ui);
this.opponentPortraits[color] = createOpponentPortrait(this, opp, x, y, portraitR, DEPTH.ui + 1); this.opponentPortraits[color] = createOpponentPortrait(this, opp, x, y, portraitR, DEPTH.ui + 1);
this.add.text(x, y + portraitR + 12, opp.name ?? color, { this.add.text(x, y + portraitR + 12, opp.name ?? color, {
fontFamily: 'system-ui, sans-serif', fontSize: '16px', color: UI.textHex, fontFamily: '"Julius Sans One"', fontSize: '16px', color: UI.textHex,
}).setOrigin(0.5, 0).setDepth(DEPTH.ui + 2); }).setOrigin(0.5, 0).setDepth(DEPTH.ui + 2);
}); });
} }
@ -481,29 +526,37 @@ export default class ParchisiGame extends Phaser.Scene {
}); });
} }
moveTurnIndicator(color, immediately = false) { moveTurnIndicator(color, immediately = false, onArrive = null) {
const { x, y, side } = this._indicatorPos(color); const { x, y, side } = this._indicatorPos(color);
// Stop both running tweens before starting new ones
if (this.turnIndicatorPulseTween) { if (this.turnIndicatorPulseTween) {
this.turnIndicatorPulseTween.stop(); this.turnIndicatorPulseTween.stop();
this.turnIndicatorPulseTween = null; this.turnIndicatorPulseTween = null;
} }
if (this.turnIndicatorMoveTween) {
this.turnIndicatorMoveTween.stop();
this.turnIndicatorMoveTween = null;
}
this.turnIndicator.setScale(1); this.turnIndicator.setScale(1);
if (immediately || this.turnIndicator.alpha === 0) { if (immediately || this.turnIndicator.alpha === 0) {
this._drawTurnTriangle(side); this._drawTurnTriangle(side);
this.turnIndicator.setPosition(x, y).setAlpha(1); this.turnIndicator.setPosition(x, y).setAlpha(1);
this._startIndicatorPulse(); this._startIndicatorPulse();
onArrive?.();
return; return;
} }
this.tweens.add({ this.turnIndicatorMoveTween = this.tweens.add({
targets: this.turnIndicator, targets: this.turnIndicator,
x, y, x, y,
duration: 550, duration: 400,
ease: 'Cubic.easeInOut', ease: 'Cubic.easeInOut',
onComplete: () => { onComplete: () => {
this.turnIndicatorMoveTween = null;
this._drawTurnTriangle(side); this._drawTurnTriangle(side);
this._startIndicatorPulse(); this._startIndicatorPulse();
onArrive?.();
}, },
}); });
} }
@ -749,13 +802,16 @@ export default class ParchisiGame extends Phaser.Scene {
afterTurn() { afterTurn() {
this.updateButtons(); this.updateButtons();
this.moveTurnIndicator(this.gs.currentPlayer);
if (this.gs.currentPlayer === 'red') { if (this.gs.currentPlayer === 'red') {
this.moveTurnIndicator('red');
this.setStatus('Your turn — roll the dice'); this.setStatus('Your turn — roll the dice');
} else { } else {
const opp = this.opponents[['blue', 'yellow', 'green'].indexOf(this.gs.currentPlayer)]; const opp = this.opponents[['blue', 'yellow', 'green'].indexOf(this.gs.currentPlayer)];
this.setStatus(`${opp?.name ?? this.gs.currentPlayer}'s turn`); this.setStatus(`${opp?.name ?? this.gs.currentPlayer}'s turn`);
this.time.delayedCall(700, () => this.runAITurn()); // Start AI dice roll only after the indicator has finished travelling
this.moveTurnIndicator(this.gs.currentPlayer, false, () => {
this.time.delayedCall(250, () => this.runAITurn());
});
} }
} }
@ -815,11 +871,6 @@ export default class ParchisiGame extends Phaser.Scene {
if (i >= moves.length || this.gs.phase === 'game_over') { if (i >= moves.length || this.gs.phase === 'game_over') {
this.animating = false; this.animating = false;
if (this.gs.phase === 'game_over') { this.onGameOver(); return; } if (this.gs.phase === 'game_over') { this.onGameOver(); return; }
// If AI got another roll (doubles), continue
if (this.gs.phase === 'roll' && this.gs.currentPlayer !== 'red') {
this.time.delayedCall(700, () => this.runAITurn());
return;
}
this.afterTurn(); this.afterTurn();
return; return;
} }
@ -935,7 +986,7 @@ export default class ParchisiGame extends Phaser.Scene {
? '🎉 You Win!\nAll four pawns home!' ? '🎉 You Win!\nAll four pawns home!'
: `${oppName} wins this round.\nBetter luck next game!`; : `${oppName} wins this round.\nBetter luck next game!`;
const txt = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 50, msg, { const txt = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 50, msg, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '32px', fontSize: '32px',
color: isHuman ? '#ffd700' : UI.textHex, color: isHuman ? '#ffd700' : UI.textHex,
align: 'center', align: 'center',

View File

@ -18,20 +18,20 @@
export const COLORS = ['red', 'blue', 'yellow', 'green']; export const COLORS = ['red', 'blue', 'yellow', 'green'];
export const ENTRY = { red: 0, blue: 17, yellow: 34, green: 51 }; export const ENTRY = { red: 11, blue: 62, yellow: 45, green: 28 };
// Home-entry = the LAST outer-track square a pawn occupies before turning // Home-entry = the LAST outer-track square a pawn occupies before turning
// into its home column. = (entry + 67) mod 68 = (entry - 1 + 68) mod 68. // into its home column. Movement is CCW (track index decrements).
export const HOME_ENTRY = { red: 67, blue: 16, yellow: 33, green: 50 }; export const HOME_ENTRY = { red: 16, blue: 67, yellow: 50, green: 33 };
export const TRACK_LEN = 68; export const TRACK_LEN = 68;
export const HOME_COL_LEN = 7; export const HOME_COL_LEN = 7;
export const PAWNS_PER_PLAYER = 4; export const PAWNS_PER_PLAYER = 4;
const SAFE_SET = new Set([ const SAFE_SET = new Set([
0, 7, 12, 4, 11, 16,
17, 24, 29, 21, 28, 33,
34, 41, 46, 38, 45, 50,
51, 58, 63, 55, 62, 67,
]); ]);
export function isSafeTrack(idx) { export function isSafeTrack(idx) {
@ -139,7 +139,7 @@ export function pawnDistanceToHome(pawn, color) {
if (pawn.track !== undefined) { if (pawn.track !== undefined) {
// Distance from track t to home-entry, then +7 (cols) + 1 (home circle) = +8. // Distance from track t to home-entry, then +7 (cols) + 1 (home circle) = +8.
const homeEntry = HOME_ENTRY[color]; const homeEntry = HOME_ENTRY[color];
const d = (homeEntry - pawn.track + TRACK_LEN) % TRACK_LEN; const d = (pawn.track - homeEntry + TRACK_LEN) % TRACK_LEN;
return d + 8; return d + 8;
} }
return 0; return 0;
@ -221,7 +221,7 @@ function projectPath(from, color, steps) {
pos = { home: 0 }; pos = { home: 0 };
path.push(pos); path.push(pos);
} else { } else {
const nextIdx = (pos.track + 1) % TRACK_LEN; const nextIdx = (pos.track - 1 + TRACK_LEN) % TRACK_LEN;
pos = { track: nextIdx }; pos = { track: nextIdx };
path.push(pos); path.push(pos);
} }

View File

@ -10,11 +10,14 @@ export default class GameMenuScene extends Phaser.Scene {
async create() { async create() {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, 120, 'Choose a game', { this.add.image(cx, GAME_HEIGHT / 2, 'bg-menu').setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
fontFamily: 'system-ui, sans-serif',
const titleText = this.add.text(cx, 120, 'Choose a game', {
fontFamily: 'Righteous',
fontSize: '64px', fontSize: '64px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5).setDepth(1);
this.add.rectangle(cx, 120, titleText.width + 64, titleText.height + 28, 0x000000, 0.7);
const loadingText = this.add.text(cx, 220, 'Loading game list…', { const loadingText = this.add.text(cx, 220, 'Loading game list…', {
fontSize: '24px', color: COLORS.mutedHex, fontSize: '24px', color: COLORS.mutedHex,
@ -40,17 +43,19 @@ export default class GameMenuScene extends Phaser.Scene {
} }
renderColumn(title, games, x, y) { renderColumn(title, games, x, y) {
const panelTop = y - 44;
const panelBot = games.length > 0 ? y + 80 + (games.length - 1) * 90 + 56 : y + 56;
const panelH = panelBot - panelTop;
this.add.rectangle(x, panelTop + panelH / 2, 420, panelH, 0x000000, 0.7);
this.add.text(x, y, title, { this.add.text(x, y, title, {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '40px', fontSize: '40px',
color: COLORS.accentHex, color: COLORS.accentHex,
}).setOrigin(0.5); }).setOrigin(0.5);
games.forEach((game, i) => { games.forEach((game, i) => {
const btn = new Button(this, x, y + 80 + i * 90, game.name, () => this.openGame(game), { new Button(this, x, y + 80 + i * 90, game.name, () => this.openGame(game), { width: 360 });
width: 360,
});
void btn;
}); });
} }

View File

@ -31,7 +31,7 @@ export default class GameRoomScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, 80, `${this.game.name}`, { this.add.text(cx, 80, `${this.game.name}`, {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '52px', fontSize: '52px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);
@ -44,7 +44,7 @@ export default class GameRoomScene extends Phaser.Scene {
// Placeholder table felt // Placeholder table felt
this.add.rectangle(cx, GAME_HEIGHT / 2 + 40, 1400, 700, 0x14532d).setStrokeStyle(4, COLORS.accent); this.add.rectangle(cx, GAME_HEIGHT / 2 + 40, 1400, 700, 0x14532d).setStrokeStyle(4, COLORS.accent);
this.add.text(cx, GAME_HEIGHT / 2 + 40, `${this.game.name} board placeholder\n(game logic plugs in here)`, { this.add.text(cx, GAME_HEIGHT / 2 + 40, `${this.game.name} board placeholder\n(game logic plugs in here)`, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '28px', fontSize: '28px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
align: 'center', align: 'center',

View File

@ -9,19 +9,29 @@ export default class LandingScene extends Phaser.Scene {
create() { create() {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.rectangle(cx, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, COLORS.bg); this.add.image(cx, GAME_HEIGHT / 2, 'bg-menu').setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
this.add.text(cx, 220, 'Fertig Classic Games', { const logo = this.add.image(cx, 290, 'main-title').setOrigin(0.5, 0.5).setAlpha(0);
fontFamily: 'Georgia, "Times New Roman", serif', logo.postFX.addShadow(3, 6, 0.005, 2, 0x000000, 10, 0.75);
fontSize: '96px',
color: COLORS.textHex,
}).setOrigin(0.5);
this.add.text(cx, 320, 'Cards, dice, and classic tables.', { this.tweens.add({
fontFamily: 'system-ui, sans-serif', targets: logo,
fontSize: '32px', alpha: 1,
color: COLORS.mutedHex, y: 260,
}).setOrigin(0.5); duration: 900,
ease: 'Power2',
onComplete: () => {
this.tweens.add({
targets: logo,
scaleX: 1.025,
scaleY: 1.025,
duration: 2200,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
},
});
this.renderButtons(); this.renderButtons();
@ -37,29 +47,66 @@ export default class LandingScene extends Phaser.Scene {
const user = auth.user; const user = auth.user;
if (user) { if (user) {
this.add.text(cx, 480, `Welcome back, ${user.username}`, { const avatarR = 28;
fontFamily: 'system-ui, sans-serif', const avatarGap = 18;
const pad = { x: 32, y: 14 };
const y = 630;
const welcomeText = this.add.text(0, y, `Welcome back, ${user.displayName ?? user.username}`, {
fontFamily: 'Righteous',
fontSize: '36px', fontSize: '36px',
color: COLORS.accentHex, color: COLORS.accentHex,
}).setOrigin(0.5); }).setOrigin(0.5).setDepth(1);
const hasAvatar = !!user.avatarPath;
const totalW = hasAvatar ? avatarR * 2 + avatarGap + welcomeText.width : welcomeText.width;
const groupLeft = cx - totalW / 2;
welcomeText.setX(hasAvatar ? groupLeft + avatarR * 2 + avatarGap + welcomeText.width / 2 : cx);
this.add.rectangle(cx, y, totalW + pad.x * 2, welcomeText.height + pad.y * 2, 0x000000, 0.45);
if (hasAvatar) {
const avatarCx = groupLeft + avatarR;
const key = `landing-avatar-${user.id}`;
(async () => {
try {
if (!this.textures.exists(key)) {
await new Promise((resolve) => {
this.load.image(key, user.avatarPath);
this.load.once('complete', resolve);
this.load.start();
});
}
if (!this.scene.isActive('Landing')) return;
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
maskG.fillStyle(0xffffff);
maskG.fillCircle(avatarCx, y, avatarR);
this.add.image(avatarCx, y, key)
.setDisplaySize(avatarR * 2, avatarR * 2)
.setMask(maskG.createGeometryMask())
.setDepth(1);
} catch { /* no avatar shown */ }
})();
}
if (!user.emailVerified) { if (!user.emailVerified) {
this.add.text(cx, 540, 'Email not yet verified — check the server console for the link in dev.', { this.add.text(cx, 690, 'Email not yet verified — check the server console for the link in dev.', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '22px', fontSize: '22px',
color: COLORS.dangerHex, color: COLORS.dangerHex,
}).setOrigin(0.5); }).setOrigin(0.5);
} }
new Button(this, cx, 660, 'Play', () => this.scene.start('GameMenu')); new Button(this, cx, 810, 'Play', () => this.scene.start('GameMenu'));
new Button(this, cx, 740, 'Profile', () => this.scene.start('Profile')); new Button(this, cx, 890, 'Profile', () => this.scene.start('Profile'));
new Button(this, cx, 820, 'Sign out', async () => { new Button(this, cx, 970, 'Sign out', async () => {
await auth.logout(); await auth.logout();
}, { variant: 'ghost' }); }, { variant: 'ghost' });
} else { } else {
new Button(this, cx, 540, 'Sign in', () => this.scene.start('Login')); new Button(this, cx, 690, 'Sign in', () => this.scene.start('Login'));
new Button(this, cx, 620, 'Create account', () => this.scene.start('Register')); new Button(this, cx, 770, 'Create account', () => this.scene.start('Register'));
new Button(this, cx, 700, 'Continue as guest', () => this.scene.start('GameMenu'), { variant: 'ghost' }); new Button(this, cx, 850, 'Continue as guest', () => this.scene.start('GameMenu'), { variant: 'ghost' });
} }
} }
} }

View File

@ -13,13 +13,13 @@ export default class LobbyScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, 100, `${this.game.name} — Lobby`, { this.add.text(cx, 100, `${this.game.name} — Lobby`, {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '52px', fontSize: '52px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);
this.listText = this.add.text(cx, 220, 'Connecting…', { this.listText = this.add.text(cx, 220, 'Connecting…', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '24px', fontSize: '24px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0.5); }).setOrigin(0.5);

View File

@ -12,7 +12,7 @@ export default class LoginScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, 200, 'Sign in', { this.add.text(cx, 200, 'Sign in', {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '64px', fontSize: '64px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);

View File

@ -35,13 +35,13 @@ export default class OpponentSelectScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, 60, this.gameDef.name, { this.add.text(cx, 60, this.gameDef.name, {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '52px', fontSize: '52px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);
this.add.text(cx, 122, 'Choose your opponent', { this.add.text(cx, 122, 'Choose your opponent', {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '36px', fontSize: '36px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0.5); }).setOrigin(0.5);
@ -185,7 +185,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
ctx.fillStyle = COLORS.panelHex; ctx.fillStyle = COLORS.panelHex;
ctx.fillRect(0, 0, portraitSize, portraitSize); ctx.fillRect(0, 0, portraitSize, portraitSize);
ctx.fillStyle = COLORS.accentHex; ctx.fillStyle = COLORS.accentHex;
ctx.font = `bold ${Math.round(r * 0.8)}px system-ui,sans-serif`; ctx.font = `bold ${Math.round(r * 0.8)}px Righteous,sans-serif`;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText((opp.name ?? '?').charAt(0).toUpperCase(), r, r); ctx.fillText((opp.name ?? '?').charAt(0).toUpperCase(), r, r);
@ -215,7 +215,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
const name = document.createElement('div'); const name = document.createElement('div');
name.textContent = opp.name ?? ''; name.textContent = opp.name ?? '';
name.style.cssText = [ name.style.cssText = [
'font-family:system-ui,sans-serif', 'font-family:"Julius Sans One"',
'font-size:22px', 'font-size:22px',
'font-weight:600', 'font-weight:600',
`color:${COLORS.textHex}`, `color:${COLORS.textHex}`,
@ -227,7 +227,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
const bio = document.createElement('div'); const bio = document.createElement('div');
bio.textContent = opp.bio ?? ''; bio.textContent = opp.bio ?? '';
bio.style.cssText = [ bio.style.cssText = [
'font-family:system-ui,sans-serif', 'font-family:"Julius Sans One"',
'font-size:15px', 'font-size:15px',
`color:${COLORS.mutedHex}`, `color:${COLORS.mutedHex}`,
'line-height:1.4', 'line-height:1.4',
@ -294,7 +294,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
buildOptionSection(label, labelY, items, selectedProp, tilesProp, onSelect, tileW = TILE_W, tileH = TILE_H, tileGap = TILE_GAP) { buildOptionSection(label, labelY, items, selectedProp, tilesProp, onSelect, tileW = TILE_W, tileH = TILE_H, tileGap = TILE_GAP) {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, labelY, label, { this.add.text(cx, labelY, label, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '24px', fontSize: '24px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0.5); }).setOrigin(0.5);
@ -333,7 +333,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
} }
const nameText = this.add.text(0, tileH / 2 - 11, item.name, { const nameText = this.add.text(0, tileH / 2 - 11, item.name, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '13px', fontSize: '13px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);

View File

@ -14,7 +14,7 @@ export default class PreloadScene extends Phaser.Scene {
const bar = this.add.rectangle(w / 2 - barWidth / 2, h / 2, 0, 20, COLORS.accent) const bar = this.add.rectangle(w / 2 - barWidth / 2, h / 2, 0, 20, COLORS.accent)
.setOrigin(0, 0.5); .setOrigin(0, 0.5);
this.add.text(w / 2, h / 2 - 60, 'Loading…', { this.add.text(w / 2, h / 2 - 60, 'Loading…', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '32px', fontSize: '32px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);
@ -30,6 +30,8 @@ export default class PreloadScene extends Phaser.Scene {
frameWidth: 320, frameWidth: 320,
frameHeight: 420, frameHeight: 420,
}); });
this.load.image('bg-menu', '/assets/images/background-menu.png');
this.load.image('main-title', '/assets/images/main-title.png');
this.load.json('playfields', '/data/playfields.json'); this.load.json('playfields', '/data/playfields.json');
this.load.json('card-backs', '/data/card-backs.json'); this.load.json('card-backs', '/data/card-backs.json');
} }

View File

@ -18,13 +18,13 @@ export default class ProfileScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, 140, 'Profile', { this.add.text(cx, 140, 'Profile', {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '64px', fontSize: '64px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);
this.statusText = this.add.text(cx, 220, 'Loading profile…', { this.statusText = this.add.text(cx, 220, 'Loading profile…', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '24px', fontSize: '24px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0.5); }).setOrigin(0.5);
@ -45,7 +45,7 @@ export default class ProfileScene extends Phaser.Scene {
const avatarBg = this.add.circle(avatarX, avatarY, 96, COLORS.panel).setStrokeStyle(3, COLORS.accent); const avatarBg = this.add.circle(avatarX, avatarY, 96, COLORS.panel).setStrokeStyle(3, COLORS.accent);
const initial = (profile.displayName ?? profile.username ?? '?').charAt(0).toUpperCase(); const initial = (profile.displayName ?? profile.username ?? '?').charAt(0).toUpperCase();
this.add.text(avatarX, avatarY, initial, { this.add.text(avatarX, avatarY, initial, {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '72px', fontSize: '72px',
color: COLORS.accentHex, color: COLORS.accentHex,
}).setOrigin(0.5); }).setOrigin(0.5);
@ -61,13 +61,13 @@ export default class ProfileScene extends Phaser.Scene {
} }
this.add.text(cx - 320, 300, profile.username, { this.add.text(cx - 320, 300, profile.username, {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '40px', fontSize: '40px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0, 0.5); }).setOrigin(0, 0.5);
this.add.text(cx - 320, 350, profile.email, { this.add.text(cx - 320, 350, profile.email, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '24px', fontSize: '24px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
}).setOrigin(0, 0.5); }).setOrigin(0, 0.5);
@ -82,7 +82,7 @@ export default class ProfileScene extends Phaser.Scene {
const chipsY = 460; const chipsY = 460;
this.add.text(cx - 320, chipsY, 'Chip balance', { fontSize: '22px', color: COLORS.mutedHex }).setOrigin(0, 0.5); this.add.text(cx - 320, chipsY, 'Chip balance', { fontSize: '22px', color: COLORS.mutedHex }).setOrigin(0, 0.5);
const chipsValueText = this.add.text(cx - 40, chipsY, `$${profile.chips.toLocaleString()}`, { const chipsValueText = this.add.text(cx - 40, chipsY, `$${profile.chips.toLocaleString()}`, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '22px', fontSize: '22px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0, 0.5); }).setOrigin(0, 0.5);
@ -91,7 +91,7 @@ export default class ProfileScene extends Phaser.Scene {
let resetBtn = null; let resetBtn = null;
if (profile.chips < 100) { if (profile.chips < 100) {
this.add.text(cx - 320, chipsY + 44, 'You are running low on chips.', { this.add.text(cx - 320, chipsY + 44, 'You are running low on chips.', {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '18px', fontSize: '18px',
color: COLORS.dangerHex, color: COLORS.dangerHex,
}).setOrigin(0, 0.5); }).setOrigin(0, 0.5);

View File

@ -12,7 +12,7 @@ export default class RegisterScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, 180, 'Create account', { this.add.text(cx, 180, 'Create account', {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '64px', fontSize: '64px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);

View File

@ -14,7 +14,7 @@ export default class VerifyScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2; const cx = GAME_WIDTH / 2;
this.add.text(cx, 220, 'Verify your email', { this.add.text(cx, 220, 'Verify your email', {
fontFamily: 'system-ui, sans-serif', fontFamily: 'Righteous',
fontSize: '64px', fontSize: '64px',
color: COLORS.textHex, color: COLORS.textHex,
}).setOrigin(0.5); }).setOrigin(0.5);
@ -24,7 +24,7 @@ export default class VerifyScene extends Phaser.Scene {
: 'SMTP is not configured. Use the dev link below to verify your email.'; : 'SMTP is not configured. Use the dev link below to verify your email.';
this.add.text(cx, 360, body, { this.add.text(cx, 360, body, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '26px', fontSize: '26px',
color: COLORS.mutedHex, color: COLORS.mutedHex,
wordWrap: { width: 1200 }, wordWrap: { width: 1200 },

View File

@ -1,6 +1,8 @@
import * as Phaser from 'phaser'; import * as Phaser from 'phaser';
import { COLORS } from '../config.js'; import { COLORS } from '../config.js';
const RADIUS = 8;
export class Button extends Phaser.GameObjects.Container { export class Button extends Phaser.GameObjects.Container {
constructor(scene, x, y, label, onClick, options = {}) { constructor(scene, x, y, label, onClick, options = {}) {
super(scene, x, y); super(scene, x, y);
@ -8,49 +10,74 @@ export class Button extends Phaser.GameObjects.Container {
width = 280, width = 280,
height = 64, height = 64,
bg = COLORS.panel, bg = COLORS.panel,
bgHover = COLORS.accent, bgHover = COLORS.gold,
textColor = COLORS.textHex, textColor = COLORS.textHex,
textHoverColor = COLORS.textDarkHex,
fontSize = 28, fontSize = 28,
variant = 'solid', variant = 'solid',
} = options; } = options;
this.options = { width, height, bg, bgHover, textColor, fontSize, variant }; this.options = { width, height, bg, bgHover, textColor, textHoverColor, fontSize, variant };
this.bgRect = scene.add.rectangle(0, 0, width, height, bg, variant === 'ghost' ? 0 : 1); const isGhost = variant === 'ghost';
this.bgRect.setStrokeStyle(2, COLORS.accent, 1); const hw = width / 2;
const hh = height / 2;
this.bgRect = scene.add.graphics();
this.bgRect.postFX.addShadow(0, 3, 0.004, 1.5, 0x000000, 8, 0.65);
const drawBg = (fillColor, fillAlpha) => {
this.bgRect.clear();
if (fillAlpha > 0) {
this.bgRect.fillStyle(fillColor, fillAlpha);
this.bgRect.fillRoundedRect(-hw, -hh, width, height, RADIUS);
}
this.bgRect.lineStyle(2, COLORS.accent, 1);
this.bgRect.strokeRoundedRect(-hw, -hh, width, height, RADIUS);
};
drawBg(bg, isGhost ? 0.35 : 1);
this.text = scene.add.text(0, 0, label, { this.text = scene.add.text(0, 0, label, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: `${fontSize}px`, fontSize: `${fontSize}px`,
color: textColor, color: textColor,
}).setOrigin(0.5); }).setOrigin(0.5);
this.add([this.bgRect, this.text]); this.add([this.bgRect, this.text]);
const bgHitArea = new Phaser.Geom.Rectangle(-hw, -hh, width, height);
const textHitArea = new Phaser.Geom.Rectangle(0, 0, width, height);
const hitCb = Phaser.Geom.Rectangle.Contains;
this.setSize(width, height); this.setSize(width, height);
this.setInteractive({ useHandCursor: true, hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains }); this.setInteractive({ useHandCursor: true, hitArea: bgHitArea, hitAreaCallback: hitCb });
this.bgRect.setInteractive({ hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains }); this.bgRect.setInteractive({ hitArea: bgHitArea, hitAreaCallback: hitCb });
this.text.setInteractive({ hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains }); this.text.setInteractive({ hitArea: textHitArea, hitAreaCallback: hitCb });
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));
this.on('pointerup', () => this.bgRect.setScale(1));
this.on('pointerupoutside', () => this.bgRect.setScale(1));
if (onClick) this.on('pointerup', onClick);
this.bgRect.on('pointerover', () => this.bgRect.setFillStyle(bgHover, 1)); const onOver = () => {
this.bgRect.on('pointerout', () => this.bgRect.setFillStyle(bg, variant === 'ghost' ? 0 : 1)); if (isGhost) {
this.bgRect.on('pointerdown', () => this.bgRect.setScale(0.97)); drawBg(bgHover, 0.18);
this.bgRect.on('pointerup', () => this.bgRect.setScale(1)); this.text.setColor(COLORS.goldHex);
this.bgRect.on('pointerupoutside', () => this.bgRect.setScale(1)); } else {
if (onClick) this.bgRect.on('pointerup', onClick); drawBg(bgHover, 1);
this.text.setColor(textHoverColor);
}
};
const onOut = () => {
drawBg(bg, isGhost ? 0.35 : 1);
this.text.setColor(textColor);
};
const onDown = () => this.bgRect.setScale(0.97);
const onUp = () => this.bgRect.setScale(1);
this.text.on('pointerover', () => this.bgRect.setFillStyle(bgHover, 1)); for (const target of [this, this.bgRect, this.text]) {
this.text.on('pointerout', () => this.bgRect.setFillStyle(bg, variant === 'ghost' ? 0 : 1)); target.on('pointerover', onOver);
this.text.on('pointerdown', () => this.bgRect.setScale(0.97)); target.on('pointerout', onOut);
this.text.on('pointerup', () => this.bgRect.setScale(1)); target.on('pointerdown', onDown);
this.text.on('pointerupoutside', () => this.bgRect.setScale(1)); target.on('pointerup', onUp);
if (onClick) this.text.on('pointerup', onClick); target.on('pointerupoutside', onUp);
if (onClick) target.on('pointerup', onClick);
}
scene.add.existing(this); scene.add.existing(this);
} }

View File

@ -11,7 +11,7 @@ export class Modal extends Phaser.GameObjects.Container {
GAME_WIDTH / 2, GAME_HEIGHT / 2, 720, 280, COLORS.panel, 1, GAME_WIDTH / 2, GAME_HEIGHT / 2, 720, 280, COLORS.panel, 1,
).setStrokeStyle(2, COLORS.accent); ).setStrokeStyle(2, COLORS.accent);
const text = scene.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 30, message, { const text = scene.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 30, message, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: '28px', fontSize: '28px',
color: options.color ?? COLORS.textHex, color: options.color ?? COLORS.textHex,
wordWrap: { width: 660 }, wordWrap: { width: 660 },

View File

@ -105,7 +105,7 @@ export function createPlayerPortrait(scene, worldX, worldY, radius, depth, scene
const initial = (auth.user?.username ?? 'You').charAt(0).toUpperCase(); const initial = (auth.user?.username ?? 'You').charAt(0).toUpperCase();
const placeholder = scene.add.text(worldX, worldY, initial, { const placeholder = scene.add.text(worldX, worldY, initial, {
fontFamily: 'system-ui, sans-serif', fontFamily: '"Julius Sans One"',
fontSize: `${Math.round(radius * 0.9)}px`, fontSize: `${Math.round(radius * 0.9)}px`,
color: COLORS.accentHex, color: COLORS.accentHex,
}).setOrigin(0.5).setDepth(depth + 1); }).setOrigin(0.5).setDepth(depth + 1);

View File

@ -1,10 +1,24 @@
@font-face {
font-family: 'Righteous';
src: url('/assets/fonts/Righteous-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Julius Sans One';
src: url('/assets/fonts/JuliusSansOne-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;
background: #0a0e14; background: #0f0d0a;
color: #e6edf3; color: #f2ead8;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-family: 'Julius Sans One', system-ui, sans-serif;
overflow: hidden; overflow: hidden;
} }
@ -49,8 +63,8 @@ html, body {
#dom-layer input:focus, #dom-layer input:focus,
#dom-layer textarea:focus { #dom-layer textarea:focus {
border-color: #5aa9e6; border-color: #c8a84b;
box-shadow: 0 0 0 2px rgba(90, 169, 230, 0.3); box-shadow: 0 0 0 2px rgba(200, 168, 75, 0.3);
} }
#dom-layer button { #dom-layer button {

View File

@ -91,8 +91,11 @@ export function findSessionUser(sessionId) {
if (!sessionId) return null; if (!sessionId) return null;
const row = db const row = db
.prepare( .prepare(
`SELECT u.id, u.email, u.username, u.email_verified, s.expires_at `SELECT u.id, u.email, u.username, u.email_verified, s.expires_at,
FROM sessions s JOIN users u ON u.id = s.user_id p.display_name, p.avatar_path
FROM sessions s
JOIN users u ON u.id = s.user_id
LEFT JOIN profiles p ON p.user_id = u.id
WHERE s.id = ?`, WHERE s.id = ?`,
) )
.get(sessionId); .get(sessionId);
@ -106,6 +109,8 @@ export function findSessionUser(sessionId) {
email: row.email, email: row.email,
username: row.username, username: row.username,
emailVerified: !!row.email_verified, emailVerified: !!row.email_verified,
displayName: row.display_name || null,
avatarPath: row.avatar_path || null,
}; };
} }