feat(catan): enhance robber mechanics, dice animations, and resource hand UI
- Add animated robber movement with custom sprite, sound effects, and glow - Implement dynamic dice roll animation with arc trajectory and sound cues - Replace static resource hand with draggable, selectable cards - Add victory points badge above player portrait - Expand trade panel width and button sizes for better usability - Load new robber asset and speech clips in preload scene
This commit is contained in:
parent
29fb4a9b2f
commit
aa5609f2f9
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -5,6 +5,7 @@ import { auth } from '../../services/auth.js';
|
||||||
import { api } from '../../services/api.js';
|
import { api } from '../../services/api.js';
|
||||||
import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js';
|
import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js';
|
||||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||||
|
import { enqueue as enqueueSpeech } from '../../ui/SpeechQueue.js';
|
||||||
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||||
import {
|
import {
|
||||||
NODES, EDGES, HEXES, PORT_SLOTS, RESOURCE_INFO, RESOURCE_TYPES, DESERT_COLOR,
|
NODES, EDGES, HEXES, PORT_SLOTS, RESOURCE_INFO, RESOURCE_TYPES, DESERT_COLOR,
|
||||||
|
|
@ -32,6 +33,9 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
this.opponentPortraits = [];
|
this.opponentPortraits = [];
|
||||||
this.buttons = {};
|
this.buttons = {};
|
||||||
this.placeMode = null; // 'road' | 'settlement' | 'city' | null
|
this.placeMode = null; // 'road' | 'settlement' | 'city' | null
|
||||||
|
this.handDisplay = [];
|
||||||
|
this.handCardObjs = [];
|
||||||
|
this.handSelectedIdx = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -264,21 +268,76 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
animateDice(values) {
|
animateDice(values) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
playSound(this, SFX.DICE_ROLL);
|
playSound(this, SFX.DICE_ROLL);
|
||||||
this.diceContainers.forEach((c) => c.setAlpha(1));
|
|
||||||
let elapsed = 0; const total = 650;
|
const landX = [1256, 1324];
|
||||||
const tick = () => {
|
const landY = 950;
|
||||||
this.drawDie(this.diceG[0], Phaser.Math.Between(1, 6));
|
const startX = GAME_WIDTH / 2; // 960 — center of bottom bar
|
||||||
this.drawDie(this.diceG[1], Phaser.Math.Between(1, 6));
|
const startY = 1015;
|
||||||
elapsed += 70;
|
const arcY = 755; // arc peak
|
||||||
if (elapsed < total) this.time.delayedCall(70, tick);
|
|
||||||
else {
|
// Move dice to throw origin, small, random angle
|
||||||
this.drawDie(this.diceG[0], values[0]);
|
this.diceContainers.forEach((c, i) => {
|
||||||
this.drawDie(this.diceG[1], values[1]);
|
c.setAlpha(1).setScale(0.35).setAngle(Phaser.Math.Between(0, 359))
|
||||||
this.diceContainers.forEach((c) => this.tweens.add({ targets: c, scale: 1.18, duration: 90, yoyo: true }));
|
.setPosition(startX + (i === 0 ? -12 : 12), startY);
|
||||||
this.time.delayedCall(140, resolve);
|
this.drawDie(this.diceG[i], Phaser.Math.Between(1, 6));
|
||||||
}
|
});
|
||||||
};
|
|
||||||
tick();
|
// Cycle random faces while airborne
|
||||||
|
let cyclerStopped = false;
|
||||||
|
const cycler = this.time.addEvent({
|
||||||
|
delay: 55, loop: true,
|
||||||
|
callback: () => {
|
||||||
|
this.drawDie(this.diceG[0], Phaser.Math.Between(1, 6));
|
||||||
|
this.drawDie(this.diceG[1], Phaser.Math.Between(1, 6));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const stopCycler = () => { if (!cyclerStopped) { cyclerStopped = true; cycler.remove(); } };
|
||||||
|
|
||||||
|
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 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' },
|
||||||
|
]});
|
||||||
|
// Scale up as die approaches
|
||||||
|
this.tweens.add({ targets: c, scale: 1, duration: outMs + backMs * 0.55, ease: 'Quad.Out' });
|
||||||
|
// Spin
|
||||||
|
this.tweens.add({
|
||||||
|
targets: c,
|
||||||
|
angle: c.angle + 540 + Math.random() * 180,
|
||||||
|
duration: totalMs,
|
||||||
|
ease: 'Quad.Out',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.time.delayedCall(totalMs, () => {
|
||||||
|
stopCycler();
|
||||||
|
this.drawDie(this.diceG[i], values[i]);
|
||||||
|
// Snap to nearest upright angle with a small wiggle
|
||||||
|
const upright = Math.round(c.angle / 90) * 90 + (Math.random() * 10 - 5);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: c, angle: upright, duration: 120, ease: 'Back.Out',
|
||||||
|
onComplete: () => {
|
||||||
|
settled++;
|
||||||
|
if (settled === 2) {
|
||||||
|
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'];
|
||||||
|
enqueueSpeech(`numbers-${numberWords[values[0] + values[1]]}`);
|
||||||
|
this.time.delayedCall(160, resolve);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,38 +349,15 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
|
|
||||||
// human portrait
|
// human portrait
|
||||||
createPlayerPortrait(this, 90, 980, 64, D.hud, 'CatanGame');
|
createPlayerPortrait(this, 90, 980, 64, D.hud, 'CatanGame');
|
||||||
|
// VP badge above portrait — graphics redrawn in updateHand() once gs is available
|
||||||
|
this.playerVpBadgeGfx = this.add.graphics().setDepth(D.hud + 5);
|
||||||
|
this.playerVpText = this.add.text(90, 906, '0', {
|
||||||
|
fontFamily: '"Julius Sans One"', fontSize: '15px', color: '#ffffff', fontStyle: 'bold',
|
||||||
|
}).setOrigin(0.5).setDepth(D.hud + 6);
|
||||||
this.add.text(90, 1056, auth.user?.username ?? 'You', {
|
this.add.text(90, 1056, auth.user?.username ?? 'You', {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.textHex,
|
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.textHex,
|
||||||
}).setOrigin(0.5).setDepth(D.hud);
|
}).setOrigin(0.5).setDepth(D.hud);
|
||||||
|
|
||||||
// resource hand
|
|
||||||
this.resText = {};
|
|
||||||
const startX = 230, gap = 86;
|
|
||||||
const cardW = 60, cardH = 84, cardR = 6, borderW = 3;
|
|
||||||
RESOURCE_TYPES.forEach((r, i) => {
|
|
||||||
const x = startX + i * gap, y = 950;
|
|
||||||
// 1. Dark background fill
|
|
||||||
const gFill = this.add.graphics().setDepth(D.hud);
|
|
||||||
gFill.fillStyle(0x111111, 0.85);
|
|
||||||
gFill.fillRoundedRect(x - cardW / 2, y - cardH / 2, cardW, cardH, cardR);
|
|
||||||
// 2. Card artwork (270×390 source → 54×78 display)
|
|
||||||
this.add.image(x, y, 'catan-cards', i).setDisplaySize(54, 78).setDepth(D.hud);
|
|
||||||
// 3. Colored border on top of artwork
|
|
||||||
const gBorder = this.add.graphics().setDepth(D.hud);
|
|
||||||
gBorder.lineStyle(borderW, RESOURCE_INFO[r].swatch, 1);
|
|
||||||
gBorder.strokeRoundedRect(x - cardW / 2, y - cardH / 2, cardW, cardH, cardR);
|
|
||||||
// 4. Resource label below card
|
|
||||||
this.add.text(x, y + cardH / 2 + 9, RESOURCE_INFO[r].label, {
|
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '11px', color: COLORS.mutedHex,
|
|
||||||
}).setOrigin(0.5, 0).setDepth(D.hud);
|
|
||||||
// 5. Quantity number over artwork
|
|
||||||
this.resText[r] = this.add.text(x, y + 6, '0', {
|
|
||||||
fontFamily: 'Righteous', fontSize: '26px', color: '#ffffff',
|
|
||||||
stroke: '#000000', strokeThickness: 4,
|
|
||||||
shadow: { color: '#000000', fill: true, offsetX: 2, offsetY: 2, blur: 3 },
|
|
||||||
}).setOrigin(0.5).setDepth(D.hud);
|
|
||||||
});
|
|
||||||
|
|
||||||
// dev card hand area label
|
// dev card hand area label
|
||||||
this.devHandContainer = this.add.container(0, 0).setDepth(D.hud);
|
this.devHandContainer = this.add.container(0, 0).setDepth(D.hud);
|
||||||
this.add.text(740, 916, 'Development Cards', {
|
this.add.text(740, 916, 'Development Cards', {
|
||||||
|
|
@ -745,19 +781,160 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
renderRobber() {
|
renderRobber() {
|
||||||
if (this.robberObj) this.robberObj.destroy();
|
if (this.robberObj) this.robberObj.destroy();
|
||||||
const { x, y } = this.hexPos(this.gs.robberHex);
|
const { x, y } = this.hexPos(this.gs.robberHex);
|
||||||
const g = this.add.graphics();
|
this.robberObj = this.add.image(x, y, 'catan-robber')
|
||||||
g.fillStyle(0x000000, 0.3); g.fillEllipse(2, 30, 30, 10);
|
.setDisplaySize(64, 64)
|
||||||
g.fillStyle(0x2b2b2b, 1);
|
.setDepth(D.robber);
|
||||||
g.fillEllipse(0, 26, 30, 14); // base
|
this.robberObj.postFX.addGlow(0x000000, 8, 0, false, 0.1, 24);
|
||||||
g.fillRoundedRect(-11, -2, 22, 30, 8); // body
|
}
|
||||||
g.fillCircle(0, -10, 12); // head
|
|
||||||
g.lineStyle(2, 0x000000, 0.5); g.strokeCircle(0, -10, 12);
|
async animateRobber(fromHexId, toHexId) {
|
||||||
this.robberObj = this.add.container(x, y - 14, [g]).setDepth(D.robber);
|
const clip = `catan-robber-0${Phaser.Math.Between(1, 4)}`;
|
||||||
|
enqueueSpeech(clip);
|
||||||
|
if (this.robberObj) this.robberObj.destroy();
|
||||||
|
const from = this.hexPos(fromHexId);
|
||||||
|
const to = this.hexPos(toHexId);
|
||||||
|
this.robberObj = this.add.image(from.x, from.y, 'catan-robber')
|
||||||
|
.setDisplaySize(64, 64)
|
||||||
|
.setDepth(D.robber);
|
||||||
|
this.robberObj.postFX.addGlow(0x000000, 8, 0, false, 0.1, 24);
|
||||||
|
const base = this.robberObj.scaleX;
|
||||||
|
await new Promise(resolve => {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: this.robberObj,
|
||||||
|
x: to.x, y: to.y,
|
||||||
|
duration: 3000,
|
||||||
|
ease: 'Sine.InOut',
|
||||||
|
});
|
||||||
|
this.tweens.chain({
|
||||||
|
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 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHand() {
|
updateHand() {
|
||||||
const p = this.gs.players[0];
|
const p = this.gs.players[0];
|
||||||
for (const r of RESOURCE_TYPES) this.resText[r].setText(String(p.resources[r]));
|
|
||||||
|
// Sync handDisplay with game state, preserving drag order
|
||||||
|
const updated = [...this.handDisplay];
|
||||||
|
for (const r of RESOURCE_TYPES) {
|
||||||
|
const have = updated.filter(x => x === r).length;
|
||||||
|
const need = p.resources[r];
|
||||||
|
if (have > need) {
|
||||||
|
let removed = 0;
|
||||||
|
for (let i = updated.length - 1; i >= 0 && removed < have - need; i--) {
|
||||||
|
if (updated[i] === r) { updated.splice(i, 1); removed++; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let k = 0; k < need - have; k++) updated.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (JSON.stringify(updated) !== JSON.stringify(this.handDisplay)) this.handSelectedIdx = null;
|
||||||
|
this.handDisplay = updated;
|
||||||
|
this.renderHand();
|
||||||
|
|
||||||
|
// VP badge
|
||||||
|
const col = PLAYER_COLORS[p.colorIndex];
|
||||||
|
this.playerVpBadgeGfx.clear();
|
||||||
|
this.playerVpBadgeGfx.fillStyle(col.hexDark, 1); this.playerVpBadgeGfx.fillCircle(90, 906, 18);
|
||||||
|
this.playerVpBadgeGfx.lineStyle(2.5, col.hex, 0.9); this.playerVpBadgeGfx.strokeCircle(90, 906, 18);
|
||||||
|
this.playerVpText.setText(String(L.victoryPoints(this.gs, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHand() {
|
||||||
|
this.handCardObjs.forEach(c => c.destroy());
|
||||||
|
this.handCardObjs = [];
|
||||||
|
const N = this.handDisplay.length;
|
||||||
|
if (N === 0) return;
|
||||||
|
const cardW = 60, cardH = 84, cardR = 6, borderW = 3;
|
||||||
|
const step = Math.min(66, Math.max(34, 490 / Math.max(N - 1, 1)));
|
||||||
|
|
||||||
|
this.handDisplay.forEach((resource, idx) => {
|
||||||
|
const x = 220 + idx * step;
|
||||||
|
const frameIdx = RESOURCE_TYPES.indexOf(resource);
|
||||||
|
const c = this.add.container(x, 950).setDepth(D.hud + idx);
|
||||||
|
|
||||||
|
const bg = this.add.graphics();
|
||||||
|
bg.fillStyle(0x111111, 0.85);
|
||||||
|
bg.fillRoundedRect(-cardW / 2, -cardH / 2, cardW, cardH, cardR);
|
||||||
|
const img = this.add.image(0, 0, 'catan-cards', frameIdx).setDisplaySize(54, 78);
|
||||||
|
const border = this.add.graphics();
|
||||||
|
border.lineStyle(borderW, RESOURCE_INFO[resource].swatch, 1);
|
||||||
|
border.strokeRoundedRect(-cardW / 2, -cardH / 2, cardW, cardH, cardR);
|
||||||
|
c.add([bg, img, border]);
|
||||||
|
|
||||||
|
if (idx === this.handSelectedIdx) {
|
||||||
|
const sel = this.add.graphics();
|
||||||
|
sel.lineStyle(3, 0xffd700, 1);
|
||||||
|
sel.strokeRoundedRect(-cardW / 2 - 3, -cardH / 2 - 3, cardW + 6, cardH + 6, cardR + 2);
|
||||||
|
c.add(sel);
|
||||||
|
c.setScale(1.1).setDepth(D.hud + 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.setSize(cardW, cardH).setInteractive();
|
||||||
|
this._setupHandDrag(c, idx);
|
||||||
|
this.handCardObjs.push(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupHandDrag(container, idx) {
|
||||||
|
const THRESHOLD = 8;
|
||||||
|
let dragging = false, origX = 0, pointerStartX = 0;
|
||||||
|
let slotIndicator = null;
|
||||||
|
|
||||||
|
const cancelDrag = () => {
|
||||||
|
if (slotIndicator) { slotIndicator.destroy(); slotIndicator = null; }
|
||||||
|
this.input.off('pointermove', onSceneMove);
|
||||||
|
this.input.off('pointerup', onSceneUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSceneMove = (ptr) => {
|
||||||
|
container.x = origX + (ptr.x - pointerStartX);
|
||||||
|
const N = this.handDisplay.length;
|
||||||
|
const step = Math.min(66, Math.max(34, 490 / Math.max(N - 1, 1)));
|
||||||
|
const newIdx = Math.max(0, Math.min(N - 1, Math.round((ptr.x - 220) / step)));
|
||||||
|
if (!slotIndicator) slotIndicator = this.add.rectangle(0, 950, 6, 84, COLORS.accent, 0.8).setDepth(D.hud + 49);
|
||||||
|
slotIndicator.x = 220 + newIdx * step - step / 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSceneUp = (ptr) => {
|
||||||
|
cancelDrag();
|
||||||
|
dragging = false;
|
||||||
|
const N = this.handDisplay.length;
|
||||||
|
const step = Math.min(66, Math.max(34, 490 / Math.max(N - 1, 1)));
|
||||||
|
const newIdx = Math.max(0, Math.min(N - 1, Math.round((ptr.x - 220) / step)));
|
||||||
|
if (newIdx !== idx) {
|
||||||
|
const [card] = this.handDisplay.splice(idx, 1);
|
||||||
|
this.handDisplay.splice(newIdx, 0, card);
|
||||||
|
}
|
||||||
|
this.handSelectedIdx = null;
|
||||||
|
this.renderHand();
|
||||||
|
};
|
||||||
|
|
||||||
|
container.on('pointerdown', (ptr) => {
|
||||||
|
origX = container.x; pointerStartX = ptr.x; dragging = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.on('pointermove', (ptr) => {
|
||||||
|
if (this.handSelectedIdx !== idx) return;
|
||||||
|
if (!dragging && Math.abs(ptr.x - pointerStartX) >= THRESHOLD) {
|
||||||
|
dragging = true;
|
||||||
|
container.setDepth(D.hud + 50);
|
||||||
|
this.input.on('pointermove', onSceneMove);
|
||||||
|
this.input.on('pointerup', onSceneUp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.on('pointerup', () => {
|
||||||
|
if (!dragging) {
|
||||||
|
// Click: toggle selection
|
||||||
|
this.handSelectedIdx = this.handSelectedIdx === idx ? null : idx;
|
||||||
|
this.renderHand();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDevHand() {
|
updateDevHand() {
|
||||||
|
|
@ -926,7 +1103,9 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
this.gs = L.playKnight(this.gs, seat);
|
this.gs = L.playKnight(this.gs, seat);
|
||||||
this.renderAll(); await this.delay(400);
|
this.renderAll(); await this.delay(400);
|
||||||
const m = AI.chooseRobberMove(this.gs, seat);
|
const m = AI.chooseRobberMove(this.gs, seat);
|
||||||
|
const preRobberHex = this.gs.robberHex;
|
||||||
this.gs = L.moveRobber(this.gs, m.hexId, m.targetSeat);
|
this.gs = L.moveRobber(this.gs, m.hexId, m.targetSeat);
|
||||||
|
await this.animateRobber(preRobberHex, m.hexId);
|
||||||
this.renderAll(); await this.delay(400);
|
this.renderAll(); await this.delay(400);
|
||||||
}
|
}
|
||||||
if (this.gs.phase === 'rollPhase') {
|
if (this.gs.phase === 'rollPhase') {
|
||||||
|
|
@ -947,8 +1126,10 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
const seat = this.gs.currentPlayer;
|
const seat = this.gs.currentPlayer;
|
||||||
await this.delay(450);
|
await this.delay(450);
|
||||||
const m = AI.chooseRobberMove(this.gs, seat);
|
const m = AI.chooseRobberMove(this.gs, seat);
|
||||||
|
const preRobberHex = this.gs.robberHex;
|
||||||
this.gs = L.moveRobber(this.gs, m.hexId, m.targetSeat);
|
this.gs = L.moveRobber(this.gs, m.hexId, m.targetSeat);
|
||||||
if (m.targetSeat != null) this.opponentPortraits[seat]?.playEmotion?.('happy');
|
if (m.targetSeat != null) this.opponentPortraits[seat]?.playEmotion?.('happy');
|
||||||
|
await this.animateRobber(preRobberHex, m.hexId);
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
await this.delay(450);
|
await this.delay(450);
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
|
|
@ -966,7 +1147,9 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
this.gs = this.applyAction(seat, a);
|
this.gs = this.applyAction(seat, a);
|
||||||
if (this.gs.phase === 'moveRobber') {
|
if (this.gs.phase === 'moveRobber') {
|
||||||
const m = AI.chooseRobberMove(this.gs, seat);
|
const m = AI.chooseRobberMove(this.gs, seat);
|
||||||
|
const preRobberHex = this.gs.robberHex;
|
||||||
this.gs = L.moveRobber(this.gs, m.hexId, m.targetSeat);
|
this.gs = L.moveRobber(this.gs, m.hexId, m.targetSeat);
|
||||||
|
await this.animateRobber(preRobberHex, m.hexId);
|
||||||
}
|
}
|
||||||
this.renderAll();
|
this.renderAll();
|
||||||
await this.delay(480);
|
await this.delay(480);
|
||||||
|
|
@ -1035,11 +1218,13 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
for (const hex of this.gs.hexes) {
|
for (const hex of this.gs.hexes) {
|
||||||
if (hex.hasRobber) continue;
|
if (hex.hasRobber) continue;
|
||||||
const { x, y } = this.hexPos(hex.id);
|
const { x, y } = this.hexPos(hex.id);
|
||||||
this.addHighlight(x, y, () => {
|
this.addHighlight(x, y, async () => {
|
||||||
this.clearHighlights();
|
this.clearHighlights();
|
||||||
const targets = L.stealTargets(this.gs, hex.id, 0);
|
const targets = L.stealTargets(this.gs, hex.id, 0);
|
||||||
if (targets.length <= 1) {
|
if (targets.length <= 1) {
|
||||||
|
const preRobberHex = this.gs.robberHex;
|
||||||
this.gs = L.moveRobber(this.gs, hex.id, targets[0] ?? null);
|
this.gs = L.moveRobber(this.gs, hex.id, targets[0] ?? null);
|
||||||
|
await this.animateRobber(preRobberHex, hex.id);
|
||||||
this.advance();
|
this.advance();
|
||||||
} else {
|
} else {
|
||||||
this.pickStealTarget(hex.id, targets);
|
this.pickStealTarget(hex.id, targets);
|
||||||
|
|
@ -1051,9 +1236,11 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
pickStealTarget(hexId, targets) {
|
pickStealTarget(hexId, targets) {
|
||||||
const panel = this.modalPanel(540, 'Steal from which player?');
|
const panel = this.modalPanel(540, 'Steal from which player?');
|
||||||
targets.forEach((seat, i) => {
|
targets.forEach((seat, i) => {
|
||||||
this.modalButton(panel, 1000, 480 + i * 64, `${this.pname(seat)} (${L.handSize(this.gs.players[seat])} cards)`, () => {
|
this.modalButton(panel, 1000, 480 + i * 64, `${this.pname(seat)} (${L.handSize(this.gs.players[seat])} cards)`, async () => {
|
||||||
panel.destroy();
|
panel.destroy();
|
||||||
|
const preRobberHex = this.gs.robberHex;
|
||||||
this.gs = L.moveRobber(this.gs, hexId, seat);
|
this.gs = L.moveRobber(this.gs, hexId, seat);
|
||||||
|
await this.animateRobber(preRobberHex, hexId);
|
||||||
this.advance();
|
this.advance();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1168,7 +1355,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
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, 470, 760, 540, COLORS.panel, 1).setStrokeStyle(3, COLORS.accent).setDepth(D.panel);
|
const box = this.add.rectangle(1000, 470, 920, 540, 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, 300, 'You give', { fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.panel + 1);
|
const hintGive = this.add.text(760, 300, 'You give', { fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.panel + 1);
|
||||||
const hintGet = this.add.text(1240, 300, 'You get', { fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.panel + 1);
|
const hintGet = this.add.text(1240, 300, 'You get', { fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.textHex }).setOrigin(0.5).setDepth(D.panel + 1);
|
||||||
|
|
@ -1208,9 +1395,9 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.flashStatus('Bank trade needs N of one resource for 1 of another (N = your ratio).');
|
this.flashStatus('Bank trade needs N of one resource for 1 of another (N = your ratio).');
|
||||||
}, { width: 220, height: 48 }).setDepth(D.panel + 1);
|
}, { width: 260, height: 48 }).setDepth(D.panel + 1);
|
||||||
|
|
||||||
const offerBtn = new Button(this, 1150, 640, 'Offer to Players', () => {
|
const offerBtn = new Button(this, 1160, 640, 'Offer to Players', () => {
|
||||||
const gCount = RESOURCE_TYPES.reduce((s, r) => s + give[r], 0);
|
const gCount = RESOURCE_TYPES.reduce((s, r) => s + give[r], 0);
|
||||||
const tCount = RESOURCE_TYPES.reduce((s, r) => s + get[r], 0);
|
const tCount = RESOURCE_TYPES.reduce((s, r) => s + get[r], 0);
|
||||||
if (!gCount || !tCount) { this.flashStatus('Set what you give and get.'); return; }
|
if (!gCount || !tCount) { this.flashStatus('Set what you give and get.'); return; }
|
||||||
|
|
@ -1225,7 +1412,7 @@ export default class CatanGame extends Phaser.Scene {
|
||||||
playSound(this, SFX.CARD_PLACE);
|
playSound(this, SFX.CARD_PLACE);
|
||||||
this.flashStatus(`${this.pname(accepted)} accepted the trade.`);
|
this.flashStatus(`${this.pname(accepted)} accepted the trade.`);
|
||||||
this.advance();
|
this.advance();
|
||||||
}, { width: 220, height: 48 }).setDepth(D.panel + 1);
|
}, { width: 300, height: 48 }).setDepth(D.panel + 1);
|
||||||
|
|
||||||
const cancelBtn = new Button(this, 1000, 700, 'Cancel', () => close(), { variant: 'ghost', width: 160, height: 44 }).setDepth(D.panel + 1);
|
const cancelBtn = new Button(this, 1000, 700, 'Cancel', () => close(), { variant: 'ghost', width: 160, height: 44 }).setDepth(D.panel + 1);
|
||||||
objs.push(bankBtn, offerBtn, cancelBtn);
|
objs.push(bankBtn, offerBtn, cancelBtn);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ 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('bg-menu', '/assets/images/background-menu.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-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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue