refactor: overhaul game menu layout, enhance Button component, and adjust Dominion AI
- Replace column-based game list with a tabbed category interface and grid layout - Add active state management to tabs and refactor Button component for consistent visual states - Increase Dominion AI Platinum purchase threshold from 5 to 9 coins in Colony games - Add Ticket to Ride card assets
This commit is contained in:
parent
2dbcb83754
commit
975d40b4b0
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
|
@ -116,7 +116,7 @@ export function chooseBuy(state, seat, skill = 3) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Treasure economy. In Colony games Platinum is the premier economy buy.
|
// 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 >= 6 && (state.supply.gold ?? 0) > 0) return 'gold';
|
||||||
if (coins >= 3 && (state.supply.silver ?? 0) > 0) return 'silver';
|
if (coins >= 3 && (state.supply.silver ?? 0) > 0) return 'silver';
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@ import { Button } from '../ui/Button.js';
|
||||||
import { addFullscreenButton } from '../ui/FullscreenButton.js';
|
import { addFullscreenButton } from '../ui/FullscreenButton.js';
|
||||||
import { playMenuMusic, stopMenuMusic } from '../ui/MenuMusic.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 {
|
export default class GameMenuScene extends Phaser.Scene {
|
||||||
constructor() { super('GameMenu'); }
|
constructor() { super('GameMenu'); }
|
||||||
|
|
||||||
|
|
@ -22,7 +29,7 @@ export default class GameMenuScene extends Phaser.Scene {
|
||||||
}).setOrigin(0.5).setDepth(1);
|
}).setOrigin(0.5).setDepth(1);
|
||||||
this.add.rectangle(cx, 120, titleText.width + 64, titleText.height + 28, 0x000000, 0.7);
|
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,
|
fontSize: '24px', color: COLORS.mutedHex,
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
|
|
@ -36,46 +43,68 @@ export default class GameMenuScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
loadingText.destroy();
|
loadingText.destroy();
|
||||||
|
|
||||||
const tabletop = games.filter((g) => g.category === 'tabletop');
|
this._gamesByCategory = {};
|
||||||
const cards = games.filter((g) => g.category === 'cards');
|
for (const { key } of CATEGORIES) {
|
||||||
const casino = games.filter((g) => g.category === 'casino');
|
this._gamesByCategory[key] = games.filter((g) => g.category === key);
|
||||||
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._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' });
|
new Button(this, cx, GAME_HEIGHT - 100, 'Back', () => this.scene.start('Landing'), { variant: 'ghost' });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderColumn(title, games, x, y) {
|
showCategory(key) {
|
||||||
const panelTop = y - 44;
|
for (const [k, btn] of Object.entries(this._tabs)) {
|
||||||
const panelBot = games.length > 0 ? y + 80 + (games.length - 1) * 90 + 56 : y + 56;
|
btn.setActive(k === key);
|
||||||
const panelH = panelBot - panelTop;
|
}
|
||||||
this.add.rectangle(x, panelTop + panelH / 2, 420, panelH, 0x000000, 0.7);
|
|
||||||
|
|
||||||
this.add.text(x, y, title, {
|
for (const obj of this._gameObjects) obj.destroy();
|
||||||
fontFamily: 'Righteous',
|
this._gameObjects = [];
|
||||||
fontSize: '40px',
|
|
||||||
color: COLORS.accentHex,
|
const games = this._gamesByCategory[key];
|
||||||
}).setOrigin(0.5);
|
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) => {
|
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) {
|
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) {
|
if (game.maxOpponents === 0) {
|
||||||
stopMenuMusic();
|
stopMenuMusic();
|
||||||
this.scene.start('GameRoom', { game, opponents: [] });
|
this.scene.start('GameRoom', { game, opponents: [] });
|
||||||
|
|
|
||||||
|
|
@ -20,23 +20,11 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
this.options = { width, height, bg, bgHover, textColor, textHoverColor, fontSize, variant };
|
this.options = { width, height, bg, bgHover, textColor, textHoverColor, fontSize, variant };
|
||||||
|
|
||||||
const isGhost = variant === 'ghost';
|
const isGhost = variant === 'ghost';
|
||||||
const hw = width / 2;
|
|
||||||
const hh = height / 2;
|
|
||||||
|
|
||||||
this.bgRect = scene.add.graphics();
|
this.bgRect = scene.add.graphics();
|
||||||
this.bgRect.postFX.addShadow(0, 3, 0.004, 1.5, 0x000000, 8, 0.65);
|
this.bgRect.postFX.addShadow(0, 3, 0.004, 1.5, 0x000000, 8, 0.65);
|
||||||
|
|
||||||
const drawBg = (fillColor, fillAlpha) => {
|
this._drawBg(bg, isGhost ? 0.35 : 1);
|
||||||
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: '"Julius Sans One"',
|
fontFamily: '"Julius Sans One"',
|
||||||
|
|
@ -54,17 +42,21 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
});
|
});
|
||||||
|
|
||||||
const onOver = () => {
|
const onOver = () => {
|
||||||
if (isGhost) {
|
if (this._active) return;
|
||||||
drawBg(bgHover, 0.18);
|
const { bgHover: bgh, textHoverColor: thc, variant: v } = this.options;
|
||||||
|
if (v === 'ghost') {
|
||||||
|
this._drawBg(bgh, 0.18);
|
||||||
this.text.setColor(COLORS.goldHex);
|
this.text.setColor(COLORS.goldHex);
|
||||||
} else {
|
} else {
|
||||||
drawBg(bgHover, 1);
|
this._drawBg(bgh, 1);
|
||||||
this.text.setColor(textHoverColor);
|
this.text.setColor(thc);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onOut = () => {
|
const onOut = () => {
|
||||||
drawBg(bg, isGhost ? 0.35 : 1);
|
if (this._active) return;
|
||||||
this.text.setColor(textColor);
|
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 onDown = () => this.bgRect.setScale(0.97);
|
||||||
const onUp = () => this.bgRect.setScale(1);
|
const onUp = () => this.bgRect.setScale(1);
|
||||||
|
|
@ -79,6 +71,32 @@ export class Button extends Phaser.GameObjects.Container {
|
||||||
scene.add.existing(this);
|
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) {
|
setLabel(label) {
|
||||||
this.text.setText(label);
|
this.text.setText(label);
|
||||||
return this;
|
return this;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue