diff --git a/public/assets/images/tickettoride-cards.png b/public/assets/images/tickettoride-cards.png new file mode 100644 index 0000000..7b2b5b7 Binary files /dev/null and b/public/assets/images/tickettoride-cards.png differ diff --git a/public/assets/images/tickettoride-cards.psd b/public/assets/images/tickettoride-cards.psd new file mode 100644 index 0000000..9eefc96 Binary files /dev/null and b/public/assets/images/tickettoride-cards.psd differ diff --git a/public/src/games/dominion/DominionAI.js b/public/src/games/dominion/DominionAI.js index 08e55d5..080c5d5 100644 --- a/public/src/games/dominion/DominionAI.js +++ b/public/src/games/dominion/DominionAI.js @@ -116,7 +116,7 @@ export function chooseBuy(state, seat, skill = 3) { } // Treasure economy. In Colony games Platinum is the premier economy buy. - if (colonyGame && coins >= 5 && (state.supply.platinum ?? 0) > 0) return 'platinum'; + if (colonyGame && coins >= 9 && (state.supply.platinum ?? 0) > 0) return 'platinum'; if (coins >= 6 && (state.supply.gold ?? 0) > 0) return 'gold'; if (coins >= 3 && (state.supply.silver ?? 0) > 0) return 'silver'; return null; diff --git a/public/src/scenes/GameMenuScene.js b/public/src/scenes/GameMenuScene.js index 815eb15..9d03f9f 100644 --- a/public/src/scenes/GameMenuScene.js +++ b/public/src/scenes/GameMenuScene.js @@ -5,6 +5,13 @@ import { Button } from '../ui/Button.js'; import { addFullscreenButton } from '../ui/FullscreenButton.js'; import { playMenuMusic, stopMenuMusic } from '../ui/MenuMusic.js'; +const CATEGORIES = [ + { key: 'tabletop', label: 'Tabletop' }, + { key: 'cards', label: 'Cards' }, + { key: 'casino', label: 'Casino' }, + { key: 'word', label: 'Word' }, +]; + export default class GameMenuScene extends Phaser.Scene { constructor() { super('GameMenu'); } @@ -22,7 +29,7 @@ export default class GameMenuScene extends Phaser.Scene { }).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, 540, 'Loading game list…', { fontSize: '24px', color: COLORS.mutedHex, }).setOrigin(0.5); @@ -36,46 +43,68 @@ export default class GameMenuScene extends Phaser.Scene { } loadingText.destroy(); - const tabletop = games.filter((g) => g.category === 'tabletop'); - const cards = games.filter((g) => g.category === 'cards'); - const casino = games.filter((g) => g.category === 'casino'); - const word = games.filter((g) => g.category === 'word'); - - const hasWord = word.length > 0; - if (hasWord) { - this.renderColumn('Tabletop', tabletop, cx - 630, 260); - this.renderColumn('Cards', cards, cx - 210, 260); - this.renderColumn('Casino', casino, cx + 210, 260); - this.renderColumn('Word', word, cx + 630, 260); - } else { - this.renderColumn('Tabletop', tabletop, cx - 420, 260); - this.renderColumn('Cards', cards, cx, 260); - this.renderColumn('Casino', casino, cx + 420, 260); + this._gamesByCategory = {}; + for (const { key } of CATEGORIES) { + this._gamesByCategory[key] = games.filter((g) => g.category === key); } + this._gameObjects = []; + this._tabs = {}; + + const activeCats = CATEGORIES.filter(({ key }) => this._gamesByCategory[key].length > 0); + const tabSpacing = Math.min(380, 1600 / activeCats.length); + const tabStartX = cx - (tabSpacing * (activeCats.length - 1)) / 2; + + activeCats.forEach(({ key, label }, i) => { + const btn = new Button(this, tabStartX + i * tabSpacing, 230, label, () => this.showCategory(key), { + width: 320, + variant: 'ghost', + }); + this._tabs[key] = btn; + }); + + this.showCategory(activeCats[0].key); + new Button(this, cx, GAME_HEIGHT - 100, 'Back', () => this.scene.start('Landing'), { variant: 'ghost' }); } - 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); + showCategory(key) { + for (const [k, btn] of Object.entries(this._tabs)) { + btn.setActive(k === key); + } - this.add.text(x, y, title, { - fontFamily: 'Righteous', - fontSize: '40px', - color: COLORS.accentHex, - }).setOrigin(0.5); + for (const obj of this._gameObjects) obj.destroy(); + this._gameObjects = []; + + const games = this._gamesByCategory[key]; + if (!games || games.length === 0) return; + + const cx = GAME_WIDTH / 2; + const COLS = 3; + const COL_SPACING = 420; + const ROW_SPACING = 90; + const GRID_TOP = 340; + const BTN_WIDTH = 360; + const PADDING = 52; + + const rows = Math.ceil(games.length / COLS); + const panelH = (rows - 1) * ROW_SPACING + PADDING * 2; + const panelW = (COLS - 1) * COL_SPACING + BTN_WIDTH + 40; + const panelCenterY = GRID_TOP + (rows - 1) * ROW_SPACING / 2; + const panel = this.add.rectangle(cx, panelCenterY, panelW, panelH, 0x000000, 0.7); + this._gameObjects.push(panel); games.forEach((game, i) => { - new Button(this, x, y + 80 + i * 90, game.name, () => this.openGame(game), { width: 360 }); + const col = i % COLS; + const row = Math.floor(i / COLS); + const x = cx + (col - (COLS - 1) / 2) * COL_SPACING; + const y = GRID_TOP + row * ROW_SPACING; + const btn = new Button(this, x, y, game.name, () => this.openGame(game), { width: BTN_WIDTH }); + this._gameObjects.push(btn); }); } openGame(game) { - // Solo-only games (maxOpponents === 0) skip the opponent-selection screen, - // so stop the menu music here just as OpponentSelectScene does on start. if (game.maxOpponents === 0) { stopMenuMusic(); this.scene.start('GameRoom', { game, opponents: [] }); diff --git a/public/src/ui/Button.js b/public/src/ui/Button.js index a7b548c..36bc4ce 100644 --- a/public/src/ui/Button.js +++ b/public/src/ui/Button.js @@ -20,23 +20,11 @@ export class Button extends Phaser.GameObjects.Container { this.options = { width, height, bg, bgHover, textColor, textHoverColor, fontSize, variant }; 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._drawBg(bg, isGhost ? 0.35 : 1); this.text = scene.add.text(0, 0, label, { fontFamily: '"Julius Sans One"', @@ -54,17 +42,21 @@ export class Button extends Phaser.GameObjects.Container { }); const onOver = () => { - if (isGhost) { - drawBg(bgHover, 0.18); + if (this._active) return; + const { bgHover: bgh, textHoverColor: thc, variant: v } = this.options; + if (v === 'ghost') { + this._drawBg(bgh, 0.18); this.text.setColor(COLORS.goldHex); } else { - drawBg(bgHover, 1); - this.text.setColor(textHoverColor); + this._drawBg(bgh, 1); + this.text.setColor(thc); } }; const onOut = () => { - drawBg(bg, isGhost ? 0.35 : 1); - this.text.setColor(textColor); + if (this._active) return; + const { bg: b, textColor: tc, variant: v } = this.options; + this._drawBg(b, v === 'ghost' ? 0.35 : 1); + this.text.setColor(tc); }; const onDown = () => this.bgRect.setScale(0.97); const onUp = () => this.bgRect.setScale(1); @@ -79,6 +71,32 @@ export class Button extends Phaser.GameObjects.Container { scene.add.existing(this); } + _drawBg(fillColor, fillAlpha) { + const { width, height } = this.options; + const hw = width / 2; + const hh = height / 2; + 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); + } + + setActive(active) { + this._active = active; + const { bg, bgHover, textColor, textHoverColor, variant } = this.options; + if (active) { + this._drawBg(bgHover, 1); + this.text.setColor(textHoverColor); + } else { + this._drawBg(bg, variant === 'ghost' ? 0.35 : 1); + this.text.setColor(textColor); + } + return this; + } + setLabel(label) { this.text.setText(label); return this;