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;
|
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) {
|
export function chooseRobberMove(state, seat) {
|
||||||
let best = null, bestScore = -Infinity, bestTarget = null;
|
let best = null, bestScore = -Infinity, bestTarget = null;
|
||||||
const geo = geoFor(state);
|
const geo = geoFor(state);
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,9 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
// Interpolate from outer dark (0x07243a) to inner lighter (0x1a5e80)
|
// Interpolate from outer dark (0x07243a) to inner lighter (0x1a5e80)
|
||||||
const ro = 0x07, go = 0x24, bo = 0x3a;
|
const ro = 0x07, go = 0x24, bo = 0x3a;
|
||||||
const ri = 0x1a, gi = 0x5e, bi = 0x80;
|
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 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;
|
const color = (red << 16) | (green << 8) | blue;
|
||||||
sea.fillStyle(color, 1);
|
sea.fillStyle(color, 1);
|
||||||
sea.fillCircle(SEA_X, SEA_Y, r);
|
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.
|
// Frame pairs per resource: pick one at random each draw.
|
||||||
static TILE_FRAMES = {
|
static TILE_FRAMES = {
|
||||||
lumber: [0, 1],
|
lumber: [0, 1],
|
||||||
wool: [2, 3],
|
wool: [2, 3],
|
||||||
brick: [4, 5],
|
brick: [4, 5],
|
||||||
ore: [6, 7],
|
ore: [6, 7],
|
||||||
grain: [8, 9],
|
grain: [8, 9],
|
||||||
desert: [10, 11],
|
desert: [10, 11],
|
||||||
|
sea: [12, 13],
|
||||||
|
gold: [14, 15],
|
||||||
|
fog: [16, 17],
|
||||||
};
|
};
|
||||||
|
|
||||||
drawHexes() {
|
drawHexes() {
|
||||||
|
|
@ -147,7 +150,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const terr = this.hexTerrain(hex);
|
const terr = this.hexTerrain(hex);
|
||||||
|
|
||||||
// Scale factors for the 7px colored ring + 4px dark ring (absolute pixels).
|
// 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 s2 = 1 - 11 / inradius; // after dark border (image area)
|
||||||
const innerPts = inset(pts, x, y, s1);
|
const innerPts = inset(pts, x, y, s1);
|
||||||
const imagePts = inset(pts, x, y, s2);
|
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)
|
// Layer 3: tile image masked to innermost polygon (land/desert only)
|
||||||
if (terr.tileFrames && this.textures.exists('catan-tiles')) {
|
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 });
|
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||||
maskG.fillStyle(0xffffff);
|
maskG.fillStyle(0xffffff);
|
||||||
maskG.fillPoints(imagePts, true);
|
maskG.fillPoints(imagePts, true);
|
||||||
|
|
@ -192,11 +198,11 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
hexTerrain(hex) {
|
hexTerrain(hex) {
|
||||||
switch (hex.kind) {
|
switch (hex.kind) {
|
||||||
case 'sea':
|
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':
|
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':
|
case 'fog':
|
||||||
return { swatch: 0x6c7a86, color: 0x55606b, label: '?', tileFrames: null };
|
return { swatch: 0x6c7a86, color: 0x55606b, label: '?', tileFrames: CatanGame.TILE_FRAMES.fog };
|
||||||
case 'desert':
|
case 'desert':
|
||||||
return { swatch: DESERT_COLOR, color: DESERT_COLOR, label: 'Desert', tileFrames: CatanGame.TILE_FRAMES.desert };
|
return { swatch: DESERT_COLOR, color: DESERT_COLOR, label: 'Desert', tileFrames: CatanGame.TILE_FRAMES.desert };
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -307,11 +313,11 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
playSound(this, SFX.DICE_ROLL);
|
playSound(this, SFX.DICE_ROLL);
|
||||||
|
|
||||||
const landX = [1256, 1324];
|
const landX = [1256, 1324];
|
||||||
const landY = 950;
|
const landY = 950;
|
||||||
const startX = GAME_WIDTH / 2; // 960 — center of bottom bar
|
const startX = GAME_WIDTH / 2; // 960 — center of bottom bar
|
||||||
const startY = 1015;
|
const startY = 1015;
|
||||||
const arcY = 755; // arc peak
|
const arcY = 755; // arc peak
|
||||||
|
|
||||||
// Move dice to throw origin, small, random angle
|
// Move dice to throw origin, small, random angle
|
||||||
this.diceContainers.forEach((c, i) => {
|
this.diceContainers.forEach((c, i) => {
|
||||||
|
|
@ -334,17 +340,19 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
let settled = 0;
|
let settled = 0;
|
||||||
this.diceContainers.forEach((c, i) => {
|
this.diceContainers.forEach((c, i) => {
|
||||||
const lx = landX[i] + (Math.random() * 8 - 4);
|
const lx = landX[i] + (Math.random() * 8 - 4);
|
||||||
const ly = landY + (Math.random() * 8 - 4);
|
const ly = landY + (Math.random() * 8 - 4);
|
||||||
const outMs = 295 + i * 32;
|
const outMs = 295 + i * 32;
|
||||||
const backMs = 430 + i * 44;
|
const backMs = 430 + i * 44;
|
||||||
const totalMs = outMs + backMs;
|
const totalMs = outMs + backMs;
|
||||||
|
|
||||||
// X flies straight to landing; Y arcs up then bounces down
|
// 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.add({ targets: c, x: lx, duration: totalMs, ease: 'Quad.Out' });
|
||||||
this.tweens.chain({ targets: c, tweens: [
|
this.tweens.chain({
|
||||||
{ y: arcY, duration: outMs, ease: 'Quad.Out' },
|
targets: c, tweens: [
|
||||||
{ y: ly, duration: backMs, ease: 'Bounce.Out' },
|
{ y: arcY, duration: outMs, ease: 'Quad.Out' },
|
||||||
]});
|
{ y: ly, duration: backMs, ease: 'Bounce.Out' },
|
||||||
|
]
|
||||||
|
});
|
||||||
// Scale up as die approaches
|
// Scale up as die approaches
|
||||||
this.tweens.add({ targets: c, scale: 1, duration: outMs + backMs * 0.55, ease: 'Quad.Out' });
|
this.tweens.add({ targets: c, scale: 1, duration: outMs + backMs * 0.55, ease: 'Quad.Out' });
|
||||||
// Spin
|
// Spin
|
||||||
|
|
@ -368,7 +376,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
this.diceContainers.forEach((dc) =>
|
this.diceContainers.forEach((dc) =>
|
||||||
this.tweens.add({ targets: dc, scaleX: 1.14, scaleY: 0.88, duration: 80, yoyo: true })
|
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]]}`);
|
enqueueSpeech(`numbers-${numberWords[values[0] + values[1]]}`);
|
||||||
this.time.delayedCall(160, resolve);
|
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; };
|
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('roll', 'Roll Dice', () => this.onRoll());
|
||||||
mk('road', 'Build Road', () => this.enterPlace('road'));
|
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('settlement', 'Build Settlement', () => this.enterPlace('settlement'));
|
||||||
mk('city', 'Build City', () => this.enterPlace('city'));
|
mk('city', 'Build City', () => this.enterPlace('city'));
|
||||||
mk('buyDev', 'Buy Dev Card', () => this.onBuyDev());
|
mk('buyDev', 'Buy Dev Card', () => this.onBuyDev());
|
||||||
|
|
@ -451,8 +460,11 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
return c;
|
return c;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cardY = this.expansion === 'seafarers' ? 830 : 760;
|
||||||
this._lrCard = makeCard(1775, 0, 0xdaa520);
|
this._lrCard = makeCard(1775, 0, 0xdaa520);
|
||||||
this._laCard = makeCard(1855, 1, 0xb03030);
|
this._laCard = makeCard(1855, 1, 0xb03030);
|
||||||
|
this._lrCard.setY(cardY);
|
||||||
|
this._laCard.setY(cardY);
|
||||||
this._prevSpecialCardOwners = { longestRoad: null, largestArmy: null };
|
this._prevSpecialCardOwners = { longestRoad: null, largestArmy: null };
|
||||||
this._specialCardAnimating = { longestRoad: false, largestArmy: false };
|
this._specialCardAnimating = { longestRoad: false, largestArmy: false };
|
||||||
|
|
||||||
|
|
@ -480,9 +492,10 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
|
|
||||||
_getSpecialCardPos(cardType, owner) {
|
_getSpecialCardPos(cardType, owner) {
|
||||||
if (owner === null) {
|
if (owner === null) {
|
||||||
|
const cardY = this.expansion === 'seafarers' ? 830 : 760;
|
||||||
return cardType === 'longestRoad'
|
return cardType === 'longestRoad'
|
||||||
? { x: 1775, y: 760, scale: 1 }
|
? { x: 1775, y: cardY, scale: 1 }
|
||||||
: { x: 1855, y: 760, scale: 1 };
|
: { x: 1855, y: cardY, scale: 1 };
|
||||||
}
|
}
|
||||||
if (owner === 0) {
|
if (owner === 0) {
|
||||||
return cardType === 'longestRoad'
|
return cardType === 'longestRoad'
|
||||||
|
|
@ -518,23 +531,26 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
|
|
||||||
_animateSpecialCardTransfer(cardType, fromPos, newOwner) {
|
_animateSpecialCardTransfer(cardType, fromPos, newOwner) {
|
||||||
const toPos = this._getSpecialCardPos(cardType, 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);
|
card.setPosition(fromPos.x, fromPos.y).setScale(fromPos.scale);
|
||||||
this._specialCardAnimating[cardType] = true;
|
this._specialCardAnimating[cardType] = true;
|
||||||
|
|
||||||
const peakY = Math.min(fromPos.y, toPos.y) - 150;
|
const peakY = Math.min(fromPos.y, toPos.y) - 150;
|
||||||
const midX = (fromPos.x + toPos.x) / 2;
|
const midX = (fromPos.x + toPos.x) / 2;
|
||||||
const midScale = (fromPos.scale + toPos.scale) / 2;
|
const midScale = (fromPos.scale + toPos.scale) / 2;
|
||||||
const half = 380;
|
const half = 380;
|
||||||
|
|
||||||
this.tweens.chain({ targets: card, tweens: [
|
this.tweens.chain({
|
||||||
{ x: midX, y: peakY, scale: midScale, duration: half, ease: 'Quad.Out' },
|
targets: card, tweens: [
|
||||||
{ x: toPos.x, y: toPos.y, scale: toPos.scale,
|
{ x: midX, y: peakY, scale: midScale, duration: half, ease: 'Quad.Out' },
|
||||||
duration: half, ease: 'Quad.In',
|
{
|
||||||
onComplete: () => { this._specialCardAnimating[cardType] = false; },
|
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');
|
enqueueSpeech(cardType === 'longestRoad' ? 'catan-card-road' : 'catan-card-army');
|
||||||
}
|
}
|
||||||
|
|
@ -595,20 +611,20 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
this._devTooltip = this.add.container(-9999, -9999, [g, titleTxt, descTxt])
|
this._devTooltip = this.add.container(-9999, -9999, [g, titleTxt, descTxt])
|
||||||
.setDepth(D.panel + 5);
|
.setDepth(D.panel + 5);
|
||||||
this._devTooltip.gfx = g;
|
this._devTooltip.gfx = g;
|
||||||
this._devTooltip.titleTxt = titleTxt;
|
this._devTooltip.titleTxt = titleTxt;
|
||||||
this._devTooltip.descTxt = descTxt;
|
this._devTooltip.descTxt = descTxt;
|
||||||
this._devTooltip.popW = popW;
|
this._devTooltip.popW = popW;
|
||||||
this._devTooltip.popH = popH;
|
this._devTooltip.popH = popH;
|
||||||
this._devTooltip.popR = popR;
|
this._devTooltip.popR = popR;
|
||||||
}
|
}
|
||||||
|
|
||||||
showDevCardTooltip(cardX, cardTopY, type, isNew, borderColor) {
|
showDevCardTooltip(cardX, cardTopY, type, isNew, borderColor) {
|
||||||
const DEV_DESC = {
|
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.',
|
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.',
|
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.',
|
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.',
|
yearOfPlenty: 'Take any 2 resources of your choice directly from the bank.',
|
||||||
};
|
};
|
||||||
const tt = this._devTooltip;
|
const tt = this._devTooltip;
|
||||||
|
|
@ -676,13 +692,6 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const panelRight = 1900;
|
const panelRight = 1900;
|
||||||
const panelW = 320;
|
const panelW = 320;
|
||||||
const cx = panelRight - panelW / 2;
|
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 = [
|
const rows = [
|
||||||
{ name: 'Road', resources: ['brick', 'lumber'] },
|
{ 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: 'City', resources: ['grain', 'grain', 'ore', 'ore', 'ore'] },
|
||||||
{ name: 'Dev Card', resources: ['wool', 'grain', '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 lx = panelRight - panelW + 14;
|
||||||
const rx = panelRight - 14;
|
const rx = panelRight - 14;
|
||||||
const rowY0 = bgCy - bgH / 2 + 44;
|
const rowY0 = bgCy - bgH / 2 + rowPad;
|
||||||
const rowStep = (bgH - 44 - 14) / (rows.length - 1);
|
const SW = 16, SH = 16, SG = 4, SR = 3;
|
||||||
const SW = 16, SH = 16, SG = 4, SR = 3; // swatch w/h/gap/radius
|
|
||||||
|
|
||||||
const g = this.add.graphics();
|
const g = this.add.graphics();
|
||||||
panel.add(g);
|
panel.add(g);
|
||||||
|
|
@ -722,8 +746,8 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const panelX = 1489, panelW = 200, panelH = 632;
|
const panelX = 1489, panelW = 200, panelH = 632;
|
||||||
// Centre vertically in the playfield zone above the bottom bar (y=10..882)
|
// Centre vertically in the playfield zone above the bottom bar (y=10..882)
|
||||||
const panelY = 10 + Math.round((872 - panelH) / 2); // 130
|
const panelY = 10 + Math.round((872 - panelH) / 2); // 130
|
||||||
const cardCx = panelX + 10 + 63; // 10px left pad + half of 126
|
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 textX = panelX + 10 + 126 + 12 + 15; // card right + 12 gap + half text ≈ 1637
|
||||||
const panelCx = panelX + panelW / 2; // for BANK title
|
const panelCx = panelX + panelW / 2; // for BANK title
|
||||||
|
|
||||||
const cardW = 126, cardH = 90, cardR = 6, borderW = 3, shadow = 4;
|
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
|
// Stacks nearly touching: 6 × (90 + 6px gap), starting 46px below panel top
|
||||||
const step = 96;
|
const step = 96;
|
||||||
const stackTops = Array.from({ length: 6 }, (_, i) => panelY + 46 + i * step);
|
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 = {};
|
this.bankCardPos = {};
|
||||||
RESOURCE_TYPES.forEach((r, i) => {
|
RESOURCE_TYPES.forEach((r, i) => {
|
||||||
|
|
@ -870,9 +894,9 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const radius = seat === 0 ? 64 : 56;
|
const radius = seat === 0 ? 64 : 56;
|
||||||
const label = this.add.text(dst.x + radius + 10, dst.y,
|
const label = this.add.text(dst.x + radius + 10, dst.y,
|
||||||
RESOURCE_INFO[resource].label.toUpperCase(), {
|
RESOURCE_INFO[resource].label.toUpperCase(), {
|
||||||
fontFamily: 'Righteous', fontSize: '26px', color: '#ffd700',
|
fontFamily: 'Righteous', fontSize: '26px', color: '#ffd700',
|
||||||
stroke: '#000000', strokeThickness: 3,
|
stroke: '#000000', strokeThickness: 3,
|
||||||
}).setOrigin(0, 0.5).setDepth(D.banner);
|
}).setOrigin(0, 0.5).setDepth(D.banner);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: label, alpha: 0, y: dst.y - 24,
|
targets: label, alpha: 0, y: dst.y - 24,
|
||||||
duration: 700, delay: 300,
|
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;
|
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(16, 0xffffff, 0.9); g.lineBetween(ax, ay, bx, by);
|
||||||
g.lineStyle(12, col.hexDark, 1); 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);
|
this.pieceObjs.push(g);
|
||||||
}
|
}
|
||||||
// ships (Seafarers): dashed maritime route in the player's colour
|
// 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
|
// pirate (Seafarers): a sea-robber token on its hex
|
||||||
if (this.gs.pirateHex != null) {
|
if (this.gs.pirateHex != null) {
|
||||||
const { x, y } = this.hexPos(this.gs.pirateHex);
|
const { x, y } = this.hexPos(this.gs.pirateHex);
|
||||||
const pg = this.add.graphics().setDepth(D.robber);
|
this.pieceObjs.push(
|
||||||
pg.fillStyle(0x000000, 0.45); pg.fillCircle(x + 2, y + 3, 20);
|
this.add.image(x, y, 'catan-pirate').setOrigin(0.5).setDisplaySize(48, 48).setDepth(D.robber)
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A ship piece: a thick coloured bar along the sea edge with a sail nub.
|
// A ship piece: a thick coloured bar along the sea edge with a sail nub.
|
||||||
makeShip(ax, ay, bx, by, col) {
|
makeShip(ax, ay, bx, by, col) {
|
||||||
const g = this.add.graphics().setDepth(D.road);
|
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(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
|
// sail at the midpoint
|
||||||
const mx = (ax + bx) / 2, my = (ay + by) / 2;
|
const mx = (ax + bx) / 2, my = (ay + by) / 2;
|
||||||
g.fillStyle(0xffffff, 0.95);
|
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)
|
// White halo (3px border around the scaled-up shape)
|
||||||
g.fillStyle(0xffffff, 1);
|
g.fillStyle(0xffffff, 1);
|
||||||
g.fillRect(x - 24, y, 27, 21); // lower block halo
|
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
|
g.fillTriangle(x - 11, y - 10, x + 26, y - 10, x + 8, y - 26); // roof halo
|
||||||
// Player color fill (~30% larger than original)
|
// Player color fill (~30% larger than original)
|
||||||
g.fillStyle(col.hex, 1);
|
g.fillStyle(col.hex, 1);
|
||||||
g.fillRect(x - 21, y, 21, 18); // lower block
|
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
|
g.fillTriangle(x - 8, y - 10, x + 23, y - 10, x + 8, y - 23); // roof
|
||||||
// Dark outlines
|
// Dark outlines
|
||||||
g.lineStyle(2.5, col.hexDark, 1);
|
g.lineStyle(2.5, col.hexDark, 1);
|
||||||
|
|
@ -1108,7 +1129,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
enqueueSpeech(clip);
|
enqueueSpeech(clip);
|
||||||
if (this.robberObj) this.robberObj.destroy();
|
if (this.robberObj) this.robberObj.destroy();
|
||||||
const from = this.hexPos(fromHexId);
|
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')
|
this.robberObj = this.add.image(from.x, from.y, 'catan-robber')
|
||||||
.setDisplaySize(64, 64)
|
.setDisplaySize(64, 64)
|
||||||
.setDepth(D.robber);
|
.setDepth(D.robber);
|
||||||
|
|
@ -1125,7 +1146,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
targets: this.robberObj,
|
targets: this.robberObj,
|
||||||
tweens: [
|
tweens: [
|
||||||
{ scaleX: base * 2, scaleY: base * 2, duration: 1500, ease: 'Sine.Out' },
|
{ 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) {
|
_flyCardToBank(srcX, srcY, frameIdx, bankPos, startFaceUp) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const cardW = 60, cardH = 84;
|
const cardW = 60, cardH = 84;
|
||||||
const bigW = 90, bigH = 126;
|
const bigW = 90, bigH = 126;
|
||||||
const resource = RESOURCE_TYPES[frameIdx];
|
const resource = RESOURCE_TYPES[frameIdx];
|
||||||
|
|
||||||
const makeBorder = (color) => {
|
const makeBorder = (color) => {
|
||||||
|
|
@ -1249,12 +1270,12 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
if (type === 'road') {
|
if (type === 'road') {
|
||||||
g.lineStyle(10, 0xffffff, 0.9); g.lineBetween(-13, 0, 13, 0);
|
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(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') {
|
} else if (type === 'settlement') {
|
||||||
g.fillStyle(0xffffff, 1);
|
g.fillStyle(0xffffff, 1);
|
||||||
g.fillRect(-11, -3, 22, 15); g.fillTriangle(-14, -3, 14, -3, 0, -17);
|
g.fillRect(-11, -3, 22, 15); g.fillTriangle(-14, -3, 14, -3, 0, -17);
|
||||||
g.fillStyle(col.hex, 1);
|
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);
|
g.lineStyle(2, col.hexDark, 1); g.strokeRect(-9, -3, 18, 12);
|
||||||
} else if (type === 'city') {
|
} else if (type === 'city') {
|
||||||
g.fillStyle(0xffffff, 1);
|
g.fillStyle(0xffffff, 1);
|
||||||
|
|
@ -1279,15 +1300,19 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const peakY = Math.min(srcPos.y, destY) - arcHeight;
|
const peakY = Math.min(srcPos.y, destY) - arcHeight;
|
||||||
const half = duration / 2;
|
const half = duration / 2;
|
||||||
this.tweens.add({ targets: container, x: destX, duration, ease: 'Quad.InOut' });
|
this.tweens.add({ targets: container, x: destX, duration, ease: 'Quad.InOut' });
|
||||||
this.tweens.chain({ targets: container, tweens: [
|
this.tweens.chain({
|
||||||
{ y: peakY, duration: half, ease: 'Quad.Out' },
|
targets: container, tweens: [
|
||||||
{ y: destY, duration: half, ease: 'Quad.In', onComplete: () => {
|
{ y: peakY, duration: half, ease: 'Quad.Out' },
|
||||||
emitter.stop();
|
{
|
||||||
container.destroy();
|
y: destY, duration: half, ease: 'Quad.In', onComplete: () => {
|
||||||
this.time.delayedCall(500, () => emitter.destroy());
|
emitter.stop();
|
||||||
resolve();
|
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 peakY = Math.min(bankPos.y, destPos.y) - arcHeight;
|
||||||
const half = duration / 2;
|
const half = duration / 2;
|
||||||
this.tweens.add({ targets: container, x: destPos.x, duration, ease: 'Quad.InOut' });
|
this.tweens.add({ targets: container, x: destPos.x, duration, ease: 'Quad.InOut' });
|
||||||
this.tweens.chain({ targets: container, tweens: [
|
this.tweens.chain({
|
||||||
{ y: peakY, duration: half, ease: 'Quad.Out' },
|
targets: container, tweens: [
|
||||||
{ y: destPos.y, duration: half, ease: 'Quad.In', onComplete: () => {
|
{ y: peakY, duration: half, ease: 'Quad.Out' },
|
||||||
this.tweens.add({
|
{
|
||||||
targets: container, alpha: 0, scaleX: 1.5, scaleY: 1.5, duration: 280,
|
y: destPos.y, duration: half, ease: 'Quad.In', onComplete: () => {
|
||||||
onComplete: () => { container.destroy(); resolve(); },
|
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 = {
|
const DEV_VISUAL = {
|
||||||
knight: { frame: 5, border: 0xb03030 },
|
knight: { frame: 5, border: 0xb03030 },
|
||||||
roadBuilding: { frame: 6, border: 0x8b5a2b },
|
roadBuilding: { frame: 6, border: 0x8b5a2b },
|
||||||
vp: { frame: 7, border: 0xdaa520 },
|
vp: { frame: 7, border: 0xdaa520 },
|
||||||
monopoly: { frame: 9, border: 0x7b2d8b },
|
monopoly: { frame: 9, border: 0x7b2d8b },
|
||||||
yearOfPlenty: { frame: 10, border: 0x2d8b57 },
|
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 hit = this.add.rectangle(x, y, cardW, cardH, 0x000000, 0).setInteractive();
|
||||||
const cx = x, topY = y - cardH / 2;
|
const cx = x, topY = y - cardH / 2;
|
||||||
hit.on('pointerover', () => this.showDevCardTooltip(cx, topY, type, isNew, visual.border));
|
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]);
|
this.devHandContainer.add([bg, img, border, hit]);
|
||||||
x += step;
|
x += step;
|
||||||
|
|
@ -1525,6 +1554,9 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const hasRoadSpot = action && L.legalRoadEdges(s, 0, false).length > 0;
|
const hasRoadSpot = action && L.legalRoadEdges(s, 0, false).length > 0;
|
||||||
set('roll', me && s.phase === 'rollPhase');
|
set('roll', me && s.phase === 'rollPhase');
|
||||||
set('road', (action && hasRoadSpot && L.canAfford(p, COSTS.road)) || (action && s.freeRoads > 0 && hasRoadSpot));
|
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('settlement', hasSettleSpot && L.canAfford(p, COSTS.settlement));
|
||||||
set('city', action && p.settlements.length > 0 && L.canAfford(p, COSTS.city));
|
set('city', action && p.settlements.length > 0 && L.canAfford(p, COSTS.city));
|
||||||
set('buyDev', action && s.devDeck.length > 0 && L.canAfford(p, COSTS.devCard));
|
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')];
|
const names = ['You', ...this.opponents.map((o) => o?.name ?? 'CPU')];
|
||||||
L.setPlayerNames(this.gs, names);
|
L.setPlayerNames(this.gs, names);
|
||||||
|
this.hexTileFrames = {};
|
||||||
this.drawHexes();
|
this.drawHexes();
|
||||||
this.drawPorts();
|
this.drawPorts();
|
||||||
this.drawChits();
|
this.drawChits();
|
||||||
|
|
@ -1579,6 +1612,8 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
} else if (s.phase === 'rollPhase') {
|
} else if (s.phase === 'rollPhase') {
|
||||||
if (me) { /* wait for Roll button */ }
|
if (me) { /* wait for Roll button */ }
|
||||||
else await this.aiRoll();
|
else await this.aiRoll();
|
||||||
|
} else if (s.phase === 'goldPick') {
|
||||||
|
await this.handleGoldPickPhase();
|
||||||
} else if (s.phase === 'discard') {
|
} else if (s.phase === 'discard') {
|
||||||
await this.handleDiscardPhase();
|
await this.handleDiscardPhase();
|
||||||
} else if (s.phase === 'moveRobber') {
|
} else if (s.phase === 'moveRobber') {
|
||||||
|
|
@ -1739,10 +1774,10 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
|
|
||||||
applyAction(seat, a) {
|
applyAction(seat, a) {
|
||||||
switch (a.type) {
|
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 'buildSettlement': return L.buildSettlement(this.gs, seat, a.nodeId);
|
||||||
case 'buildRoad': return L.buildRoad(this.gs, seat, a.edgeId);
|
case 'buildRoad': return L.buildRoad(this.gs, seat, a.edgeId);
|
||||||
case 'buyDev': return L.buyDevCard(this.gs, seat);
|
case 'buyDev': return L.buyDevCard(this.gs, seat);
|
||||||
case 'bankTrade': return L.tradeWithBank(this.gs, seat, a.give, a.get);
|
case 'bankTrade': return L.tradeWithBank(this.gs, seat, a.give, a.get);
|
||||||
case 'playDev':
|
case 'playDev':
|
||||||
if (a.card === 'knight') return L.playKnight(this.gs, seat);
|
if (a.card === 'knight') return L.playKnight(this.gs, seat);
|
||||||
|
|
@ -1835,10 +1870,10 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
objs.push(g);
|
objs.push(g);
|
||||||
return 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 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' });
|
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(); } });
|
objs.push({ destroy: () => { t1.stop(); t2.stop(); } });
|
||||||
|
|
||||||
|
|
@ -1881,15 +1916,15 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
// ── opponent dev card reveal ──────────────────────────────────────────────────
|
// ── opponent dev card reveal ──────────────────────────────────────────────────
|
||||||
async animateOppDevCardPlay(seat, cardType, resource) {
|
async animateOppDevCardPlay(seat, cardType, resource) {
|
||||||
const VISUAL = {
|
const VISUAL = {
|
||||||
knight: { frame: 5, border: 0xb03030 },
|
knight: { frame: 5, border: 0xb03030 },
|
||||||
roadBuilding: { frame: 6, border: 0x8b5a2b },
|
roadBuilding: { frame: 6, border: 0x8b5a2b },
|
||||||
monopoly: { frame: 9, border: 0x7b2d8b },
|
monopoly: { frame: 9, border: 0x7b2d8b },
|
||||||
yearOfPlenty: { frame: 10, border: 0x2d8b57 },
|
yearOfPlenty: { frame: 10, border: 0x2d8b57 },
|
||||||
};
|
};
|
||||||
const SPEECH = {
|
const SPEECH = {
|
||||||
knight: 'catan-dev-knight',
|
knight: 'catan-dev-knight',
|
||||||
roadBuilding: 'catan-dev-road',
|
roadBuilding: 'catan-dev-road',
|
||||||
monopoly: 'catan-dev-monopoly',
|
monopoly: 'catan-dev-monopoly',
|
||||||
yearOfPlenty: 'catan-dev-year',
|
yearOfPlenty: 'catan-dev-year',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1933,7 +1968,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
|
|
||||||
const bursts = [
|
const bursts = [
|
||||||
{ x: toX - 90, y: textY - 10 }, { x: toX + 90, y: textY - 10 },
|
{ 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 },
|
{ x: toX + 50, y: textY + 22 },
|
||||||
];
|
];
|
||||||
bursts.forEach((b, i) =>
|
bursts.forEach((b, i) =>
|
||||||
|
|
@ -1977,6 +2012,31 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
this.advance();
|
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 ─────────────────────────────────────────────────────────
|
// ── human: discards ─────────────────────────────────────────────────────────
|
||||||
async handleDiscardPhase() {
|
async handleDiscardPhase() {
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
|
|
@ -2051,6 +2111,11 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const { x, y } = this.nodePos(nid);
|
const { x, y } = this.nodePos(nid);
|
||||||
this.addHighlight(x, y, () => this.doBuild('city', nid), 0xffd700);
|
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)`);
|
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}`);
|
enqueueSpeech(`catan-purchase-${type}`);
|
||||||
await this.animateCostPayment(0, type);
|
await this.animateCostPayment(0, type);
|
||||||
await this.animatePiecePlacement(0, type, dest.x, dest.y);
|
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 === '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);
|
playSound(this, SFX.PIECE_CLICK);
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.advance();
|
this.advance();
|
||||||
|
|
@ -2148,12 +2220,12 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
if (this.busy || this.gs.phase !== 'action') return;
|
if (this.busy || this.gs.phase !== 'action') return;
|
||||||
this.clearHighlights();
|
this.clearHighlights();
|
||||||
const give = { brick: 0, lumber: 0, wool: 0, grain: 0, ore: 0 };
|
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 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 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 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 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 objs = [overlay, box, title, hintGive, hintGet];
|
||||||
|
|
||||||
const stepper = (col, r, i, side) => {
|
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 label = (RESOURCE_INFO[resource]?.label ?? resource).toUpperCase();
|
||||||
const txt = this.add.text(GAME_WIDTH / 2, 855,
|
const txt = this.add.text(GAME_WIDTH / 2, 855,
|
||||||
`CARD STOLEN: ${label} BY ${robberName.toUpperCase()}`, {
|
`CARD STOLEN: ${label} BY ${robberName.toUpperCase()}`, {
|
||||||
fontFamily: 'Righteous', fontSize: '38px', color: '#ffd700',
|
fontFamily: 'Righteous', fontSize: '38px', color: '#ffd700',
|
||||||
stroke: '#000000', strokeThickness: 5,
|
stroke: '#000000', strokeThickness: 5,
|
||||||
}
|
}
|
||||||
).setOrigin(0.5).setDepth(D.banner + 2);
|
).setOrigin(0.5).setDepth(D.banner + 2);
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: txt, alpha: 0, delay: 3000, duration: 500,
|
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 },
|
backgroundColor: '#111923ee', padding: { x: 26, y: 12 },
|
||||||
}).setOrigin(0.5).setDepth(D.banner);
|
}).setOrigin(0.5).setDepth(D.banner);
|
||||||
banner.setAlpha(0);
|
banner.setAlpha(0);
|
||||||
this.tweens.add({ targets: banner, alpha: 1, y: 140, duration: 280, ease: 'Back.easeOut',
|
this.tweens.add({
|
||||||
onComplete: () => this.time.delayedCall(900, () => this.tweens.add({ targets: banner, alpha: 0, y: 120, duration: 220, onComplete: () => banner.destroy() })) });
|
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 ─────────────────────────────────────────────────────────────────
|
// ── game over ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -2346,11 +2420,11 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
this.recordHistory();
|
this.recordHistory();
|
||||||
|
|
||||||
const PW = 760, PH = 660, PX = 1000, PY = 540;
|
const PW = 760, PH = 660, PX = 1000, PY = 540;
|
||||||
const titleY = PY - PH / 2 + 70; // 280
|
const titleY = PY - PH / 2 + 70; // 280
|
||||||
const RADIUS = 80;
|
const RADIUS = 80;
|
||||||
const portraitY = titleY + 140; // 420
|
const portraitY = titleY + 140; // 420
|
||||||
const bodyY = portraitY + RADIUS + 95; // 595
|
const bodyY = portraitY + RADIUS + 95; // 595
|
||||||
const buttonsY = PY + PH / 2 - 72; // 798
|
const buttonsY = PY + PH / 2 - 72; // 798
|
||||||
|
|
||||||
// Fireworks across the popup for all winners
|
// Fireworks across the popup for all winners
|
||||||
const fwEmitter = this.add.particles(PX, PY, 'catanParticle', {
|
const fwEmitter = this.add.particles(PX, PY, 'catanParticle', {
|
||||||
|
|
@ -2405,7 +2479,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
videoEl.autoplay = true;
|
videoEl.autoplay = true;
|
||||||
videoEl.style.cssText = `width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;display:block;`;
|
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.src = `/assets/videos/${opp.id}-happy.mp4`;
|
||||||
videoEl.play().catch(() => {});
|
videoEl.play().catch(() => { });
|
||||||
videoEl.addEventListener('error', () => { videoEl.style.display = 'none'; }, { once: true });
|
videoEl.addEventListener('error', () => { videoEl.style.display = 'none'; }, { once: true });
|
||||||
portraitDom = this.add.dom(PX, portraitY, videoEl).setDepth(D.banner + 3);
|
portraitDom = this.add.dom(PX, portraitY, videoEl).setDepth(D.banner + 3);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ export function createInitialState(playerCount = 3, { tilePlacement = 'random',
|
||||||
diceTotal: null,
|
diceTotal: null,
|
||||||
robberReturnPhase: 'action',
|
robberReturnPhase: 'action',
|
||||||
discardQueue: [],
|
discardQueue: [],
|
||||||
|
goldPickQueue: [],
|
||||||
freeRoads: 0,
|
freeRoads: 0,
|
||||||
freeShips: 0, // Seafarers: free ships (e.g. from a future card)
|
freeShips: 0, // Seafarers: free ships (e.g. from a future card)
|
||||||
longestRoad: { owner: null, length: 0 },
|
longestRoad: { owner: null, length: 0 },
|
||||||
|
|
@ -306,7 +307,7 @@ export function rollDice(state) {
|
||||||
s.phase = s.discardQueue.length ? 'discard' : 'moveRobber';
|
s.phase = s.discardQueue.length ? 'discard' : 'moveRobber';
|
||||||
} else {
|
} else {
|
||||||
produceResources(s, s.diceTotal);
|
produceResources(s, s.diceTotal);
|
||||||
s.phase = 'action';
|
if (s.phase !== 'goldPick') s.phase = 'action';
|
||||||
}
|
}
|
||||||
return s;
|
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; }
|
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.
|
// Gold hexes: each settlement/city adjacent earns 1/2 free-choice resources.
|
||||||
getExpansion(s.expansion).onProduce?.(s, total);
|
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 ───────────────────────────────────────────────────────────
|
// ── discard / robber ───────────────────────────────────────────────────────────
|
||||||
|
|
@ -398,11 +422,29 @@ export function buildRoad(state, seat, edgeId) {
|
||||||
if (!free && !canAfford(s.players[seat], COSTS.road)) return s;
|
if (!free && !canAfford(s.players[seat], COSTS.road)) return s;
|
||||||
if (free) s.freeRoads--; else pay(s, s.players[seat], COSTS.road);
|
if (free) s.freeRoads--; else pay(s, s.players[seat], COSTS.road);
|
||||||
s.players[seat].roads.push(edgeId);
|
s.players[seat].roads.push(edgeId);
|
||||||
|
revealFogHexes(s, edgeId);
|
||||||
recomputeLongestRoad(s);
|
recomputeLongestRoad(s);
|
||||||
checkWin(s, seat);
|
checkWin(s, seat);
|
||||||
return s;
|
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) ──────────────────────────────────────────────────────────
|
// ── ships (Seafarers) ──────────────────────────────────────────────────────────
|
||||||
// Ship cost is supplied by the active expansion; null in the base game (no ships).
|
// Ship cost is supplied by the active expansion; null in the base game (no ships).
|
||||||
export function shipCost(state) {
|
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 && !canAfford(s.players[seat], cost)) return s;
|
||||||
if (free) s.freeShips--; else pay(s, s.players[seat], cost);
|
if (free) s.freeShips--; else pay(s, s.players[seat], cost);
|
||||||
s.players[seat].ships.push(edgeId);
|
s.players[seat].ships.push(edgeId);
|
||||||
|
revealFogHexes(s, edgeId);
|
||||||
recomputeLongestRoad(s);
|
recomputeLongestRoad(s);
|
||||||
checkWin(s, seat);
|
checkWin(s, seat);
|
||||||
return s;
|
return s;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
// buildBoard() produces the topology and per-hex assignments at game start.
|
// buildBoard() produces the topology and per-hex assignments at game start.
|
||||||
//
|
//
|
||||||
// An expansion may expose any of:
|
// 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)
|
// scenarios – { id: scenarioObj } selectable layouts (see seafarers.js)
|
||||||
// setupRules – [ (state) => void ] applied once after base setup
|
// setupRules – [ (state) => void ] applied once after base setup
|
||||||
// onProduce – (state, total) => void extra production (e.g. gold hexes)
|
// onProduce – (state, total) => void extra production (e.g. gold hexes)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
HEX_SIZE, BOARD_CX, BOARD_CY, PORT_BAG,
|
HEX_SIZE, BOARD_CX, BOARD_CY, PORT_BAG,
|
||||||
} from '../../CatanBoard.js';
|
} 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.
|
// Larger Seafarers boards use a slightly smaller hex so they fit the canvas.
|
||||||
export const SEA_HEX_SIZE = 74;
|
export const SEA_HEX_SIZE = 74;
|
||||||
|
|
@ -68,6 +68,8 @@ export function assembleBoard({ id, rows, cells, homeIds = [], pirate = null, wi
|
||||||
resource: c.resource ?? null,
|
resource: c.resource ?? null,
|
||||||
number: c.number ?? null,
|
number: c.number ?? null,
|
||||||
hasRobber: false,
|
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.
|
// Robber starts on the (first) desert, if any.
|
||||||
|
|
|
||||||
|
|
@ -38,30 +38,31 @@ export default class PreloadScene extends Phaser.Scene {
|
||||||
frameWidth: 312,
|
frameWidth: 312,
|
||||||
frameHeight: 312,
|
frameHeight: 312,
|
||||||
});
|
});
|
||||||
this.load.image('catan-robber', '/assets/images/catan-robber.png');
|
this.load.image('catan-robber', '/assets/images/catan-robber.png');
|
||||||
this.load.image('bg-menu', '/assets/images/background-menu.png');
|
this.load.image('catan-pirate', '/assets/images/catan-pirate.png');
|
||||||
this.load.image('bg-room', '/assets/images/background-room.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('bg-casino', '/assets/images/background-casino.png');
|
||||||
this.load.image('main-title', '/assets/images/main-title.png');
|
this.load.image('main-title', '/assets/images/main-title.png');
|
||||||
this.load.json('playfields', '/data/playfields.json');
|
this.load.json('playfields', '/data/playfields.json');
|
||||||
this.load.json('card-backs', '/data/card-backs.json');
|
this.load.json('card-backs', '/data/card-backs.json');
|
||||||
this.load.json('music', '/data/music.json');
|
this.load.json('music', '/data/music.json');
|
||||||
|
|
||||||
this.load.audio('sfx-card-deal', '/assets/fx/card-deal.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-place', '/assets/fx/card-place.mp3');
|
||||||
this.load.audio('sfx-card-show', '/assets/fx/card-show.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-card-shuffle', '/assets/fx/card-shuffle.mp3');
|
||||||
this.load.audio('sfx-coins', '/assets/fx/coins.mp3');
|
this.load.audio('sfx-coins', '/assets/fx/coins.mp3');
|
||||||
this.load.audio('sfx-purchase', '/assets/fx/purchase.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-blackjack', '/assets/fx/casino-blackjack.mp3');
|
||||||
this.load.audio('sfx-casino-lose', '/assets/fx/casino-lose.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-casino-win', '/assets/fx/casino-win.mp3');
|
||||||
this.load.audio('sfx-chip-bet', '/assets/fx/chip-bet.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-dice-roll', '/assets/fx/dice-roll.mp3');
|
||||||
this.load.audio('sfx-bingo-balls', '/assets/fx/bingo-balls.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-pencil-write', '/assets/fx/pencil-write.mp3');
|
||||||
this.load.audio('sfx-piece-click', '/assets/fx/piece-click.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-roulette', '/assets/fx/roulette.mp3');
|
||||||
|
|
||||||
this.load.spritesheet('catan-special-cards', '/assets/images/catan-special-cards.png', { frameWidth: 270, frameHeight: 390 });
|
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 = [
|
const toLoad = [
|
||||||
...(pfd?.playfields ?? []).filter((pf) => pf.path && !this.textures.exists(pf.key)),
|
...(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) {
|
if (toLoad.length > 0) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue