feat(risk): enhance UI, add card visuals, battle portraits, and phase announcements
- Add visual card stack in side panel with clickable modal to view/trade cards - Redesign cards modal with chip-style selection and trade/dismiss buttons - Add animated phase announcements (Reinforce/Attack/Fortify) that slide from map to panel - Introduce static portraits for attacker/defender in battle scene with avatar support - Enhance move modal with optional +/-10 buttons for large army ranges - Improve fortify flow with animation and proper busy state handling - Add 3 new music tracks to game soundtrack - Adjust UI font sizes, spacing, and button positioning throughout - Deduplicate phase announcements using _lastAnnouncedPhase tracking
This commit is contained in:
parent
cb506696a6
commit
546e1de240
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -49,6 +49,21 @@
|
|||
"file": "track10.mp3",
|
||||
"artist": "Back to Basics",
|
||||
"title": "M83"
|
||||
},
|
||||
{
|
||||
"file": "track11.mp3",
|
||||
"artist": "Along the Boardwalk",
|
||||
"title": "Billy Joel"
|
||||
},
|
||||
{
|
||||
"file": "track12.mp3",
|
||||
"artist": "New Day",
|
||||
"title": "Paramore"
|
||||
},
|
||||
{
|
||||
"file": "track13.mp3",
|
||||
"artist": "Dance All Night",
|
||||
"title": "Michael Jackson"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -68,6 +68,7 @@ export default class RiskGame extends Phaser.Scene {
|
|||
this.portraits = [];
|
||||
this.debug = false;
|
||||
this.gameOverShown = false;
|
||||
this._lastAnnouncedPhase = null; // tracks which phase was last announced for the human
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -267,7 +268,7 @@ export default class RiskGame extends Phaser.Scene {
|
|||
const t1 = this.add.text(textX, py - 15, name, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '27px', color: isCur ? COLORS.goldHex : col,
|
||||
}).setOrigin(0, 0.5).setDepth(DEPTH.panel + 1); this.dyn.push(t1);
|
||||
const t2 = this.add.text(textX, py + 17, `${terr}⬡ ${army}⚔`, {
|
||||
const t2 = this.add.text(textX, py + 17, `${terr}⬡ ${army}⚔ ${p.cards.length}♠`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0, 0.5).setDepth(DEPTH.panel + 1); this.dyn.push(t2);
|
||||
}
|
||||
|
|
@ -282,23 +283,47 @@ export default class RiskGame extends Phaser.Scene {
|
|||
({ reinforce: 'Reinforce', attack: 'Attack', fortify: 'Fortify' }[s.phase] ?? s.phase);
|
||||
const who = s.current === this.humanSeat ? 'Your' : `${s.players[s.current].name}'s`;
|
||||
const ph = this.add.text(lx, y, `${who} turn — ${phaseName}`, {
|
||||
fontFamily: 'Righteous', fontSize: '18px', color: COLORS.textHex, wordWrap: { width: PANEL_W - 32 },
|
||||
fontFamily: 'Righteous', fontSize: '21px', color: COLORS.textHex, wordWrap: { width: PANEL_W - 32 },
|
||||
}).setOrigin(0, 0).setDepth(DEPTH.panel + 1); this.dyn.push(ph);
|
||||
y += 34;
|
||||
y += 38;
|
||||
|
||||
if (s.phase === 'reinforce' && !isGameOver(s)) {
|
||||
const info = this.add.text(lx, y, `Armies to place: ${s.reinforcements}`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '17px', color: COLORS.goldHex,
|
||||
fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.goldHex,
|
||||
}).setOrigin(0, 0).setDepth(DEPTH.panel + 1); this.dyn.push(info);
|
||||
y += 28;
|
||||
y += 32;
|
||||
}
|
||||
|
||||
// current player's card count
|
||||
const cardN = s.players[s.current].cards.length;
|
||||
const cc = this.add.text(lx, y, `Cards: ${cardN}` + (s.current === this.humanSeat ? '' : ''), {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0, 0).setDepth(DEPTH.panel + 1); this.dyn.push(cc);
|
||||
y += 30;
|
||||
// face-down card stack for current player
|
||||
const cardCount = s.players[s.current].cards.length;
|
||||
const CARD_W = 20, CARD_H = 28, CARD_STEP = 10;
|
||||
const cardsLbl = this.add.text(lx, y + 6, 'Cards:', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0, 0).setDepth(DEPTH.panel + 1); this.dyn.push(cardsLbl);
|
||||
const stackX0 = lx + cardsLbl.width + 10;
|
||||
const stackTop = y + 3;
|
||||
if (cardCount > 0) {
|
||||
const cg = this.add.graphics().setDepth(DEPTH.panel + 2);
|
||||
for (let i = 0; i < cardCount; i++) {
|
||||
const cx2 = stackX0 + i * CARD_STEP;
|
||||
cg.fillStyle(0x080e18, 0.8); cg.fillRoundedRect(cx2 + 1, stackTop + 1, CARD_W, CARD_H, 3);
|
||||
cg.fillStyle(0x1a2840, 1); cg.fillRoundedRect(cx2, stackTop, CARD_W, CARD_H, 3);
|
||||
cg.lineStyle(1, 0x4a7aaa, 0.85); cg.strokeRoundedRect(cx2, stackTop, CARD_W, CARD_H, 3);
|
||||
const m = 4;
|
||||
cg.lineStyle(1, 0x2a4a70, 0.65);
|
||||
cg.lineBetween(cx2 + m, stackTop + m, cx2 + CARD_W - m, stackTop + CARD_H - m);
|
||||
cg.lineBetween(cx2 + CARD_W - m, stackTop + m, cx2 + m, stackTop + CARD_H - m);
|
||||
}
|
||||
this.dyn.push(cg);
|
||||
if (s.current === this.humanSeat) {
|
||||
const tw = CARD_W + (cardCount - 1) * CARD_STEP;
|
||||
const zone = this.add.zone(stackX0 + tw / 2, stackTop + CARD_H / 2, tw + 6, CARD_H + 6)
|
||||
.setInteractive({ useHandCursor: true }).setDepth(DEPTH.panel + 3);
|
||||
zone.on('pointerdown', () => this.openCardsModal());
|
||||
this.dyn.push(zone);
|
||||
}
|
||||
}
|
||||
y += 34;
|
||||
|
||||
this.panelControlsY = y;
|
||||
this.renderControls();
|
||||
|
|
@ -321,7 +346,7 @@ export default class RiskGame extends Phaser.Scene {
|
|||
};
|
||||
const mkHint = (text, y) => {
|
||||
const h = this.add.text(cx, y, text, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex,
|
||||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex,
|
||||
align: 'center', wordWrap: { width: PANEL_W - 36 },
|
||||
}).setOrigin(0.5, 0.5).setDepth(DEPTH.ui); this.dyn.push(h);
|
||||
};
|
||||
|
|
@ -352,10 +377,13 @@ export default class RiskGame extends Phaser.Scene {
|
|||
if (s.phase === 'reinforce') {
|
||||
if (s.owner[id] !== seat) return;
|
||||
if (mustTradeCards(s, seat)) { this.openTradeModal(); return; }
|
||||
playSound(this, SFX.PIECE_CLICK);
|
||||
this.gs = placeArmies(s, id, 1);
|
||||
this.render();
|
||||
if (this.gs.phase !== 'reinforce') this.advance(); // pool emptied → attack
|
||||
const terrId = id;
|
||||
this.openMoveModal(1, s.reinforcements, s.reinforcements, (n) => {
|
||||
playSound(this, SFX.PIECE_CLICK);
|
||||
this.gs = placeArmies(this.gs, terrId, n);
|
||||
this.render();
|
||||
if (this.gs.phase !== 'reinforce') this.advance();
|
||||
}, { title: 'Place armies', showTen: s.reinforcements > 20 });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -377,10 +405,14 @@ export default class RiskGame extends Phaser.Scene {
|
|||
if (id === this.selFrom) { this.selFrom = null; this.render(); return; }
|
||||
if (s.owner[id] === seat && connectedOwned(s, seat, this.selFrom).includes(id)) {
|
||||
const from = this.selFrom, to = id;
|
||||
this.openMoveModal(s.armies[from] - 1, s.armies[from] - 1, 1, (n) => {
|
||||
this.openMoveModal(1, s.armies[from] - 1, s.armies[from] - 1, async (n) => {
|
||||
this.selFrom = null;
|
||||
this.busy = true;
|
||||
this.gs = fortify(this.gs, from, to, n);
|
||||
this.render(); this.advance();
|
||||
this.render();
|
||||
await this.animateConquest(from, to, n, this.humanSeat);
|
||||
this.busy = false;
|
||||
this.advance();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -417,25 +449,42 @@ export default class RiskGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
// ── trade modal ───────────────────────────────────────────────────────────────
|
||||
openTradeModal() {
|
||||
openCardsModal() {
|
||||
const seat = this.humanSeat;
|
||||
const cards = this.gs.players[seat].cards;
|
||||
const s = this.gs;
|
||||
const cards = s.players[seat].cards;
|
||||
const canTrade = s.phase === 'reinforce';
|
||||
const forced = mustTradeCards(s, seat);
|
||||
|
||||
const objs = [];
|
||||
const overlay = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6)
|
||||
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
|
||||
const W = 720, H = 420;
|
||||
|
||||
const overlay = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6)
|
||||
.setInteractive().setDepth(DEPTH.popup); objs.push(overlay);
|
||||
const W = 720, H = 420, cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
|
||||
const panel = this.add.graphics().setDepth(DEPTH.popup + 1); objs.push(panel);
|
||||
panel.fillStyle(COLORS.panel, 1); panel.fillRoundedRect(cx - W / 2, cy - H / 2, W, H, 14);
|
||||
panel.lineStyle(2, COLORS.accent, 1); panel.strokeRoundedRect(cx - W / 2, cy - H / 2, W, H, 14);
|
||||
objs.push(this.add.text(cx, cy - H / 2 + 32, `Trade a Set (next set = ${setValue(this.gs.setsCashed)} armies)`, {
|
||||
fontFamily: 'Righteous', fontSize: '26px', color: COLORS.goldHex,
|
||||
|
||||
const titleText = canTrade
|
||||
? `Your Cards · trade a set for +${setValue(s.setsCashed)} armies`
|
||||
: 'Your Cards';
|
||||
objs.push(this.add.text(cx, cy - H / 2 + 32, titleText, {
|
||||
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.popup + 2));
|
||||
|
||||
if (cards.length === 0) {
|
||||
objs.push(this.add.text(cx, cy, 'No cards yet', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.popup + 2));
|
||||
}
|
||||
|
||||
const selected = new Set();
|
||||
const chipObjs = [];
|
||||
let tradeBtn = null;
|
||||
|
||||
const refreshTradeBtn = () => {
|
||||
if (!tradeBtn) return;
|
||||
const sel = [...selected].map((i) => cards[i]);
|
||||
const valid = sel.length === 3 && hasValidSet(sel);
|
||||
tradeBtn.setAlpha(valid ? 1 : 0.4);
|
||||
|
|
@ -457,7 +506,8 @@ export default class RiskGame extends Phaser.Scene {
|
|||
const on = selected.has(i);
|
||||
g.fillStyle(on ? COLORS.gold : 0x2a2418, 1); g.fillRoundedRect(x - cw / 2, yy - ch / 2, cw, ch, 10);
|
||||
g.lineStyle(2, on ? 0xffffff : COLORS.accent, 0.9); g.strokeRoundedRect(x - cw / 2, yy - ch / 2, cw, ch, 10);
|
||||
const glyph = this.add.text(x, yy - 30, CARD_GLYPH[card.type], { fontSize: '40px' }).setOrigin(0.5).setDepth(DEPTH.popup + 3);
|
||||
const glyph = this.add.text(x, yy - 30, CARD_GLYPH[card.type], { fontSize: '40px' })
|
||||
.setOrigin(0.5).setDepth(DEPTH.popup + 3);
|
||||
const tl = this.add.text(x, yy + 16, CARD_LABEL[card.type], {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: on ? COLORS.textDarkHex : COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.popup + 3);
|
||||
|
|
@ -465,51 +515,61 @@ export default class RiskGame extends Phaser.Scene {
|
|||
fontFamily: '"Julius Sans One"', fontSize: '12px', color: on ? COLORS.textDarkHex : COLORS.mutedHex,
|
||||
wordWrap: { width: cw - 12 }, align: 'center',
|
||||
}).setOrigin(0.5, 0).setDepth(DEPTH.popup + 3);
|
||||
const z = this.add.zone(x, yy, cw, ch).setInteractive({ useHandCursor: true }).setDepth(DEPTH.popup + 4);
|
||||
z.on('pointerdown', () => {
|
||||
if (selected.has(i)) selected.delete(i);
|
||||
else { if (selected.size >= 3) selected.delete([...selected][0]); selected.add(i); }
|
||||
drawChips(); refreshTradeBtn();
|
||||
});
|
||||
chipObjs.push(g, glyph, tl, tn, z);
|
||||
chipObjs.push(g, glyph, tl, tn);
|
||||
if (canTrade) {
|
||||
const z = this.add.zone(x, yy, cw, ch).setInteractive({ useHandCursor: true }).setDepth(DEPTH.popup + 4);
|
||||
z.on('pointerdown', () => {
|
||||
if (selected.has(i)) selected.delete(i);
|
||||
else { if (selected.size >= 3) selected.delete([...selected][0]); selected.add(i); }
|
||||
drawChips(); refreshTradeBtn();
|
||||
});
|
||||
chipObjs.push(z);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => { objs.forEach((o) => o.destroy()); chipObjs.forEach((o) => o.destroy()); this.render(); };
|
||||
|
||||
tradeBtn = new Button(this, cx - 130, cy + H / 2 - 40, 'Trade', () => {
|
||||
if (tradeBtn.disabledForTrade) return;
|
||||
const ids = [...selected].map((i) => cards[i].id);
|
||||
this.gs = tradeCards(this.gs, ids);
|
||||
playSound(this, SFX.COINS);
|
||||
close();
|
||||
if (!mustTradeCards(this.gs, seat)) { /* allow closing */ }
|
||||
else { this.openTradeModal(); return; } // still forced → reopen
|
||||
this.render();
|
||||
}, { width: 220, height: 48, fontSize: 22 }).setDepth(DEPTH.popup + 3);
|
||||
objs.push(tradeBtn);
|
||||
if (canTrade) {
|
||||
const tradeBtnX = forced ? cx : cx - 130;
|
||||
tradeBtn = new Button(this, tradeBtnX, cy + H / 2 - 40, 'Trade Set', () => {
|
||||
if (tradeBtn.disabledForTrade) return;
|
||||
const ids = [...selected].map((i) => cards[i].id);
|
||||
this.gs = tradeCards(this.gs, ids);
|
||||
playSound(this, SFX.COINS);
|
||||
close();
|
||||
if (mustTradeCards(this.gs, seat)) { this.openCardsModal(); return; }
|
||||
this.render();
|
||||
}, { width: 220, height: 48, fontSize: 22 }).setDepth(DEPTH.popup + 3);
|
||||
objs.push(tradeBtn);
|
||||
}
|
||||
|
||||
if (!mustTradeCards(this.gs, seat)) {
|
||||
const closeBtn = new Button(this, cx + 130, cy + H / 2 - 40, 'Close', close,
|
||||
if (!forced) {
|
||||
const dismissX = canTrade ? cx + 130 : cx;
|
||||
const dismissBtn = new Button(this, dismissX, cy + H / 2 - 40, 'Dismiss', close,
|
||||
{ width: 220, height: 48, fontSize: 22, variant: 'ghost' }).setDepth(DEPTH.popup + 3);
|
||||
objs.push(closeBtn);
|
||||
objs.push(dismissBtn);
|
||||
}
|
||||
|
||||
drawChips(); refreshTradeBtn();
|
||||
}
|
||||
|
||||
// ── move-amount modal (advance after conquest / fortify) ────────────────────────
|
||||
openMoveModal(min, max, initial, onConfirm) {
|
||||
openTradeModal() { this.openCardsModal(); }
|
||||
|
||||
// ── move-amount modal (place armies / advance after conquest / fortify) ───────
|
||||
openMoveModal(min, max, initial, onConfirm, opts = {}) {
|
||||
if (max <= min) { onConfirm(min); return; }
|
||||
let val = Math.max(min, Math.min(initial, max));
|
||||
const objs = [];
|
||||
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
|
||||
const W = 640, H = 240;
|
||||
const showTen = opts.showTen ?? false;
|
||||
const title = opts.title ?? 'Move armies';
|
||||
const W = showTen ? 860 : 640, H = 240;
|
||||
const overlay = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55).setInteractive().setDepth(DEPTH.popup); objs.push(overlay);
|
||||
const panel = this.add.graphics().setDepth(DEPTH.popup + 1); objs.push(panel);
|
||||
panel.fillStyle(COLORS.panel, 1); panel.fillRoundedRect(cx - W / 2, cy - H / 2, W, H, 14);
|
||||
panel.lineStyle(2, COLORS.accent, 1); panel.strokeRoundedRect(cx - W / 2, cy - H / 2, W, H, 14);
|
||||
objs.push(this.add.text(cx, cy - H / 2 + 30, 'Move armies', {
|
||||
objs.push(this.add.text(cx, cy - H / 2 + 30, title, {
|
||||
fontFamily: 'Righteous', fontSize: '24px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.popup + 2));
|
||||
const valTxt = this.add.text(cx, cy - 10, String(val), {
|
||||
|
|
@ -519,10 +579,24 @@ export default class RiskGame extends Phaser.Scene {
|
|||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.popup + 2); objs.push(rng);
|
||||
const upd = () => valTxt.setText(String(val));
|
||||
const btnMin = new Button(this, cx - 230, cy - 4, 'Min', () => { val = min; upd(); }, { width: 80, height: 64, fontSize: 20, variant: 'ghost' }).setDepth(DEPTH.popup + 2); objs.push(btnMin);
|
||||
const minus = new Button(this, cx - 130, cy - 4, '−', () => { val = Math.max(min, val - 1); upd(); }, { width: 64, height: 64, fontSize: 34 }).setDepth(DEPTH.popup + 2); objs.push(minus);
|
||||
const plus = new Button(this, cx + 130, cy - 4, '+', () => { val = Math.min(max, val + 1); upd(); }, { width: 64, height: 64, fontSize: 34 }).setDepth(DEPTH.popup + 2); objs.push(plus);
|
||||
const btnMax = new Button(this, cx + 230, cy - 4, 'Max', () => { val = max; upd(); }, { width: 80, height: 64, fontSize: 20, variant: 'ghost' }).setDepth(DEPTH.popup + 2); objs.push(btnMax);
|
||||
const mk = (label, x, fn, w, variant) => {
|
||||
const b = new Button(this, cx + x, cy - 4, label, fn,
|
||||
{ width: w, height: 64, fontSize: w >= 80 ? 20 : 34, variant }).setDepth(DEPTH.popup + 2);
|
||||
objs.push(b); return b;
|
||||
};
|
||||
if (showTen) {
|
||||
mk('Min', -350, () => { val = min; upd(); }, 80, 'ghost');
|
||||
mk('−10', -240, () => { val = Math.max(min, val - 10); upd(); }, 80);
|
||||
mk('−', -140, () => { val = Math.max(min, val - 1); upd(); }, 64);
|
||||
mk('+', +140, () => { val = Math.min(max, val + 1); upd(); }, 64);
|
||||
mk('+10', +240, () => { val = Math.min(max, val + 10); upd(); }, 80);
|
||||
mk('Max', +350, () => { val = max; upd(); }, 80, 'ghost');
|
||||
} else {
|
||||
mk('Min', -230, () => { val = min; upd(); }, 80, 'ghost');
|
||||
mk('−', -130, () => { val = Math.max(min, val - 1); upd(); }, 64);
|
||||
mk('+', +130, () => { val = Math.min(max, val + 1); upd(); }, 64);
|
||||
mk('Max', +230, () => { val = max; upd(); }, 80, 'ghost');
|
||||
}
|
||||
const ok = new Button(this, cx, cy + H / 2 - 34, 'Confirm', () => { objs.forEach((o) => o.destroy()); onConfirm(val); }, { width: 260, height: 46, fontSize: 22 }).setDepth(DEPTH.popup + 2); objs.push(ok);
|
||||
}
|
||||
|
||||
|
|
@ -630,7 +704,7 @@ export default class RiskGame extends Phaser.Scene {
|
|||
const attackerName = this.gs.players[attackerSeat].name;
|
||||
const defenderName = this.gs.players[defenderSeat].name;
|
||||
|
||||
const W = 560, H = 320;
|
||||
const W = 700, H = 320;
|
||||
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
|
||||
const startPos = this.boardToScreen(TERRITORIES[b.to].x, TERRITORIES[b.to].y);
|
||||
|
||||
|
|
@ -706,6 +780,16 @@ export default class RiskGame extends Phaser.Scene {
|
|||
});
|
||||
});
|
||||
|
||||
// ── Portraits (world-space, appear after zoom so masks align) ─────────────
|
||||
const PORTRAIT_R = 26;
|
||||
const headWorldY = cy + (-H / 2 + 36);
|
||||
const portraitObjs = [
|
||||
...this.makeStaticPortrait(cx - W / 2 + PORTRAIT_R + 10, headWorldY, PORTRAIT_R, attackerSeat, DEPTH.popup + 1),
|
||||
...this.makeStaticPortrait(cx + W / 2 - PORTRAIT_R - 10, headWorldY, PORTRAIT_R, defenderSeat, DEPTH.popup + 1),
|
||||
];
|
||||
portraitObjs.forEach((o) => o.setAlpha(0));
|
||||
this.tweens.add({ targets: portraitObjs, alpha: 1, duration: 300, ease: 'Linear' });
|
||||
|
||||
// ── Dice rolling ─────────────────────────────────────────────────────────
|
||||
playSound(this, SFX.DICE_ROLL);
|
||||
for (let tick = 0; tick < 10; tick++) {
|
||||
|
|
@ -738,6 +822,55 @@ export default class RiskGame extends Phaser.Scene {
|
|||
|
||||
container.destroy(true);
|
||||
diceArrows.forEach((g) => g.destroy());
|
||||
portraitObjs.forEach((o) => o.destroy());
|
||||
}
|
||||
|
||||
makeStaticPortrait(worldX, worldY, radius, seat, depth) {
|
||||
const objs = [];
|
||||
const color = this.colorOf(seat);
|
||||
const size = radius * 2;
|
||||
|
||||
// Backing circle
|
||||
const backing = this.add.graphics().setDepth(depth);
|
||||
backing.fillStyle(0x1a1a2e, 1); backing.fillCircle(worldX, worldY, radius + 3);
|
||||
backing.fillStyle(COLORS.panel, 1); backing.fillCircle(worldX, worldY, radius + 1);
|
||||
objs.push(backing);
|
||||
|
||||
// Sprite or avatar (circular geometry mask)
|
||||
let textureKey = null;
|
||||
if (seat === this.humanSeat) {
|
||||
textureKey = Object.keys(this.textures.list).find((k) => k.startsWith('player-avatar-')) ?? null;
|
||||
} else {
|
||||
const opp = this.opponents[seat - 1];
|
||||
if (this.textures.exists('opponents') && opp != null) {
|
||||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff); maskG.fillCircle(worldX, worldY, radius);
|
||||
const img = this.add.image(worldX, worldY, 'opponents', opp.spriteIndex ?? 0)
|
||||
.setDisplaySize(size, size).setMask(maskG.createGeometryMask()).setDepth(depth + 1);
|
||||
objs.push(img);
|
||||
}
|
||||
}
|
||||
|
||||
if (textureKey) {
|
||||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff); maskG.fillCircle(worldX, worldY, radius);
|
||||
const img = this.add.image(worldX, worldY, textureKey)
|
||||
.setDisplaySize(size, size).setMask(maskG.createGeometryMask()).setDepth(depth + 1);
|
||||
objs.push(img);
|
||||
} else if (seat === this.humanSeat) {
|
||||
// No avatar cached yet — letter placeholder
|
||||
const initial = this.gs.players[seat].name.charAt(0).toUpperCase();
|
||||
objs.push(this.add.text(worldX, worldY, initial, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: `${Math.round(radius * 0.9)}px`, color: COLORS.accentHex,
|
||||
}).setOrigin(0.5).setDepth(depth + 1));
|
||||
}
|
||||
|
||||
// Colored ring
|
||||
const ring = this.add.graphics().setDepth(depth + 2);
|
||||
ring.lineStyle(5, color, 1); ring.strokeCircle(worldX, worldY, radius + 2);
|
||||
objs.push(ring);
|
||||
|
||||
return objs;
|
||||
}
|
||||
|
||||
async animateDiceArrow(from, to, color) {
|
||||
|
|
@ -893,7 +1026,15 @@ export default class RiskGame extends Phaser.Scene {
|
|||
async advance() {
|
||||
if (this.busy) return;
|
||||
if (isGameOver(this.gs)) { this.showGameOver(); return; }
|
||||
if (this.gs.current === this.humanSeat) { this.render(); return; } // wait for input
|
||||
if (this.gs.current === this.humanSeat) {
|
||||
this.render();
|
||||
if (this.gs.phase !== this._lastAnnouncedPhase) {
|
||||
this._lastAnnouncedPhase = this.gs.phase;
|
||||
this.announcePhase(this.gs.phase);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._lastAnnouncedPhase = null; // reset so next human turn re-announces each phase
|
||||
this.busy = true;
|
||||
try {
|
||||
await this.runAiTurn(this.gs.current);
|
||||
|
|
@ -905,6 +1046,44 @@ export default class RiskGame extends Phaser.Scene {
|
|||
this.time.delayedCall(60, () => this.advance());
|
||||
}
|
||||
|
||||
announcePhase(phase) {
|
||||
const label = ({ reinforce: 'Reinforce', attack: 'Attack', fortify: 'Fortify' }[phase] ?? phase) + ' Phase';
|
||||
const mapCx = MAP_X + DISP_W / 2;
|
||||
const mapCy = MAP_Y + DISP_H / 2;
|
||||
const targetX = PANEL_X + PANEL_W / 2;
|
||||
const targetY = this.panelRowY + 25;
|
||||
|
||||
const txt = this.add.text(mapCx, mapCy, label, {
|
||||
fontFamily: 'Righteous',
|
||||
fontSize: '72px',
|
||||
color: '#ffe619',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 5,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.panel - 2);
|
||||
|
||||
this.time.delayedCall(1000, () => {
|
||||
const prog = { t: 0 };
|
||||
this.tweens.add({
|
||||
targets: prog,
|
||||
t: 1,
|
||||
duration: 1000,
|
||||
ease: 'Sine.easeInOut',
|
||||
onUpdate: () => {
|
||||
const t = prog.t;
|
||||
const lerp = (a, b) => a + (b - a) * t;
|
||||
txt.setX(lerp(mapCx, targetX));
|
||||
txt.setY(lerp(mapCy, targetY));
|
||||
txt.setScale(lerp(1, 0.25));
|
||||
const r = Math.round(lerp(0xff, 0xf2));
|
||||
const g = Math.round(lerp(0xe6, 0xea));
|
||||
const b = Math.round(lerp(0x19, 0xd8));
|
||||
txt.setColor(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`);
|
||||
},
|
||||
onComplete: () => { if (txt.active) txt.destroy(); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async runAiTurn(seat) {
|
||||
const skill = this.gs.players[seat].skill;
|
||||
this.setPortraitThinking(seat, true);
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export default class GameMenuScene extends Phaser.Scene {
|
|||
const startKey = allActiveCats.find(c => c.key === _lastCategory) ? _lastCategory : allActiveCats[0].key;
|
||||
this.showCategory(startKey);
|
||||
|
||||
new Button(this, cx, GAME_HEIGHT - 100, 'Back', () => this.scene.start('Landing'), { variant: 'ghost' });
|
||||
new Button(this, cx, GAME_HEIGHT - 60, 'Back', () => this.scene.start('Landing'), { variant: 'ghost' });
|
||||
}
|
||||
|
||||
showCategory(key) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue