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 COLORS = {
bg: 0x0a0e14,
bgHex: '#0a0e14',
panel: 0x111923,
panelHex: '#111923',
accent: 0x5aa9e6,
accentHex: '#5aa9e6',
text: 0xe6edf3,
textHex: '#e6edf3',
muted: 0x8a94a6,
mutedHex: '#8a94a6',
bg: 0x0f0d0a,
bgHex: '#0f0d0a',
panel: 0x1e1a12,
panelHex: '#1e1a12',
accent: 0xc8a84b,
accentHex: '#c8a84b',
text: 0xf2ead8,
textHex: '#f2ead8',
muted: 0x9e9080,
mutedHex: '#9e9080',
danger: 0xe06c75,
dangerHex: '#e06c75',
gold: 0xd4a017,
goldHex: '#d4a017',
textDark: 0x1a1208,
textDarkHex: '#1a1208',
};
export const API_BASE = '/api';

View File

@ -151,7 +151,7 @@ export default class BackgammonGame extends Phaser.Scene {
const isBottom = idx < 12;
const ly = isBottom ? FY + FH + 14 : FY - 14;
this.add.text(cx, ly, label, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '18px',
color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.board);
@ -181,20 +181,20 @@ export default class BackgammonGame extends Phaser.Scene {
// Labels
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);
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);
// Pip count labels
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);
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);
}
@ -246,13 +246,13 @@ export default class BackgammonGame extends Phaser.Scene {
// Status message (also serves as turn label at bottom)
this.turnText = this.add.text(cx, BY + BH + 18, '', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '24px',
color: COLORS.mutedHex,
}).setOrigin(0.5).setDepth(DEPTH.ui);
this.statusText = this.add.text(cx, BY + BH + 46, '', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '22px',
color: COLORS.mutedHex,
}).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.opponentPortrait = createOpponentPortrait(this, opp, avatarX, oppAY, r, depth + 1);
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',
}).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);
createPlayerPortrait(this, avatarX, plrAY, r, depth + 1, 'Backgammon');
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',
}).setOrigin(0.5, 1).setDepth(depth + 2);
}
@ -352,7 +352,7 @@ export default class BackgammonGame extends Phaser.Scene {
if (pt.count > 5) {
const pos = this.checkerScreenPos(idx, 4);
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',
}).setOrigin(0.5).setDepth(DEPTH.checker + 1);
}
@ -377,7 +377,7 @@ export default class BackgammonGame extends Phaser.Scene {
if (count > 4) {
const dy = color === 'white' ? 3 * (CR * 2 + 2) : -(3 * (CR * 2 + 2));
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,
}).setOrigin(0.5).setDepth(DEPTH.checker + 1);
}
@ -817,7 +817,7 @@ export default class BackgammonGame extends Phaser.Scene {
showTurnBanner(text) {
const cx = BX + BW / 2;
const banner = this.add.text(cx, BY - 80, text, {
fontFamily: 'system-ui, sans-serif',
fontFamily: 'Righteous',
fontSize: '36px',
color: COLORS.textHex,
backgroundColor: '#111923ee',
@ -888,7 +888,7 @@ export default class BackgammonGame extends Phaser.Scene {
.setStrokeStyle(3, COLORS.accent)
.setDepth(DEPTH.banner);
const txt = this.add.text(BX + BW / 2, BY + BH / 2 - 40, msg, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '32px',
color: isHuman ? '#ffd700' : COLORS.textHex,
align: 'center',

View File

@ -116,15 +116,15 @@ export default class BlackjackGame extends Phaser.Scene {
// ── Dealer area ───────────────────────────────────────────────────────────
buildDealerArea() {
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);
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);
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);
}
@ -159,11 +159,11 @@ export default class BlackjackGame extends Phaser.Scene {
}
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);
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);
// 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, '', {
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);
// Portraits
@ -215,7 +215,7 @@ export default class BlackjackGame extends Phaser.Scene {
container.on('pointerout', () => container.setAlpha(1));
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',
}).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', {
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);
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);
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
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',
color: '#ffffff',
align: 'center',
@ -512,7 +512,7 @@ export default class BlackjackGame extends Phaser.Scene {
circle.lineStyle(2, 0xf0e8d0, 0.8);
circle.strokeCircle(0, 0, 28);
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);
cont.add([circle, txt]);
this.betGraphics[seat] = cont;
@ -575,7 +575,7 @@ export default class BlackjackGame extends Phaser.Scene {
container.add(g);
const color = card.isRed ? '#c0392b' : '#1a1a2e';
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' } : {}),
});
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 badge = this.add.text(pos.x, textY, LABELS[result] ?? result, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: SIZES[result] ?? '48px',
color: COLORS[result] ?? '#ffffff',
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 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)', {
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);
modal.add([bg, txt]);

