feat(catan): implement Seafarers expansion features (gold hexes, ships, fog)
- Add gold hex resource picking phase with AI support - Implement ship building mechanics and update build costs UI - Introduce fog tiles that reveal terrain when roads/ships are built adjacent - Replace robber token with pirate ship graphic - Persist random tile frames for consistent hex visuals - Update game state machine to handle gold pick queue and phase transitions - Adjust UI layout (card positions, build panel) to accommodate Seafarers elements
This commit is contained in:
parent
c5f34b7c28
commit
9dbf3feae4
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -86,6 +86,15 @@ export function chooseDiscard(state, seat) {
|
|||
return discard;
|
||||
}
|
||||
|
||||
// Pick `amount` free resources from a gold hex — prioritise what is under-produced.
|
||||
export function chooseGoldPick(state, seat, amount) {
|
||||
const prod = productionByResource(state, seat);
|
||||
const order = [...RESOURCE_TYPES].sort((a, b) => prod[a] - prod[b]);
|
||||
const picks = [];
|
||||
for (let i = 0; i < amount; i++) picks.push(order[i % order.length]);
|
||||
return picks;
|
||||
}
|
||||
|
||||
export function chooseRobberMove(state, seat) {
|
||||
let best = null, bestScore = -Infinity, bestTarget = null;
|
||||
const geo = geoFor(state);
|
||||
|
|
|
|||
|
|
@ -91,9 +91,9 @@ export default class CatanGame extends Phaser.Scene {
|
|||
// Interpolate from outer dark (0x07243a) to inner lighter (0x1a5e80)
|
||||
const ro = 0x07, go = 0x24, bo = 0x3a;
|
||||
const ri = 0x1a, gi = 0x5e, bi = 0x80;
|
||||
const red = Math.round(ro + (ri - ro) * (1 - t));
|
||||
const red = Math.round(ro + (ri - ro) * (1 - t));
|
||||
const green = Math.round(go + (gi - go) * (1 - t));
|
||||
const blue = Math.round(bo + (bi - bo) * (1 - t));
|
||||
const blue = Math.round(bo + (bi - bo) * (1 - t));
|
||||
const color = (red << 16) | (green << 8) | blue;
|
||||
sea.fillStyle(color, 1);
|
||||
sea.fillCircle(SEA_X, SEA_Y, r);
|
||||
|
|
@ -113,12 +113,15 @@ export default class CatanGame extends Phaser.Scene {
|
|||
|
||||
// Frame pairs per resource: pick one at random each draw.
|
||||
static TILE_FRAMES = {
|
||||
lumber: [0, 1],
|
||||
wool: [2, 3],
|
||||
brick: [4, 5],
|
||||
ore: [6, 7],
|
||||
grain: [8, 9],
|
||||
desert: [10, 11],
|
||||
lumber: [0, 1],
|
||||
wool: [2, 3],
|
||||
brick: [4, 5],
|
||||
ore: [6, 7],
|
||||
grain: [8, 9],
|
||||
desert: [10, 11],
|
||||
sea: [12, 13],
|
||||
gold: [14, 15],
|
||||
fog: [16, 17],
|
||||
};
|
||||
|
||||
drawHexes() {
|
||||
|
|
@ -147,7 +150,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const terr = this.hexTerrain(hex);
|
||||
|
||||
// Scale factors for the 7px colored ring + 4px dark ring (absolute pixels).
|
||||
const s1 = 1 - 7 / inradius; // after colored border
|
||||
const s1 = 1 - 7 / inradius; // after colored border
|
||||
const s2 = 1 - 11 / inradius; // after dark border (image area)
|
||||
const innerPts = inset(pts, x, y, s1);
|
||||
const imagePts = inset(pts, x, y, s2);
|
||||
|
|
@ -162,7 +165,10 @@ export default class CatanGame extends Phaser.Scene {
|
|||
|
||||
// Layer 3: tile image masked to innermost polygon (land/desert only)
|
||||
if (terr.tileFrames && this.textures.exists('catan-tiles')) {
|
||||
const frame = terr.tileFrames[Math.floor(Math.random() * 2)];
|
||||
if (this.hexTileFrames[hex.id] == null) {
|
||||
this.hexTileFrames[hex.id] = terr.tileFrames[Math.floor(Math.random() * 2)];
|
||||
}
|
||||
const frame = this.hexTileFrames[hex.id];
|
||||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff);
|
||||
maskG.fillPoints(imagePts, true);
|
||||
|
|
@ -192,11 +198,11 @@ export default class CatanGame extends Phaser.Scene {
|
|||
hexTerrain(hex) {
|
||||
switch (hex.kind) {
|
||||
case 'sea':
|
||||
return { swatch: 0x2f6f9e, color: 0x2f6f9e, label: 'Sea', tileFrames: null };
|
||||
return { swatch: 0x2f6f9e, color: 0x2f6f9e, label: 'Sea', tileFrames: CatanGame.TILE_FRAMES.sea };
|
||||
case 'gold':
|
||||
return { swatch: 0xe8c14a, color: 0xd9a91f, label: 'Gold', tileFrames: null };
|
||||
return { swatch: 0xe8c14a, color: 0xd9a91f, label: 'Gold', tileFrames: CatanGame.TILE_FRAMES.gold };
|
||||
case 'fog':
|
||||
return { swatch: 0x6c7a86, color: 0x55606b, label: '?', tileFrames: null };
|
||||
return { swatch: 0x6c7a86, color: 0x55606b, label: '?', tileFrames: CatanGame.TILE_FRAMES.fog };
|
||||
case 'desert':
|
||||
return { swatch: DESERT_COLOR, color: DESERT_COLOR, label: 'Desert', tileFrames: CatanGame.TILE_FRAMES.desert };
|
||||
default: {
|
||||
|
|
@ -307,11 +313,11 @@ export default class CatanGame extends Phaser.Scene {
|
|||
return new Promise((resolve) => {
|
||||
playSound(this, SFX.DICE_ROLL);
|
||||
|
||||
const landX = [1256, 1324];
|
||||
const landY = 950;
|
||||
const landX = [1256, 1324];
|
||||
const landY = 950;
|
||||
const startX = GAME_WIDTH / 2; // 960 — center of bottom bar
|
||||
const startY = 1015;
|
||||
const arcY = 755; // arc peak
|
||||
const arcY = 755; // arc peak
|
||||
|
||||
// Move dice to throw origin, small, random angle
|
||||
this.diceContainers.forEach((c, i) => {
|
||||
|
|
@ -334,17 +340,19 @@ export default class CatanGame extends Phaser.Scene {
|
|||
let settled = 0;
|
||||
this.diceContainers.forEach((c, i) => {
|
||||
const lx = landX[i] + (Math.random() * 8 - 4);
|
||||
const ly = landY + (Math.random() * 8 - 4);
|
||||
const outMs = 295 + i * 32;
|
||||
const ly = landY + (Math.random() * 8 - 4);
|
||||
const outMs = 295 + i * 32;
|
||||
const backMs = 430 + i * 44;
|
||||
const totalMs = outMs + backMs;
|
||||
|
||||
// X flies straight to landing; Y arcs up then bounces down
|
||||
this.tweens.add({ targets: c, x: lx, duration: totalMs, ease: 'Quad.Out' });
|
||||
this.tweens.chain({ targets: c, tweens: [
|
||||
{ y: arcY, duration: outMs, ease: 'Quad.Out' },
|
||||
{ y: ly, duration: backMs, ease: 'Bounce.Out' },
|
||||
]});
|
||||
this.tweens.chain({
|
||||
targets: c, tweens: [
|
||||
{ y: arcY, duration: outMs, ease: 'Quad.Out' },
|
||||
{ y: ly, duration: backMs, ease: 'Bounce.Out' },
|
||||
]
|
||||
});
|
||||
// Scale up as die approaches
|
||||
this.tweens.add({ targets: c, scale: 1, duration: outMs + backMs * 0.55, ease: 'Quad.Out' });
|
||||
// Spin
|
||||
|
|
@ -368,7 +376,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
this.diceContainers.forEach((dc) =>
|
||||
this.tweens.add({ targets: dc, scaleX: 1.14, scaleY: 0.88, duration: 80, yoyo: true })
|
||||
);
|
||||
const numberWords = ['','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve'];
|
||||
const numberWords = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve'];
|
||||
enqueueSpeech(`numbers-${numberWords[values[0] + values[1]]}`);
|
||||
this.time.delayedCall(160, resolve);
|
||||
}
|
||||
|
|
@ -421,6 +429,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const mk = (key, label, fn) => { const b = new Button(this, bx, by, label, fn, { width: 200, height: 46, fontSize: 19 }).setDepth(D.hud); this.buttons[key] = b; by += step; return b; };
|
||||
mk('roll', 'Roll Dice', () => this.onRoll());
|
||||
mk('road', 'Build Road', () => this.enterPlace('road'));
|
||||
if (this.expansion === 'seafarers') mk('ship', 'Build Ship', () => this.enterPlace('ship'));
|
||||
mk('settlement', 'Build Settlement', () => this.enterPlace('settlement'));
|
||||
mk('city', 'Build City', () => this.enterPlace('city'));
|
||||
mk('buyDev', 'Buy Dev Card', () => this.onBuyDev());
|
||||
|
|
@ -451,8 +460,11 @@ export default class CatanGame extends Phaser.Scene {
|
|||
return c;
|
||||
};
|
||||
|
||||
const cardY = this.expansion === 'seafarers' ? 830 : 760;
|
||||
this._lrCard = makeCard(1775, 0, 0xdaa520);
|
||||
this._laCard = makeCard(1855, 1, 0xb03030);
|
||||
this._lrCard.setY(cardY);
|
||||
this._laCard.setY(cardY);
|
||||
this._prevSpecialCardOwners = { longestRoad: null, largestArmy: null };
|
||||
this._specialCardAnimating = { longestRoad: false, largestArmy: false };
|
||||
|
||||
|
|
@ -480,9 +492,10 @@ export default class CatanGame extends Phaser.Scene {
|
|||
|
||||
_getSpecialCardPos(cardType, owner) {
|
||||
if (owner === null) {
|
||||
const cardY = this.expansion === 'seafarers' ? 830 : 760;
|
||||
return cardType === 'longestRoad'
|
||||
? { x: 1775, y: 760, scale: 1 }
|
||||
: { x: 1855, y: 760, scale: 1 };
|
||||
? { x: 1775, y: cardY, scale: 1 }
|
||||
: { x: 1855, y: cardY, scale: 1 };
|
||||
}
|
||||
if (owner === 0) {
|
||||
return cardType === 'longestRoad'
|
||||
|
|
@ -518,23 +531,26 @@ export default class CatanGame extends Phaser.Scene {
|
|||
|
||||
_animateSpecialCardTransfer(cardType, fromPos, newOwner) {
|
||||
const toPos = this._getSpecialCardPos(cardType, newOwner);
|
||||
const card = cardType === 'longestRoad' ? this._lrCard : this._laCard;
|
||||
const card = cardType === 'longestRoad' ? this._lrCard : this._laCard;
|
||||
|
||||
card.setPosition(fromPos.x, fromPos.y).setScale(fromPos.scale);
|
||||
this._specialCardAnimating[cardType] = true;
|
||||
|
||||
const peakY = Math.min(fromPos.y, toPos.y) - 150;
|
||||
const midX = (fromPos.x + toPos.x) / 2;
|
||||
const peakY = Math.min(fromPos.y, toPos.y) - 150;
|
||||
const midX = (fromPos.x + toPos.x) / 2;
|
||||
const midScale = (fromPos.scale + toPos.scale) / 2;
|
||||
const half = 380;
|
||||
const half = 380;
|
||||
|
||||
this.tweens.chain({ targets: card, tweens: [
|
||||
{ x: midX, y: peakY, scale: midScale, duration: half, ease: 'Quad.Out' },
|
||||
{ x: toPos.x, y: toPos.y, scale: toPos.scale,
|
||||
duration: half, ease: 'Quad.In',
|
||||
onComplete: () => { this._specialCardAnimating[cardType] = false; },
|
||||
},
|
||||
]});
|
||||
this.tweens.chain({
|
||||
targets: card, tweens: [
|
||||
{ x: midX, y: peakY, scale: midScale, duration: half, ease: 'Quad.Out' },
|
||||
{
|
||||
x: toPos.x, y: toPos.y, scale: toPos.scale,
|
||||
duration: half, ease: 'Quad.In',
|
||||
onComplete: () => { this._specialCardAnimating[cardType] = false; },
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
enqueueSpeech(cardType === 'longestRoad' ? 'catan-card-road' : 'catan-card-army');
|
||||
}
|
||||
|
|
@ -595,20 +611,20 @@ export default class CatanGame extends Phaser.Scene {
|
|||
}).setOrigin(0.5);
|
||||
this._devTooltip = this.add.container(-9999, -9999, [g, titleTxt, descTxt])
|
||||
.setDepth(D.panel + 5);
|
||||
this._devTooltip.gfx = g;
|
||||
this._devTooltip.gfx = g;
|
||||
this._devTooltip.titleTxt = titleTxt;
|
||||
this._devTooltip.descTxt = descTxt;
|
||||
this._devTooltip.popW = popW;
|
||||
this._devTooltip.popH = popH;
|
||||
this._devTooltip.popR = popR;
|
||||
this._devTooltip.descTxt = descTxt;
|
||||
this._devTooltip.popW = popW;
|
||||
this._devTooltip.popH = popH;
|
||||
this._devTooltip.popR = popR;
|
||||
}
|
||||
|
||||
showDevCardTooltip(cardX, cardTopY, type, isNew, borderColor) {
|
||||
const DEV_DESC = {
|
||||
knight: 'Move the Robber to any tile and steal a resource from a player there.',
|
||||
knight: 'Move the Robber to any tile and steal a resource from a player there.',
|
||||
roadBuilding: 'Place 2 roads anywhere you could legally build them, for free.',
|
||||
vp: 'Worth 1 Victory Point. Kept hidden until you reach 10 VP and win.',
|
||||
monopoly: 'Name a resource. Every other player gives you all of that resource.',
|
||||
vp: 'Worth 1 Victory Point. Kept hidden until you reach 10 VP and win.',
|
||||
monopoly: 'Name a resource. Every other player gives you all of that resource.',
|
||||
yearOfPlenty: 'Take any 2 resources of your choice directly from the bank.',
|
||||
};
|
||||
const tt = this._devTooltip;
|
||||
|
|
@ -676,13 +692,6 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const panelRight = 1900;
|
||||
const panelW = 320;
|
||||
const cx = panelRight - panelW / 2;
|
||||
const bgCy = 980, bgH = 164;
|
||||
|
||||
const panel = this.add.container(0, 0).setDepth(D.hud);
|
||||
panel.add(this.add.rectangle(cx, bgCy, panelW, bgH, 0x000000, 0.3).setStrokeStyle(1, COLORS.accent, 0.5));
|
||||
panel.add(this.add.text(cx, bgCy - bgH / 2 + 7, 'Build Costs', {
|
||||
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5, 0));
|
||||
|
||||
const rows = [
|
||||
{ name: 'Road', resources: ['brick', 'lumber'] },
|
||||
|
|
@ -690,12 +699,27 @@ export default class CatanGame extends Phaser.Scene {
|
|||
{ name: 'City', resources: ['grain', 'grain', 'ore', 'ore', 'ore'] },
|
||||
{ name: 'Dev Card', resources: ['wool', 'grain', 'ore'] },
|
||||
];
|
||||
if (this.expansion === 'seafarers') {
|
||||
rows.push({ name: 'Ship', resources: ['lumber', 'wool'] });
|
||||
}
|
||||
|
||||
// Base: original fixed values. Seafarers: taller panel to fit 5th row, still inside bar (y 893–1080).
|
||||
const seafarers = this.expansion === 'seafarers';
|
||||
const bgH = seafarers ? 180 : 164;
|
||||
const bgCy = seafarers ? 987 : 980;
|
||||
const rowPad = 44;
|
||||
const rowStep = (bgH - rowPad - 14) / (rows.length - 1);
|
||||
|
||||
const panel = this.add.container(0, 0).setDepth(D.hud);
|
||||
panel.add(this.add.rectangle(cx, bgCy, panelW, bgH, 0x000000, 0.3).setStrokeStyle(1, COLORS.accent, 0.5));
|
||||
panel.add(this.add.text(cx, bgCy - bgH / 2 + 7, 'Build Costs', {
|
||||
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5, 0));
|
||||
|
||||
const lx = panelRight - panelW + 14;
|
||||
const rx = panelRight - 14;
|
||||
const rowY0 = bgCy - bgH / 2 + 44;
|
||||
const rowStep = (bgH - 44 - 14) / (rows.length - 1);
|
||||
const SW = 16, SH = 16, SG = 4, SR = 3; // swatch w/h/gap/radius
|
||||
const rowY0 = bgCy - bgH / 2 + rowPad;
|
||||
const SW = 16, SH = 16, SG = 4, SR = 3;
|
||||
|
||||
const g = this.add.graphics();
|
||||
panel.add(g);
|
||||
|
|
@ -722,8 +746,8 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const panelX = 1489, panelW = 200, panelH = 632;
|
||||
// Centre vertically in the playfield zone above the bottom bar (y=10..882)
|
||||
const panelY = 10 + Math.round((872 - panelH) / 2); // 130
|
||||
const cardCx = panelX + 10 + 63; // 10px left pad + half of 126
|
||||
const textX = panelX + 10 + 126 + 12 + 15; // card right + 12 gap + half text ≈ 1637
|
||||
const cardCx = panelX + 10 + 63; // 10px left pad + half of 126
|
||||
const textX = panelX + 10 + 126 + 12 + 15; // card right + 12 gap + half text ≈ 1637
|
||||
const panelCx = panelX + panelW / 2; // for BANK title
|
||||
|
||||
const cardW = 126, cardH = 90, cardR = 6, borderW = 3, shadow = 4;
|
||||
|
|
@ -732,7 +756,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
// Stacks nearly touching: 6 × (90 + 6px gap), starting 46px below panel top
|
||||
const step = 96;
|
||||
const stackTops = Array.from({ length: 6 }, (_, i) => panelY + 46 + i * step);
|
||||
const dividerY = stackTops[4] + cardH + 3; // 3px below resource-5 bottom
|
||||
const dividerY = stackTops[4] + cardH + 3; // 3px below resource-5 bottom
|
||||
|
||||
this.bankCardPos = {};
|
||||
RESOURCE_TYPES.forEach((r, i) => {
|
||||
|
|
@ -870,9 +894,9 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const radius = seat === 0 ? 64 : 56;
|
||||
const label = this.add.text(dst.x + radius + 10, dst.y,
|
||||
RESOURCE_INFO[resource].label.toUpperCase(), {
|
||||
fontFamily: 'Righteous', fontSize: '26px', color: '#ffd700',
|
||||
stroke: '#000000', strokeThickness: 3,
|
||||
}).setOrigin(0, 0.5).setDepth(D.banner);
|
||||
fontFamily: 'Righteous', fontSize: '26px', color: '#ffd700',
|
||||
stroke: '#000000', strokeThickness: 3,
|
||||
}).setOrigin(0, 0.5).setDepth(D.banner);
|
||||
this.tweens.add({
|
||||
targets: label, alpha: 0, y: dst.y - 24,
|
||||
duration: 700, delay: 300,
|
||||
|
|
@ -1015,7 +1039,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const ax = N[a].x, ay = N[a].y, bx = N[b].x, by = N[b].y;
|
||||
g.lineStyle(16, 0xffffff, 0.9); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(12, col.hexDark, 1); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(7, col.hex, 1); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(7, col.hex, 1); g.lineBetween(ax, ay, bx, by);
|
||||
this.pieceObjs.push(g);
|
||||
}
|
||||
// ships (Seafarers): dashed maritime route in the player's colour
|
||||
|
|
@ -1033,21 +1057,18 @@ export default class CatanGame extends Phaser.Scene {
|
|||
// pirate (Seafarers): a sea-robber token on its hex
|
||||
if (this.gs.pirateHex != null) {
|
||||
const { x, y } = this.hexPos(this.gs.pirateHex);
|
||||
const pg = this.add.graphics().setDepth(D.robber);
|
||||
pg.fillStyle(0x000000, 0.45); pg.fillCircle(x + 2, y + 3, 20);
|
||||
pg.fillStyle(0x1b1b1b, 1); pg.fillCircle(x, y, 18);
|
||||
pg.lineStyle(3, 0xe8e4d8, 1); pg.strokeCircle(x, y, 18);
|
||||
this.pieceObjs.push(pg);
|
||||
this.pieceObjs.push(this.add.text(x, y, '☠', { fontSize: '22px', color: '#e8e4d8' }).setOrigin(0.5).setDepth(D.robber));
|
||||
this.pieceObjs.push(
|
||||
this.add.image(x, y, 'catan-pirate').setOrigin(0.5).setDisplaySize(48, 48).setDepth(D.robber)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A ship piece: a thick coloured bar along the sea edge with a sail nub.
|
||||
makeShip(ax, ay, bx, by, col) {
|
||||
const g = this.add.graphics().setDepth(D.road);
|
||||
g.lineStyle(15, 0xffffff, 0.9); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(15, 0xffffff, 0.9); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(11, col.hexDark, 1); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(6, col.hex, 1); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(6, col.hex, 1); g.lineBetween(ax, ay, bx, by);
|
||||
// sail at the midpoint
|
||||
const mx = (ax + bx) / 2, my = (ay + by) / 2;
|
||||
g.fillStyle(0xffffff, 0.95);
|
||||
|
|
@ -1080,12 +1101,12 @@ export default class CatanGame extends Phaser.Scene {
|
|||
// White halo (3px border around the scaled-up shape)
|
||||
g.fillStyle(0xffffff, 1);
|
||||
g.fillRect(x - 24, y, 27, 21); // lower block halo
|
||||
g.fillRect(x - 8, y - 10, 32, 32); // tower halo
|
||||
g.fillRect(x - 8, y - 10, 32, 32); // tower halo
|
||||
g.fillTriangle(x - 11, y - 10, x + 26, y - 10, x + 8, y - 26); // roof halo
|
||||
// Player color fill (~30% larger than original)
|
||||
g.fillStyle(col.hex, 1);
|
||||
g.fillRect(x - 21, y, 21, 18); // lower block
|
||||
g.fillRect(x - 5, y - 10, 26, 29); // tower
|
||||
g.fillRect(x - 5, y - 10, 26, 29); // tower
|
||||
g.fillTriangle(x - 8, y - 10, x + 23, y - 10, x + 8, y - 23); // roof
|
||||
// Dark outlines
|
||||
g.lineStyle(2.5, col.hexDark, 1);
|
||||
|
|
@ -1108,7 +1129,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
enqueueSpeech(clip);
|
||||
if (this.robberObj) this.robberObj.destroy();
|
||||
const from = this.hexPos(fromHexId);
|
||||
const to = this.hexPos(toHexId);
|
||||
const to = this.hexPos(toHexId);
|
||||
this.robberObj = this.add.image(from.x, from.y, 'catan-robber')
|
||||
.setDisplaySize(64, 64)
|
||||
.setDepth(D.robber);
|
||||
|
|
@ -1125,7 +1146,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
targets: this.robberObj,
|
||||
tweens: [
|
||||
{ scaleX: base * 2, scaleY: base * 2, duration: 1500, ease: 'Sine.Out' },
|
||||
{ scaleX: base, scaleY: base, duration: 1500, ease: 'Sine.In', onComplete: resolve },
|
||||
{ scaleX: base, scaleY: base, duration: 1500, ease: 'Sine.In', onComplete: resolve },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
@ -1171,7 +1192,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
_flyCardToBank(srcX, srcY, frameIdx, bankPos, startFaceUp) {
|
||||
return new Promise(resolve => {
|
||||
const cardW = 60, cardH = 84;
|
||||
const bigW = 90, bigH = 126;
|
||||
const bigW = 90, bigH = 126;
|
||||
const resource = RESOURCE_TYPES[frameIdx];
|
||||
|
||||
const makeBorder = (color) => {
|
||||
|
|
@ -1249,12 +1270,12 @@ export default class CatanGame extends Phaser.Scene {
|
|||
if (type === 'road') {
|
||||
g.lineStyle(10, 0xffffff, 0.9); g.lineBetween(-13, 0, 13, 0);
|
||||
g.lineStyle(7, col.hexDark, 1); g.lineBetween(-13, 0, 13, 0);
|
||||
g.lineStyle(4, col.hex, 1); g.lineBetween(-13, 0, 13, 0);
|
||||
g.lineStyle(4, col.hex, 1); g.lineBetween(-13, 0, 13, 0);
|
||||
} else if (type === 'settlement') {
|
||||
g.fillStyle(0xffffff, 1);
|
||||
g.fillRect(-11, -3, 22, 15); g.fillTriangle(-14, -3, 14, -3, 0, -17);
|
||||
g.fillStyle(col.hex, 1);
|
||||
g.fillRect(-9, -3, 18, 12); g.fillTriangle(-11, -3, 11, -3, 0, -14);
|
||||
g.fillRect(-9, -3, 18, 12); g.fillTriangle(-11, -3, 11, -3, 0, -14);
|
||||
g.lineStyle(2, col.hexDark, 1); g.strokeRect(-9, -3, 18, 12);
|
||||
} else if (type === 'city') {
|
||||
g.fillStyle(0xffffff, 1);
|
||||
|
|
@ -1279,15 +1300,19 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const peakY = Math.min(srcPos.y, destY) - arcHeight;
|
||||
const half = duration / 2;
|
||||
this.tweens.add({ targets: container, x: destX, duration, ease: 'Quad.InOut' });
|
||||
this.tweens.chain({ targets: container, tweens: [
|
||||
{ y: peakY, duration: half, ease: 'Quad.Out' },
|
||||
{ y: destY, duration: half, ease: 'Quad.In', onComplete: () => {
|
||||
emitter.stop();
|
||||
container.destroy();
|
||||
this.time.delayedCall(500, () => emitter.destroy());
|
||||
resolve();
|
||||
}},
|
||||
]});
|
||||
this.tweens.chain({
|
||||
targets: container, tweens: [
|
||||
{ y: peakY, duration: half, ease: 'Quad.Out' },
|
||||
{
|
||||
y: destY, duration: half, ease: 'Quad.In', onComplete: () => {
|
||||
emitter.stop();
|
||||
container.destroy();
|
||||
this.time.delayedCall(500, () => emitter.destroy());
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1308,15 +1333,19 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const peakY = Math.min(bankPos.y, destPos.y) - arcHeight;
|
||||
const half = duration / 2;
|
||||
this.tweens.add({ targets: container, x: destPos.x, duration, ease: 'Quad.InOut' });
|
||||
this.tweens.chain({ targets: container, tweens: [
|
||||
{ y: peakY, duration: half, ease: 'Quad.Out' },
|
||||
{ y: destPos.y, duration: half, ease: 'Quad.In', onComplete: () => {
|
||||
this.tweens.add({
|
||||
targets: container, alpha: 0, scaleX: 1.5, scaleY: 1.5, duration: 280,
|
||||
onComplete: () => { container.destroy(); resolve(); },
|
||||
});
|
||||
}},
|
||||
]});
|
||||
this.tweens.chain({
|
||||
targets: container, tweens: [
|
||||
{ y: peakY, duration: half, ease: 'Quad.Out' },
|
||||
{
|
||||
y: destPos.y, duration: half, ease: 'Quad.In', onComplete: () => {
|
||||
this.tweens.add({
|
||||
targets: container, alpha: 0, scaleX: 1.5, scaleY: 1.5, duration: 280,
|
||||
onComplete: () => { container.destroy(); resolve(); },
|
||||
});
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1456,10 +1485,10 @@ export default class CatanGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
const DEV_VISUAL = {
|
||||
knight: { frame: 5, border: 0xb03030 },
|
||||
roadBuilding: { frame: 6, border: 0x8b5a2b },
|
||||
vp: { frame: 7, border: 0xdaa520 },
|
||||
monopoly: { frame: 9, border: 0x7b2d8b },
|
||||
knight: { frame: 5, border: 0xb03030 },
|
||||
roadBuilding: { frame: 6, border: 0x8b5a2b },
|
||||
vp: { frame: 7, border: 0xdaa520 },
|
||||
monopoly: { frame: 9, border: 0x7b2d8b },
|
||||
yearOfPlenty: { frame: 10, border: 0x2d8b57 },
|
||||
};
|
||||
|
||||
|
|
@ -1488,7 +1517,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const hit = this.add.rectangle(x, y, cardW, cardH, 0x000000, 0).setInteractive();
|
||||
const cx = x, topY = y - cardH / 2;
|
||||
hit.on('pointerover', () => this.showDevCardTooltip(cx, topY, type, isNew, visual.border));
|
||||
hit.on('pointerout', () => this.hideDevCardTooltip());
|
||||
hit.on('pointerout', () => this.hideDevCardTooltip());
|
||||
|
||||
this.devHandContainer.add([bg, img, border, hit]);
|
||||
x += step;
|
||||
|
|
@ -1525,6 +1554,9 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const hasRoadSpot = action && L.legalRoadEdges(s, 0, false).length > 0;
|
||||
set('roll', me && s.phase === 'rollPhase');
|
||||
set('road', (action && hasRoadSpot && L.canAfford(p, COSTS.road)) || (action && s.freeRoads > 0 && hasRoadSpot));
|
||||
const hasShipSpot = action && L.legalShipEdges(s, 0).length > 0;
|
||||
const sCost = L.shipCost(s);
|
||||
set('ship', hasShipSpot && ((action && sCost && L.canAfford(p, sCost)) || (action && s.freeShips > 0)));
|
||||
set('settlement', hasSettleSpot && L.canAfford(p, COSTS.settlement));
|
||||
set('city', action && p.settlements.length > 0 && L.canAfford(p, COSTS.city));
|
||||
set('buyDev', action && s.devDeck.length > 0 && L.canAfford(p, COSTS.devCard));
|
||||
|
|
@ -1561,6 +1593,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
});
|
||||
const names = ['You', ...this.opponents.map((o) => o?.name ?? 'CPU')];
|
||||
L.setPlayerNames(this.gs, names);
|
||||
this.hexTileFrames = {};
|
||||
this.drawHexes();
|
||||
this.drawPorts();
|
||||
this.drawChits();
|
||||
|
|
@ -1579,6 +1612,8 @@ export default class CatanGame extends Phaser.Scene {
|
|||
} else if (s.phase === 'rollPhase') {
|
||||
if (me) { /* wait for Roll button */ }
|
||||
else await this.aiRoll();
|
||||
} else if (s.phase === 'goldPick') {
|
||||
await this.handleGoldPickPhase();
|
||||
} else if (s.phase === 'discard') {
|
||||
await this.handleDiscardPhase();
|
||||
} else if (s.phase === 'moveRobber') {
|
||||
|
|
@ -1739,10 +1774,10 @@ export default class CatanGame extends Phaser.Scene {
|
|||
|
||||
applyAction(seat, a) {
|
||||
switch (a.type) {
|
||||
case 'buildCity': return L.buildCity(this.gs, seat, a.nodeId);
|
||||
case 'buildCity': return L.buildCity(this.gs, seat, a.nodeId);
|
||||
case 'buildSettlement': return L.buildSettlement(this.gs, seat, a.nodeId);
|
||||
case 'buildRoad': return L.buildRoad(this.gs, seat, a.edgeId);
|
||||
case 'buyDev': return L.buyDevCard(this.gs, seat);
|
||||
case 'buildRoad': return L.buildRoad(this.gs, seat, a.edgeId);
|
||||
case 'buyDev': return L.buyDevCard(this.gs, seat);
|
||||
case 'bankTrade': return L.tradeWithBank(this.gs, seat, a.give, a.get);
|
||||
case 'playDev':
|
||||
if (a.card === 'knight') return L.playKnight(this.gs, seat);
|
||||
|
|
@ -1835,10 +1870,10 @@ export default class CatanGame extends Phaser.Scene {
|
|||
objs.push(g);
|
||||
return g;
|
||||
};
|
||||
const getArrow = drawArrow(812, 'down'); // left of the card you receive
|
||||
const getArrow = drawArrow(812, 'down'); // left of the card you receive
|
||||
const giveArrow = drawArrow(1188, 'up'); // right of the card you give
|
||||
|
||||
const t1 = this.tweens.add({ targets: getArrow, y: baseY + amp, duration: 700, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
|
||||
const t1 = this.tweens.add({ targets: getArrow, y: baseY + amp, duration: 700, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
|
||||
const t2 = this.tweens.add({ targets: giveArrow, y: baseY - amp, duration: 700, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
|
||||
objs.push({ destroy: () => { t1.stop(); t2.stop(); } });
|
||||
|
||||
|
|
@ -1881,15 +1916,15 @@ export default class CatanGame extends Phaser.Scene {
|
|||
// ── opponent dev card reveal ──────────────────────────────────────────────────
|
||||
async animateOppDevCardPlay(seat, cardType, resource) {
|
||||
const VISUAL = {
|
||||
knight: { frame: 5, border: 0xb03030 },
|
||||
roadBuilding: { frame: 6, border: 0x8b5a2b },
|
||||
monopoly: { frame: 9, border: 0x7b2d8b },
|
||||
knight: { frame: 5, border: 0xb03030 },
|
||||
roadBuilding: { frame: 6, border: 0x8b5a2b },
|
||||
monopoly: { frame: 9, border: 0x7b2d8b },
|
||||
yearOfPlenty: { frame: 10, border: 0x2d8b57 },
|
||||
};
|
||||
const SPEECH = {
|
||||
knight: 'catan-dev-knight',
|
||||
knight: 'catan-dev-knight',
|
||||
roadBuilding: 'catan-dev-road',
|
||||
monopoly: 'catan-dev-monopoly',
|
||||
monopoly: 'catan-dev-monopoly',
|
||||
yearOfPlenty: 'catan-dev-year',
|
||||
};
|
||||
|
||||
|
|
@ -1933,7 +1968,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
|
||||
const bursts = [
|
||||
{ x: toX - 90, y: textY - 10 }, { x: toX + 90, y: textY - 10 },
|
||||
{ x: toX, y: textY - 28 }, { x: toX - 50, y: textY + 22 },
|
||||
{ x: toX, y: textY - 28 }, { x: toX - 50, y: textY + 22 },
|
||||
{ x: toX + 50, y: textY + 22 },
|
||||
];
|
||||
bursts.forEach((b, i) =>
|
||||
|
|
@ -1977,6 +2012,31 @@ export default class CatanGame extends Phaser.Scene {
|
|||
this.advance();
|
||||
}
|
||||
|
||||
// ── gold picks ───────────────────────────────────────────────────────────────
|
||||
async handleGoldPickPhase() {
|
||||
this.busy = true;
|
||||
// Process all AI entries in queue order.
|
||||
for (const entry of [...this.gs.goldPickQueue]) {
|
||||
if (entry.seat === 0) continue;
|
||||
const picks = AI.chooseGoldPick(this.gs, entry.seat, entry.amount);
|
||||
this.gs = L.resolveGoldPick(this.gs, entry.seat, picks);
|
||||
}
|
||||
this.renderAll();
|
||||
// If human has a pick, let them choose.
|
||||
const humanEntry = this.gs.goldPickQueue.find((e) => e.seat === 0);
|
||||
if (humanEntry) {
|
||||
this.busy = false;
|
||||
this.pickResources(humanEntry.amount, `Gold Field! Choose ${humanEntry.amount} resource${humanEntry.amount > 1 ? 's' : ''}`, (rs) => {
|
||||
this.gs = L.resolveGoldPick(this.gs, 0, rs);
|
||||
this.advance();
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this.delay(300);
|
||||
this.busy = false;
|
||||
this.advance();
|
||||
}
|
||||
|
||||
// ── human: discards ─────────────────────────────────────────────────────────
|
||||
async handleDiscardPhase() {
|
||||
this.busy = true;
|
||||
|
|
@ -2051,6 +2111,11 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const { x, y } = this.nodePos(nid);
|
||||
this.addHighlight(x, y, () => this.doBuild('city', nid), 0xffd700);
|
||||
}
|
||||
} else if (type === 'ship') {
|
||||
for (const eid of L.legalShipEdges(s, 0)) {
|
||||
const { x, y } = this.edgePos(eid);
|
||||
this.addHighlight(x, y, () => this.doBuild('ship', eid), COLORS.gold, 13);
|
||||
}
|
||||
}
|
||||
this.statusText.setText(`Choose where to build a ${type} (or pick another action)`);
|
||||
}
|
||||
|
|
@ -2063,9 +2128,16 @@ export default class CatanGame extends Phaser.Scene {
|
|||
enqueueSpeech(`catan-purchase-${type}`);
|
||||
await this.animateCostPayment(0, type);
|
||||
await this.animatePiecePlacement(0, type, dest.x, dest.y);
|
||||
if (type === 'road') this.gs = L.buildRoad(this.gs, 0, id);
|
||||
const prevGs = this.gs;
|
||||
if (type === 'road') this.gs = L.buildRoad(this.gs, 0, id);
|
||||
if (type === 'settlement') this.gs = L.buildSettlement(this.gs, 0, id);
|
||||
if (type === 'city') this.gs = L.buildCity(this.gs, 0, id);
|
||||
if (type === 'city') this.gs = L.buildCity(this.gs, 0, id);
|
||||
if (type === 'ship') this.gs = L.buildShip(this.gs, 0, id);
|
||||
// Redraw board if any fog hexes were revealed by this road/ship.
|
||||
if (type === 'road' || type === 'ship') {
|
||||
const revealed = this.gs.hexes.some((h, i) => prevGs.hexes[i].kind === 'fog' && h.kind !== 'fog');
|
||||
if (revealed) { this.drawHexes(); this.drawChits(); }
|
||||
}
|
||||
playSound(this, SFX.PIECE_CLICK);
|
||||
this.busy = false;
|
||||
this.advance();
|
||||
|
|
@ -2148,12 +2220,12 @@ export default class CatanGame extends Phaser.Scene {
|
|||
if (this.busy || this.gs.phase !== 'action') return;
|
||||
this.clearHighlights();
|
||||
const give = { brick: 0, lumber: 0, wool: 0, grain: 0, ore: 0 };
|
||||
const get = { brick: 0, lumber: 0, wool: 0, grain: 0, ore: 0 };
|
||||
const get = { brick: 0, lumber: 0, wool: 0, grain: 0, ore: 0 };
|
||||
const overlay = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6).setInteractive().setDepth(D.panel);
|
||||
const box = this.add.rectangle(1000, 480, 920, 580, COLORS.panel, 1).setStrokeStyle(3, COLORS.accent).setDepth(D.panel);
|
||||
const title = this.add.text(1000, 240, 'Trade', { fontFamily: 'Righteous', fontSize: '34px', color: COLORS.goldHex }).setOrigin(0.5).setDepth(D.panel + 1);
|
||||
const hintGive = this.add.text(760, 308, 'You give', { fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.panel + 1);
|
||||
const hintGet = this.add.text(1240, 308, 'You get', { fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.panel + 1);
|
||||
const hintGet = this.add.text(1240, 308, 'You get', { fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.panel + 1);
|
||||
const objs = [overlay, box, title, hintGive, hintGet];
|
||||
|
||||
const stepper = (col, r, i, side) => {
|
||||
|
|
@ -2318,9 +2390,9 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const label = (RESOURCE_INFO[resource]?.label ?? resource).toUpperCase();
|
||||
const txt = this.add.text(GAME_WIDTH / 2, 855,
|
||||
`CARD STOLEN: ${label} BY ${robberName.toUpperCase()}`, {
|
||||
fontFamily: 'Righteous', fontSize: '38px', color: '#ffd700',
|
||||
stroke: '#000000', strokeThickness: 5,
|
||||
}
|
||||
fontFamily: 'Righteous', fontSize: '38px', color: '#ffd700',
|
||||
stroke: '#000000', strokeThickness: 5,
|
||||
}
|
||||
).setOrigin(0.5).setDepth(D.banner + 2);
|
||||
this.tweens.add({
|
||||
targets: txt, alpha: 0, delay: 3000, duration: 500,
|
||||
|
|
@ -2334,8 +2406,10 @@ export default class CatanGame extends Phaser.Scene {
|
|||
backgroundColor: '#111923ee', padding: { x: 26, y: 12 },
|
||||
}).setOrigin(0.5).setDepth(D.banner);
|
||||
banner.setAlpha(0);
|
||||
this.tweens.add({ targets: banner, alpha: 1, y: 140, duration: 280, ease: 'Back.easeOut',
|
||||
onComplete: () => this.time.delayedCall(900, () => this.tweens.add({ targets: banner, alpha: 0, y: 120, duration: 220, onComplete: () => banner.destroy() })) });
|
||||
this.tweens.add({
|
||||
targets: banner, alpha: 1, y: 140, duration: 280, ease: 'Back.easeOut',
|
||||
onComplete: () => this.time.delayedCall(900, () => this.tweens.add({ targets: banner, alpha: 0, y: 120, duration: 220, onComplete: () => banner.destroy() }))
|
||||
});
|
||||
}
|
||||
|
||||
// ── game over ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -2346,11 +2420,11 @@ export default class CatanGame extends Phaser.Scene {
|
|||
this.recordHistory();
|
||||
|
||||
const PW = 760, PH = 660, PX = 1000, PY = 540;
|
||||
const titleY = PY - PH / 2 + 70; // 280
|
||||
const RADIUS = 80;
|
||||
const titleY = PY - PH / 2 + 70; // 280
|
||||
const RADIUS = 80;
|
||||
const portraitY = titleY + 140; // 420
|
||||
const bodyY = portraitY + RADIUS + 95; // 595
|
||||
const buttonsY = PY + PH / 2 - 72; // 798
|
||||
const bodyY = portraitY + RADIUS + 95; // 595
|
||||
const buttonsY = PY + PH / 2 - 72; // 798
|
||||
|
||||
// Fireworks across the popup for all winners
|
||||
const fwEmitter = this.add.particles(PX, PY, 'catanParticle', {
|
||||
|
|
@ -2405,7 +2479,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
videoEl.autoplay = true;
|
||||
videoEl.style.cssText = `width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;display:block;`;
|
||||
videoEl.src = `/assets/videos/${opp.id}-happy.mp4`;
|
||||
videoEl.play().catch(() => {});
|
||||
videoEl.play().catch(() => { });
|
||||
videoEl.addEventListener('error', () => { videoEl.style.display = 'none'; }, { once: true });
|
||||
portraitDom = this.add.dom(PX, portraitY, videoEl).setDepth(D.banner + 3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export function createInitialState(playerCount = 3, { tilePlacement = 'random',
|
|||
diceTotal: null,
|
||||
robberReturnPhase: 'action',
|
||||
discardQueue: [],
|
||||
goldPickQueue: [],
|
||||
freeRoads: 0,
|
||||
freeShips: 0, // Seafarers: free ships (e.g. from a future card)
|
||||
longestRoad: { owner: null, length: 0 },
|
||||
|
|
@ -306,7 +307,7 @@ export function rollDice(state) {
|
|||
s.phase = s.discardQueue.length ? 'discard' : 'moveRobber';
|
||||
} else {
|
||||
produceResources(s, s.diceTotal);
|
||||
s.phase = 'action';
|
||||
if (s.phase !== 'goldPick') s.phase = 'action';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
|
@ -326,8 +327,31 @@ function produceResources(s, total) {
|
|||
if (give > 0) { s.players[bld.seat].resources[hex.resource] += give; s.bank[hex.resource] -= give; }
|
||||
}
|
||||
}
|
||||
// Seafarers gold hexes (and any other expansion production) resolve here.
|
||||
getExpansion(s.expansion).onProduce?.(s, total);
|
||||
// Gold hexes: each settlement/city adjacent earns 1/2 free-choice resources.
|
||||
for (const hex of s.hexes) {
|
||||
if (hex.kind !== 'gold' || hex.number !== total || hex.hasRobber) continue;
|
||||
for (const nodeId of geo.hexes[hex.id].corners) {
|
||||
const bld = nodeBuilding(s, nodeId);
|
||||
if (!bld) continue;
|
||||
const amt = bld.type === 'city' ? 2 : 1;
|
||||
s.goldPickQueue.push({ seat: bld.seat, amount: amt });
|
||||
}
|
||||
}
|
||||
if (s.goldPickQueue.length) s.phase = 'goldPick';
|
||||
}
|
||||
|
||||
// resources: array of resource strings with length matching the queue entry's amount.
|
||||
export function resolveGoldPick(state, seat, resources) {
|
||||
const s = cloneState(state);
|
||||
const idx = s.goldPickQueue.findIndex((e) => e.seat === seat);
|
||||
if (idx === -1) return s;
|
||||
for (const r of resources) {
|
||||
if (s.bank[r] > 0) { s.players[seat].resources[r]++; s.bank[r]--; }
|
||||
}
|
||||
logEvent(s, `${playerName(s, seat)} takes ${resources.join(', ')} from gold.`);
|
||||
s.goldPickQueue.splice(idx, 1);
|
||||
if (s.goldPickQueue.length === 0) s.phase = 'action';
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── discard / robber ───────────────────────────────────────────────────────────
|
||||
|
|
@ -398,11 +422,29 @@ export function buildRoad(state, seat, edgeId) {
|
|||
if (!free && !canAfford(s.players[seat], COSTS.road)) return s;
|
||||
if (free) s.freeRoads--; else pay(s, s.players[seat], COSTS.road);
|
||||
s.players[seat].roads.push(edgeId);
|
||||
revealFogHexes(s, edgeId);
|
||||
recomputeLongestRoad(s);
|
||||
checkWin(s, seat);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Reveal any fog hexes that share an edge with edgeId.
|
||||
function revealFogHexes(s, edgeId) {
|
||||
const geo = geoFor(s);
|
||||
for (const hexId of geo.edges[edgeId].hexes) {
|
||||
const hex = s.hexes[hexId];
|
||||
if (!hex || hex.kind !== 'fog') continue;
|
||||
if (hex.fogData) {
|
||||
hex.kind = hex.fogData.kind;
|
||||
hex.resource = hex.fogData.resource;
|
||||
hex.number = hex.fogData.number;
|
||||
} else {
|
||||
hex.kind = 'sea';
|
||||
}
|
||||
hex.fogData = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── ships (Seafarers) ──────────────────────────────────────────────────────────
|
||||
// Ship cost is supplied by the active expansion; null in the base game (no ships).
|
||||
export function shipCost(state) {
|
||||
|
|
@ -457,6 +499,7 @@ export function buildShip(state, seat, edgeId) {
|
|||
if (!free && !canAfford(s.players[seat], cost)) return s;
|
||||
if (free) s.freeShips--; else pay(s, s.players[seat], cost);
|
||||
s.players[seat].ships.push(edgeId);
|
||||
revealFogHexes(s, edgeId);
|
||||
recomputeLongestRoad(s);
|
||||
checkWin(s, seat);
|
||||
return s;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
// buildBoard() produces the topology and per-hex assignments at game start.
|
||||
//
|
||||
// An expansion may expose any of:
|
||||
// costs – extra build costs (e.g. { ship: { brick:1, lumber:1 } })
|
||||
// costs – extra build costs (e.g. { ship: { lumber:1, wool:1 } })
|
||||
// scenarios – { id: scenarioObj } selectable layouts (see seafarers.js)
|
||||
// setupRules – [ (state) => void ] applied once after base setup
|
||||
// onProduce – (state, total) => void extra production (e.g. gold hexes)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
HEX_SIZE, BOARD_CX, BOARD_CY, PORT_BAG,
|
||||
} from '../../CatanBoard.js';
|
||||
|
||||
export const SHIP_COST = { brick: 1, lumber: 1 };
|
||||
export const SHIP_COST = { wool: 1, lumber: 1 };
|
||||
|
||||
// Larger Seafarers boards use a slightly smaller hex so they fit the canvas.
|
||||
export const SEA_HEX_SIZE = 74;
|
||||
|
|
@ -68,6 +68,8 @@ export function assembleBoard({ id, rows, cells, homeIds = [], pirate = null, wi
|
|||
resource: c.resource ?? null,
|
||||
number: c.number ?? null,
|
||||
hasRobber: false,
|
||||
// Fog hexes carry hidden data that replaces kind/resource/number on reveal.
|
||||
fogData: c.kind === 'fog' && c.reveal ? { kind: c.reveal.kind, resource: c.reveal.resource ?? null, number: c.reveal.number ?? null } : null,
|
||||
}));
|
||||
|
||||
// Robber starts on the (first) desert, if any.
|
||||
|
|
|
|||
|
|
@ -38,30 +38,31 @@ export default class PreloadScene extends Phaser.Scene {
|
|||
frameWidth: 312,
|
||||
frameHeight: 312,
|
||||
});
|
||||
this.load.image('catan-robber', '/assets/images/catan-robber.png');
|
||||
this.load.image('bg-menu', '/assets/images/background-menu.png');
|
||||
this.load.image('bg-room', '/assets/images/background-room.png');
|
||||
this.load.image('catan-robber', '/assets/images/catan-robber.png');
|
||||
this.load.image('catan-pirate', '/assets/images/catan-pirate.png');
|
||||
this.load.image('bg-menu', '/assets/images/background-menu.png');
|
||||
this.load.image('bg-room', '/assets/images/background-room.png');
|
||||
this.load.image('bg-casino', '/assets/images/background-casino.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');
|
||||
this.load.json('music', '/data/music.json');
|
||||
this.load.json('playfields', '/data/playfields.json');
|
||||
this.load.json('card-backs', '/data/card-backs.json');
|
||||
this.load.json('music', '/data/music.json');
|
||||
|
||||
this.load.audio('sfx-card-deal', '/assets/fx/card-deal.mp3');
|
||||
this.load.audio('sfx-card-place', '/assets/fx/card-place.mp3');
|
||||
this.load.audio('sfx-card-show', '/assets/fx/card-show.mp3');
|
||||
this.load.audio('sfx-card-shuffle', '/assets/fx/card-shuffle.mp3');
|
||||
this.load.audio('sfx-coins', '/assets/fx/coins.mp3');
|
||||
this.load.audio('sfx-purchase', '/assets/fx/purchase.mp3');
|
||||
this.load.audio('sfx-card-deal', '/assets/fx/card-deal.mp3');
|
||||
this.load.audio('sfx-card-place', '/assets/fx/card-place.mp3');
|
||||
this.load.audio('sfx-card-show', '/assets/fx/card-show.mp3');
|
||||
this.load.audio('sfx-card-shuffle', '/assets/fx/card-shuffle.mp3');
|
||||
this.load.audio('sfx-coins', '/assets/fx/coins.mp3');
|
||||
this.load.audio('sfx-purchase', '/assets/fx/purchase.mp3');
|
||||
this.load.audio('sfx-casino-blackjack', '/assets/fx/casino-blackjack.mp3');
|
||||
this.load.audio('sfx-casino-lose', '/assets/fx/casino-lose.mp3');
|
||||
this.load.audio('sfx-casino-win', '/assets/fx/casino-win.mp3');
|
||||
this.load.audio('sfx-chip-bet', '/assets/fx/chip-bet.mp3');
|
||||
this.load.audio('sfx-dice-roll', '/assets/fx/dice-roll.mp3');
|
||||
this.load.audio('sfx-bingo-balls', '/assets/fx/bingo-balls.mp3');
|
||||
this.load.audio('sfx-pencil-write', '/assets/fx/pencil-write.mp3');
|
||||
this.load.audio('sfx-piece-click', '/assets/fx/piece-click.mp3');
|
||||
this.load.audio('sfx-roulette', '/assets/fx/roulette.mp3');
|
||||
this.load.audio('sfx-casino-lose', '/assets/fx/casino-lose.mp3');
|
||||
this.load.audio('sfx-casino-win', '/assets/fx/casino-win.mp3');
|
||||
this.load.audio('sfx-chip-bet', '/assets/fx/chip-bet.mp3');
|
||||
this.load.audio('sfx-dice-roll', '/assets/fx/dice-roll.mp3');
|
||||
this.load.audio('sfx-bingo-balls', '/assets/fx/bingo-balls.mp3');
|
||||
this.load.audio('sfx-pencil-write', '/assets/fx/pencil-write.mp3');
|
||||
this.load.audio('sfx-piece-click', '/assets/fx/piece-click.mp3');
|
||||
this.load.audio('sfx-roulette', '/assets/fx/roulette.mp3');
|
||||
|
||||
this.load.spritesheet('catan-special-cards', '/assets/images/catan-special-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||
|
||||
|
|
@ -81,7 +82,7 @@ export default class PreloadScene extends Phaser.Scene {
|
|||
|
||||
const toLoad = [
|
||||
...(pfd?.playfields ?? []).filter((pf) => pf.path && !this.textures.exists(pf.key)),
|
||||
...(cbd?.cardBacks ?? []).filter((cb) => cb.path && !this.textures.exists(cb.key)),
|
||||
...(cbd?.cardBacks ?? []).filter((cb) => cb.path && !this.textures.exists(cb.key)),
|
||||
];
|
||||
|
||||
if (toLoad.length > 0) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue