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:
Brian Fertig 2026-06-18 21:39:33 -06:00
parent cb506696a6
commit 546e1de240
6 changed files with 251 additions and 57 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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"
}
]
}

View File

@ -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);

View File

@ -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) {