diff --git a/assets/images/superkart/backdrop-speedway.png b/assets/images/superkart/backdrop-speedway.png
index 0267ca9..cb5af30 100644
Binary files a/assets/images/superkart/backdrop-speedway.png and b/assets/images/superkart/backdrop-speedway.png differ
diff --git a/assets/images/superkart/theme-nightcity.png b/assets/images/superkart/theme-nightcity.png
new file mode 100644
index 0000000..c4066a8
Binary files /dev/null and b/assets/images/superkart/theme-nightcity.png differ
diff --git a/assets/images/superkart/theme-nightcity.psd b/assets/images/superkart/theme-nightcity.psd
new file mode 100644
index 0000000..78e8805
Binary files /dev/null and b/assets/images/superkart/theme-nightcity.psd differ
diff --git a/assets/images/superkart/theme-scrapyard.png b/assets/images/superkart/theme-scrapyard.png
new file mode 100644
index 0000000..b135496
Binary files /dev/null and b/assets/images/superkart/theme-scrapyard.png differ
diff --git a/assets/images/superkart/theme-scrapyard.psd b/assets/images/superkart/theme-scrapyard.psd
new file mode 100644
index 0000000..0f422ac
Binary files /dev/null and b/assets/images/superkart/theme-scrapyard.psd differ
diff --git a/assets/images/superkart/theme-speedway.png b/assets/images/superkart/theme-speedway.png
new file mode 100644
index 0000000..9049a6f
Binary files /dev/null and b/assets/images/superkart/theme-speedway.png differ
diff --git a/assets/images/superkart/theme-speedway.psd b/assets/images/superkart/theme-speedway.psd
new file mode 100644
index 0000000..c0c2e83
Binary files /dev/null and b/assets/images/superkart/theme-speedway.psd differ
diff --git a/assets/images/superkart/theme-swamp.png b/assets/images/superkart/theme-swamp.png
new file mode 100644
index 0000000..8e3c986
Binary files /dev/null and b/assets/images/superkart/theme-swamp.png differ
diff --git a/assets/images/superkart/theme-swamp.psd b/assets/images/superkart/theme-swamp.psd
new file mode 100644
index 0000000..c69b194
Binary files /dev/null and b/assets/images/superkart/theme-swamp.psd differ
diff --git a/assets/images/superkart/theme-volcano.png b/assets/images/superkart/theme-volcano.png
new file mode 100644
index 0000000..7b15020
Binary files /dev/null and b/assets/images/superkart/theme-volcano.png differ
diff --git a/assets/images/superkart/theme-volcano.psd b/assets/images/superkart/theme-volcano.psd
new file mode 100644
index 0000000..163626c
Binary files /dev/null and b/assets/images/superkart/theme-volcano.psd differ
diff --git a/data/superkart-artwork.json b/data/superkart-artwork.json
index 6e45e38..84b79a8 100644
--- a/data/superkart-artwork.json
+++ b/data/superkart-artwork.json
@@ -42,11 +42,11 @@
},
"themeSheets": {
"beach": { "key": "superkart-theme-beach", "path": "assets/images/superkart/theme-beach.png", "frameWidth": 64, "frameHeight": 64 },
- "volcano": { "key": "superkart-theme-volcano", "path": null, "frameWidth": 64, "frameHeight": 64 },
- "scrapyard": { "key": "superkart-theme-scrapyard", "path": null, "frameWidth": 64, "frameHeight": 64 },
- "swamp": { "key": "superkart-theme-swamp", "path": null, "frameWidth": 64, "frameHeight": 64 },
- "speedway": { "key": "superkart-theme-speedway", "path": null, "frameWidth": 64, "frameHeight": 64 },
- "nightcity": { "key": "superkart-theme-nightcity", "path": null, "frameWidth": 64, "frameHeight": 64 }
+ "volcano": { "key": "superkart-theme-volcano", "path": "assets/images/superkart/theme-volcano.png", "frameWidth": 64, "frameHeight": 64 },
+ "scrapyard": { "key": "superkart-theme-scrapyard", "path": "assets/images/superkart/theme-scrapyard.png", "frameWidth": 64, "frameHeight": 64 },
+ "swamp": { "key": "superkart-theme-swamp", "path": "assets/images/superkart/theme-swamp.png", "frameWidth": 64, "frameHeight": 64 },
+ "speedway": { "key": "superkart-theme-speedway", "path": "assets/images/superkart/theme-speedway.png", "frameWidth": 64, "frameHeight": 64 },
+ "nightcity": { "key": "superkart-theme-nightcity", "path": "assets/images/superkart/theme-nightcity.png", "frameWidth": 64, "frameHeight": 64 }
},
"backdrops": {
"beach": { "key": "superkart-backdrop-beach", "path": "assets/images/superkart/backdrop-beach.png" },
diff --git a/data/superkart-rules.json b/data/superkart-rules.json
index cb3de30..061d14d 100644
--- a/data/superkart-rules.json
+++ b/data/superkart-rules.json
@@ -15,7 +15,7 @@
"baseTopSpeed": 560,
"topSpeedSpread": 160,
"baseAccel": 320,
- "accelSpread": 240,
+ "accelSpread": 480,
"brakeDecel": 760,
"coastDrag": 1.1,
"reverseTopSpeed": 170,
diff --git a/src/games/civilization/CivilizationGame.js b/src/games/civilization/CivilizationGame.js
index 36c6bc1..06af087 100644
--- a/src/games/civilization/CivilizationGame.js
+++ b/src/games/civilization/CivilizationGame.js
@@ -1454,6 +1454,24 @@ export default class CivilizationGame extends Phaser.Scene {
});
},
});
+ } else if (e.type === 'spaceshipPart' && e.civ === human) {
+ const city = Logic.cityById(this.state, e.cityId);
+ const part = this.rules.units[e.part];
+ if (!city || !part) continue;
+ this.announceStatus(`${part.name} completed in ${city.name}!`, undefined,
+ (proceed) => { this.view.panToTile(city.x, city.y); this.time.delayedCall(500, proceed); },
+ {
+ label: 'VIEW CITY',
+ onClick: () => {
+ this.modalOpen = true;
+ openCityScreen(this, this.rules, this.state, city, () => {
+ this.modalOpen = false;
+ this.view.refresh();
+ this.refreshHud();
+ this.showNextStatus();
+ });
+ },
+ });
}
}
}
diff --git a/src/games/civilization/CivilizationLogic.js b/src/games/civilization/CivilizationLogic.js
index 96bc088..424e78f 100644
--- a/src/games/civilization/CivilizationLogic.js
+++ b/src/games/civilization/CivilizationLogic.js
@@ -535,7 +535,7 @@ function completeBuild(rules, state, city) {
if (id === 'sscomponent') ship.component += 1;
if (id === 'ssmodule') ship.module += 1;
city.shieldBox = 0;
- state.events.push({ type: 'spaceshipPart', civ: city.civ, part: id });
+ state.events.push({ type: 'spaceshipPart', civ: city.civ, cityId: city.id, part: id });
pickNextBuild(rules, state, city);
return;
}
diff --git a/src/games/superkart/SuperKartEditor.js b/src/games/superkart/SuperKartEditor.js
index 82b99a9..03dda4a 100644
--- a/src/games/superkart/SuperKartEditor.js
+++ b/src/games/superkart/SuperKartEditor.js
@@ -1,7 +1,8 @@
// Super Kart track editor. Secret entrance: index.html?superkart-editor=1
-// (see PreloadScene). Mirrors the Peggle editor workflow: edit → Test Play →
-// ⬇ Export Track (track-NNN.json) + ⬇ Export Cups Index (cups.json), then
-// hand-drop the files into assets/gamedata/superkart/.
+// (see PreloadScene). Mirrors the Peggle editor workflow: load (existing
+// track from assets/gamedata/superkart/ or any local .json) → edit →
+// Test Play → ⬇ Export Track (track-NNN.json) + ⬇ Export Cups Index
+// (cups.json), then hand-drop the files into assets/gamedata/superkart/.
//
// The track is a closed centerline spline with per-point width; item rows and
// boost pads are arc-length addressed so they ride along with spline edits.
@@ -69,10 +70,11 @@ export default class SuperKartEditor extends Phaser.Scene {
// cups.json is lazy-loaded by the game, so the editor fetches it itself.
this.cupsIndex = { version: 1, cups: [], tracks: [] };
fetch(`${GAMEDATA}/cups.json`).then((r) => r.json())
- .then((d) => { this.cupsIndex = d; })
+ .then((d) => { this.cupsIndex = d; this.refreshLoadOptions(); })
.catch(() => {});
this.buildToolbar();
+ this.buildLoadPanel();
this.bindPointer();
this.bindKeys();
this.rebuildModel();
@@ -213,6 +215,107 @@ export default class SuperKartEditor extends Phaser.Scene {
this.helpText?.setText(help[id] ?? '');
}
+ // ── Load ─────────────────────────────────────────────────────────────────
+
+ buildLoadPanel() {
+ const rx = BOARD_X + BOARD_SIZE + 90 + 250 + 40;
+ const rw = 460;
+ const input = 'background:#1e1a12; color:#f2ead8; border:1px solid #ffd028; border-radius:6px; padding:7px 9px; font-size:15px; width:100%;';
+
+ const el = document.createElement('div');
+ el.style.cssText = `width:${rw}px; font-family:"Julius Sans One",sans-serif; color:${COLORS.textHex};`;
+ el.innerHTML = `
+
+
LOAD TRACK
+
+
+
+
`;
+
+ this.loadDom = this.add.dom(rx + rw / 2, 220, el).setDepth(20);
+ const q = (id) => el.querySelector(id);
+ this.loadSelect = q('#sk-load');
+ this.loadWarn = q('#sk-load-warn');
+
+ this.loadSelect.addEventListener('change', (ev) => {
+ const file = ev.target.value;
+ if (file) this.loadFromManifest(file);
+ ev.target.value = '';
+ });
+ q('#sk-fileinput').addEventListener('change', (ev) => {
+ const f = ev.target.files?.[0];
+ if (!f) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ try { this.loadTrack(JSON.parse(reader.result), f.name); } catch (_) { this.flashLoadWarn(`Could not parse ${f.name}`); }
+ };
+ reader.readAsText(f);
+ ev.target.value = '';
+ });
+
+ this.refreshLoadOptions();
+ }
+
+ refreshLoadOptions() {
+ if (!this.loadSelect) return;
+ const opts = [``,
+ ...(this.cupsIndex?.tracks ?? []).map((t) => ``)].join('');
+ this.loadSelect.innerHTML = opts;
+ }
+
+ flashLoadWarn(msg) {
+ if (!this.loadWarn) return;
+ this.loadWarn.textContent = msg;
+ this.time.delayedCall(4000, () => { if (this.loadWarn) this.loadWarn.textContent = ''; });
+ }
+
+ async loadFromManifest(file) {
+ try {
+ const res = await fetch(`${GAMEDATA}/${file}`);
+ if (!res.ok) throw new Error('not found');
+ this.loadTrack(await res.json(), file);
+ } catch (_) {
+ this.flashLoadWarn(`Could not load ${file}`);
+ }
+ }
+
+ loadTrack(rawData, fileName) {
+ let data;
+ try { data = JSON.parse(JSON.stringify(rawData)); } catch (_) { data = null; }
+ if (!data || !Array.isArray(data.spline) || data.spline.length < 3) {
+ this.flashLoadWarn(`${fileName}: not a valid track file`);
+ return;
+ }
+ this.pushUndo();
+ const d = this.defaultTrack();
+ this.track = {
+ version: data.version ?? 1,
+ id: data.id ?? d.id,
+ name: data.name ?? d.name,
+ theme: this.themes.includes(data.theme) ? data.theme : (this.themes?.[0] ?? 'speedway'),
+ laps: data.laps ?? d.laps,
+ world: data.world ?? d.world,
+ spline: data.spline,
+ startIndex: Phaser.Math.Clamp(data.startIndex ?? 0, 0, data.spline.length - 1),
+ surfaces: data.surfaces ?? [],
+ walls: data.walls ?? [],
+ boosts: data.boosts ?? [],
+ itemRows: data.itemRows ?? [],
+ hazards: data.hazards ?? [],
+ decor: data.decor ?? [],
+ coins: data.coins ?? [],
+ };
+ this.selected = -1;
+ this.draftPoly = [];
+ this.metaName?.setLabel(`Name: ${this.track.name}`);
+ this.metaFile?.setLabel(`File: ${this.track.id}.json`);
+ this.metaTheme?.setLabel(`Theme: ${this.track.theme}`);
+ this.metaLaps?.setLabel(`Laps: ${this.track.laps}`);
+ this.rebuildModel();
+ playSound(this, SFX.UI_CHIME);
+ this.flash(`Loaded ${fileName}`);
+ }
+
// ── Input ────────────────────────────────────────────────────────────────
bindPointer() {
diff --git a/src/scenes/GameMenuScene.js b/src/scenes/GameMenuScene.js
index b26553e..71fcfbf 100644
--- a/src/scenes/GameMenuScene.js
+++ b/src/scenes/GameMenuScene.js
@@ -14,7 +14,7 @@ const CATEGORIES = [
{ key: 'casino', label: 'Casino' },
{ key: 'word', label: 'Words & Numbers' },
{ key: 'logic', label: 'Logic & Puzzle' },
- { key: 'arcade-console-pc', label: 'Arcade, Console & PC' },
+ { key: 'arcade-console-pc', label: 'Video Games' },
];
const TAB_ICON_FRAMES = { tabletop: 0, cards: 1, casino: 2, word: 3, logic: 4, 'arcade-console-pc': 5 };
diff --git a/src/scenes/ProfileScene.js b/src/scenes/ProfileScene.js
index 3d1d3ad..836973d 100644
--- a/src/scenes/ProfileScene.js
+++ b/src/scenes/ProfileScene.js
@@ -5,6 +5,7 @@ import { auth } from '../services/auth.js';
import { Button } from '../ui/Button.js';
import { TextInput } from '../ui/TextInput.js';
import { Modal } from '../ui/Modal.js';
+import { downloadProfile, parseExportFile, applyImportedProfile } from '../services/profileTransfer.js';
export default class ProfileScene extends Phaser.Scene {
constructor() { super('Profile'); }
@@ -15,6 +16,8 @@ export default class ProfileScene extends Phaser.Scene {
const cx = GAME_WIDTH / 2;
+ this.add.image(cx, GAME_HEIGHT / 2, 'bg-menu').setDisplaySize(GAME_WIDTH, GAME_HEIGHT);
+
this.add.text(cx, 140, 'Profile', {
fontFamily: 'Righteous',
fontSize: '64px',
@@ -37,6 +40,15 @@ export default class ProfileScene extends Phaser.Scene {
}
this.statusText.destroy();
+ // Soft card behind the profile info so text stays readable over the
+ // light background image — large radius + shadow instead of a hard box.
+ const infoCard = this.add.graphics();
+ infoCard.postFX.addShadow(0, 6, 0.006, 3, 0x000000, 14, 0.55);
+ infoCard.fillStyle(COLORS.panel, 0.82);
+ infoCard.fillRoundedRect(320, 230, 1080, 630, 32);
+ infoCard.lineStyle(2, COLORS.accent, 0.45);
+ infoCard.strokeRoundedRect(320, 230, 1080, 630, 32);
+
// Avatar slot (placeholder vector)
const avatarX = cx - 480;
const avatarY = 380;
@@ -50,10 +62,15 @@ export default class ProfileScene extends Phaser.Scene {
void avatarBg;
if (profile.avatarPath) {
- this.load.image(`avatar-${profile.id}`, profile.avatarPath);
+ const avatarKey = `avatar-${profile.id}`;
+ this.load.image(avatarKey, profile.avatarPath);
this.load.once('complete', () => {
- const img = this.add.image(avatarX, avatarY, `avatar-${profile.id}`);
- img.setDisplaySize(180, 180);
+ const maskG = this.make.graphics({ x: 0, y: 0, add: false });
+ maskG.fillStyle(0xffffff);
+ maskG.fillCircle(avatarX, avatarY, 96);
+ this.add.image(avatarX, avatarY, avatarKey)
+ .setDisplaySize(192, 192)
+ .setMask(maskG.createGeometryMask());
});
this.load.start();
}
@@ -115,7 +132,14 @@ export default class ProfileScene extends Phaser.Scene {
this.add.text(cx - 320, 680, 'Bio', { fontSize: '22px', color: COLORS.mutedHex }).setOrigin(0, 0.5);
const bioInput = new TextInput(this, cx + 80, 740, { width: 600, height: 120, multiline: true, value: profile.bio ?? '', maxLength: 500 });
- new Button(this, cx - 200, 900, 'Save profile', async () => {
+ // Download profile sits to the right of the profile picture.
+ new Button(this, cx + 400, avatarY + 50, 'Download Profile', () => this.handleDownloadProfile(), { width: 360 });
+
+ // Upload Profile takes the slot next to the picture; Save profile moves to the bottom row.
+ new Button(this, cx + 400, avatarY - 50, 'Upload Profile', () => this.pickProfileFile(), { variant: 'ghost', width: 360 });
+
+ new Button(this, cx - 400, 900, 'Upload avatar', () => this.pickAvatar());
+ new Button(this, cx - 100, 900, 'Save profile', async () => {
try {
const { profile: updated } = await api.patch('/profile', {
displayName: displayNameInput.value,
@@ -127,9 +151,61 @@ export default class ProfileScene extends Phaser.Scene {
new Modal(this, err.message, { color: COLORS.dangerHex, autoCloseMs: 2400 });
}
});
+ new Button(this, cx + 200, 900, 'Back', () => this.scene.start('Landing'), { variant: 'ghost' });
+ }
- new Button(this, cx + 100, 900, 'Upload avatar', () => this.pickAvatar());
- new Button(this, cx + 400, 900, 'Back', () => this.scene.start('Landing'), { variant: 'ghost' });
+ handleDownloadProfile() {
+ try {
+ downloadProfile();
+ new Modal(this, 'Profile downloaded.', { autoCloseMs: 1500 });
+ } catch {
+ new Modal(this, 'Failed to download profile.', { color: COLORS.dangerHex, autoCloseMs: 2400 });
+ }
+ }
+
+ pickProfileFile() {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.fcg,application/json';
+ input.onchange = async () => {
+ const file = input.files?.[0];
+ if (!file) return;
+ let text;
+ try {
+ text = await file.text();
+ } catch {
+ new Modal(this, 'Could not read that file.', { color: COLORS.dangerHex, autoCloseMs: 2400 });
+ return;
+ }
+ let payload;
+ try {
+ payload = parseExportFile(text);
+ } catch (err) {
+ new Modal(this, err.message, { color: COLORS.dangerHex, autoCloseMs: 2800 });
+ return;
+ }
+ this.confirmImport(payload);
+ };
+ input.click();
+ }
+
+ confirmImport(payload) {
+ new Modal(
+ this,
+ "This will overwrite all local save data on this device (chips, history, and every game's progress) with the contents of this file. This cannot be undone. Continue?",
+ {
+ buttons: [
+ {
+ label: 'Confirm',
+ onClick: () => {
+ new Modal(this, 'Profile restored. Reloading…', { autoCloseMs: 900 });
+ this.time.delayedCall(900, () => applyImportedProfile(payload));
+ },
+ },
+ { label: 'Cancel', variant: 'ghost', onClick: () => {} },
+ ],
+ },
+ );
}
pickAvatar() {
diff --git a/src/services/profileTransfer.js b/src/services/profileTransfer.js
new file mode 100644
index 0000000..8c128a0
--- /dev/null
+++ b/src/services/profileTransfer.js
@@ -0,0 +1,83 @@
+// Packages/restores the entire local save state (localStorage) so a player's
+// progress can be moved between browsers/devices via a downloadable .fcg file.
+
+export const EXPORT_APP_ID = 'fertig-classic-games';
+export const EXPORT_FORMAT_VERSION = 1;
+
+export function buildExportPayload() {
+ const keys = {};
+ for (let i = 0; i < window.localStorage.length; i++) {
+ const key = window.localStorage.key(i);
+ keys[key] = window.localStorage.getItem(key);
+ }
+ return {
+ app: EXPORT_APP_ID,
+ formatVersion: EXPORT_FORMAT_VERSION,
+ exportedAt: new Date().toISOString(),
+ keys,
+ };
+}
+
+// MM-DD-YY, local time (not UTC).
+export function buildExportFilename(date = new Date()) {
+ const mm = String(date.getMonth() + 1).padStart(2, '0');
+ const dd = String(date.getDate()).padStart(2, '0');
+ const yy = String(date.getFullYear() % 100).padStart(2, '0');
+ return `FERTIG-CLASSIC-GAMES-${mm}-${dd}-${yy}.fcg`;
+}
+
+export function downloadProfile() {
+ const payload = buildExportPayload();
+ const blob = new Blob([`${JSON.stringify(payload, null, 2)}\n`], { type: 'application/json' });
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = buildExportFilename();
+ a.click();
+ setTimeout(() => URL.revokeObjectURL(a.href), 5000);
+}
+
+// Checks a parsed JSON value actually looks like one of our exports.
+export function validateExportPayload(parsed) {
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
+ return { valid: false, reason: 'That file is not a valid profile export.' };
+ }
+ if (parsed.app !== EXPORT_APP_ID) {
+ return { valid: false, reason: 'That file was not exported from Fertig Classic Games.' };
+ }
+ if (typeof parsed.formatVersion !== 'number' || parsed.formatVersion > EXPORT_FORMAT_VERSION) {
+ return { valid: false, reason: 'This profile file was created by a newer, unsupported version of the app.' };
+ }
+ if (!parsed.keys || typeof parsed.keys !== 'object' || Array.isArray(parsed.keys)) {
+ return { valid: false, reason: 'That file is missing profile data.' };
+ }
+ for (const [k, v] of Object.entries(parsed.keys)) {
+ if (typeof k !== 'string' || typeof v !== 'string') {
+ return { valid: false, reason: 'That file is not a valid profile export.' };
+ }
+ }
+ return { valid: true };
+}
+
+// Parses raw file text and validates it. Throws an Error with a
+// user-displayable message on failure; returns the payload on success.
+export function parseExportFile(text) {
+ let parsed;
+ try {
+ parsed = JSON.parse(text);
+ } catch {
+ throw new Error('That file is not valid JSON.');
+ }
+ const result = validateExportPayload(parsed);
+ if (!result.valid) throw new Error(result.reason);
+ return parsed;
+}
+
+// Overwrites localStorage with the payload's keys, then reloads the page so
+// every scene/game re-initializes against the restored data.
+export function applyImportedProfile(payload) {
+ window.localStorage.clear();
+ for (const [k, v] of Object.entries(payload.keys)) {
+ window.localStorage.setItem(k, v);
+ }
+ window.location.reload();
+}
diff --git a/src/ui/Modal.js b/src/ui/Modal.js
index e98b5d8..b8fa802 100644
--- a/src/ui/Modal.js
+++ b/src/ui/Modal.js
@@ -1,16 +1,20 @@
import * as Phaser from 'phaser';
import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js';
+import { Button } from './Button.js';
export class Modal extends Phaser.GameObjects.Container {
constructor(scene, message, options = {}) {
super(scene, 0, 0);
+ const hasButtons = Array.isArray(options.buttons) && options.buttons.length > 0;
+ const panelHeight = hasButtons ? 320 : 280;
const overlay = scene.add.rectangle(
GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.6,
).setInteractive();
const panel = scene.add.rectangle(
- GAME_WIDTH / 2, GAME_HEIGHT / 2, 720, 280, COLORS.panel, 1,
+ GAME_WIDTH / 2, GAME_HEIGHT / 2, 720, panelHeight, COLORS.panel, 1,
).setStrokeStyle(2, COLORS.accent);
- const text = scene.add.text(GAME_WIDTH / 2, GAME_HEIGHT / 2 - 30, message, {
+ const textY = hasButtons ? GAME_HEIGHT / 2 - 60 : GAME_HEIGHT / 2 - 30;
+ const text = scene.add.text(GAME_WIDTH / 2, textY, message, {
fontFamily: '"Julius Sans One"',
fontSize: '28px',
color: options.color ?? COLORS.textHex,
@@ -24,7 +28,20 @@ export class Modal extends Phaser.GameObjects.Container {
if (domLayer) domLayer.style.visibility = 'hidden';
const restore = () => { if (domLayer) domLayer.style.visibility = ''; };
- if (options.autoCloseMs) {
+ if (hasButtons) {
+ const n = options.buttons.length;
+ const spacing = 260;
+ const startX = GAME_WIDTH / 2 - ((n - 1) * spacing) / 2;
+ options.buttons.forEach((btn, i) => {
+ const button = new Button(scene, startX + i * spacing, GAME_HEIGHT / 2 + 70, btn.label, () => {
+ restore();
+ this.destroy();
+ btn.onClick?.();
+ }, { variant: btn.variant ?? 'solid' });
+ this.add(button);
+ });
+ // No overlay-click-to-dismiss and no auto-close: the user must choose an option.
+ } else if (options.autoCloseMs) {
scene.time.delayedCall(options.autoCloseMs, () => { restore(); this.destroy(); });
} else {
overlay.on('pointerdown', () => { restore(); this.destroy(); });