284 lines
8.3 KiB
JavaScript
284 lines
8.3 KiB
JavaScript
import Phaser from '../vendor/phaser.js';
|
|
import { config } from '../config/Config.js';
|
|
import { toColor } from '../utils/Color.js';
|
|
import { MenuButton } from '../ui/MenuButton.js';
|
|
import { Rng } from '../utils/Rng.js';
|
|
import { NameGenerator } from '../utils/NameGenerator.js';
|
|
import { Galaxy } from '../galaxy/Galaxy.js';
|
|
|
|
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
|
|
const MONO = "'Cascadia Mono', 'Consolas', 'Menlo', monospace";
|
|
const SEED_STORAGE_KEY = 'orbit.galaxySeed';
|
|
const SEED_MAX = 32;
|
|
const SEED_CHAR = /^[A-Za-z0-9._-]$/;
|
|
|
|
/**
|
|
* Main menu. All text/colors/layout come from data/menu.json.
|
|
*
|
|
* The Galaxy Seed panel is where a new universe is chosen:
|
|
* - the seed is displayed, editable (click it, type; Backspace; Enter to
|
|
* commit), and rerollable;
|
|
* - the same seed always builds the same galaxy (Galaxy.create), and the
|
|
* panel shows what that seed contains before you commit;
|
|
* - "New Game" builds the galaxy roster from the seed and hands it to the
|
|
* rest of the game via the shared registry.
|
|
*/
|
|
export class MenuScene extends Phaser.Scene {
|
|
constructor() {
|
|
super({ key: 'MenuScene' });
|
|
}
|
|
|
|
create() {
|
|
const menu = config.section('menu', {});
|
|
const fontFamily = menu.fontFamily ?? FONT_FALLBACK;
|
|
const colors = menu.colors ?? {};
|
|
const { width, height } = this.scale;
|
|
const cx = width / 2;
|
|
|
|
// Title
|
|
this.add
|
|
.text(cx, height * 0.34, menu.title ?? 'ORBIT', {
|
|
fontFamily,
|
|
fontSize: `${menu.titleFontSize ?? 84}px`,
|
|
fontStyle: 'bold',
|
|
color: colors.title ?? '#e9edf8',
|
|
shadow: {
|
|
color: colors.titleGlow ?? '#2f6df6',
|
|
blur: 28,
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
},
|
|
})
|
|
.setOrigin(0.5);
|
|
|
|
// Subtitle
|
|
this.add
|
|
.text(cx, height * 0.46, menu.subtitle ?? '', {
|
|
fontFamily,
|
|
fontSize: `${menu.subtitleFontSize ?? 20}px`,
|
|
color: colors.subtitle ?? '#8fa0c9',
|
|
})
|
|
.setOrigin(0.5);
|
|
|
|
// New Game
|
|
const btn = menu.buttons?.newGame ?? {};
|
|
new MenuButton(
|
|
this,
|
|
(btn.position?.x ?? 0.5) * width,
|
|
(btn.position?.y ?? 0.60) * height,
|
|
btn.label ?? 'New Game',
|
|
() => this.startNewGame(),
|
|
btn,
|
|
);
|
|
|
|
// Galaxy seed panel
|
|
this.createSeedPanel(menu, fontFamily, cx, height);
|
|
|
|
// Global input: typing goes to the seed field; a click elsewhere blurs it.
|
|
this.input.keyboard.on('keydown', (e) => this.onSeedKey(e));
|
|
this.input.on('pointerdown', (pointer) => {
|
|
if (this.seedFieldHit && pointer.over(this.seedFieldHit)) {
|
|
this.setSeedFocus(true);
|
|
} else if (this.seedFocused) {
|
|
this.setSeedFocus(false);
|
|
}
|
|
});
|
|
this.cursorTimer = this.time.addEvent({
|
|
delay: 430,
|
|
loop: true,
|
|
callback: () => {
|
|
if (this.seedFocused) {
|
|
this.cursorOn = !this.cursorOn;
|
|
this.seedText.setText(this.seedValue + (this.cursorOn ? ' \u258c' : ' '));
|
|
}
|
|
},
|
|
});
|
|
|
|
// Version footer
|
|
this.add
|
|
.text(width - 16, height - 14, config.get('game.version', ''), {
|
|
fontFamily,
|
|
fontSize: '12px',
|
|
color: colors.footer ?? '#54608a',
|
|
})
|
|
.setOrigin(1, 0.5);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Galaxy seed panel
|
|
// ------------------------------------------------------------------
|
|
|
|
createSeedPanel(menu, fontFamily, cx, height) {
|
|
const cfg = menu.seed ?? {};
|
|
const colors = cfg.colors ?? {};
|
|
const fieldW = cfg.fieldWidth ?? 300;
|
|
const fieldH = cfg.fieldHeight ?? 46;
|
|
const cy = (cfg.position?.y ?? 0.755) * height;
|
|
|
|
this.seedValue = this.loadSavedSeed() ?? Rng.randomSeedString(8);
|
|
this.seedFocused = false;
|
|
this.cursorOn = true;
|
|
|
|
// Label
|
|
this.add
|
|
.text(cx, cy - fieldH / 2 - 10, cfg.label ?? 'GALAXY SEED', {
|
|
fontFamily,
|
|
fontSize: `${cfg.labelFontSize ?? 12}px`,
|
|
color: colors.label ?? '#54608a',
|
|
letterSpacing: 3,
|
|
})
|
|
.setOrigin(0.5, 1);
|
|
|
|
// Field (click to edit)
|
|
this.seedFieldHit = this.add
|
|
.rectangle(cx, cy, fieldW, fieldH, toColor(colors.fieldBg ?? '#0b1226'), 1)
|
|
.setStrokeStyle(1, toColor(colors.border ?? '#2a3a63'), 0.9);
|
|
this.seedFieldHit.setInteractive({ useHandCursor: true });
|
|
this.seedFieldHit.on('pointerdown', () => this.setSeedFocus(true));
|
|
|
|
this.seedText = this.add
|
|
.text(cx - fieldW / 2 + 14, cy, '', {
|
|
fontFamily: MONO,
|
|
fontSize: `${cfg.fontSize ?? 24}px`,
|
|
color: colors.value ?? '#e9edf8',
|
|
})
|
|
.setOrigin(0, 0.5);
|
|
|
|
// Reroll
|
|
new MenuButton(
|
|
this,
|
|
cx + fieldW / 2 + 26,
|
|
cy,
|
|
cfg.rerollLabel ?? 'reroll',
|
|
() => {
|
|
this.seedValue = Rng.randomSeedString(8);
|
|
this.setSeedFocus(false);
|
|
},
|
|
{
|
|
fontSize: cfg.rerollFontSize ?? 14,
|
|
paddingX: 18,
|
|
paddingY: 10,
|
|
},
|
|
);
|
|
|
|
// Hint: what this seed will build (galaxy name + scale), live-updated.
|
|
this.seedHint = this.add
|
|
.text(cx, cy + fieldH / 2 + 12, '', {
|
|
fontFamily,
|
|
fontSize: '12px',
|
|
color: colors.hint ?? '#54608a',
|
|
})
|
|
.setOrigin(0.5, 0);
|
|
|
|
this.drawSeed();
|
|
}
|
|
|
|
drawSeed() {
|
|
if (!this.seedText || this.seedDead) return;
|
|
this.seedText.setText(this.seedValue + (this.seedFocused ? (this.cursorOn ? ' \u258c' : ' ') : ''));
|
|
|
|
const cfg = config.get('menu.seed.colors', {});
|
|
const border = this.seedFocused
|
|
? toColor(cfg.activeBorder ?? '#41c7ff')
|
|
: toColor(cfg.border ?? '#2a3a63');
|
|
this.seedFieldHit.setStrokeStyle(1, border, this.seedFocused ? 1 : 0.9);
|
|
|
|
const count = config.get('galaxy.systemCount', 0);
|
|
const archetypes = Object.keys(config.get('systems.types', {})).length;
|
|
let hint = `${Number(count).toLocaleString('en-US')} systems \u00b7 ${archetypes} archetypes \u00b7 same seed \u2192 same galaxy`;
|
|
if (this.seedValue.trim()) {
|
|
const gname = NameGenerator.galaxy(Rng.derive(this.seedValue.trim(), 'galaxy', 'name'));
|
|
hint = `the galaxy of ${gname} \u00b7 ${hint}`;
|
|
}
|
|
this.seedHint.setText(hint);
|
|
}
|
|
|
|
setSeedFocus(focused) {
|
|
this.seedFocused = !!focused;
|
|
this.cursorOn = true;
|
|
this.drawSeed();
|
|
}
|
|
|
|
onSeedKey(e) {
|
|
const key =
|
|
e && typeof e.key === 'string' && e.key.length > 0
|
|
? e.key
|
|
: e?.keyCode === 13 ? 'Enter'
|
|
: e?.keyCode === 8 ? 'Backspace'
|
|
: '';
|
|
|
|
if (key === 'Enter' || key === 'Escape') {
|
|
this.setSeedFocus(false);
|
|
return;
|
|
}
|
|
if (key === 'Backspace') {
|
|
if (this.seedFocused) {
|
|
this.seedValue = this.seedValue.slice(0, -1);
|
|
this.drawSeed();
|
|
}
|
|
return;
|
|
}
|
|
if (SEED_CHAR.test(key)) {
|
|
this.setSeedFocus(true);
|
|
if (this.seedValue.length < SEED_MAX) {
|
|
this.seedValue += key;
|
|
this.drawSeed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
startNewGame() {
|
|
let seed = this.seedValue.trim();
|
|
if (!seed) {
|
|
seed = Rng.randomSeedString(8);
|
|
this.seedValue = seed;
|
|
this.drawSeed();
|
|
}
|
|
try {
|
|
this.setSeedFocus(false);
|
|
const galaxy = Galaxy.create(seed);
|
|
this.registry.set('galaxy', galaxy);
|
|
this.registry.set('seed', seed);
|
|
this.saveSeed(seed);
|
|
console.info(`orbit \u2014 the galaxy of ${galaxy.name} (seed ${seed})`, galaxy.summary());
|
|
this.scene.start('GameScene');
|
|
} catch (err) {
|
|
console.error(err);
|
|
const msg = this.add
|
|
.text(this.scale.width / 2, this.scale.height - 64, `could not build the galaxy: ${err.message}`, {
|
|
fontFamily: FONT_FALLBACK,
|
|
fontSize: '14px',
|
|
color: '#ff9b9b',
|
|
})
|
|
.setOrigin(0.5);
|
|
this.time.delayedCall(4000, () => msg.destroy());
|
|
}
|
|
}
|
|
|
|
/** Guard for post-shutdown input events (e.g. a click that also fired
|
|
* "New Game" on the same frame). */
|
|
shutdown() {
|
|
this.seedDead = true;
|
|
if (this.cursorTimer) this.time.removeEvent(this.cursorTimer);
|
|
}
|
|
|
|
loadSavedSeed() {
|
|
try {
|
|
const v = globalThis.localStorage?.getItem?.(SEED_STORAGE_KEY);
|
|
return typeof v === 'string' && v.trim() ? v.trim() : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
saveSeed(seed) {
|
|
try {
|
|
globalThis.localStorage?.setItem?.(SEED_STORAGE_KEY, seed);
|
|
} catch {
|
|
/* private mode / no storage — fine */
|
|
}
|
|
}
|
|
}
|