View File

@ -150,7 +150,7 @@ export default class HoldemGame extends Phaser.Scene {
buildPotDisplay() {
this.potText = this.add.text(CX, CY - CARD_H / 2 - 36, 'Pot: $0', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '26px',
color: '#f0e8d0',
}).setOrigin(0.5).setDepth(D.ui);
@ -158,13 +158,13 @@ export default class HoldemGame extends Phaser.Scene {
buildBlindDisplay() {
this.blindText = this.add.text(24, 24, 'Blinds $5/$10', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '20px',
color: COLORS.mutedHex,
}).setOrigin(0, 0).setDepth(D.ui);
this.timerText = this.add.text(24, 50, '', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '16px',
color: COLORS.mutedHex,
}).setOrigin(0, 0).setDepth(D.ui);
@ -181,7 +181,7 @@ export default class HoldemGame extends Phaser.Scene {
// Name label
const nameY = isHuman ? -CARD_H / 2 - 48 : CARD_H / 2 + 28;
const name = this.add.text(0, nameY, '—', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '18px',
color: COLORS.textHex,
}).setOrigin(0.5);
@ -189,7 +189,7 @@ export default class HoldemGame extends Phaser.Scene {
// Chip count
const chipY = isHuman ? -CARD_H / 2 - 24 : CARD_H / 2 + 52;
const chipTxt = this.add.text(0, chipY, '$0', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '20px',
color: '#f0e8d0',
}).setOrigin(0.5);
@ -197,7 +197,7 @@ export default class HoldemGame extends Phaser.Scene {
// 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 betTxt = this.add.text(0, betY, '', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '18px',
color: '#ffd700',
}).setOrigin(0.5);
@ -205,7 +205,7 @@ export default class HoldemGame extends Phaser.Scene {
// Dealer chip
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', {
fontFamily: 'system-ui, sans-serif', fontSize: '14px', color: '#1a1a1a',
fontFamily: '"Julius Sans One"', fontSize: '14px', color: '#1a1a1a',
}).setOrigin(0.5).setVisible(false);
// 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)
.setStrokeStyle(1, COLORS.accent);
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);
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);
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);
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);
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);
const modalItems = [overlay, panel, title, balanceTxt, buyInTxt];
if (this.globalChips <= 0) {
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);
return;
}
@ -742,7 +742,7 @@ export default class HoldemGame extends Phaser.Scene {
}
const label = this.add.text(0, 22, `$${p.bet}`, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '15px',
color: '#f0e8d0',
stroke: '#000000',
@ -951,7 +951,7 @@ export default class HoldemGame extends Phaser.Scene {
const color = card.isRed ? '#c0392b' : '#1a1a2e';
const sym = card.suitSymbol;
const style = (size, bold = false) => ({
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: `${size}px`,
color,
...(bold ? { fontStyle: 'bold' } : {}),
@ -995,10 +995,10 @@ export default class HoldemGame extends Phaser.Scene {
const isHuman = seat === 0;
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 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(), {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '22px',
color: colors[action.type] ?? COLORS.textHex,
backgroundColor: '#111923cc',
@ -1043,7 +1043,7 @@ export default class HoldemGame extends Phaser.Scene {
// Title
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);
S.push(title);
this.tweens.add({ targets: title, alpha: 1, duration: 300, delay: 200 });
@ -1143,7 +1143,7 @@ export default class HoldemGame extends Phaser.Scene {
} else {
const initial = (auth.user?.username ?? 'You').charAt(0).toUpperCase();
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);
S.push(initTxt);
this.tweens.add({ targets: initTxt, alpha: 1, duration: 350, delay });
@ -1152,7 +1152,7 @@ export default class HoldemGame extends Phaser.Scene {
// Player name
const nameX = 250;
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);
S.push(nameTxt);
this.tweens.add({ targets: nameTxt, alpha: 1, duration: 350, delay });
@ -1172,7 +1172,7 @@ export default class HoldemGame extends Phaser.Scene {
}
if (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);
S.push(statusTxt);
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;
if (won > 0) {
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',
}).setOrigin(0, 0.5).setDepth(D.modal + 2).setAlpha(0);
S.push(wonTxt);
@ -1212,7 +1212,7 @@ export default class HoldemGame extends Phaser.Scene {
// Chip count
const chipStr = player.eliminated ? 'Eliminated' : `$${player.chips} chips`;
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);
S.push(chipTxt);
this.tweens.add({ targets: chipTxt, alpha: 1, duration: 350, delay });
@ -1246,19 +1246,19 @@ export default class HoldemGame extends Phaser.Scene {
: '🎉 You win!';
this.add.text(CX, CY - 140, headline, {
fontFamily: 'system-ui, sans-serif', fontSize: '40px',
fontFamily: 'Righteous', fontSize: '40px',
color: won ? '#ffd700' : COLORS.textHex,
}).setOrigin(0.5).setDepth(D.modal);
this.add.text(CX, CY - 70, won
? `+$${net} profit`
: net === 0 ? 'Broke even' : `-$${Math.abs(net)} loss`, {
fontFamily: 'system-ui, sans-serif', fontSize: '28px',
color: won ? '#5aa9e6' : COLORS.dangerHex,
fontFamily: '"Julius Sans One"', fontSize: '28px',
color: won ? COLORS.accentHex : COLORS.dangerHex,
}).setOrigin(0.5).setDepth(D.modal);
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);
new Button(this, CX - 110, CY + 100, 'Play Again', () => {

View File

@ -52,19 +52,19 @@ function trackXY(idx) {
}
function homeXY(color, idx) {
if (color === 'red') return { col: 9, row: 17 - idx };
if (color === 'blue') return { col: 17 - idx, row: 9 };
if (color === 'yellow') return { col: 9, row: 1 + idx };
if (color === 'green') return { col: 1 + idx, row: 9 };
if (color === 'red') return { col: 1 + idx, row: 9 }; // west spoke
if (color === 'blue') return { col: 9, row: 17 - idx }; // south spoke
if (color === 'yellow') return { col: 17 - idx, row: 9 }; // east spoke
if (color === 'green') return { col: 9, row: 1 + idx }; // north spoke
throw new Error(`bad color ${color}`);
}
// Final "home" cell at the center boundary per color.
function homeFinalXY(color) {
if (color === 'red') return { col: 9, row: 10 };
if (color === 'blue') return { col: 10, row: 9 };
if (color === 'yellow') return { col: 9, row: 8 };
if (color === 'green') return { col: 8, row: 9 };
if (color === 'red') return { col: 8, row: 9 }; // west spoke
if (color === 'blue') return { col: 9, row: 10 }; // south spoke
if (color === 'yellow') return { col: 10, row: 9 }; // east spoke
if (color === 'green') return { col: 9, row: 8 }; // north spoke
}
function cellWorld(col, row) {
@ -146,6 +146,9 @@ export default class ParchisiGame extends Phaser.Scene {
this.turnIndicator = null;
this.turnIndicatorGfx = null;
this.turnIndicatorPulseTween = null;
this.turnIndicatorMoveTween = null;
this.bonusChip20 = null;
this._bonusChip20Visible = false;
}
create() {
@ -153,6 +156,7 @@ export default class ParchisiGame extends Phaser.Scene {
this.buildBoard();
this.buildDice();
this.buildUI();
this.buildBonusChip();
this.buildPlayerCards();
this.buildTurnIndicator();
this.buildPawns();
@ -229,6 +233,7 @@ export default class ParchisiGame extends Phaser.Scene {
if (isSafeTrack(idx) && !entryColor) {
this.drawStar(g, wp.x, wp.y, 5, 10, 5, 0x2b5d80, 0.7);
}
}
drawHomeColumn(color) {
@ -263,7 +268,7 @@ export default class ParchisiGame extends Phaser.Scene {
g.lineStyle(2, TRACK_STROKE, 0.9);
g.strokeCircle(cx, cy, r);
this.add.text(cx, cy, 'HOME', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '16px',
color: '#3a2010',
fontStyle: 'bold',
@ -287,7 +292,7 @@ export default class ParchisiGame extends Phaser.Scene {
// ── Dice ──────────────────────────────────────────────────────────────────
buildDice() {
const baseX = ORIGIN_X - 80;
const baseY = GAME_HEIGHT / 2 - 150;
const baseY = GAME_HEIGHT / 2 - 80;
for (let i = 0; i < 2; i++) {
const g = this.add.graphics();
const container = this.add.container(baseX, baseY + i * 80).setDepth(DEPTH.dice);
@ -344,6 +349,7 @@ export default class ParchisiGame extends Phaser.Scene {
}
updateDiceDisplay() {
this.updateBonusChip();
if (!this.gs.dice) {
this.diceContainers.forEach((c) => c.setAlpha(0.25));
return;
@ -364,27 +370,66 @@ export default class ParchisiGame extends Phaser.Scene {
// ── UI / Portraits ────────────────────────────────────────────────────────
buildUI() {
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(), {
width: 110, height: 44, fontSize: 22,
});
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,
}).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,
}).setDepth(DEPTH.ui);
this.statusText = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 30, '', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '22px',
color: UI.textHex,
}).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() {
const portraitR = 56;
// 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);
createPlayerPortrait(this, xLeft, plY, portraitR, DEPTH.ui + 1, 'ParchisiGame');
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);
// 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.opponentPortraits[color] = createOpponentPortrait(this, opp, x, y, portraitR, DEPTH.ui + 1);
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);
});
}
@ -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);
// Stop both running tweens before starting new ones
if (this.turnIndicatorPulseTween) {
this.turnIndicatorPulseTween.stop();
this.turnIndicatorPulseTween = null;
}
if (this.turnIndicatorMoveTween) {
this.turnIndicatorMoveTween.stop();
this.turnIndicatorMoveTween = null;
}
this.turnIndicator.setScale(1);
if (immediately || this.turnIndicator.alpha === 0) {
this._drawTurnTriangle(side);
this.turnIndicator.setPosition(x, y).setAlpha(1);
this._startIndicatorPulse();
onArrive?.();
return;
}
this.tweens.add({
this.turnIndicatorMoveTween = this.tweens.add({
targets: this.turnIndicator,
x, y,
duration: 550,
duration: 400,
ease: 'Cubic.easeInOut',
onComplete: () => {
this.turnIndicatorMoveTween = null;
this._drawTurnTriangle(side);
this._startIndicatorPulse();
onArrive?.();
},
});
}
@ -749,13 +802,16 @@ export default class ParchisiGame extends Phaser.Scene {
afterTurn() {
this.updateButtons();
this.moveTurnIndicator(this.gs.currentPlayer);
if (this.gs.currentPlayer === 'red') {
this.moveTurnIndicator('red');
this.setStatus('Your turn — roll the dice');
} else {
const opp = this.opponents[['blue', 'yellow', 'green'].indexOf(this.gs.currentPlayer)];
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') {
this.animating = false;
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();
return;
}
@ -935,7 +986,7 @@ export default class ParchisiGame extends Phaser.Scene {
? '🎉 You Win!\nAll four pawns home!'
: `${oppName} wins this round.\nBetter luck next game!`;
const txt = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 50, msg, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '32px',
color: isHuman ? '#ffd700' : UI.textHex,
align: 'center',

View File

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

View File

@ -10,11 +10,14 @@ export default class GameMenuScene extends Phaser.Scene {
async create() {
const cx = GAME_WIDTH / 2;
this.add.text(cx, 120, 'Choose a game', {
fontFamily: 'system-ui, sans-serif',
this.add.image(cx, GAME_HEIGHT / 2, 'bg-menu').setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
const titleText = this.add.text(cx, 120, 'Choose a game', {
fontFamily: 'Righteous',
fontSize: '64px',
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…', {
fontSize: '24px', color: COLORS.mutedHex,
@ -40,17 +43,19 @@ export default class GameMenuScene extends Phaser.Scene {
}
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, {
fontFamily: 'system-ui, sans-serif',
fontFamily: 'Righteous',
fontSize: '40px',
color: COLORS.accentHex,
}).setOrigin(0.5);
games.forEach((game, i) => {
const btn = new Button(this, x, y + 80 + i * 90, game.name, () => this.openGame(game), {
width: 360,
});
void btn;
new Button(this, x, y + 80 + i * 90, game.name, () => this.openGame(game), { width: 360 });
});
}

View File

@ -31,7 +31,7 @@ export default class GameRoomScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2;
this.add.text(cx, 80, `${this.game.name}`, {
fontFamily: 'system-ui, sans-serif',
fontFamily: 'Righteous',
fontSize: '52px',
color: COLORS.textHex,
}).setOrigin(0.5);
@ -44,7 +44,7 @@ export default class GameRoomScene extends Phaser.Scene {
// Placeholder table felt
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)`, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '28px',
color: COLORS.mutedHex,
align: 'center',

View File

@ -9,19 +9,29 @@ export default class LandingScene extends Phaser.Scene {
create() {
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', {
fontFamily: 'Georgia, "Times New Roman", serif',
fontSize: '96px',
color: COLORS.textHex,
}).setOrigin(0.5);
const logo = this.add.image(cx, 290, 'main-title').setOrigin(0.5, 0.5).setAlpha(0);
logo.postFX.addShadow(3, 6, 0.005, 2, 0x000000, 10, 0.75);
this.add.text(cx, 320, 'Cards, dice, and classic tables.', {
fontFamily: 'system-ui, sans-serif',
fontSize: '32px',
color: COLORS.mutedHex,
}).setOrigin(0.5);
this.tweens.add({
targets: logo,
alpha: 1,
y: 260,
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();
@ -37,29 +47,66 @@ export default class LandingScene extends Phaser.Scene {
const user = auth.user;
if (user) {
this.add.text(cx, 480, `Welcome back, ${user.username}`, {
fontFamily: 'system-ui, sans-serif',
const avatarR = 28;
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',
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) {
this.add.text(cx, 540, 'Email not yet verified — check the server console for the link in dev.', {
fontFamily: 'system-ui, sans-serif',
this.add.text(cx, 690, 'Email not yet verified — check the server console for the link in dev.', {
fontFamily: '"Julius Sans One"',
fontSize: '22px',
color: COLORS.dangerHex,
}).setOrigin(0.5);
}
new Button(this, cx, 660, 'Play', () => this.scene.start('GameMenu'));
new Button(this, cx, 740, 'Profile', () => this.scene.start('Profile'));
new Button(this, cx, 820, 'Sign out', async () => {
new Button(this, cx, 810, 'Play', () => this.scene.start('GameMenu'));
new Button(this, cx, 890, 'Profile', () => this.scene.start('Profile'));
new Button(this, cx, 970, 'Sign out', async () => {
await auth.logout();
}, { variant: 'ghost' });
} else {
new Button(this, cx, 540, 'Sign in', () => this.scene.start('Login'));
new Button(this, cx, 620, 'Create account', () => this.scene.start('Register'));
new Button(this, cx, 700, 'Continue as guest', () => this.scene.start('GameMenu'), { variant: 'ghost' });
new Button(this, cx, 690, 'Sign in', () => this.scene.start('Login'));
new Button(this, cx, 770, 'Create account', () => this.scene.start('Register'));
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;
this.add.text(cx, 100, `${this.game.name} — Lobby`, {
fontFamily: 'system-ui, sans-serif',
fontFamily: 'Righteous',
fontSize: '52px',
color: COLORS.textHex,
}).setOrigin(0.5);
this.listText = this.add.text(cx, 220, 'Connecting…', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '24px',
color: COLORS.mutedHex,
}).setOrigin(0.5);

View File

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

View File

@ -35,13 +35,13 @@ export default class OpponentSelectScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2;
this.add.text(cx, 60, this.gameDef.name, {
fontFamily: 'system-ui, sans-serif',
fontFamily: 'Righteous',
fontSize: '52px',
color: COLORS.textHex,
}).setOrigin(0.5);
this.add.text(cx, 122, 'Choose your opponent', {
fontFamily: 'system-ui, sans-serif',
fontFamily: 'Righteous',
fontSize: '36px',
color: COLORS.mutedHex,
}).setOrigin(0.5);
@ -185,7 +185,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
ctx.fillStyle = COLORS.panelHex;
ctx.fillRect(0, 0, portraitSize, portraitSize);
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.textBaseline = 'middle';
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');
name.textContent = opp.name ?? '';
name.style.cssText = [
'font-family:system-ui,sans-serif',
'font-family:"Julius Sans One"',
'font-size:22px',
'font-weight:600',
`color:${COLORS.textHex}`,
@ -227,7 +227,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
const bio = document.createElement('div');
bio.textContent = opp.bio ?? '';
bio.style.cssText = [
'font-family:system-ui,sans-serif',
'font-family:"Julius Sans One"',
'font-size:15px',
`color:${COLORS.mutedHex}`,
'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) {
const cx = GAME_WIDTH / 2;
this.add.text(cx, labelY, label, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '24px',
color: COLORS.mutedHex,
}).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, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '13px',
color: COLORS.textHex,
}).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)
.setOrigin(0, 0.5);
this.add.text(w / 2, h / 2 - 60, 'Loading…', {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '32px',
color: COLORS.textHex,
}).setOrigin(0.5);
@ -30,6 +30,8 @@ export default class PreloadScene extends Phaser.Scene {
frameWidth: 320,
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('card-backs', '/data/card-backs.json');
}

View File

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

View File

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

View File

@ -14,7 +14,7 @@ export default class VerifyScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2;
this.add.text(cx, 220, 'Verify your email', {
fontFamily: 'system-ui, sans-serif',
fontFamily: 'Righteous',
fontSize: '64px',
color: COLORS.textHex,
}).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.';
this.add.text(cx, 360, body, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '26px',
color: COLORS.mutedHex,
wordWrap: { width: 1200 },

View File

@ -1,6 +1,8 @@
import * as Phaser from 'phaser';
import { COLORS } from '../config.js';
const RADIUS = 8;
export class Button extends Phaser.GameObjects.Container {
constructor(scene, x, y, label, onClick, options = {}) {
super(scene, x, y);
@ -8,49 +10,74 @@ export class Button extends Phaser.GameObjects.Container {
width = 280,
height = 64,
bg = COLORS.panel,
bgHover = COLORS.accent,
bgHover = COLORS.gold,
textColor = COLORS.textHex,
textHoverColor = COLORS.textDarkHex,
fontSize = 28,
variant = 'solid',
} = 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);
this.bgRect.setStrokeStyle(2, COLORS.accent, 1);
const isGhost = variant === 'ghost';
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, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: `${fontSize}px`,
color: textColor,
}).setOrigin(0.5);
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.setInteractive({ useHandCursor: true, hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains });
this.bgRect.setInteractive({ hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains });
this.text.setInteractive({ hitArea: new Phaser.Geom.Rectangle(0, 0, width, height), hitAreaCallback: Phaser.Geom.Rectangle.Contains });
this.on('pointerover', () => this.bgRect.setFillStyle(bgHover, 1));
this.on('pointerout', () => this.bgRect.setFillStyle(bg, variant === 'ghost' ? 0 : 1));
this.on('pointerdown', () => this.bgRect.setScale(0.97));
this.on('pointerup', () => this.bgRect.setScale(1));
this.on('pointerupoutside', () => this.bgRect.setScale(1));
if (onClick) this.on('pointerup', onClick);
this.setInteractive({ useHandCursor: true, hitArea: bgHitArea, hitAreaCallback: hitCb });
this.bgRect.setInteractive({ hitArea: bgHitArea, hitAreaCallback: hitCb });
this.text.setInteractive({ hitArea: textHitArea, hitAreaCallback: hitCb });
this.bgRect.on('pointerover', () => this.bgRect.setFillStyle(bgHover, 1));
this.bgRect.on('pointerout', () => this.bgRect.setFillStyle(bg, variant === 'ghost' ? 0 : 1));
this.bgRect.on('pointerdown', () => this.bgRect.setScale(0.97));
this.bgRect.on('pointerup', () => this.bgRect.setScale(1));
this.bgRect.on('pointerupoutside', () => this.bgRect.setScale(1));
if (onClick) this.bgRect.on('pointerup', onClick);
const onOver = () => {
if (isGhost) {
drawBg(bgHover, 0.18);
this.text.setColor(COLORS.goldHex);
} else {
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));
this.text.on('pointerout', () => this.bgRect.setFillStyle(bg, variant === 'ghost' ? 0 : 1));
this.text.on('pointerdown', () => this.bgRect.setScale(0.97));
this.text.on('pointerup', () => this.bgRect.setScale(1));
this.text.on('pointerupoutside', () => this.bgRect.setScale(1));
if (onClick) this.text.on('pointerup', onClick);
for (const target of [this, this.bgRect, this.text]) {
target.on('pointerover', onOver);
target.on('pointerout', onOut);
target.on('pointerdown', onDown);
target.on('pointerup', onUp);
target.on('pointerupoutside', onUp);
if (onClick) target.on('pointerup', onClick);
}
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,
).setStrokeStyle(2, COLORS.accent);
const text = scene.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 30, message, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: '28px',
color: options.color ?? COLORS.textHex,
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 placeholder = scene.add.text(worldX, worldY, initial, {
fontFamily: 'system-ui, sans-serif',
fontFamily: '"Julius Sans One"',
fontSize: `${Math.round(radius * 0.9)}px`,
color: COLORS.accentHex,
}).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 {
margin: 0;
padding: 0;
height: 100%;
background: #0a0e14;
color: #e6edf3;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: #0f0d0a;
color: #f2ead8;
font-family: 'Julius Sans One', system-ui, sans-serif;
overflow: hidden;
}
@ -49,8 +63,8 @@ html, body {
#dom-layer input:focus,
#dom-layer textarea:focus {
border-color: #5aa9e6;
box-shadow: 0 0 0 2px rgba(90, 169, 230, 0.3);
border-color: #c8a84b;
box-shadow: 0 0 0 2px rgba(200, 168, 75, 0.3);
}
#dom-layer button {

View File

@ -91,8 +91,11 @@ export function findSessionUser(sessionId) {
if (!sessionId) return null;
const row = db
.prepare(
`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
`SELECT u.id, u.email, u.username, u.email_verified, s.expires_at,
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 = ?`,
)
.get(sessionId);
@ -106,6 +109,8 @@ export function findSessionUser(sessionId) {
email: row.email,
username: row.username,
emailVerified: !!row.email_verified,
displayName: row.display_name || null,
avatarPath: row.avatar_path || null,
};
}