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',
|
'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.
|
// 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];
|
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.opponents = data.opponents ?? [];
|
||||||
this.playfield = data.playfield ?? null;
|
this.playfield = data.playfield ?? null;
|
||||||
this.cardBack = data.cardBack ?? null;
|
this.cardBack = data.cardBack ?? null;
|
||||||
|
this.tilePlacement = data.tilePlacement ?? 'random';
|
||||||
this.gs = null;
|
this.gs = null;
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.highlights = [];
|
this.highlights = [];
|
||||||
|
|
@ -1019,7 +1020,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.placeMode = null;
|
this.placeMode = null;
|
||||||
const playerCount = Math.min(4, 1 + this.opponents.length);
|
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')];
|
const names = ['You', ...this.opponents.map((o) => o?.name ?? 'CPU')];
|
||||||
L.setPlayerNames(this.gs, names);
|
L.setPlayerNames(this.gs, names);
|
||||||
this.drawHexes();
|
this.drawHexes();
|
||||||
|
|
@ -1128,8 +1129,9 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const m = AI.chooseRobberMove(this.gs, seat);
|
const m = AI.chooseRobberMove(this.gs, seat);
|
||||||
const preRobberHex = this.gs.robberHex;
|
const preRobberHex = this.gs.robberHex;
|
||||||
this.gs = L.moveRobber(this.gs, m.hexId, m.targetSeat);
|
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');
|
if (m.targetSeat != null) this.opponentPortraits[seat]?.playEmotion?.('happy');
|
||||||
await this.animateRobber(preRobberHex, m.hexId);
|
await animPromise;
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
await this.delay(450);
|
await this.delay(450);
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NODES, EDGES, HEXES, PORT_SLOTS, edgeBetween,
|
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,
|
CHIT_SPIRAL, CHIT_SEQUENCE,
|
||||||
} from './CatanBoard.js';
|
} from './CatanBoard.js';
|
||||||
|
|
||||||
|
|
@ -42,11 +42,11 @@ export function edgeOwner(state, edgeId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── initial state ─────────────────────────────────────────────────────────────
|
// ── initial state ─────────────────────────────────────────────────────────────
|
||||||
export function createInitialState(playerCount = 3) {
|
export function createInitialState(playerCount = 3, { tilePlacement = 'random' } = {}) {
|
||||||
const n = Math.max(3, Math.min(4, playerCount));
|
const n = Math.max(3, Math.min(4, playerCount));
|
||||||
|
|
||||||
// Resources onto hexes.
|
// Resources onto hexes.
|
||||||
const resources = shuffle(RESOURCE_BAG);
|
const resources = tilePlacement === 'standard' ? [...STANDARD_RESOURCES] : shuffle(RESOURCE_BAG);
|
||||||
const hexes = HEXES.map((h, i) => ({
|
const hexes = HEXES.map((h, i) => ({
|
||||||
id: h.id,
|
id: h.id,
|
||||||
resource: resources[i],
|
resource: resources[i],
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export default class GameRoomScene extends Phaser.Scene {
|
||||||
this.opponents = data.opponents ?? [];
|
this.opponents = data.opponents ?? [];
|
||||||
this.playfield = data.playfield ?? null;
|
this.playfield = data.playfield ?? null;
|
||||||
this.cardBack = data.cardBack ?? null;
|
this.cardBack = data.cardBack ?? null;
|
||||||
|
this.tilePlacement = data.tilePlacement ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -22,6 +23,7 @@ export default class GameRoomScene extends Phaser.Scene {
|
||||||
opponents: this.opponents,
|
opponents: this.opponents,
|
||||||
playfield: this.playfield,
|
playfield: this.playfield,
|
||||||
cardBack: this.cardBack,
|
cardBack: this.cardBack,
|
||||||
|
tilePlacement: this.tilePlacement,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
||||||
this.playfieldTiles = [];
|
this.playfieldTiles = [];
|
||||||
this.selectedCardBack = null;
|
this.selectedCardBack = null;
|
||||||
this.cardBackTiles = [];
|
this.cardBackTiles = [];
|
||||||
|
this.selectedTilePlacement = 'random';
|
||||||
this._initializing = false;
|
this._initializing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,6 +39,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
||||||
playMenuMusic();
|
playMenuMusic();
|
||||||
setMenuMusicVolume(0.25);
|
setMenuMusicVolume(0.25);
|
||||||
const cx = GAME_WIDTH / 2;
|
const cx = GAME_WIDTH / 2;
|
||||||
|
const isCatan = this.gameDef.slug === 'catan';
|
||||||
|
|
||||||
const bgKey = this.gameDef.category === 'casino' ? 'bg-casino' : 'bg-room';
|
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);
|
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 (!this._startingGame) setMenuMusicVolume(0.6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isCatan) this.buildTilePlacementSection(340, 1013);
|
||||||
|
|
||||||
this.buildOptionSection('Playfield', 630, this.cache.json.get('playfields')?.playfields ?? [],
|
this.buildOptionSection('Playfield', 630, this.cache.json.get('playfields')?.playfields ?? [],
|
||||||
'selectedPlayfield', 'playfieldTiles', (pf) => this.selectPlayfield(pf));
|
'selectedPlayfield', 'playfieldTiles', (pf) => this.selectPlayfield(pf));
|
||||||
|
|
||||||
|
|
@ -400,6 +404,52 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
||||||
this.startBtn.setEnabled(this.selected.size >= min);
|
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 ─────────────────────────────────────────
|
// ── Generic option section builder ─────────────────────────────────────────
|
||||||
|
|
||||||
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) {
|
||||||
|
|
@ -519,6 +569,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
||||||
opponents,
|
opponents,
|
||||||
playfield: this.selectedPlayfield,
|
playfield: this.selectedPlayfield,
|
||||||
cardBack: this.selectedCardBack,
|
cardBack: this.selectedCardBack,
|
||||||
|
tilePlacement: this.selectedTilePlacement,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue