diff --git a/README.md b/README.md index 2201054..7963f3c 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,14 @@ orbit/ │ ├── manifest.json # which config files exist │ ├── game.json # dimensions, colors, starfield, … │ ├── menu.json # menu text, colors, button layout, seed panel +│ ├── theme.json # shared UI theme: fonts, palette, CRT/glitch tuning │ ├── ship.json # ship feel: thrust, drag, maxSpeed, … │ ├── galaxy.json # galaxy scale & shape (count, radius, spiral…) │ ├── systems.json # system archetypes: theme, attributes, distribution │ ├── settlements.json # the lived-in layer: settlement kinds & populations │ └── naming.json # syllable pools for names ├── assets/images/ # art: planets.png (1024×1024 spritesheet frames) +├── assets/fonts/ # UI typefaces: Ethnocentric (headers), Centauri (body) ├── lib/ # vendored third-party libs (Phaser 4.2.1) ├── js/ │ ├── main.js # entry point: load config → boot Phaser @@ -76,8 +78,8 @@ orbit/ │ ├── scenes/ # MenuScene, GameScene (thin, orchestration) │ ├── entities/ # Ship (own behavior), Planet (home world, solid) │ ├── galaxy/ # Galaxy (seeded world model), SystemGenerator, SystemReport -│ ├── ui/ # MenuButton (reusable) -│ ├── visuals/ # Starfield (decorative) +│ ├── ui/ # MenuButton, GlitchText, CyberShape (reusable) +│ ├── visuals/ # Starfield, CyberOverlay (CRT/glitch, shared) │ ├── utils/ # small pure helpers (Color, Rng, NameGenerator) │ └── vendor/ # shim to the vendored Phaser └── docs/PROJECT_NOTES.md # ← project conventions: read this diff --git a/assets/images/originals/base-ship-01-iso.png b/assets/images/originals/base-ship-01-iso.png new file mode 100644 index 0000000..667499a Binary files /dev/null and b/assets/images/originals/base-ship-01-iso.png differ diff --git a/assets/images/originals/base-ship-01.png b/assets/images/originals/base-ship-01.png new file mode 100644 index 0000000..3ef802e Binary files /dev/null and b/assets/images/originals/base-ship-01.png differ diff --git a/data/manifest.json b/data/manifest.json index b1c727b..d9e80d4 100644 --- a/data/manifest.json +++ b/data/manifest.json @@ -1,6 +1,7 @@ { "files": [ "game.json", + "theme.json", "menu.json", "ship.json", "planets.json", diff --git a/data/menu.json b/data/menu.json index 6ee0687..20cba6d 100644 --- a/data/menu.json +++ b/data/menu.json @@ -1,43 +1,54 @@ { "title": "ORBIT", - "titleFontSize": 84, - "subtitle": "a procedural space rpg", + "titleFontSize": 118, + "titleLetterSpacing": 12, + "subtitle": "A PROCEDURAL SPACE RPG", "subtitleFontSize": 20, - "fontFamily": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", - "buttonFontSize": 24, - "colors": { - "title": "#e9edf8", - "titleGlow": "#2f6df6", - "subtitle": "#8fa0c9", - "buttonText": "#e9edf8", - "buttonBg": "#16203a", - "buttonHoverBg": "#24345c", - "buttonBorder": "#33456f", - "footer": "#54608a" - }, + "subtitleLetterSpacing": 8, + "buttonFontSize": 26, "buttons": { + "upper": true, "newGame": { "label": "New Game", - "position": { "x": 0.5, "y": 0.60 }, - "fontSize": 24 + "position": { "x": 0.5, "y": 0.585 }, + "fontSize": 26, + "paddingX": 46, + "paddingY": 18 } }, "seed": { "label": "GALAXY SEED", "position": { "x": 0.5, "y": 0.755 }, - "labelFontSize": 12, - "fontSize": 24, - "fieldWidth": 300, - "fieldHeight": 46, - "rerollLabel": "reroll", + "labelFontSize": 13, + "fontSize": 25, + "fieldWidth": 320, + "fieldHeight": 52, + "rerollLabel": "Reroll", "rerollFontSize": 14, "colors": { - "label": "#54608a", - "value": "#e9edf8", - "fieldBg": "#0b1226", - "border": "#2a3a63", - "activeBorder": "#41c7ff", - "hint": "#54608a" + "label": "#5c74a8", + "value": "#eaf6ff", + "fieldBg": "#070d1a", + "border": "#22405f", + "activeBorder": "#00e5ff", + "hint": "#5c74a8" } + }, + "colors": { + "title": "#eaf6ff", + "titleGlow": "#00e5ff", + "subtitle": "#7d92c4", + "buttonText": "#eaf6ff", + "buttonBg": "#0a1120", + "buttonHoverBg": "#13233f", + "buttonBorder": "#2aa9c9", + "footer": "#3d4c74" + }, + "chrome": { + "topLeft": "ORBIT // MAIN CONSOLE", + "topRight": "SYS // ONLINE", + "bottomLeft": "ORBIT // v{version}", + "bottomRight": "RUNTIME // GIEDI 4.2.1", + "frameMargin": 10 } } diff --git a/data/theme.json b/data/theme.json new file mode 100644 index 0000000..a33a37d --- /dev/null +++ b/data/theme.json @@ -0,0 +1,42 @@ +{ + "$comment": "The game's visual language — shared by every scene's UI. Fonts: Ethnocentric = headers/titles, Centauri = body text & buttons. Palette: deep-space black + electric cyan + magenta (retro-futurism). Tuning for the CRT/glitch overlay lives here too (see js/visuals/CyberOverlay.js, js/ui/GlitchText.js).", + "fonts": { + "header": { + "family": "Ethnocentric", + "cssFallbacks": ["'Arial Black'", "'Impact'", "sans-serif"] + }, + "body": { + "family": "Centauri", + "cssFallbacks": ["'Segoe UI'", "'Helvetica Neue'", "Arial", "sans-serif"] + } + }, + "colors": { + "bg": "#04060d", + "panel": "#0a1120", + "ink": "#eaf6ff", + "dim": "#7d92c4", + "faint": "#3d4c74", + "neon": "#00e5ff", + "neon2": "#ff2d6f", + "amber": "#ffc94d" + }, + "overlay": { + "scanline": { "pitch": 3, "alpha": 0.14 }, + "grid": { "cell": 48, "alpha": 0.05 }, + "vignette": { "alpha": 0.5 }, + "sweep": { "alpha": 0.09, "duration": 9000, "band": 140 } + }, + "glitch": { + "enabled": true, + "interval": [2200, 6800], + "duration": [110, 340], + "slices": [2, 6], + "maxOffset": 12 + }, + "gameScene": { + "overlay": true, + "scanlineAlpha": 0.1, + "vignetteAlpha": 0.45, + "burstOnEntry": true + } +} diff --git a/dev/cdp-eval.mjs b/dev/cdp-eval.mjs new file mode 100644 index 0000000..3e4ba80 --- /dev/null +++ b/dev/cdp-eval.mjs @@ -0,0 +1,38 @@ +// Evaluate an expression in a headless Chrome (CDP) page and print the result. +// Usage: node cdp-eval.mjs [waitMs] +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; + +const [url, expr, waitMs = '6000'] = process.argv.slice(2); +const PORT = 9333 + Math.floor(Math.random() * 200); +const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; +const udd = 'C:/Users/BRIAN~1.FER/AppData/Local/Temp/cdp-eval-' + process.pid + '-' + Date.now(); +const chrome = spawn(CHROME, [ + '--headless', `--remote-debugging-port=${PORT}`, `--user-data-dir=${udd}`, + '--no-first-run', '--disable-gpu', '--enable-unsafe-swiftshader', 'about:blank', +], { stdio: 'ignore' }); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +try { + let v; + for (let i = 0; i < 100; i++) { + try { v = await (await fetch(`http://127.0.0.1:${PORT}/json/version`)).json(); break; } catch { await sleep(200); } + } + if (!v) throw new Error('CDP never came up'); + let t; + try { t = await (await fetch(`http://127.0.0.1:${PORT}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' })).json(); } + catch { t = await (await fetch(`http://127.0.0.1:${PORT}/json/new?${encodeURIComponent(url)}`, { method: 'POST' })).json(); } + const ws = new WebSocket(t.webSocketDebuggerUrl); + let id = 0; const p = new Map(); + const send = (m, pa = {}) => new Promise((res, rej) => { const i = ++id; p.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method: m, params: pa })); }); + ws.onmessage = (m) => { const x = JSON.parse(m.data); if (x.id && p.has(x.id)) { const { res, rej } = p.get(x.id); p.delete(x.id); x.error ? rej(new Error(x.error.message)) : res(x.result); } }; + await new Promise((r) => (ws.onopen = r)); + await send('Runtime.enable'); + await sleep(Number(waitMs)); + const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); + if (r.exceptionDetails) throw new Error(JSON.stringify(r.exceptionDetails).slice(0, 400)); + console.log(typeof r.result.value === 'object' ? JSON.stringify(r.result.value) : r.result.value); + ws.close(); +} finally { + chrome.kill(); + setTimeout(() => fs.rmSync(udd, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }), 1500); +} diff --git a/dev/cdpshot.mjs b/dev/cdpshot.mjs new file mode 100644 index 0000000..1799e54 --- /dev/null +++ b/dev/cdpshot.mjs @@ -0,0 +1,118 @@ +// Minimal CDP driver — real-time (no virtual-time budget), so +// requestAnimationFrame keeps ticking like in a normal browser session. +// (Headless Chrome with --virtual-time-budget stops rAF after a couple of +// frames — the old `chrome --headless --screenshot` path is broken for this +// game; this is not.) +// +// CLI: node cdpshot.mjs [waitMs] [w] [h] +// Module: import { CDP, launch } from './cdpshot.mjs' +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; + +const CHROME = process.env.CHROME_PATH || 'C:/Program Files/Google/Chrome/Application/chrome.exe'; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +/** + * Launch an isolated headless Chrome with CDP enabled. + * Returns { url, kill() } — url = the webSocketDebuggerUrl of a new tab at + * `targetUrl` (blank if omitted). kill() terminates Chrome and cleans up + * its profile dir. + */ +export async function launch(targetUrl, port = 9333 + Math.floor(Math.random() * 100), width = 1280, height = 720, waitMs = 4000) { + const userDataDir = `C:/Users/BRIAN~1.FER/AppData/Local/Temp/cdp-prof-${process.pid}-${port}`; + fs.rmSync(userDataDir, { recursive: true, force: true }); + + const chrome = spawn(CHROME, [ + '--headless', + `--remote-debugging-port=${port}`, + `--user-data-dir=${userDataDir}`, + '--no-first-run', + '--no-default-browser-check', + '--disable-gpu', + '--enable-unsafe-swiftshader', + '--hide-scrollbars', + `--window-size=${width},${height}`, + 'about:blank', + ], { stdio: 'ignore' }); + + for (let i = 0; i < waitMs / 100; i++) { + try { + const r = await fetch(`http://127.0.0.1:${port}/json/version`); + if (r.ok) break; + } catch { /* not up yet */ } + await sleep(100); + } + + // A fresh tab at the target URL (PUT = newer Chrome, POST = older). + const newUrl = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(targetUrl || 'about:blank')}`; + let target; + try { + target = await (await fetch(newUrl, { method: 'PUT' })).json(); + } catch { + target = await (await fetch(newUrl, { method: 'POST' })).json(); + } + + const kill = () => { + chrome.kill(); + // Chrome may still hold profile files for a moment after kill; retry politely. + (async () => { + for (let i = 0; i < 12; i++) { + try { fs.rmSync(userDataDir, { recursive: true, force: true }); return; } catch { await sleep(250); } + } + })(); + }; + return { url: target.webSocketDebuggerUrl, kill }; +} + +/** Connect a CDP session to an already-running page. */ +export class CDP { + constructor(wsUrl) { + this.ws = new WebSocket(wsUrl); + this.id = 0; + this.pending = new Map(); + this.listeners = new Map(); + this.ws.onmessage = (m) => { + const msg = JSON.parse(m.data); + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id); + this.pending.delete(msg.id); + msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result); + } else if (msg.method && this.listeners.has(msg.method)) { + for (const fn of this.listeners.get(msg.method)) fn(msg.params); + } + }; + } + open() { return new Promise((r, j) => { this.ws.onopen = r; this.ws.onerror = j; }); } + send(method, params = {}) { + return new Promise((resolve, reject) => { + const mid = ++this.id; + this.pending.set(mid, { resolve, reject }); + this.ws.send(JSON.stringify({ id: mid, method, params })); + }); + } + on(method, fn) { + if (!this.listeners.has(method)) this.listeners.set(method, []); + this.listeners.get(method).push(fn); + } + close() { try { this.ws.close(); } catch { /* already closed */ } } +} + +// ---- CLI entry ------------------------------------------------------ +if (process.argv[1] && /cdpshot\.mjs$/.test(process.argv[1])) { + const [url, out, waitMs = '7000', W = '1280', H = '720'] = process.argv.slice(2); + const { url: wsUrl, kill } = await launch(url, undefined, Number(W), Number(H)); + const cdp = new CDP(wsUrl); + try { + await cdp.open(); + await cdp.send('Page.enable'); + await cdp.send('Runtime.enable'); + await cdp.send('Page.navigate', { url }); + await sleep(Number(waitMs)); + const shot = await cdp.send('Page.captureScreenshot', { format: 'png' }); + fs.writeFileSync(out, Buffer.from(shot.data, 'base64')); + console.log('saved', out); + } finally { + cdp.close(); + kill(); + } +} diff --git a/docs/PROJECT_NOTES.md b/docs/PROJECT_NOTES.md index ee52c43..42a578f 100644 --- a/docs/PROJECT_NOTES.md +++ b/docs/PROJECT_NOTES.md @@ -151,7 +151,24 @@ unclaimed". Model & seams: - Pinned: **Phaser 4.2.1** ("Giedi"), vendored in `lib/phaser.min.js`. - App code is v4-specific where v4 changed things (e.g. `Phaser.Math.Angle.RotateTo`, `banner: false`, `Clamp(value, min, max)`). -- To upgrade: replace the vendored file + note the version here. +- **v4 quirks that bite (verified against this build, Sept 2026):** + - The engine calls a scene's `update(time, delta)` directly but does **not** + step the scene's `TimeClock` or `TweenManager` (the v3 scene-events + PRE_UPDATE/UPDATE plumbing is not fired in the v4 loop). Each scene must + call `this.time.update(time, delta)` and `this.tweens.update()` at the top + of `update()`, or `delayedCall`/`addEvent`/tweens silently never run. + (Done in `MenuScene.update` / `GameScene.update`.) + - A GameObject constructed with `new` (e.g. our `Container` subclasses in + `js/ui/`) is **not** added to the scene display list — the `scene.add.*` + factories do that. Call `scene.add.existing(this)` in the constructor + (Ship/Planet already do; GlitchText/MenuButton now do too). + - Text colors go straight to the canvas: v4 writes `fillStyle = + style.color`, so a **numeric** color is an invalid fillStyle and the text + silently renders **black**. Text styles and `setColor()` must get CSS + strings — use `toCss()` from `js/utils/Color.js` (keep `toColor()` for + Graphics/shape APIs). +- To upgrade: replace the vendored file + note the version here (and re-check + the quirks above — they may go away). ## Roadmap (working list, intentionally rough) diff --git a/index.html b/index.html index 83f6900..6c1eb57 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,22 @@ Orbit