feat(catan): add standard tile placement option and update robber voice lines
- Add `STANDARD_RESOURCES` array to `CatanBoard.js` for the classic beginner layout - Allow users to choose between 'random' and 'standard' tile placement in `OpponentSelectScene` - Pass `tilePlacement` config through `GameRoomScene` to `CatanGame` and `CatanLogic` - Update robber voice clip files to new recordings
This commit is contained in:
parent
aa5609f2f9
commit
10df4781ef
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -36,6 +36,16 @@ export const RESOURCE_BAG = [
|
|||
'desert',
|
||||
];
|
||||
|
||||
// Fixed hex resources for the standard Catan beginner layout.
|
||||
// Indexed by hex ID (rows [3,4,5,4,3], left-to-right top-to-bottom).
|
||||
export const STANDARD_RESOURCES = [
|
||||
'ore', 'wool', 'lumber', // row 0: hex 0-2
|
||||
'grain', 'brick', 'wool', 'brick', // row 1: hex 3-6
|
||||
'lumber', 'grain', 'desert','grain', 'ore', // row 2: hex 7-11
|
||||
'lumber', 'ore', 'grain', 'wool', // row 3: hex 12-15
|
||||
'brick', 'wool', 'lumber', // row 4: hex 16-18
|
||||
];
|
||||
|
||||
// 18 number chits for the 18 non-desert hexes.
|
||||
export const CHIT_BAG = [2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 8, 9, 9, 10, 10, 11, 11, 12];
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
this.opponents = data.opponents ?? [];
|
||||
this.playfield = data.playfield ?? null;
|
||||
this.cardBack = data.cardBack ?? null;
|
||||
this.tilePlacement = data.tilePlacement ?? 'random';
|
||||
this.gs = null;
|
||||
this.busy = false;
|
||||
this.highlights = [];
|
||||
|
|
@ -1019,7 +1020,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
this.busy = false;
|
||||
this.placeMode = null;
|
||||
const playerCount = Math.min(4, 1 + this.opponents.length);
|
||||
this.gs = L.createInitialState(playerCount);
|
||||
this.gs = L.createInitialState(playerCount, { tilePlacement: this.tilePlacement });
|
||||
const names = ['You', ...this.opponents.map((o) => o?.name ?? 'CPU')];
|
||||
L.setPlayerNames(this.gs, names);
|
||||
this.drawHexes();
|
||||
|
|
@ -1128,8 +1129,9 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const m = AI.chooseRobberMove(this.gs, seat);
|
||||
const preRobberHex = this.gs.robberHex;
|
||||
this.gs = L.moveRobber(this.gs, m.hexId, m.targetSeat);
|
||||
const animPromise = this.animateRobber(preRobberHex, m.hexId);
|
||||
if (m.targetSeat != null) this.opponentPortraits[seat]?.playEmotion?.('happy');
|
||||
await this.animateRobber(preRobberHex, m.hexId);
|
||||
await animPromise;
|
||||
this.renderAll();
|
||||
await this.delay(450);
|
||||
this.busy = false;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {
|
||||
NODES, EDGES, HEXES, PORT_SLOTS, edgeBetween,
|
||||
RESOURCE_BAG, PORT_BAG, COSTS, DEV_DECK, RESOURCE_TYPES, WIN_VP,
|
||||
RESOURCE_BAG, STANDARD_RESOURCES, PORT_BAG, COSTS, DEV_DECK, RESOURCE_TYPES, WIN_VP,
|
||||
CHIT_SPIRAL, CHIT_SEQUENCE,
|
||||
} from './CatanBoard.js';
|
||||
|
||||
|
|
@ -42,11 +42,11 @@ export function edgeOwner(state, edgeId) {
|
|||
}
|
||||
|
||||
// ── initial state ─────────────────────────────────────────────────────────────
|
||||
export function createInitialState(playerCount = 3) {
|
||||
export function createInitialState(playerCount = 3, { tilePlacement = 'random' } = {}) {
|
||||
const n = Math.max(3, Math.min(4, playerCount));
|
||||
|
||||
// Resources onto hexes.
|
||||
const resources = shuffle(RESOURCE_BAG);
|
||||
const resources = tilePlacement === 'standard' ? [...STANDARD_RESOURCES] : shuffle(RESOURCE_BAG);
|
||||
const hexes = HEXES.map((h, i) => ({
|
||||
id: h.id,
|
||||
resource: resources[i],
|
||||
|
|
|
|||
|
|
@ -8,20 +8,22 @@ export default class GameRoomScene extends Phaser.Scene {
|
|||
constructor() { super('GameRoom'); }
|
||||
|
||||
init(data) {
|
||||
this.game = data.game;
|
||||
this.opponents = data.opponents ?? [];
|
||||
this.playfield = data.playfield ?? null;
|
||||
this.cardBack = data.cardBack ?? null;
|
||||
this.game = data.game;
|
||||
this.opponents = data.opponents ?? [];
|
||||
this.playfield = data.playfield ?? null;
|
||||
this.cardBack = data.cardBack ?? null;
|
||||
this.tilePlacement = data.tilePlacement ?? null;
|
||||
}
|
||||
|
||||
create() {
|
||||
const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame' };
|
||||
if (slugDispatch[this.game.slug]) {
|
||||
this.scene.start(slugDispatch[this.game.slug], {
|
||||
game: this.game,
|
||||
opponents: this.opponents,
|
||||
playfield: this.playfield,
|
||||
cardBack: this.cardBack,
|
||||
game: this.game,
|
||||
opponents: this.opponents,
|
||||
playfield: this.playfield,
|
||||
cardBack: this.cardBack,
|
||||
tilePlacement: this.tilePlacement,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
this.playfieldTiles = [];
|
||||
this.selectedCardBack = null;
|
||||
this.cardBackTiles = [];
|
||||
this.selectedTilePlacement = 'random';
|
||||
this._initializing = false;
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +39,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
playMenuMusic();
|
||||
setMenuMusicVolume(0.25);
|
||||
const cx = GAME_WIDTH / 2;
|
||||
const isCatan = this.gameDef.slug === 'catan';
|
||||
|
||||
const bgKey = this.gameDef.category === 'casino' ? 'bg-casino' : 'bg-room';
|
||||
this.add.image(cx, GAME_HEIGHT / 2, bgKey).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(-1);
|
||||
|
|
@ -103,6 +105,8 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
if (!this._startingGame) setMenuMusicVolume(0.6);
|
||||
});
|
||||
|
||||
if (isCatan) this.buildTilePlacementSection(340, 1013);
|
||||
|
||||
this.buildOptionSection('Playfield', 630, this.cache.json.get('playfields')?.playfields ?? [],
|
||||
'selectedPlayfield', 'playfieldTiles', (pf) => this.selectPlayfield(pf));
|
||||
|
||||
|
|
@ -400,6 +404,52 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
this.startBtn.setEnabled(this.selected.size >= min);
|
||||
}
|
||||
|
||||
// ── Catan: tile placement toggle ───────────────────────────────────────────
|
||||
buildTilePlacementSection(centerX, centerY) {
|
||||
const options = [
|
||||
{ id: 'random', label: 'Random' },
|
||||
{ id: 'standard', label: 'Standard' },
|
||||
];
|
||||
const pillW = 150, pillH = 40, pillGap = 12;
|
||||
const totalW = options.length * pillW + (options.length - 1) * pillGap;
|
||||
const labelY = centerY - 28;
|
||||
const pillY = centerY + 10;
|
||||
|
||||
const labelText = this.add.text(centerX, labelY, 'Tile Placement', {
|
||||
fontFamily: '"Julius Sans One"',
|
||||
fontSize: '20px',
|
||||
color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5);
|
||||
const labelBg = this.add.rectangle(centerX, labelY, labelText.width + 32, labelText.height + 14, 0x000000, 0.72);
|
||||
this.children.moveBelow(labelBg, labelText);
|
||||
|
||||
this._tilePlacementBtns = [];
|
||||
options.forEach((opt, i) => {
|
||||
const x = centerX - totalW / 2 + i * (pillW + pillGap) + pillW / 2;
|
||||
const isSelected = this.selectedTilePlacement === opt.id;
|
||||
const bg = this.add.rectangle(x, pillY, pillW, pillH, COLORS.panel)
|
||||
.setStrokeStyle(3, isSelected ? COLORS.accent : COLORS.muted)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
const pillBg = this.add.rectangle(x, pillY, pillW, pillH, 0x000000, 0.72);
|
||||
this.children.moveBelow(pillBg, bg);
|
||||
this.add.text(x, pillY, opt.label, {
|
||||
fontFamily: '"Julius Sans One"',
|
||||
fontSize: '16px',
|
||||
color: COLORS.textHex,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
const refresh = () => {
|
||||
this._tilePlacementBtns.forEach(({ bg: b, id }) =>
|
||||
b.setStrokeStyle(3, id === this.selectedTilePlacement ? COLORS.accent : COLORS.muted)
|
||||
);
|
||||
};
|
||||
bg.on('pointerup', () => { this.selectedTilePlacement = opt.id; refresh(); });
|
||||
bg.on('pointerover', () => { if (this.selectedTilePlacement !== opt.id) bg.setStrokeStyle(3, COLORS.text); });
|
||||
bg.on('pointerout', () => { if (this.selectedTilePlacement !== opt.id) bg.setStrokeStyle(3, COLORS.muted); });
|
||||
this._tilePlacementBtns.push({ bg, id: opt.id });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Generic option section builder ─────────────────────────────────────────
|
||||
|
||||
buildOptionSection(label, labelY, items, selectedProp, tilesProp, onSelect, tileW = TILE_W, tileH = TILE_H, tileGap = TILE_GAP) {
|
||||
|
|
@ -517,8 +567,9 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
this.scene.start('GameRoom', {
|
||||
game: this.gameDef,
|
||||
opponents,
|
||||
playfield: this.selectedPlayfield,
|
||||
cardBack: this.selectedCardBack,
|
||||
playfield: this.selectedPlayfield,
|
||||
cardBack: this.selectedCardBack,
|
||||
tilePlacement: this.selectedTilePlacement,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue