diff --git a/public/assets/music/track11.mp3 b/public/assets/music/track11.mp3 new file mode 100644 index 0000000..fbcc754 Binary files /dev/null and b/public/assets/music/track11.mp3 differ diff --git a/public/assets/music/track12.mp3 b/public/assets/music/track12.mp3 new file mode 100644 index 0000000..96bf314 Binary files /dev/null and b/public/assets/music/track12.mp3 differ diff --git a/public/assets/music/track13.mp3 b/public/assets/music/track13.mp3 new file mode 100644 index 0000000..74eac9e Binary files /dev/null and b/public/assets/music/track13.mp3 differ diff --git a/public/data/music.json b/public/data/music.json index 13e695e..519193e 100644 --- a/public/data/music.json +++ b/public/data/music.json @@ -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" } ] } \ No newline at end of file diff --git a/public/src/games/risk/RiskGame.js b/public/src/games/risk/RiskGame.js index 1e11b16..4229792 100644 --- a/public/src/games/risk/RiskGame.js +++ b/public/src/games/risk/RiskGame.js @@ -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); diff --git a/public/src/scenes/GameMenuScene.js b/public/src/scenes/GameMenuScene.js index f5f213a..1f10818 100644 --- a/public/src/scenes/GameMenuScene.js +++ b/public/src/scenes/GameMenuScene.js @@ -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) {