407 lines
20 KiB
JavaScript
407 lines
20 KiB
JavaScript
// BalatroViews.js
|
|
// Non-play view renderers for BalatroGame (title, deck select, blind select,
|
|
// shop, pack opening, cash-out overlay, game over). Each receives the scene
|
|
// and uses its helpers (text/panel/button/draw*Card) so all styling stays in
|
|
// one place. Pure rendering + Logic calls; no game rules here.
|
|
|
|
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
|
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
|
import {
|
|
DECKS, DECK_BY_ID, BLINDS, fmtChips, anteBase, VOUCHER_BY_ID, BOOSTER_BY_ID,
|
|
HAND_BY_ID, EDITIONS,
|
|
} from './BalatroData.js';
|
|
import { JOKER_BY_ID } from './BalatroJokers.js';
|
|
import {
|
|
passives, selectBlind, skipBlind, blindChipsFor, bossFor, collectCashOut,
|
|
continueEndless, leaveShop, rerollShop, buyShopCard, buyVoucher, buyPack,
|
|
pickFromPack, skipPack,
|
|
} from './BalatroLogic.js';
|
|
import * as store from '../../services/localStore.js';
|
|
import { C } from './BalatroGame.js';
|
|
|
|
const CX = GAME_WIDTH / 2;
|
|
|
|
// ── Title ────────────────────────────────────────────────────────────────────
|
|
export function renderTitle(scene) {
|
|
const letters = 'BALATRO';
|
|
const colors = ['#ff5e5e', '#5aa9f4', '#ffd25e', '#7ce07c', '#c88aff', '#ff9a5e', '#7ce0e0'];
|
|
const size = 128;
|
|
const total = letters.length * (size * 0.72);
|
|
letters.split('').forEach((ch, i) => {
|
|
const t = scene.text(CX - total / 2 + i * size * 0.72 + size * 0.36, 260, ch, size, colors[i % colors.length], { ox: 0.5, oy: 0.5, bold: true });
|
|
t.setStroke('#141019', 12);
|
|
t.setAngle(Math.sin(i * 1.7) * 6);
|
|
scene.tweens.add({
|
|
targets: t, y: 260 + Math.sin(i) * 6, angle: t.angle + 4,
|
|
duration: 1600 + i * 130, yoyo: true, repeat: -1, ease: 'Sine.easeInOut',
|
|
});
|
|
});
|
|
scene.text(CX, 356, 'a poker roguelike', 26, C.muted, { ox: 0.5 });
|
|
|
|
let y = 500;
|
|
if (scene._savedRun) {
|
|
const r = scene._savedRun;
|
|
scene.button(CX, y, `Continue (Ante ${r.ante} · ${DECK_BY_ID[r.deckId].name})`, () => scene.continueRun(), { width: 520, height: 66, fontSize: 24 });
|
|
y += 90;
|
|
scene.button(CX, y, 'New Run', () => confirmNewRun(scene), { width: 320, height: 58, fontSize: 22, variant: 'ghost' });
|
|
} else {
|
|
scene.button(CX, y, 'New Run', () => scene.setView('deckselect'), { width: 360, height: 66, fontSize: 26 });
|
|
}
|
|
y += 90;
|
|
scene.button(CX, y, 'Main Menu', () => scene.scene.start('GameMenu'), { width: 280, height: 52, fontSize: 20, variant: 'ghost' });
|
|
|
|
const stats = store.read('balatro:stats');
|
|
if (stats && stats.runs) {
|
|
scene.text(CX, y + 90, `Runs ${stats.runs} · Wins ${stats.wins} · Best Ante ${stats.bestAnte} · Best Hand ${fmtChips(stats.bestHandScore)}`, 20, C.muted, { ox: 0.5 });
|
|
}
|
|
}
|
|
|
|
function confirmNewRun(scene) {
|
|
const ov = scene.add.container(0, 0).setDepth(95);
|
|
scene.fxLayer.add(ov);
|
|
const shade = scene.add.rectangle(CX, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6).setInteractive();
|
|
const g = scene.add.graphics();
|
|
g.fillStyle(C.panel, 0.97); g.fillRoundedRect(CX - 280, 420, 560, 240, 16);
|
|
g.lineStyle(2, C.panelEdge, 1); g.strokeRoundedRect(CX - 280, 420, 560, 240, 16);
|
|
ov.add([shade, g]);
|
|
ov.add(scene.add.text(CX, 470, 'Abandon the saved run?', { fontFamily: '"Julius Sans One"', fontSize: '28px', color: C.ink }).setOrigin(0.5));
|
|
const yes = scene.button(CX - 110, 590, 'Yes', () => { scene._savedRun = null; store.write('balatro:run', null); ov.destroy(); scene.setView('deckselect'); }, { width: 160, height: 54, fontSize: 22 });
|
|
const no = scene.button(CX + 110, 590, 'No', () => ov.destroy(), { width: 160, height: 54, fontSize: 22, variant: 'ghost' });
|
|
ov.add([yes, no]);
|
|
shade.on('pointerdown', () => ov.destroy());
|
|
}
|
|
|
|
// ── Deck select ──────────────────────────────────────────────────────────────
|
|
export function renderDeckSelect(scene) {
|
|
scene.text(CX, 90, 'Choose a Deck', 52, C.ink, { ox: 0.5, bold: true });
|
|
const w = 300, h = 330;
|
|
DECKS.forEach((deck, i) => {
|
|
const col = i % 4, row = Math.floor(i / 4);
|
|
const x = CX - 1.5 * (w + 60) + col * (w + 60);
|
|
const y = 330 + row * (h + 80);
|
|
const cont = scene.add.container(x, y);
|
|
const g = scene.add.graphics();
|
|
g.fillStyle(C.panel, 0.92); g.fillRoundedRect(-w / 2, -h / 2, w, h, 14);
|
|
g.lineStyle(2, deck.color, 1); g.strokeRoundedRect(-w / 2, -h / 2, w, h, 14);
|
|
cont.add(g);
|
|
// Deck back: art frame if present, else colored card back.
|
|
const af = scene.artFrame('deckBackSheet', 'deckBacks', deck.id);
|
|
if (af) cont.add(scene.add.image(0, -60, af.key, af.frame).setDisplaySize(120, 164));
|
|
else {
|
|
const bg = scene.add.graphics();
|
|
bg.fillStyle(deck.color, 0.95); bg.fillRoundedRect(-60, -142, 120, 164, 8);
|
|
bg.lineStyle(2, 0xffffff, 0.5); bg.strokeRoundedRect(-60, -142, 120, 164, 8);
|
|
bg.lineStyle(1, 0xffffff, 0.25); bg.strokeRoundedRect(-48, -130, 96, 140, 6);
|
|
cont.add(bg);
|
|
}
|
|
cont.add(scene.add.text(0, 52, deck.name, { fontFamily: '"Julius Sans One"', fontSize: '26px', color: C.ink, fontStyle: 'bold' }).setOrigin(0.5));
|
|
cont.add(scene.add.text(0, 108, deck.desc, { fontFamily: '"Julius Sans One"', fontSize: '17px', color: C.muted, align: 'center', wordWrap: { width: w - 30 } }).setOrigin(0.5));
|
|
cont.setSize(w, h);
|
|
scene.add2(cont);
|
|
scene.addHoverTilt(cont);
|
|
cont.on('pointerdown', () => { playSound(scene, SFX.CARD_SHUFFLE); scene.startRun(deck.id); });
|
|
});
|
|
scene.button(CX, GAME_HEIGHT - 50, 'Back', () => scene.setView('title'), { width: 220, height: 50, fontSize: 20, variant: 'ghost' });
|
|
}
|
|
|
|
// ── Blind select ─────────────────────────────────────────────────────────────
|
|
export function renderBlindSelect(scene) {
|
|
const run = scene.run;
|
|
scene.text(CX, 70, `Ante ${run.endless ? run.ante : `${run.ante} of 8`}`, 46, C.ink, { ox: 0.5, bold: true });
|
|
scene.text(CX, 124, `$${run.gold}`, 30, C.money, { ox: 0.5 });
|
|
|
|
const stages = ['small', 'big', 'boss'];
|
|
const curIdx = stages.indexOf(run.blind);
|
|
const w = 400, h = 560;
|
|
stages.forEach((stage, i) => {
|
|
const x = CX - (w + 60) + i * (w + 60);
|
|
const y = 480;
|
|
const isCur = i === curIdx;
|
|
const done = i < curIdx;
|
|
const boss = stage === 'boss' ? bossFor(run) : null;
|
|
const color = stage === 'boss' ? 0xc22f3a : stage === 'big' ? 0x8556d6 : 0x2f6fce;
|
|
scene.panel(x - w / 2, y - h / 2, w, h, { fill: isCur ? color : C.panel, alpha: isCur ? 0.4 : 0.75, strokeColor: color });
|
|
scene.text(x, y - h / 2 + 46, boss ? boss.name : BLINDS[stage].name, 32, C.ink, { ox: 0.5, bold: true });
|
|
// Requirement uses the boss multiplier for the boss column.
|
|
const req = stage === 'boss'
|
|
? Math.floor(anteBase(run.ante) * ((boss && boss.reqMult) || 2))
|
|
: Math.floor(anteBase(run.ante) * BLINDS[stage].mult);
|
|
scene.text(x, y - h / 2 + 110, 'Score at least', 18, C.muted, { ox: 0.5 });
|
|
scene.text(x, y - h / 2 + 142, fmtChips(req), 36, '#ff8a6a', { ox: 0.5, bold: true });
|
|
scene.text(x, y - h / 2 + 200, `Reward: $${BLINDS[stage].reward}`, 22, C.money, { ox: 0.5 });
|
|
if (boss) scene.text(x, y - h / 2 + 260, boss.desc, 20, '#ffb9a8', { ox: 0.5, align: 'center', wrap: w - 60 });
|
|
|
|
if (done) {
|
|
scene.text(x, y + h / 2 - 120, '✔ cleared', 26, '#7ce07c', { ox: 0.5 });
|
|
} else if (isCur) {
|
|
scene.button(x, y + h / 2 - 120, 'Select', () => {
|
|
const r = selectBlind(run);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
playSound(scene, SFX.CARD_SHUFFLE);
|
|
scene.save();
|
|
scene.animating = true;
|
|
scene._pendingDeal = new Set(run.hand);
|
|
scene.setView('play');
|
|
}, { width: 240, height: 64, fontSize: 26, bg: color });
|
|
if (BLINDS[stage].skippable) {
|
|
scene.button(x, y + h / 2 - 48, 'Skip Blind', () => {
|
|
const r = skipBlind(run);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
scene.save();
|
|
scene.renderView();
|
|
}, { width: 200, height: 46, fontSize: 18, variant: 'ghost' });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Cash out overlay (rendered over the frozen play view) ────────────────────
|
|
export function renderCashout(scene) {
|
|
const run = scene.run;
|
|
const cash = run.cashout || { lines: [], total: 0 };
|
|
const w = 620, h = 200 + cash.lines.length * 44;
|
|
const x = CX - w / 2, y = 300;
|
|
scene.panel(x, y, w, h, { alpha: 0.97, layer: scene.fxLayer });
|
|
scene.text(CX, y + 30, 'Round Won!', 40, '#7ce07c', { ox: 0.5, bold: true, layer: scene.fxLayer });
|
|
cash.lines.forEach((line, i) => {
|
|
scene.text(x + 50, y + 100 + i * 44, line.label, 24, C.ink, { layer: scene.fxLayer });
|
|
scene.text(x + w - 50, y + 100 + i * 44, `$${line.amount}`, 24, C.money, { ox: 1, layer: scene.fxLayer });
|
|
});
|
|
scene.button(CX, y + h - 46, `Cash Out $${cash.total}`, () => {
|
|
const r = collectCashOut(run);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
playSound(scene, SFX.COINS);
|
|
if (r.destroyed && r.destroyed.length) scene.toast(`${r.destroyed.map((id) => JOKER_BY_ID[id].name).join(', ')} destroyed`);
|
|
scene.save();
|
|
if (run.phase === 'over') { scene.onRunOver(); scene.setView('gameover'); }
|
|
else scene.setView('shop');
|
|
}, { width: 340, height: 64, fontSize: 26, bg: 0x2e9e6a, layer: scene.fxLayer });
|
|
}
|
|
|
|
// ── Shop ─────────────────────────────────────────────────────────────────────
|
|
export function renderShop(scene) {
|
|
const run = scene.run;
|
|
const shop = run.shop;
|
|
if (!shop) { scene.setView('blindselect'); return; }
|
|
const p = passives(run);
|
|
|
|
// Owned jokers/consumables stay on screen (sellable while shopping).
|
|
scene.renderJokerRow(false);
|
|
scene.renderConsumableRow(false);
|
|
scene.renderDeckThumb();
|
|
|
|
scene.text(120, 40, 'SHOP', 54, '#7ce0c0', { bold: true });
|
|
scene._goldText = scene.text(120, 110, `$${run.gold}`, 40, C.money, { bold: true });
|
|
const nextBlind = run.blind === 'small' ? 'Big Blind' : run.blind === 'big' ? (bossFor(run) ? bossFor(run).name : 'Boss Blind') : `Ante ${run.ante + 1}`;
|
|
scene.text(120, 168, `Next: ${nextBlind}`, 20, C.muted);
|
|
|
|
const refresh = () => { scene.save(); scene.renderView(); };
|
|
|
|
// Card slots
|
|
const cy = 430;
|
|
scene.text(470, cy - 150, 'For sale', 20, C.muted);
|
|
shop.cards.forEach((item, i) => {
|
|
const x = 560 + i * 190;
|
|
let cont, info;
|
|
if (item.slot === 'joker') {
|
|
cont = scene.drawJokerCard(x, cy, item.id, { edition: item.edition, w: 160, h: 214 });
|
|
info = () => scene.jokerInfo(item.id, null, item.edition);
|
|
} else if (item.slot === 'playing') {
|
|
cont = scene.drawPlayingCard(x, cy, { ...item.card, uid: -1, faceDown: false, debuffed: false }, { w: 160, h: 214 });
|
|
info = () => scene.playingCardInfo(item.card);
|
|
} else {
|
|
cont = scene.drawConsumableCard(x, cy, item.slot, item.id, { w: 160, h: 214 });
|
|
info = () => scene.consumableInfo(item.slot, item.id);
|
|
}
|
|
scene.add2(cont);
|
|
scene.addHoverTilt(cont, { info });
|
|
scene.text(x, cy + 132, `$${item.cost}`, 26, C.money, { ox: 0.5, bold: true });
|
|
cont.on('pointerdown', () => {
|
|
const r = buyShopCard(run, i);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
playSound(scene, SFX.PURCHASE);
|
|
refresh();
|
|
});
|
|
});
|
|
|
|
// Reroll
|
|
scene.button(560 + p.shopCards * 190 + 40, cy, shop.freeRerolls > 0 ? 'Reroll (free)' : `Reroll $${shop.rerollCost}`, () => {
|
|
const r = rerollShop(run);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
playSound(scene, SFX.CARD_SHUFFLE);
|
|
refresh();
|
|
}, { width: 190, height: 60, fontSize: 20 });
|
|
|
|
// Packs
|
|
const py2 = 760;
|
|
scene.text(470, py2 - 140, 'Booster packs', 20, C.muted);
|
|
shop.packs.forEach((slot, i) => {
|
|
const x = 580 + i * 240;
|
|
const def = BOOSTER_BY_ID[slot.id];
|
|
const cont = scene.add.container(x, py2);
|
|
const af = scene.artFrame('packSheet', 'packs', slot.id);
|
|
if (af) cont.add(scene.add.image(0, 0, af.key, af.frame).setDisplaySize(180, 240));
|
|
else {
|
|
const tint = { playing: 0x4f9ad4, tarot: C.tarot, planet: C.planet, joker: 0xd4884f, spectral: C.spectral }[def.what];
|
|
const g = scene.add.graphics();
|
|
g.fillStyle(tint, 0.85); g.fillRoundedRect(-90, -120, 180, 240, 12);
|
|
g.lineStyle(3, 0xffffff, 0.5); g.strokeRoundedRect(-90, -120, 180, 240, 12);
|
|
g.fillStyle(0x141019, 0.35); g.fillRoundedRect(-90, -30, 180, 60, 0);
|
|
cont.add(g);
|
|
cont.add(scene.add.text(0, 0, def.name.replace(' Pack', '\nPack'), { fontFamily: '"Julius Sans One"', fontSize: '22px', color: '#ffffff', align: 'center', fontStyle: 'bold' }).setOrigin(0.5));
|
|
}
|
|
cont.setSize(180, 240);
|
|
scene.add2(cont);
|
|
const kindName = { playing: 'playing', tarot: 'Tarot', planet: 'Planet', joker: 'Joker', spectral: 'Spectral' }[def.what];
|
|
scene.addHoverTilt(cont, {
|
|
info: () => ({
|
|
title: def.name,
|
|
tag: 'Booster Pack',
|
|
lines: [{ text: `Choose ${def.picks} of ${def.options} ${kindName} cards`, color: C.ink }],
|
|
}),
|
|
});
|
|
scene.text(x, py2 + 148, `$${slot.cost}`, 26, C.money, { ox: 0.5, bold: true });
|
|
cont.on('pointerdown', () => {
|
|
const r = buyPack(run, i);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
playSound(scene, SFX.PURCHASE);
|
|
scene.save();
|
|
scene.setView('packopen');
|
|
});
|
|
});
|
|
|
|
// Voucher
|
|
const vx = 1560, vy = 560;
|
|
if (shop.voucher && !shop.voucher.sold) {
|
|
const v = VOUCHER_BY_ID[shop.voucher.id];
|
|
scene.text(vx, vy - 180, 'Voucher', 20, C.muted, { ox: 0.5 });
|
|
const cont = scene.add.container(vx, vy);
|
|
const af = scene.artFrame('voucherSheet', 'vouchers', v.id);
|
|
if (af) cont.add(scene.add.image(0, 0, af.key, af.frame).setDisplaySize(190, 250));
|
|
else {
|
|
const g = scene.add.graphics();
|
|
g.fillStyle(0x143a2a, 0.95); g.fillRoundedRect(-95, -125, 190, 250, 10);
|
|
g.lineStyle(3, C.voucher, 1); g.strokeRoundedRect(-95, -125, 190, 250, 10);
|
|
g.lineStyle(1, C.voucher, 0.5); g.strokeRoundedRect(-85, -115, 170, 230, 8);
|
|
cont.add(g);
|
|
cont.add(scene.add.text(0, -80, v.name, { fontFamily: '"Julius Sans One"', fontSize: '22px', color: '#7ce0c0', align: 'center', fontStyle: 'bold', wordWrap: { width: 170 } }).setOrigin(0.5));
|
|
cont.add(scene.add.text(0, 20, v.desc, { fontFamily: '"Julius Sans One"', fontSize: '16px', color: C.ink, align: 'center', wordWrap: { width: 165 } }).setOrigin(0.5));
|
|
}
|
|
cont.setSize(190, 250);
|
|
scene.add2(cont);
|
|
scene.addHoverTilt(cont, {
|
|
info: () => ({
|
|
title: v.name,
|
|
tag: 'Voucher',
|
|
tagColor: '#7ce0c0',
|
|
stroke: C.voucher,
|
|
lines: [{ text: v.desc, color: C.ink }],
|
|
}),
|
|
});
|
|
scene.text(vx, vy + 152, `$${shop.voucher.cost}`, 26, C.money, { ox: 0.5, bold: true });
|
|
cont.on('pointerdown', () => {
|
|
const r = buyVoucher(run);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
playSound(scene, SFX.PURCHASE);
|
|
refresh();
|
|
});
|
|
}
|
|
|
|
scene.button(GAME_WIDTH - 200, GAME_HEIGHT - 60, 'Next Round →', () => {
|
|
const r = leaveShop(run);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
scene.save();
|
|
scene.setView('blindselect');
|
|
}, { width: 300, height: 64, fontSize: 24, bg: 0xc26a2f });
|
|
}
|
|
|
|
// ── Pack opening ─────────────────────────────────────────────────────────────
|
|
export function renderPackOpen(scene) {
|
|
const run = scene.run;
|
|
const pack = run.pack;
|
|
if (!pack) { scene.setView('shop'); return; }
|
|
const def = BOOSTER_BY_ID[pack.boosterId];
|
|
scene.text(CX, 110, def.name, 46, C.ink, { ox: 0.5, bold: true });
|
|
scene.text(CX, 170, `Choose ${pack.picksLeft}`, 24, C.muted, { ox: 0.5 });
|
|
|
|
const n = pack.options.length;
|
|
const w = 190, h = 254;
|
|
const step = Math.min(w + 46, 1500 / Math.max(n, 1));
|
|
const x0 = CX - ((n - 1) * step) / 2;
|
|
pack.options.forEach((opt, i) => {
|
|
const x = x0 + i * step, y = 480;
|
|
let cont, info;
|
|
if (opt.what === 'playing') {
|
|
cont = scene.drawPlayingCard(x, y, { ...opt.card, uid: -1, faceDown: false, debuffed: false }, { w, h });
|
|
info = () => scene.playingCardInfo(opt.card);
|
|
} else if (opt.what === 'joker') {
|
|
cont = scene.drawJokerCard(x, y, opt.id, { edition: opt.edition, w, h });
|
|
info = () => scene.jokerInfo(opt.id, null, opt.edition);
|
|
} else {
|
|
cont = scene.drawConsumableCard(x, y, opt.what, opt.id, { w, h });
|
|
info = () => scene.consumableInfo(opt.what, opt.id);
|
|
}
|
|
scene.add2(cont);
|
|
scene.addHoverTilt(cont, { info });
|
|
cont.on('pointerdown', () => {
|
|
const r = pickFromPack(run, i);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
playSound(scene, SFX.CARD_SHOW);
|
|
scene.save();
|
|
scene.setView(run.pack ? 'packopen' : 'shop');
|
|
});
|
|
});
|
|
if (def.what === 'planet') scene.text(CX, 680, 'Planets from packs are used immediately', 18, C.muted, { ox: 0.5 });
|
|
|
|
scene.button(CX, 860, 'Skip Pack', () => {
|
|
skipPack(run);
|
|
scene.save();
|
|
scene.setView('shop');
|
|
}, { width: 240, height: 56, fontSize: 22, variant: 'ghost' });
|
|
}
|
|
|
|
// ── Game over ────────────────────────────────────────────────────────────────
|
|
export function renderGameOver(scene) {
|
|
const run = scene.run;
|
|
if (!run) { scene.setView('title'); return; }
|
|
const won = run.won;
|
|
scene.text(CX, 200, won ? 'YOU WIN!' : 'GAME OVER', 96, won ? '#ffd25e' : '#ff6b5e', { ox: 0.5, bold: true }).setStroke('#141019', 10);
|
|
if (!won && run.lostAt) {
|
|
scene.text(CX, 300, `Defeated at Ante ${run.lostAt.ante} — ${run.lostAt.blind === 'boss' ? (bossFor(run) ? bossFor(run).name : 'Boss Blind') : BLINDS[run.lostAt.blind].name}`, 26, C.muted, { ox: 0.5 });
|
|
}
|
|
if (won && !Number.isFinite(run.roundScore)) {
|
|
scene.text(CX, 300, 'Your score exceeded the bounds of reality.', 26, C.muted, { ox: 0.5 });
|
|
}
|
|
|
|
const w2 = 720, y2 = 380, h2 = 300;
|
|
scene.panel(CX - w2 / 2, y2, w2, h2, {});
|
|
const lines = [
|
|
['Ante reached', run.endless ? String(run.ante) : `${Math.min(run.ante, 8)}/8`],
|
|
['Best hand', fmtChips(run.stats.bestHandScore)],
|
|
['Hands played', String(run.stats.handsPlayed)],
|
|
['Cards added to deck', String(run.stats.cardsAddedToDeck)],
|
|
['Tarots / Planets used', `${run.stats.tarotsUsed} / ${run.stats.planetsUsed}`],
|
|
['Deck', DECK_BY_ID[run.deckId].name],
|
|
];
|
|
lines.forEach(([label, val], i) => {
|
|
const col = i % 2, row = Math.floor(i / 2);
|
|
const x = CX - w2 / 2 + 60 + col * (w2 / 2);
|
|
scene.text(x, y2 + 44 + row * 84, label, 18, C.muted);
|
|
scene.text(x, y2 + 70 + row * 84, val, 28, C.ink, { bold: true });
|
|
});
|
|
|
|
let by = 780;
|
|
if (won && !run.endless) {
|
|
scene.button(CX, by, 'Continue in Endless Mode', () => {
|
|
const r = continueEndless(run);
|
|
if (!r.ok) { scene.toast(r.error); return; }
|
|
scene._recorded = true; // already recorded the win
|
|
scene.save();
|
|
scene.setView('shop');
|
|
}, { width: 440, height: 64, fontSize: 24, bg: 0x8556d6 });
|
|
by += 90;
|
|
}
|
|
scene.button(CX - 170, by, 'New Run', () => { scene.run = null; scene.setView('deckselect'); }, { width: 280, height: 60, fontSize: 24 });
|
|
scene.button(CX + 170, by, 'Main Menu', () => scene.scene.start('GameMenu'), { width: 280, height: 60, fontSize: 24, variant: 'ghost' });
|
|
}
|