From e14e43da427c4a2e6d7c5e183c258ec779d04279 Mon Sep 17 00:00:00 2001 From: francy51 Date: Mon, 25 May 2026 13:00:20 -0400 Subject: [PATCH] Initial commit --- .gitignore | 2 + combat-hud-2.html | 1193 +++++++++++ combat-hud-game.html | 1467 ++++++++++++++ combat-hud.html | 1192 +++++++++++ css/base.css | 71 + css/components.css | 272 +++ css/layout.css | 224 +++ css/tokens.css | 80 + game-hud.html | 1745 +++++++++++++++++ gap-analysis.md | 305 +++ index.html | 969 +++++++++ js/app.js | 157 ++ js/components/sidebar.js | 79 + js/components/topbar.js | 46 + js/demos/bounty.js | 425 ++++ js/demos/chat.js | 394 ++++ js/demos/combat.js | 1044 ++++++++++ js/demos/fitting.js | 377 ++++ js/demos/galaxy.js | 1429 ++++++++++++++ js/demos/gamehud.js | 497 +++++ js/demos/market.js | 1182 +++++++++++ js/demos/movement.js | 850 ++++++++ js/demos/progression.js | 381 ++++ js/demos/refining.js | 525 +++++ js/demos/starmap.js | 1044 ++++++++++ js/demos/zora.js | 495 +++++ js/fake-backend.js | 278 +++ js/lib/three-helpers.js | 836 ++++++++ js/loader.js | 172 ++ js/pages/agents.js | 673 +++++++ js/pages/architecture.js | 481 +++++ js/pages/backend.js | 676 +++++++ js/pages/demo-gallery.js | 186 ++ js/pages/economy.js | 1050 ++++++++++ js/pages/gameplay.js | 1430 ++++++++++++++ js/pages/overview.js | 315 +++ js/pages/risks.js | 142 ++ js/pages/roadmap.js | 303 +++ js/pages/ship-ai.js | 1700 ++++++++++++++++ js/pages/ships.js | 555 ++++++ js/pages/social.js | 590 ++++++ js/pages/techstack.js | 174 ++ js/router.js | 25 + js/state.js | 30 + ...like_multiplayer_prototype_design_doc.docx | Bin 0 -> 46949 bytes mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png | Bin 0 -> 316795 bytes mph38gxn-drawing-2026-05-22T15-42-07-328Z.png | Bin 0 -> 279665 bytes mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png | Bin 0 -> 251985 bytes void-nav-game-hud.html | 831 ++++++++ 49 files changed, 26892 insertions(+) create mode 100644 .gitignore create mode 100644 combat-hud-2.html create mode 100644 combat-hud-game.html create mode 100644 combat-hud.html create mode 100644 css/base.css create mode 100644 css/components.css create mode 100644 css/layout.css create mode 100644 css/tokens.css create mode 100644 game-hud.html create mode 100644 gap-analysis.md create mode 100644 index.html create mode 100644 js/app.js create mode 100644 js/components/sidebar.js create mode 100644 js/components/topbar.js create mode 100644 js/demos/bounty.js create mode 100644 js/demos/chat.js create mode 100644 js/demos/combat.js create mode 100644 js/demos/fitting.js create mode 100644 js/demos/galaxy.js create mode 100644 js/demos/gamehud.js create mode 100644 js/demos/market.js create mode 100644 js/demos/movement.js create mode 100644 js/demos/progression.js create mode 100644 js/demos/refining.js create mode 100644 js/demos/starmap.js create mode 100644 js/demos/zora.js create mode 100644 js/fake-backend.js create mode 100644 js/lib/three-helpers.js create mode 100644 js/loader.js create mode 100644 js/pages/agents.js create mode 100644 js/pages/architecture.js create mode 100644 js/pages/backend.js create mode 100644 js/pages/demo-gallery.js create mode 100644 js/pages/economy.js create mode 100644 js/pages/gameplay.js create mode 100644 js/pages/overview.js create mode 100644 js/pages/risks.js create mode 100644 js/pages/roadmap.js create mode 100644 js/pages/ship-ai.js create mode 100644 js/pages/ships.js create mode 100644 js/pages/social.js create mode 100644 js/pages/techstack.js create mode 100644 js/router.js create mode 100644 js/state.js create mode 100644 mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx create mode 100644 mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png create mode 100644 mph38gxn-drawing-2026-05-22T15-42-07-328Z.png create mode 100644 mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png create mode 100644 void-nav-game-hud.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3657ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.od-skills/ +*.artifact.json diff --git a/combat-hud-2.html b/combat-hud-2.html new file mode 100644 index 0000000..725ae3a --- /dev/null +++ b/combat-hud-2.html @@ -0,0 +1,1193 @@ + + + + + + + VOID::NAV — Combat HUD + + + +
+ + + + + + + + + + + diff --git a/combat-hud-game.html b/combat-hud-game.html new file mode 100644 index 0000000..221115a --- /dev/null +++ b/combat-hud-game.html @@ -0,0 +1,1467 @@ + + + + + +COMBAT HUD — VOID NAV + + + + + + + +
+ + + + + + diff --git a/combat-hud.html b/combat-hud.html new file mode 100644 index 0000000..bc1056b --- /dev/null +++ b/combat-hud.html @@ -0,0 +1,1192 @@ + + + + + + VOID::NAV — Combat HUD + + + +
+ + + + + + + + + + + diff --git a/css/base.css b/css/base.css new file mode 100644 index 0000000..bbefd3b --- /dev/null +++ b/css/base.css @@ -0,0 +1,71 @@ +/* ============================================================ + Base — Reset + Typography + ============================================================ */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { + font-size: 14px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +body { + font-family: var(--font-body); + color: var(--fg); + background: var(--bg); + line-height: 1.6; + overflow: hidden; + height: 100vh; +} + +#root { height: 100vh; display: flex; } + +a { color: var(--cyan); text-decoration: none; transition: color var(--transition-fast); } +a:hover { color: var(--accent); } + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-display); + color: var(--fg-bright); + line-height: 1.25; + letter-spacing: -0.01em; +} +h1 { font-size: 2rem; font-weight: 700; } +h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: var(--sp-4); } +h3 { font-size: 1.2rem; font-weight: 600; margin-bottom: var(--sp-3); } +h4 { font-size: 1.05rem; font-weight: 600; margin-bottom: var(--sp-2); } + +p { margin-bottom: var(--sp-4); } + +code { + font-family: var(--font-mono); + font-size: 0.9em; + background: var(--surface-raised); + padding: 2px 6px; + border-radius: var(--radius-sm); + color: var(--accent); +} + +pre { + font-family: var(--font-mono); + font-size: 0.85rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: var(--sp-4); + overflow-x: auto; + line-height: 1.5; + margin-bottom: var(--sp-4); +} + +ul, ol { padding-left: var(--sp-6); margin-bottom: var(--sp-4); } +li { margin-bottom: var(--sp-1); } + +::selection { background: var(--accent); color: var(--bg); } + +* { scrollbar-width: thin; scrollbar-color: var(--border-light) transparent; } + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: var(--radius-pill); } +::-webkit-scrollbar-thumb:hover { background: var(--muted); } diff --git a/css/components.css b/css/components.css new file mode 100644 index 0000000..7ddb772 --- /dev/null +++ b/css/components.css @@ -0,0 +1,272 @@ +/* ============================================================ + Components — Shared UI elements + ============================================================ */ + +/* ---------- Card ---------- */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-6); + margin-bottom: var(--sp-6); +} +.card-accent { + border-left: 3px solid var(--accent); +} + +/* ---------- Pill / Badge ---------- */ +.pill { + font-family: var(--font-mono); + font-size: 0.7rem; + padding: 2px 10px; + border-radius: var(--radius-pill); + display: inline-flex; + align-items: center; + gap: var(--sp-1); + white-space: nowrap; +} +.pill-amber { background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent-border); } +.pill-cyan { background: var(--cyan-bg); color: var(--cyan); border: 1px solid rgba(34,211,238,0.25); } +.pill-green { background: var(--green-bg); color: var(--green); border: 1px solid rgba(34,197,94,0.25); } +.pill-red { background: var(--red-bg); color: var(--red); border: 1px solid rgba(239,68,68,0.25); } +.pill-purple { background: var(--purple-bg); color: var(--purple); border: 1px solid rgba(167,139,250,0.25); } + +/* ---------- Data Table ---------- */ +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} +.data-table th { + font-family: var(--font-mono); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--muted); + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} +.data-table td { + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + color: var(--fg); + font-family: var(--font-mono); + font-size: 0.8rem; +} +.data-table tr:hover td { background: var(--surface-raised); } +.data-table .mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } + +/* ---------- Code Block ---------- */ +.code-block { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: var(--sp-4) var(--sp-5); + overflow-x: auto; + margin-bottom: var(--sp-5); +} +.code-block code { + font-family: var(--font-mono); + font-size: 0.82rem; + line-height: 1.6; + color: var(--fg-dim); + background: none; + padding: 0; +} +.code-block .kw { color: var(--purple); } +.code-block .str { color: var(--green); } +.code-block .cm { color: var(--muted); font-style: italic; } +.code-block .type { color: var(--cyan); } +.code-block .fn { color: var(--accent); } + +/* ---------- Stat Grid ---------- */ +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: var(--sp-4); + margin-bottom: var(--sp-6); +} +.stat-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-5); +} +.stat-value { + font-family: var(--font-mono); + font-size: 1.6rem; + font-weight: 700; + color: var(--fg-bright); + font-variant-numeric: tabular-nums; +} +.stat-label { + font-size: 0.75rem; + color: var(--muted); + margin-top: var(--sp-1); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* ---------- Section Header ---------- */ +.section-header { + display: flex; + align-items: center; + gap: var(--sp-3); + margin-bottom: var(--sp-5); + padding-bottom: var(--sp-3); + border-bottom: 1px solid var(--border); +} +.section-header .section-num { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--accent); + background: var(--accent-bg); + padding: 2px 8px; + border-radius: var(--radius-pill); +} + +/* ---------- Roadmap Phase ---------- */ +.phase-item { + display: flex; + gap: var(--sp-4); + margin-bottom: var(--sp-5); +} +.phase-marker { + display: flex; + flex-direction: column; + align-items: center; + min-width: 40px; +} +.phase-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 8px rgba(240, 160, 48, 0.3); +} +.phase-dot.future { background: var(--border-light); box-shadow: none; } +.phase-line { + flex: 1; + width: 2px; + background: var(--border); + margin-top: var(--sp-1); +} +.phase-content { flex: 1; } + +/* ---------- Demo Container ---------- */ +.demo-container { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + margin-bottom: var(--sp-6); +} +/* Inner panels inside demos that need independent scrolling */ +.demo-scroll-panel { + overflow-y: auto; + overscroll-behavior: contain; + flex-shrink: 0; +} +.demo-toolbar { + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + background: var(--surface-raised); + border-bottom: 1px solid var(--border); + font-size: 0.8rem; +} +.demo-toolbar .demo-title { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.05em; +} +.demo-canvas-wrap { + position: relative; + background: var(--bg); + min-height: 400px; +} +.demo-canvas-wrap canvas { + display: block; + width: 100%; + height: 100%; +} +.demo-sidebar { + width: 280px; + background: var(--surface); + border-left: 1px solid var(--border); + padding: var(--sp-4); + overflow-y: auto; + font-size: 0.8rem; +} + +/* ---------- Button ---------- */ +.btn { + font-family: var(--font-body); + font-size: 0.8rem; + font-weight: 500; + padding: var(--sp-2) var(--sp-4); + border-radius: var(--radius-md); + border: 1px solid var(--border); + background: var(--surface-raised); + color: var(--fg); + cursor: pointer; + transition: all var(--transition-fast); + display: inline-flex; + align-items: center; + gap: var(--sp-2); +} +.btn:hover { border-color: var(--border-light); background: var(--surface-hover); } +.btn-primary { + background: var(--accent); + color: var(--bg); + border-color: var(--accent); + font-weight: 600; +} +.btn-primary:hover { background: var(--accent-hover); } +.btn-sm { padding: var(--sp-1) var(--sp-3); font-size: 0.75rem; } +.btn-danger { border-color: var(--red-dim); color: var(--red); } +.btn-danger:hover { background: var(--red-bg); } + +/* ---------- Tags / Keywords ---------- */ +.tag-list { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-4); } + +/* ---------- Alert / Callout ---------- */ +.callout { + padding: var(--sp-4) var(--sp-5); + border-radius: var(--radius-md); + margin-bottom: var(--sp-5); + font-size: 0.85rem; + line-height: 1.5; +} +.callout-info { background: var(--cyan-bg); border: 1px solid rgba(34,211,238,0.2); color: var(--cyan); } +.callout-warn { background: var(--accent-bg); border: 1px solid var(--accent-border); color: var(--accent); } +.callout-danger { background: var(--red-bg); border: 1px solid rgba(239,68,68,0.2); color: var(--red); } + +/* ---------- Grid Layouts ---------- */ +.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); } +.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--sp-5); } +@media (max-width: 900px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } } + +/* ---------- Animations ---------- */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +/* ---------- Progress Bar ---------- */ +.progress-bar { + height: 4px; + background: var(--border); + border-radius: var(--radius-pill); + overflow: hidden; +} +.progress-bar .fill { + height: 100%; + border-radius: var(--radius-pill); + transition: width var(--transition-base); +} diff --git a/css/layout.css b/css/layout.css new file mode 100644 index 0000000..a19af2c --- /dev/null +++ b/css/layout.css @@ -0,0 +1,224 @@ +/* ============================================================ + Layout — App Shell + ============================================================ */ +.app-shell { + display: flex; + width: 100%; + height: 100vh; + overflow: hidden; +} + +/* ---------- Sidebar ---------- */ +.sidebar { + width: var(--sidebar-width); + min-width: var(--sidebar-width); + height: 100vh; + background: var(--surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + overflow: hidden; + transition: width var(--transition-base), min-width var(--transition-base); + z-index: 10; +} +.sidebar.collapsed { + width: var(--sidebar-collapsed); + min-width: var(--sidebar-collapsed); +} + +.sidebar-header { + padding: var(--sp-4) var(--sp-5); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: var(--sp-3); + min-height: var(--topbar-height); +} +.sidebar.collapsed .sidebar-header { padding: var(--sp-4); justify-content: center; } + +.sidebar-logo { + font-family: var(--font-mono); + font-size: 0.8rem; + font-weight: 700; + color: var(--accent); + letter-spacing: 0.05em; + text-transform: uppercase; + white-space: nowrap; + overflow: hidden; +} +.sidebar.collapsed .sidebar-logo { font-size: 0; } +.sidebar-logo .logo-dot { color: var(--cyan); } + +.sidebar-nav { + flex: 1; + overflow-y: auto; + padding: var(--sp-3) var(--sp-3); +} +.sidebar.collapsed .sidebar-nav { padding: var(--sp-2) var(--sp-2); } + +.nav-section-title { + font-family: var(--font-mono); + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--muted); + padding: var(--sp-4) var(--sp-3) var(--sp-2); + white-space: nowrap; + overflow: hidden; +} +.sidebar.collapsed .nav-section-title { font-size: 0; padding: var(--sp-2); height: 8px; } + +.nav-item { + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-2) var(--sp-3); + border-radius: var(--radius-md); + color: var(--fg-dim); + font-size: 0.85rem; + cursor: pointer; + transition: all var(--transition-fast); + white-space: nowrap; + overflow: hidden; + user-select: none; + margin-bottom: 2px; + text-decoration: none; +} +.nav-item:hover { + background: var(--surface-raised); + color: var(--fg-bright); +} +.nav-item.active { + background: var(--accent-bg); + color: var(--accent); + border: 1px solid var(--accent-border); +} +.nav-item .nav-icon { + font-size: 1rem; + min-width: 20px; + text-align: center; +} +.sidebar.collapsed .nav-item { padding: var(--sp-3); justify-content: center; } +.sidebar.collapsed .nav-item .nav-label { display: none; } + +.nav-badge { + font-family: var(--font-mono); + font-size: 0.65rem; + background: var(--accent-bg); + color: var(--accent); + padding: 1px 6px; + border-radius: var(--radius-pill); + margin-left: auto; +} +.sidebar.collapsed .nav-badge { display: none; } + +/* ---------- Main Area ---------- */ +.main-area { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; +} + +/* ---------- Top Bar ---------- */ +.topbar { + height: var(--topbar-height); + min-height: var(--topbar-height); + background: var(--surface); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + padding: 0 var(--sp-5); + gap: var(--sp-4); + font-size: 0.8rem; +} + +.topbar-toggle { + background: none; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--muted); + cursor: pointer; + padding: var(--sp-1) var(--sp-2); + font-size: 1rem; + transition: all var(--transition-fast); + display: flex; + align-items: center; +} +.topbar-toggle:hover { color: var(--fg); border-color: var(--border-light); } + +.topbar-breadcrumb { + font-family: var(--font-mono); + color: var(--muted); + font-size: 0.75rem; + display: flex; + align-items: center; + gap: var(--sp-2); +} +.topbar-breadcrumb .bc-sep { color: var(--border-light); } +.topbar-breadcrumb .bc-current { color: var(--fg-bright); } + +.topbar-status { + margin-left: auto; + display: flex; + align-items: center; + gap: var(--sp-4); + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--muted); +} + +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + display: inline-block; + margin-right: var(--sp-1); +} +.status-dot.online { background: var(--green); box-shadow: 0 0 6px var(--green); } +.status-dot.offline { background: var(--red); } + +/* ---------- Content ---------- */ +.content { + flex: 1; + overflow-y: auto; + min-height: 0; /* allow flex child to shrink and scroll */ + padding: var(--sp-8) var(--sp-10); + background: var(--bg); + overscroll-behavior: contain; +} + +.content-inner { + max-width: var(--content-max-width); + margin: 0 auto; +} + +/* ---------- Fullscreen Demo ---------- */ +.app-shell.fullscreen-demo { + background: #000; +} +.app-shell.fullscreen-demo .main-area { + flex: 1; + overflow: hidden; /* main-area contains the scroll child */ +} +.app-shell.fullscreen-demo .content--flush { + padding: 0; + overflow-y: auto; + flex: 1; + min-height: 0; /* allow flex child to shrink and scroll */ + overscroll-behavior: contain; +} + +/* ---------- Responsive ---------- */ +@media (max-width: 768px) { + .sidebar { + position: fixed; + left: 0; + top: 0; + transform: translateX(-100%); + z-index: 100; + } + .sidebar.open { transform: translateX(0); } + .content { padding: var(--sp-4) var(--sp-5); } +} diff --git a/css/tokens.css b/css/tokens.css new file mode 100644 index 0000000..fcd6ddf --- /dev/null +++ b/css/tokens.css @@ -0,0 +1,80 @@ +/* ============================================================ + Star Trek LCARS × Modern Minimal — Design Tokens + ============================================================ */ +:root { + /* Core palette */ + --bg: #080c14; + --bg-subtle: #0b1120; + --surface: #0f1623; + --surface-raised: #162032; + --surface-hover: #1c2d45; + --fg: #d4dce8; + --fg-bright: #f1f5f9; + --fg-dim: #94a3b8; + --muted: #5a6b82; + --border: #1c2a3f; + --border-light: #253550; + + /* Accent — LCARS amber/gold */ + --accent: #f0a030; + --accent-hover: #fbbf24; + --accent-dim: #b47818; + --accent-bg: rgba(240, 160, 48, 0.08); + --accent-border: rgba(240, 160, 48, 0.25); + + /* Secondary accents */ + --cyan: #22d3ee; + --cyan-dim: #0891b2; + --cyan-bg: rgba(34, 211, 238, 0.08); + --red: #ef4444; + --red-dim: #dc2626; + --red-bg: rgba(239, 68, 68, 0.08); + --green: #22c55e; + --green-dim: #16a34a; + --green-bg: rgba(34, 197, 94, 0.08); + --purple: #a78bfa; + --purple-dim: #8b5cf6; + --purple-bg: rgba(167, 139, 250, 0.08); + + /* Typography */ + --font-display: 'SF Pro Display', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + --font-body: 'SF Pro Text', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, Menlo, monospace; + + /* Spacing */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 20px; + --sp-6: 24px; + --sp-8: 32px; + --sp-10: 40px; + --sp-12: 48px; + --sp-16: 64px; + + /* Radii — LCARS inspired (pill shapes) */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 20px; + --radius-pill: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0,0,0,0.3); + --shadow-md: 0 4px 12px rgba(0,0,0,0.4); + --shadow-lg: 0 8px 32px rgba(0,0,0,0.5); + --shadow-glow-accent: 0 0 20px rgba(240, 160, 48, 0.15); + --shadow-glow-cyan: 0 0 20px rgba(34, 211, 238, 0.15); + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-base: 250ms ease; + --transition-slow: 400ms ease; + + /* Layout */ + --sidebar-width: 260px; + --sidebar-collapsed: 60px; + --topbar-height: 48px; + --content-max-width: 1100px; +} diff --git a/game-hud.html b/game-hud.html new file mode 100644 index 0000000..6cd3c76 --- /dev/null +++ b/game-hud.html @@ -0,0 +1,1745 @@ + + + + + + VOID::NAV — Game HUD + + + +
+ + + + + + + + diff --git a/gap-analysis.md b/gap-analysis.md new file mode 100644 index 0000000..1c4732c --- /dev/null +++ b/gap-analysis.md @@ -0,0 +1,305 @@ +# Gap Analysis — EVE-Inspired Multiplayer Prototype GDD + +**Date:** 2026-05-24 +**Last Updated:** 2026-05-25 (Session 7) +**Scope:** Full cross-reference of design documentation vs. interactive demos vs. roadmap readiness + +--- + +## Session 7 Progress (2026-05-25) + +Comprehensive fresh gap analysis identified 20 missing specs. The 3 critical blockers for Phase 0 have been addressed: + +| Gap | Resolution | +|---|---| +| **Galaxy Generation Spec** (Critical #1) | Added "Galaxy Generation" sub-tab to Backend → Galaxy Simulation. Concrete parameters: 4 regions, ~50 systems (MVP), Poisson disk placement, MST + random edges stargate topology, starter system template, faction territory seeding, station/belt placement rules, full pseudocode. Deterministic seeded RNG. | +| **Ship Acquisition Flow** (Critical #2) | Added "🚀 Acquisition" tab to Ships page. Free Rookie Frigate on spawn and death respawn. 5 acquisition methods (free grant, NPC market, player market, manufacturing, LP store). NPC ship pricing table. Ship switching flow (dock → hangar → select → activate). Hangar storage model. Backend schema changes (storage_location, is_rookie, switch_ship reducer). | +| **Warp & Travel Mechanics** (Critical #3) | Added "🚀 Travel & Warp" tab to Gameplay page. 3 travel modes (sub-warp, warp, gate jump) with speeds. Warp sequence (align → accelerate → cruise → exit). Warp speeds by ship class (3.0–6.0 AU/s). Stargate mechanics (2.5km activation, 5s jump cooldown, 30s gate cloak). Gate guns by security level. Docking/undocking with invulnerability. Autopilot vs manual piloting. Warp disruption. Backend state columns (travel_mode, gate_cloak_until, weapons_timer_until, etc.). | + +### Remaining 17 Gaps (from 20-item analysis) + +| Priority | Gap | Status | +|---|---|---| +| 🟠 Important | Mining Mechanics Detail (cycle time, yield, depletion) | Not started | +| 🟠 Important | Module Activation & Cycle System | Not started | +| 🟠 Important | Damage Types & Resistances | Not started | +| 🟠 Important | Stargate Mechanics | **Resolved** — covered in Travel & Warp tab | +| 🟠 Important | Multiplayer Combat (Phase 13) | Not started | +| 🟠 Important | Scanning / Exploration System | Not started | +| 🟠 Important | Fleet System | Not started | +| 🟡 Nice-to-have | Contract system | Not started | +| 🟡 Nice-to-have | Cloaking/stealth | Not started | +| 🟡 Nice-to-have | Server admin/GM tools | Not started | +| 🟡 Nice-to-have | Settings/preferences panel | Not started | +| 🟡 Nice-to-have | Performance targets | Not started | +| 🟡 Nice-to-have | Galaxy Generation demo | **Resolved** — Interactive Galaxy Generation demo created. Validates seeded RNG, region/constellation/system placement, MST stargate topology, station/belt seeding, security distribution, connectivity check, route finding. 12th demo. | +| 🟡 Nice-to-have | Direct player-to-player trade | Not started | +| 🟡 Nice-to-have | Death/respawn full UX flow | Partially addressed (respawn mechanism defined in Ship Acquisition) | +| 🟡 Nice-to-have | Corp backend tables missing from master list | Not started | +| 🟡 Nice-to-have | Progression demo XP curve mismatch | Not started | + +--- + +## Session 6 Progress (2026-05-24) + +All remaining gap analysis items have been addressed: + +| Gap | Resolution | +|---|---| +| **Era 1 System Map demo** | Added to demo gallery as a known-needed demo. Listed in demo gallery header note. Implementation is Phase 1 scope (after movement model is built). | +| **Missing backend tables (factions, regions, constellations)** | Added `regions`, `constellations`, and `factions` to Backend → Tables tab with full field descriptions. `systems` table now has `constellation_id` FK. | +| **Debug Panel spec** | Expanded from 4 bullet points to 8 items in Gameplay → Screen Specifications. Added: SpacetimeDB table row counts, Agent tick scheduler status, Force-spawn controls (dev mode), Game time display. | +| **Tutorial / Onboarding spec** | Already resolved in Session 5 (OV-05). 5-mission guided sequence, tutorial principles, Zora as guide, skip mechanics, stuck-player detection. | +| **Error handling / Reconnection spec** | Added to Architecture → ARCH-4. 7 disconnection scenarios, reconnection flow with exponential backoff, anti-exploit (combat logging) rules. | +| **Session persistence / Save-Load spec** | Added to Architecture → ARCH-5. No save button, no localStorage. SpacetimeDB is continuous persistence. Full table-by-table persistence guarantee. | +| **Sound / Audio design spec** | Added to Architecture → ARCH-6. 6 audio categories, 6 volume sliders, spatial audio rules, Phase 7 scope. | +| **Localization / i18n decision** | Added to Architecture → ARCH-7. MVP English-only with day-one i18n architecture (string keys, Intl formatters, RTL-ready CSS). | +| **Accessibility spec** | Added to Architecture → ARCH-8. 8 accessibility areas with requirements and implementation details. Gate 4 acceptance tests. | +| **Corporations & Territory spec** | Added as new tab to Social page (SOC-CORP). Full spec: corp lifecycle, 5 roles, wallet/tax system, territory & sovereignty (3 structure tiers), 6 new backend tables, 11 new reducers. Phase 14 scope. | + +--- + +## Session 5 Progress (2026-05-24) + +The following gaps have been addressed based on a full 15-point critique review: + +| Gap | Resolution | +|---|---| +| **Economy first-30-minutes walkthrough** | Added "First 30 Minutes" tab to Economy page (ECON-30). 14-step walkthrough from spawn to loop-set, emphasizing price discovery as the aha moment. | +| **Power allocation failure modes** | Added GP-FAIL section to Gameplay → Core Loop tab. Per-subsystem failure table (Weapons→no firing, Shields→no recharge, Engines→immobile, Aux→no specials). Reroute timing (1.5–3s). | +| **Flight Mode HUD cognitive load** | Added Red Alert mode to GP-FAIL. When shields <25% and taking armor damage, non-essential HUD elements collapse. Combat HUD expands. Cannot be disabled. | +| **Roadmap integration gates** | Added 6 integration gates to Roadmap page (Gate 1–6). Each gate covers a phase group and defines a focused playtest. | +| **Gap analysis partial resolutions** | (a) Bounty is now sector-specific (petty=system-local, standard=regional, dangerous+=galaxy-wide). (b) AI module examples added to both fitting examples in Ships page. (c) Onboarding/tutorial section added to Overview → OV-05 with 5-mission guided sequence. | +| **Zora phased delivery** | Added phased delivery milestones table to Ship AI → Implementation Tiers section. Maps Zora features to roadmap phases (Phase 0 stub → Phase 7 Tier 0 complete → Phase 12 Tier 1 → Phase 15+ Tier 2). Full design kept intact. | +| **Faucet/sink untestable in Era 1** | Added Balancing Agent system (new tab: Gameplay → Balancing Agent). Monitors 5 metrics (ISK velocity, price index, death rate, faucet/sink ratio, engagement). Controls 4 levers (NPC spawn rate, difficulty tier, ISK faucet multiplier, world event frequency). New tables: balance_metrics, balance_levers, balance_audit. | +| **ER diagram** | Added ER Diagram tab to Backend page. 5 clusters (Player, Economy, World, Social, Ship AI) with all 50+ tables, PKs, FKs, and cross-cluster flow descriptions. | +| **Chinese characters** | Fixed: "PvP主力" → "PvP main combat ships" in Insurance table. | +| **Currency naming** | Added ISK temporary placeholder note to Economy → Flow Overview tab. All ISK references are subject to renaming. | +| **Security level 0.0 gap** | Fixed: Security 0.0 is explicitly null-sec. Added clarification to Gameplay → Security Levels intro text. | +| **Architecture page localStorage** | Fixed: Architecture overview now states "There is no localStorage — persistence is always through SpacetimeDB, even in single-player Era 1." | +| **No respec mentioned** | Added respec mechanics to Social → Progression tab (SOC-RESPEC). 20% XP penalty, 7-day cooldown, full or single-skill respec, requires Neural Remapping facility. | +| **Roadmap localStorage** | Fixed: Phase 0 goal now mentions "local SpacetimeDB instance". Phase 7 doneWhen says "SpacetimeDB persists all state — no localStorage". Era 1 subtitle reflects SpacetimeDB-from-day-1. | +| **World events missing lightweight Era 1 version** | Phase 7 doneWhen now includes "Lightweight exploration events spawn in visited systems." Integration Gate 4 validates exploration events as part of Era 1 completion. | + +--- + +## Session 4 Progress (2026-05-24) + +The following gaps have been addressed: + +| Gap | Resolution | +|---|---| +| **HUD ambiguity resolved** | Decision documented in Overview → OV-04 "HUD & View Mode Architecture": game uses two view modes — Flight Mode (diegetic overlays on 3D viewport when undocked) and Station Mode (traditional panel UI when docked). Map Mode is shared. Demo gallery updated: gamehud now labeled "Game HUD (Flight Mode)", starmap labeled "Star Map (Era 2 Galaxy Map)". Transition rules specified (undock, dock, open map, combat ambush). | +| **Starmap demo labeled correctly** | Demo gallery entry renamed from "Star Map" to "Star Map (Era 2 Galaxy Map)" with explicit validates/limitations noting it covers the Era 2 Galaxy Map, NOT the Era 1 System Map. Era 1 System Map still needs a separate demo. | +| **Mission system spec** | Full spec added to Gameplay → Missions tab: 6 mission types (Kill/Courier/Mining/Survey/Escort/Trade), NPC agent interaction flow (6 steps), standing mechanics (6 tiers from Hostile to Inner Circle), reward scaling table (4 levels), Loyalty Points secondary currency, 6 new backend tables (npc_agents, mission_templates, active_missions, player_standing, player_loyalty_points, mission_offers), mission-related reducers. | +| **World event UX spec** | Full spec added to Gameplay → World Events UX tab: 3 notification tiers (Critical/Nearby/Background) with Flight Mode and Station Mode behavior, Event Detail Panel layout (7 sections), event map integration (System Map Era 1 + Galaxy Map Era 2), contribution tracking with anti-AFK, 3 reward tiers (Bronze/Silver/Gold), Galaxy Story Log with search/export. | +| **Economy spec brought to Market demo level** | New "Market Surface" section added to Economy → Flow Overview tab: order book & depth with bid/ask spread, candlestick price history charts, contract specifications, margin accounts & long/short positions (Era 2), commodity ticker, station-filtered view. Acknowledges the Market demo implements all of these. | +| **Manufacturing depth expanded** | Manufacturing tab expanded from 2 cards to 4 sections: Full Production Chain (5-tier chain: Ore → Mineral → Component → Module → Ship), Blueprint Research with ME/TE levels and cost/time curves, Production Queues with concurrent job limits by skill, Station Facility tiers, BPC copies, and Invention (post-MVP T2). | +| **Backend tables for missions** | Added 6 new mission-related tables to Backend → Tables tab: npc_agents, mission_templates, active_missions, player_standing, player_loyalty_points, mission_offers. Total tables now 44. | + +--- + +## Session 3 Progress (2026-05-24) + +The following gaps have been addressed: + +| Gap | Resolution | +|---|---| +| **NPC price adjustment algorithm** | Full spec added to Economy → NPC Pricing tab: demand pressure algorithm with EMA, price formula (base × regional × demand × station type), worked tick-by-tick example, regional price seed table with 6 regions, anti-arbitrage safeguards, 3 new backend tables (`station_commodity_demand`, `commodity_price_params`, `regional_price_seeds`), `market_price_adjust` agent updated. | +| **Chat/comms prototype** | New interactive demo (`js/demos/chat.js`): 4 channels (Local/Trade/Private/Fleet), light-speed delay simulation with formula `2 × √(jumps)`, pre-seeded message corpus, pilot proximity sidebar with delay map, NPC auto-responses in local channel. Registered in sidebar, loader, app, and demo gallery. | +| **Zora Tier 0 demo** | New interactive demo (`js/demos/zora.js`): deterministic template engine with 15 event triggers × 5 soul depths, module gating logic (6 modules), personality axes sliders, response history. Validates the soul depth progression from raw status codes to full personality. Registered in sidebar, loader, app, and demo gallery. | +| **Backend tables for NPC pricing** | Added `station_commodity_demand`, `commodity_price_params`, `regional_price_seeds` to Backend → Tables tab. | +| **Agent updated for NPC pricing** | Updated `market_price_adjust` agent interval from 900s to 300s, added cross-reference to Economy → NPC Pricing tab. | + +--- + +## Session 2 Progress (2026-05-24) + +The following gaps from the original analysis have been addressed: + +| Gap | Resolution | +|---|---| +| **Currency naming** | Standardized to "ISK" with symbol `₢`. Removed all "credits" references. Updated overview, ships, roadmap. | +| **Chat scope ambiguity** | Core loop Step 8 now explicitly states "Requires multiplayer — ships in Phase 11 (Era 2)." Summary paragraph clarified Steps 1–7 as Era 1. | +| **Security level system** | Full spec added to Gameplay → Security Levels tab: 4 security bands (+1.0 to −1.0), player security status (−10 to +5), CONCORD response tiers, PvP rules per band, backend schema changes. | +| **NPC pirate AI behavior** | Full spec added to Gameplay → NPC Pirates tab: spawning rules, location-based triggers, difficulty tiers by security band, 4 behavior templates (orbit kiter, brawler, shield tank, EWAR support), state machine (idle→aggro→combat→flee→dead), new backend tables + agents. | +| **CONCORD response model** | Full spec added to Gameplay → CONCORD tab: response time by system sec (3s–15s), CONCORD force scaling, 6-step response pipeline, anti-exploit rules, suspect vs. criminal flag system, weapons timer. | +| **Insurance system** | Full spec added to Gameplay → Insurance tab: 4 coverage tiers (None/Basic/Standard/Platinum), premium/payout structure, ISK faucet/sink analysis, anti-abuse rules, new backend tables + reducers. | +| **Missing backend tables** | Added 13 new tables to Backend → Tables tab: `ship_types`, `modules_catalog`, `ship_fittings`, `npc_entities`, `npc_class_templates`, `loot_tables`, `blueprints`, `manufacturing_jobs`, `skills_catalog`, `chat_channels`, `insurance_policies`, `ship_type_base_values`. | +| **Missing agents** | Added `pirate_spawn`, `pirate_combat_tick`, `pirate_loot_drop`, `concord_response`, `security_status_tick` agents to catalog. | +| **AI Crew vs Zora confusion** | Added clarifying callout to Ships → AI Crew tab explaining the two systems are separate. | +| **Economy faucet/sink completeness** | Added Insurance Payout as explicit faucet and Insurance Premiums as explicit sink with cross-reference to the new spec. | + +--- + +## Executive Summary + +The GDD is remarkably thorough — 12 design pages, 9 interactive demos, a 16-phase roadmap, and a detailed backend schema. The documentation quality is high and internally consistent. However, several substantive gaps exist between **what is specified**, **what is prototyped**, and **what is ready for Phase 0 implementation**. These gaps fall into four categories: + +1. **Specified but not prototyped** — features with design detail but no interactive demo +2. **Prototyped but not fully specified** — demos that go beyond or diverge from the spec +3. **Cross-reference inconsistencies** — details that conflict between pages +4. **Missing specifications** — systems implied by the roadmap but not yet designed + +--- + +## 1. Specified but Not Prototyped + +These features have meaningful design documentation but no interactive demo to validate UX feel. + +| Feature | Spec Location | Gap Description | Risk | +|---|---|---|---| +| **Chat & Comms** | Social → Chat & Comms | ~~No chat demo exists.~~ Range-based propagation, light-speed delay, local/system channels are all specified but untested for UX feel. | **Resolved** — Chat & Comms demo created. Validates delay mechanics, channel switching, and pilot proximity. | +| **Galaxy Map** | Overview → Era 2 Screens | ~~The starmap demo shows a *system* map (single system scale). The *galaxy* map (region/constellation/system hierarchy with faction overlay, world event icons, migration routes) has no demo.~~ **Resolved:** Demo gallery now labels starmap as Era 2 Galaxy Map. The remaining gap is a separate Era 1 System Map demo. | **Partially Resolved** — Era 1 System Map still needs a demo. | +| **World Events** | Gameplay → Dynamic Galaxy / World Events UX | ~~No demo for world event spawning, propagation, participation, or the story log.~~ Full player-facing UX spec added to Gameplay → World Events UX tab: notification tiers, event detail panel, map integration, contribution tracking, reward tiers, story log. No interactive demo yet. | **Spec Resolved** — UX fully designed. Demo is nice-to-have, not blocking. | +| **NPC Economy Sim** | Economy → NPC Pricing | ~~The market demo shows a contract exchange with fixed seed data.~~ NPC pricing algorithm now fully specified with demand pressure model, regional seeds, and anti-arbitrage safeguards. No demo for full NPC supply/demand simulation over time yet. | **Spec Resolved** — Algorithm documented. Demo for full multi-station simulation is nice-to-have, not blocking. | +| **Manufacturing** | Economy → Manufacturing | ~~The refining demo covers ore → mineral but stops there. No demo for manufacturing jobs, blueprint research, production queues, or the mineral → module → ship chain.~~ Manufacturing tab expanded with full production chain (5 tiers), ME/TE research with cost curves, production queues, station facility levels, BPC copies, and invention. No demo yet. | **Spec Resolved** — Full manufacturing chain specified. Demo is Phase 5 scope. | +| **Ship AI (Zora)** | Ship AI (entire page) | ~~No demo at all.~~ Zora Tier 0 demo created validating deterministic template engine. Tier 1 (LLM-assisted) and Tier 2 (full agent) still unvalidated. | **Partially Resolved** — Tier 0 demo validates soul depth + module gating + personality axes. Higher tiers are post-MVP. | +| **Corporations & Territory** | ~~Mentioned in roadmap but zero design detail anywhere.~~ **Resolved:** Full spec added to Social → Corporations tab. Corp lifecycle, roles, wallet/tax, territory & sovereignty, 6 new backend tables, 11 new reducers. Phase 14 scope. | +| **Debug Panel** | ~~Listed as an Era 1 screen with minimal spec.~~ **Resolved:** Expanded to 8 items including SpacetimeDB row counts, agent scheduler status, force-spawn controls, game time display. Utility panel — no demo needed. | +| **Waypoints & Bookmarks** | Fully specified with backend tables, but no demo. The starmap demo doesn't integrate waypoint creation or route planning. | **Medium** — Navigation UX demo deferred to Phase 1 when System Map is built. Spec is complete. | + +--- + +## 2. Prototyped but Not Fully Specified (Demo Divergences) + +These demos contain features or behaviors not reflected in the design docs. + +| Demo | Divergence | Impact | +|---|---|---| +| **Market** | ~~The market demo implements a full *contract/commodities exchange* with bid/ask spread, price history charts, long/short positions, and margin accounts. The Economy page describes a simpler "order book, price per unit, place sell order from inventory" model. The demo is significantly more ambitious than the spec.~~ **Resolved:** Economy → Flow Overview tab now has a full "Market Surface" section covering order book depth, candlestick charts, contract specs, margin accounts, commodity ticker, and station-filtered view. Spec now matches the demo. | **Resolved** — Economy spec updated to match Market demo's feature set. | +| **Market** | Uses `₢` symbol for ISK in some places. The Overview page uses "ISK" and "credits" interchangeably. No canonical symbol defined. | **Inconsistency** — minor but should be standardized. | +| **Combat** | The combat demo implements 3D projectile rendering with beam/bolt types, subsystem damage, and multiple damage types (EM, thermal, kinetic, explosive). The spec mentions "generic damage" as a demo limitation but the actual demo goes further. | **Positive divergence** — spec should acknowledge what the demo actually validates. | +| **Combat** | Demo uses 4 power subsystems (Weapons/Shields/Engines/Aux). The spec consistently uses the same 4. This is consistent — no gap. | ✓ Aligned | +| **Game HUD** | ~~The gamehud demo renders a full 3D space scene with diegetic HUD overlays. This is more immersive than the "UI panels" model described in the Overview. The spec should clarify whether the final game uses diegetic overlays or traditional panel layout.~~ **Resolved:** Overview → OV-04 "HUD & View Mode Architecture" documents the decision: Flight Mode uses diegetic overlays (gamehud demo), Station Mode uses traditional panels (market/fitting/refining demos). Hybrid approach. | **Resolved** — HUD ambiguity resolved with two-view-mode architecture. | +| **Progression** | The progression demo uses a flat XP curve, but the Social page specifies an exponential curve (100 → 500 → 2,000 → 8,000 → 32,000). The demo's "limitations" callout acknowledges this but it means the demo doesn't validate the actual intended feel. | **Known limitation** — should be tracked for Phase 7. | + +--- + +## 3. Cross-Reference Inconsistencies + +Details that conflict or are unclear across pages. + +| Area | Inconsistency | Pages Involved | Resolution Needed | +|---|---|---|---| +| **Currency name** | ~~"ISK", "credits", and `₢` used interchangeably~~ | Overview, Economy, Market Demo | ✅ **Resolved:** Standardized to "ISK" with symbol `₢`. All "credits" references replaced. | +| **Ship AI module slot type** | The Ships page says "AI module slot type added to fitting schema" in Phase 4 done-when. The Ship AI page says AI modules occupy "medium or low, depending on the module." The Ships page doesn't have a dedicated AI slot column in the ship classes table. | Ships, Ship AI, Roadmap | The slot allocation model is clear in Ship AI (medium for comms/tactical/nav, low for economic/memory). The Ships page should reflect this in the fitting section or acknowledge the overlap. | +| **Ship AI Tier 0 timeline** | The Roadmap says "Tier 0 Zora" ships in Phase 7. The Ship AI page says Tier 0 = deterministic, no LLM. But Phase 7 also says "bare-bones soul state vector in SpacetimeDB." The Ship AI page's implementation tiers section says Tier 0 is "MVP launch." These are consistent but the overlap between Phase 7 and "MVP" should be explicit. | Roadmap, Ship AI | Clarify: is Phase 7 the Tier 0 launch, or does Tier 0 come later? The current text is consistent but could be more explicit. | +| **Galaxy map vs star system map** | ~~The Overview distinguishes between "3D Star-System Map" (Era 1, single system) and "Galaxy Map" (Era 2, multi-system). The starmap demo renders a multi-system galaxy view with warp routes. This is the Era 2 galaxy map, not the Era 1 system map.~~ **Resolved:** Demo gallery now labels starmap as "Star Map (Era 2 Galaxy Map)". Overview → OV-04 explicitly notes Era 1 System Map as needed. | **Partially Resolved** — Labeling fixed. Era 1 System Map demo still needed. | +| **Fitting affects combat** | The Ships page says "Fitting affects combat and mining stats" as Phase 4 done-when. The combat demo and fitting demo are independent — fitting changes don't carry over to combat. | Ships, Demo Gallery | Expected for demos; flagged for implementation planning. | +| **Information diffusion model** | The Economy page has a detailed diffusion model with specific propagation times (2 min adjacent, 5 min hub, 15 min region, 30 min galaxy). The Backend page's `propagate_market_data` reducer mentions "1 system per 2 minutes per jump" which is consistent. No gap, but the propagation pipeline has no demo. | Economy, Backend | Not a gap per se, but the diffusion pipeline is complex and unvalidated. | +| **Era 2 screens vs social features** | ~~Chat listed as core loop step but is Phase 11 (Era 2)~~ | Overview, Roadmap, Gameplay | ✅ **Resolved:** Gameplay page now explicitly states Steps 1–7 are Era 1, Step 8 requires multiplayer. | +| **Kill feed visibility** | Social page says "galaxy-wide feed." Bounty tiers have visibility ranges (system-local, regional, galaxy-wide). The kill feed demo shows all events everywhere. | Social, Bounty Demo | Kill feed should probably follow the same visibility tiering as bounties, or be explicitly always-galaxy-wide. | +| **AI crew vs Ship AI (Zora)** | ~~Two different AI systems with confusing shared terminology~~ | Ships → Crew, Ship AI | ✅ **Resolved:** Added clarifying callout to Ships → AI Crew tab explaining the two systems are separate. Future expansion may connect them post-MVP. | +| **NPC pirate spawning** | ~~No spec for pirate spawning, AI behavior, difficulty curves~~ | Gameplay, Agents, Combat Demo | ✅ **Resolved:** Full spec added to Gameplay → NPC Pirates tab. New agents added: `pirate_spawn`, `pirate_combat_tick`, `pirate_loot_drop`. | + +--- + +## 4. Missing Specifications + +Systems implied by the roadmap or cross-referenced but not yet designed. + +| Missing Spec | Referenced By | Impact | +|---|---|---| +| **Pirate / NPC AI behavior model** | ~~Gameplay (PvE Content), Roadmap Phase 3~~ | ✅ **Resolved:** Full spec added to Gameplay → NPC Pirates tab. | +| **Security level system** | ~~Gameplay (high-sec/low-sec/null-sec mentioned), Ships (security status mentioned), Social (high-sec attacks trigger CONCORD)~~ | ✅ **Resolved:** Full spec added to Gameplay → Security Levels tab. | +| **CONCORD / law enforcement** | ~~Social (piracy lowers security status), Gameplay (high-sec attacks trigger CONCORD response)~~ | ✅ **Resolved:** Full spec added to Gameplay → CONCORD tab. | +| **Insurance system** | ~~Economy (faucet: insurance payout), Ships (ship destruction section), Social (insurance premium sink)~~ | ✅ **Resolved:** Full spec added to Gameplay → Insurance tab. | +| **Mission system** | ~~Economy (faucet: mission rewards), Agents (npc_mission_refresh agent)~~ | ✅ **Resolved:** Full spec added to Gameplay → Missions tab. 6 mission types, agent interaction, standing, rewards, LP, backend tables. | +| **Tutorial / onboarding** | ~~Roadmap Phase 15, Overview OV-05~~ | ✅ **Resolved:** Full 5-mission tutorial spec in Overview → OV-05. Zora as guide. Skip allowed. Stuck detection. | +| **Error handling / reconnection** | ~~Roadmap Phase 15~~ | ✅ **Resolved:** Architecture → ARCH-4. 7 disconnection scenarios, reconnection flow with exponential backoff, anti-exploit rules. | +| **Session persistence / save-load** | ~~Roadmap Phase 7~~ | ✅ **Resolved:** Architecture → ARCH-5. No save button, no localStorage. SpacetimeDB is continuous persistence. Full table-by-table persistence guarantee. | +| **Sound / audio design** | ~~Ship AI Voice Synthesizer only~~ | ✅ **Resolved:** Architecture → ARCH-6. 6 audio categories, 6 volume sliders, spatial audio rules. | +| **Localization / i18n** | ~~Not mentioned~~ | ✅ **Resolved:** Architecture → ARCH-7. MVP English-only with day-one i18n architecture. | +| **Accessibility** | ~~Not mentioned~~ | ✅ **Resolved:** Architecture → ARCH-8. 8 accessibility areas, Gate 4 acceptance tests. | + +--- + +## 5. Backend Schema Gaps + +The Backend page lists 44 tables. Cross-referencing with the design docs reveals: + +| Missing Table | Referenced By | Note | +|---|---|---| +| `blueprints` | ~~Economy (Manufacturing)~~ | ✅ **Added** to Backend → Tables tab. | +| `manufacturing_jobs` | ~~Economy (Manufacturing), Agents (`production_cycle`)~~ | ✅ **Added** to Backend → Tables tab. | +| `ship_fittings` | ~~Ships (Fitting System)~~ | ✅ **Added** to Backend → Tables tab. | +| `modules_catalog` | ~~Ships (Fitting System)~~ | ✅ **Added** to Backend → Tables tab. | +| `ship_types` | ~~Ships (Ship Classes)~~ | ✅ **Added** to Backend → Tables tab. | +| `factions` | ~~Backend (Galaxy Simulation) lists it, not in Tables tab~~ | ✅ **Added** to Backend → Tables tab with full field descriptions. | +| `skills_catalog` | ~~Social (XP & Skills)~~ | ✅ **Added** to Backend → Tables tab. | +| `npc_entities` | ~~Gameplay (PvE), Agents (enemy_regen, aggro_scan)~~ | ✅ **Added** to Backend → Tables tab + NPC Pirates spec. | +| `loot_tables` | ~~Gameplay (combat drops)~~ | ✅ **Added** to Backend → Tables tab + NPC Pirates spec. | +| `regions` / `constellations` | ~~Backend (Galaxy Simulation) lists them, not in Tables tab~~ | ✅ **Added** to Backend → Tables tab. `systems` now has `constellation_id` FK. | +| `corporations` | ~~Roadmap Phase 14 — not designed~~ | ✅ **Specified** in Social → Corporations tab. 6 new tables added. Phase 14 scope. | +| `chat_channels` | ~~Social (Chat)~~ | ✅ **Added** to Backend → Tables tab. | + +--- + +## 6. Roadmap Readiness Assessment + +Assessing whether each Phase has enough specification to begin implementation. + +| Phase | Title | Spec Readiness | Blockers | +|---|---|---|---| +| **0** | Local Skeleton | ✅ Ready | None. Tech stack is specified. File structure is defined. | +| **1** | Movement & Commands | ✅ Ready | Movement model is fully specified in Backend → Movement Model. | +| **2** | Mining & Inventory | ✅ Ready | Mining cycle, inventory panel, and fake-backend data models exist. | +| **3** | Combat — FTL Power Allocation | ✅ Ready | ~~NPC pirate AI behavior was missing.~~ **Resolved:** NPC Pirates, Security Levels, CONCORD, and Insurance specs added to Gameplay page. | +| **4** | Ship Fitting | ✅ Ready | ~~`ship_types`, `modules_catalog`, and `ship_fittings` tables were missing.~~ **Resolved:** All three added to Backend → Tables tab. | +| **5** | Refining & Manufacturing | ✅ Ready | ~~Refining is well-specified. Manufacturing is described but `blueprints` and `manufacturing_jobs` tables were missing.~~ **Resolved:** Both tables added to Backend → Tables tab. | +| **6** | NPC Economy Sim | ✅ Ready | ~~Economy philosophy is clear, but NPC price adjustment logic, regional price seeding, and the full diffusion pipeline lack implementation detail beyond pseudocode.~~ **Resolved:** NPC pricing algorithm fully specified. Regional price seeds documented. Demand pressure model defined. Backend tables added. | +| **7** | Single-Player Polish | ✅ Ready | Tutorial/onboarding (OV-05), error handling (ARCH-4), persistence (ARCH-5), audio (ARCH-6), accessibility (ARCH-8) all fully specified. Tier 0 Zora demo exists. Mission system fully specified. | +| **8–15** | Era 2 | ✅ Ready | Era 2 depends on SpacetimeDB integration. Chat demo exists. World event UX fully specified. Corporations and territory fully specified (SOC-CORP). Multiplayer combat needs spec work (Phase 13 scope). | + +--- + +## 7. Priority Recommendations + +### Immediate (before starting Phase 0) + +1. ~~**Standardize currency naming**~~ ✅ **Done.** ISK + ₢ symbol. +2. ~~**Clarify core loop scope**~~ ✅ **Done.** Steps 1–7 = Era 1, Step 8 = Era 2. +3. ~~**Create `ship_types` and `modules_catalog` table specs**~~ ✅ **Done.** Added to Backend → Tables tab. + +### Before Phase 3 (Combat) + +4. ~~**Specify NPC pirate AI behavior**~~ ✅ **Done.** Gameplay → NPC Pirates tab. +5. ~~**Define security level system**~~ ✅ **Done.** Gameplay → Security Levels tab. +6. ~~**Specify CONCORD response model**~~ ✅ **Done.** Gameplay → CONCORD tab. + +### Before Phase 6 (Economy) + +7. ~~**Detail NPC price adjustment algorithm**~~ ✅ **Done.** Economy → NPC Pricing tab with full algorithm, regional seeds, anti-arbitrage safeguards, backend tables. +8. ~~**Create a chat prototype**~~ ✅ **Done.** Chat & Comms demo validates range-based propagation, light-speed delay, channel switching. + +### Before Era 2 + +9. ~~**Design the Galaxy Map**~~ ✅ **Done.** Era 2 Galaxy Map demo exists. Era 1 System Map is Phase 1 implementation scope. +10. ~~**World event UX prototype**~~ ✅ **Done.** Full UX spec added to Gameplay → World Events UX tab. +11. ~~**Clarify AI Crew vs Zora relationship**~~ ✅ **Done.** Added clarifying callout to Ships → AI Crew tab. +12. ~~**Corporations & Territory spec**~~ ✅ **Done.** Full spec added to Social → Corporations tab. 6 new tables, 11 new reducers, 3 structure tiers, sovereignty mechanics. Phase 14 scope. + +### Ongoing + +12. ~~**Add missing backend tables**~~ ✅ **Done.** All tables added to Backend → Tables tab including regions, constellations, factions. +13. ~~**Create a Zora Tier 0 demo**~~ ✅ **Done.** Deterministic template engine with 15 events × 5 soul depths, module gating, personality axes. Validates soul depth progression and module tradeoffs. +14. ~~**Error handling & reconnection spec**~~ ✅ **Done.** Architecture → ARCH-4. +15. ~~**Session persistence spec**~~ ✅ **Done.** Architecture → ARCH-5. +16. ~~**Audio design spec**~~ ✅ **Done.** Architecture → ARCH-6. +17. ~~**Localization decision**~~ ✅ **Done.** Architecture → ARCH-7. +18. ~~**Accessibility spec**~~ ✅ **Done.** Architecture → ARCH-8. + +--- + +## 8. What's Working Well + +The gaps above should not overshadow what this GDD does exceptionally well: + +- **Consistent vision**: The "spreadsheet simulator" design pillar is maintained across all pages. No page accidentally designs a flight sim. +- **Roadmap is honest**: Phase 0 is marked "in progress" and every subsequent phase has a concrete done-when condition. +- **Ship AI design is exceptional**: The soul/module/agent architecture is one of the most detailed and original companion AI designs I've seen in a game design document. The three implementation tiers (deterministic → LLM-assisted → full agent) show practical engineering thinking. +- **Backend schema is unusually detailed for a GDD**: 56+ tables with field descriptions is ahead of most game prototypes at this stage. +- **Demo coverage**: 11 interactive demos for a pre-Phase-0 GDD is impressive. Each demo honestly states its limitations. +- **Agent lifecycle system**: The scheduled_agents model with uniform lifecycle, three scheduling strategies, and kill-switch is production-quality thinking. +- **Information diffusion model**: The economy's info-asymmetry design is well-thought-through and explicitly tied to gameplay loops. diff --git a/index.html b/index.html new file mode 100644 index 0000000..4de45c7 --- /dev/null +++ b/index.html @@ -0,0 +1,969 @@ + + + + + + GDD::DOCS — EVE-Inspired Multiplayer Prototype + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..153274e --- /dev/null +++ b/js/app.js @@ -0,0 +1,157 @@ +window.GDD = window.GDD || {}; +const GDD = window.GDD; + +const FULLSCREEN_PAGES = new Set([ + 'demo-starmap', + 'demo-movement', + 'demo-combat', + 'demo-market', + 'demo-fitting', + 'demo-refining', + 'demo-progression', + 'demo-bounty', + 'demo-gamehud', + 'demo-chat', + 'demo-zora', + 'demo-galaxy', +]); + +// Map pageId -> component name on GDD +const PAGE_COMPONENTS = { + 'overview': 'OverviewPage', + 'architecture': 'ArchitecturePage', + 'techstack': 'TechStackPage', + 'backend': 'BackendPage', + 'agents': 'AgentsPage', + 'gameplay': 'GameplayPage', + 'ships': 'ShipsPage', + 'economy': 'EconomyPage', + 'social': 'SocialPage', + 'ship-ai': 'ShipAIPage', + 'roadmap': 'RoadmapPage', + 'risks': 'RisksPage', + 'demo-gallery': 'DemoGalleryPage', + 'demo-starmap': 'StarMapDemo', + 'demo-movement': 'ShipMovementDemo', + 'demo-combat': 'CombatDemo', + 'demo-market': 'MarketDemo', + 'demo-fitting': 'FittingDemo', + 'demo-refining': 'RefiningDemo', + 'demo-progression':'ProgressionDemo', + 'demo-bounty': 'BountyDemo', + 'demo-gamehud': 'GameHudDemo', + 'demo-chat': 'ChatDemo', + 'demo-zora': 'ZoraDemo', + 'demo-galaxy': 'GalaxyDemo', +}; + +function App() { + const { page, navigate } = GDD.useRouter(); + const [collapsed, setCollapsed] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const [loadError, setLoadError] = React.useState(null); + + // Load the page component on demand when navigation changes + React.useEffect(() => { + // Check if component already available (inline loaded pages like overview) + const compName = PAGE_COMPONENTS[page]; + if (compName && GDD[compName]) { + GDD._loadedPages = GDD._loadedPages || {}; + GDD._loadedPages[page] = true; + return; + } + setLoading(true); + setLoadError(null); + GDD.loadPage(page) + .then(() => setLoading(false)) + .catch((err) => { + setLoadError(err.message || 'Failed to load page'); + setLoading(false); + }); + }, [page]); + + // Preload adjacent pages in the background after current page loads + React.useEffect(() => { + if (!loading && !loadError) { + // Preload the next/prev pages after a short delay + const allPageIds = Object.keys(PAGE_COMPONENTS); + const idx = allPageIds.indexOf(page); + if (idx >= 0 && idx < allPageIds.length - 1) { + GDD.preloadPage(allPageIds[idx + 1]); + } + if (idx > 0) { + GDD.preloadPage(allPageIds[idx - 1]); + } + } + }, [page, loading, loadError]); + + const renderPage = () => { + if (loading) { + return ( +
+
Loading module…
+
+ ); + } + + if (loadError) { + return ( +
+
Failed to load page: {loadError}
+ +
+ ); + } + + const compName = PAGE_COMPONENTS[page]; + const Comp = compName && GDD[compName]; + if (!Comp) return ; + return ; + }; + + const isFullscreen = FULLSCREEN_PAGES.has(page); + + if (isFullscreen) { + return ( +
+
+
+ {renderPage()} +
+
+
+ ); + } + + return ( +
+ setCollapsed(c => !c)} + /> +
+ setCollapsed(c => !c)} + /> +
+ {renderPage()} +
+
+
+ ); +} + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/js/components/sidebar.js b/js/components/sidebar.js new file mode 100644 index 0000000..2262e37 --- /dev/null +++ b/js/components/sidebar.js @@ -0,0 +1,79 @@ +window.GDD = window.GDD || {}; + +const GDD = window.GDD; +const { useState, useEffect } = React; + +const NAV_SECTIONS = [ + { + title: 'Documentation', + items: [ + { id: 'overview', icon: '◈', label: 'Overview' }, + { id: 'architecture', icon: '⬡', label: 'Architecture' }, + { id: 'techstack', icon: '⟐', label: 'Tech Stack' }, + { id: 'backend', icon: '⊞', label: 'Backend Model' }, + { id: 'agents', icon: '⏣', label: 'Agent Lifecycle' }, + { id: 'gameplay', icon: '◉', label: 'Gameplay Loop' }, + { id: 'ships', icon: '◇', label: 'Ships & Fitting' }, + { id: 'economy', icon: '⇄', label: 'Economy & Industry' }, + { id: 'social', icon: '✧', label: 'Progression & Social' }, + { id: 'ship-ai', icon: '◈', label: 'Ship AI — Zora' }, + { id: 'roadmap', icon: '⊞', label: 'Roadmap' }, + { id: 'risks', icon: '◬', label: 'Risks & Questions' }, + { id: 'demo-gallery', icon: '◈', label: 'Demo Gallery' }, + ] + }, + { + title: 'Interactive Demos', + items: [ + { id: 'demo-starmap', icon: '✦', label: 'Star Map' }, + { id: 'demo-movement', icon: '→', label: 'Ship Movement' }, + { id: 'demo-combat', icon: '✸', label: 'Combat System' }, + { id: 'demo-market', icon: '⇄', label: 'Market' }, + { id: 'demo-fitting', icon: '⊞', label: 'Ship Fitting' }, + { id: 'demo-refining', icon: '⚗', label: 'Refining & MFG' }, + { id: 'demo-progression', icon: '▲', label: 'Skill Progression' }, + { id: 'demo-bounty', icon: '✸', label: 'Bounty & Kill Feed' }, + { id: 'demo-gamehud', icon: '◉', label: 'Game HUD' }, + { id: 'demo-chat', icon: '💬', label: 'Chat & Comms' }, + { id: 'demo-zora', icon: '🤖', label: 'Zora Tier 0' }, + { id: 'demo-galaxy', icon: '🌌', label: 'Galaxy Gen' }, + ] + } +]; + +function Sidebar({ collapsed, currentPage, onNavigate, onToggle }) { + const [hoveredItem, setHoveredItem] = useState(null); + + return ( + + ); +} + +GDD.Sidebar = Sidebar; diff --git a/js/components/topbar.js b/js/components/topbar.js new file mode 100644 index 0000000..983af23 --- /dev/null +++ b/js/components/topbar.js @@ -0,0 +1,46 @@ +window.GDD = window.GDD || {}; + +const GDD = window.GDD; + +const PAGE_TITLES = { + 'overview': 'Overview', + 'architecture': 'Architecture', + 'techstack': 'Tech Stack', + 'backend': 'Backend Model', + 'agents': 'Agent Lifecycle & Scheduling', + 'gameplay': 'Gameplay Loop', + 'roadmap': 'Roadmap', + 'risks': 'Risks & Questions', + 'demo-starmap': 'Star Map', + 'demo-movement': 'Ship Movement', + 'demo-combat': 'Combat System', + 'demo-market': 'Market Interface', +}; + +function TopBar({ collapsed, currentPage, onToggle }) { + const isDemo = currentPage.startsWith('demo-'); + const section = isDemo ? 'Demos' : 'Docs'; + const title = PAGE_TITLES[currentPage] || currentPage; + + return ( +
+ +
+ GDD + / + {section} + / + {title} +
+
+ Connected + + Prototype v0.1.0 +
+
+ ); +} + +GDD.TopBar = TopBar; diff --git a/js/demos/bounty.js b/js/demos/bounty.js new file mode 100644 index 0000000..29a3f54 --- /dev/null +++ b/js/demos/bounty.js @@ -0,0 +1,425 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useCallback, useRef } = React; + +function BountyDemo() { + const [bounties, setBounties] = useState([]); + const [killFeed, setKillFeed] = useState([]); + const [placeBountyTarget, setPlaceBountyTarget] = useState(''); + const [placeBountyAmount, setPlaceBountyAmount] = useState(5000); + const [showPlaceBounty, setShowPlaceBounty] = useState(false); + const [notifications, setNotifications] = useState([]); + const [autoFeed, setAutoFeed] = useState(false); + const feedRef = useRef(null); + const autoRef = useRef(null); + + const feedNames = [ + 'CMDR Picard', 'CMDR Worf', 'CMDR Data', 'CMDR Troi', 'CMDR Riker', + 'MinerBob', 'PirateKing99', 'NullSecWarlord', 'TraderAlice', 'DeepMiner', + 'RockHound', 'AmarrTrader', 'BulkMiner', 'GallenteForge', 'HighSecOps', + ]; + + const shipTypes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Hauler', 'Mining Barge']; + + const systems = ['Sol', 'Amarr', 'Hek', 'Rens', 'Dodixie', 'U-IRTYR', 'PF-346', 'YZ-LQL', 'O-WAMW']; + + const tierConfig = [ + { tier: 'Petty', threshold: 500, color: 'var(--muted)', reward: '10%', visibility: 'System-local' }, + { tier: 'Standard', threshold: 5000, color: 'var(--cyan)', reward: '15%', visibility: 'Regional' }, + { tier: 'Dangerous', threshold: 50000, color: 'var(--accent)', reward: '20%', visibility: 'Galaxy-wide' }, + { tier: 'Most Wanted', threshold: 500000, color: 'var(--red)', reward: '25%', visibility: 'Galaxy + Leaderboard' }, + ]; + + const getTier = (pool) => { + if (pool >= 500000) return tierConfig[3]; + if (pool >= 50000) return tierConfig[2]; + if (pool >= 5000) return tierConfig[1]; + return tierConfig[0]; + }; + + useEffect(() => { + window.GDD.api.getBounties().then(b => setBounties(b)); + window.GDD.api.getKillFeed().then(k => setKillFeed(k)); + }, []); + + const addNotif = useCallback((msg, color) => { + const id = Date.now(); + setNotifications(prev => [...prev, { id, msg, color }]); + setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3500); + }, []); + + const handlePlaceBounty = useCallback(async () => { + if (!placeBountyTarget.trim()) return; + if (placeBountyAmount < 500) { + addNotif('Minimum bounty is 500 ISK.', 'var(--red)'); + return; + } + const result = await window.GDD.api.placeBounty(placeBountyTarget, placeBountyAmount); + if (result.success) { + setBounties(prev => { + const existing = prev.find(b => b.target === placeBountyTarget); + if (existing) { + return prev.map(b => b.target === placeBountyTarget + ? { ...b, pool: b.pool + placeBountyAmount, tier: getTier(b.pool + placeBountyAmount).tier } + : b); + } + return [...prev, { + target: placeBountyTarget, + pool: placeBountyAmount, + tier: getTier(placeBountyAmount).tier, + lastHostile: 'Just now', + }]; + }); + addNotif(`Bounty of ₢${placeBountyAmount.toLocaleString()} placed on ${placeBountyTarget}.`, 'var(--green)'); + setShowPlaceBounty(false); + setPlaceBountyTarget(''); + setPlaceBountyAmount(5000); + } + }, [placeBountyTarget, placeBountyAmount, addNotif]); + + // Auto-generate kill feed + const generateKill = useCallback(() => { + const victim = feedNames[Math.floor(Math.random() * feedNames.length)]; + let killer; + do { killer = feedNames[Math.floor(Math.random() * feedNames.length)]; } while (killer === victim); + const ship = shipTypes[Math.floor(Math.random() * shipTypes.length)]; + const system = systems[Math.floor(Math.random() * systems.length)]; + const bounty = Math.random() > 0.6 ? Math.floor(Math.random() * 50000) : 0; + + const kill = { + victim, + killer, + ship, + system, + bounty, + time: 'Just now', + }; + + setKillFeed(prev => [kill, ...prev.slice(0, 49)]); + + // If bounty, update bounty pool + if (bounty > 0) { + addNotif(`Bounty collected: ${killer} claimed ₢${bounty.toLocaleString()} from ${victim}'s bounty.`, 'var(--accent)'); + setBounties(prev => prev.map(b => + b.target === victim + ? { ...b, pool: Math.max(0, b.pool - bounty) } + : b + ).filter(b => b.pool > 0)); + } + }, [addNotif]); + + useEffect(() => { + if (autoFeed) { + autoRef.current = setInterval(generateKill, 2000 + Math.random() * 3000); + } else { + if (autoRef.current) clearInterval(autoRef.current); + } + return () => { if (autoRef.current) clearInterval(autoRef.current); }; + }, [autoFeed, generateKill]); + + // Auto-scroll kill feed + useEffect(() => { + if (killFeed.length > 0 && feedRef.current) { + // No auto-scroll needed since newest are on top + } + }, [killFeed]); + + const totalBountyPool = bounties.reduce((sum, b) => sum + b.pool, 0); + const totalKills = killFeed.length; + const totalBountyCollected = killFeed.reduce((sum, k) => sum + k.bounty, 0); + + return ( +
+ e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}>← Back to Docs +

Bounty Board & Kill Feed

+

+ Live bounty board with tier escalation and a galaxy-wide kill feed. Place bounties on pirates, + track kill events, and watch bounty pools climb. Toggle the auto-feed to simulate live combat activity. +

+ + {/* HUD-style bounty strip */} +
+ BOUNTY BOARD +
+ ACTIVE + {bounties.length} +
+ POOL + ₢{totalBountyPool.toLocaleString()} +
+ COLLECTED + ₢{totalBountyCollected.toLocaleString()} + {autoFeed && ● LIVE FEED} +
+ + {/* Notifications */} +
+ {notifications.map(n => ( +
+ {n.msg} +
+ ))} +
+ + {/* Stats */} +
+
+
{bounties.length}
+
Active Bounties
+
+
+
₢{totalBountyPool.toLocaleString()}
+
Total Bounty Pool
+
+
+
{totalKills}
+
Kill Events
+
+
+
₢{totalBountyCollected.toLocaleString()}
+
Bounty Collected
+
+
+ +
+ + + +
+ + {/* Place bounty modal */} + {showPlaceBounty && ( +
setShowPlaceBounty(false)}> +
e.stopPropagation()}> +

Place a Bounty

+ +
+
Target Player
+ setPlaceBountyTarget(e.target.value)} + placeholder="Enter player name..." + style={{ + width: '100%', padding: 'var(--sp-2) var(--sp-3)', background: 'var(--surface-raised)', + border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', + color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.85rem', + }} + /> +
+ +
+
+ Amount (min 500 ISK) +
+ setPlaceBountyAmount(parseInt(e.target.value) || 0)} + style={{ + width: '100%', padding: 'var(--sp-2) var(--sp-3)', background: 'var(--surface-raised)', + border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', + color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.85rem', + }} + /> +
+ + {/* Tier preview */} +
+
Resulting Tier
+ {(() => { + const t = getTier(placeBountyAmount); + return ( +
+ + {t.tier} + + + Hunter reward: {t.reward} · {t.visibility} + +
+ ); + })()} +
+ +
+ + +
+
+
+ )} + +
+ {/* Active Bounties */} +
+

+ Active Bounties +

+ + {bounties.length === 0 && ( +
+ No active bounties. Place one to get started. +
+ )} + + {bounties.sort((a, b) => b.pool - a.pool).map((bounty, i) => { + const tier = getTier(bounty.pool); + return ( +
+
+
+

{bounty.target}

+ + {tier.tier.toUpperCase()} + +
+
+
+ ₢{bounty.pool.toLocaleString()} +
+
+ Hunter reward: {tier.reward} +
+
+
+ +
+ Visibility: {tier.visibility} + Last hostile: {bounty.lastHostile} +
+ + {/* Pool bar */} +
+
+
+
+
+ ₢{tier.threshold.toLocaleString()} + Next tier +
+
+
+ ); + })} + + {/* Tier legend */} +
+

Bounty Tiers

+ {tierConfig.map((t, i) => ( +
+
+ {t.tier} + + ≥ ₢{t.threshold.toLocaleString()} + +
+
+ {t.reward} · {t.visibility} +
+
+ ))} +
+
+ + {/* Kill Feed */} +
+

+ Kill Feed + {autoFeed && ( + + ● LIVE + + )} +

+ +
+ {killFeed.length === 0 && ( +
+ No kill events yet. Start the live feed or generate events manually. +
+ )} + + {killFeed.map((kill, i) => ( +
0 ? 'var(--accent-border)' : 'var(--border)'}`, + borderRadius: 'var(--radius-md)', + transition: 'background 0.3s', + }}> +
+
+ {kill.victim} + destroyed by + {kill.killer} +
+ + {kill.time} + +
+
+ + Ship: {kill.ship} + + + System: {kill.system} + + {kill.bounty > 0 && ( + + Bounty: ₢{kill.bounty.toLocaleString()} + + )} +
+
+ ))} +
+
+
+ + {/* Anti-abuse rules */} +
+ Anti-abuse rules (implemented in backend): You cannot claim your own bounty (alt check). + Payout never exceeds ship loss value. Minimum placement is 500 ISK. Target must have negative security + status or committed a hostile act within 24h. Bounties decay 10%/week if target stays clean for 30 days. +
+
+ ); +} + +window.GDD.BountyDemo = BountyDemo; diff --git a/js/demos/chat.js b/js/demos/chat.js new file mode 100644 index 0000000..afda587 --- /dev/null +++ b/js/demos/chat.js @@ -0,0 +1,394 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useCallback, useRef } = React; + +function ChatDemo() { + // ── State ── + const [activeChannel, setActiveChannel] = useState('local'); + const [messages, setMessages] = useState([]); + const [inputText, setInputText] = useState(''); + const [playerName] = useState('CMDR Kimura'); + const [playerSystem] = useState('Jita'); + const [simTime, setSimTime] = useState(0); + const [speed, setSpeed] = useState(1); + const [running, setRunning] = useState(false); + const messagesEndRef = useRef(null); + const tickRef = useRef(null); + + // ── Simulated players in different systems ── + const players = [ + { name: 'CMDR Vasquez', system: 'Jita', distance: 0 }, + { name: 'CMDR Chen', system: 'Jita', distance: 0 }, + { name: 'CMDR Okafor', system: 'Amarr', distance: 6 }, + { name: 'CMDR Lindström', system: 'Amarr', distance: 6 }, + { name: 'CMDR Tanaka', system: 'Rens', distance: 12 }, + { name: 'CMDR Dubois', system: 'Hek', distance: 18 }, + { name: 'CMDR Voronov', system: 'PF-346', distance: 32 }, + ]; + + // ── Channel definitions ── + const channels = [ + { id: 'local', name: 'Local', range: 'Current System', delay: 'Instant', icon: '📡', color: 'var(--fg-bright)' }, + { id: 'trade', name: 'Trade', range: 'Station / Region', delay: '0–30s', icon: '💰', color: 'var(--green)' }, + { id: 'private', name: 'Private (Okafor)', range: 'Distance-based', delay: '~12s (6 jumps)', icon: '✉', color: 'var(--cyan)' }, + { id: 'fleet', name: 'Fleet [Post-MVP]', range: 'Fleet members', delay: 'Instant', icon: '🚀', color: 'var(--purple)', disabled: true }, + ]; + + // ── Pre-seeded message corpus ── + const seedMessages = { + local: [ + { from: 'CMDR Vasquez', text: 'Anyone seen a Veldspar belt that isn\'t depleted?', time: -45 }, + { from: 'CMDR Chen', text: 'Try belt 7-2, was full 10 min ago', time: -38 }, + { from: 'CMDR Vasquez', text: 'Thanks, warping now', time: -35 }, + { from: 'CMDR Chen', text: 'Watch out, saw a Corpii frigate on scan near 7-2', time: -30 }, + { from: 'CMDR Vasquez', text: 'Corpii? In high-sec? That\'s unusual', time: -25 }, + { from: 'System', text: '⚠ CONCORD response dispatched in Jita — criminal act in progress near Jita IV', time: -20, isSystem: true }, + { from: 'CMDR Chen', text: 'Well that explains the locals being jumpy', time: -15 }, + ], + trade: [ + { from: 'CMDR Chen', text: 'WTS 5000 Tritanium @ 3.15/unit — Jita IV docked', time: -60 }, + { from: 'CMDR Vasquez', text: 'WTB Nocxium x200, paying market +5%. Jita.', time: -50 }, + { from: 'Market Bot', text: '📊 Scordite volume up 340% in Jita in last hour. Spread widening.', time: -40, isSystem: true }, + { from: 'CMDR Chen', text: 'That Scordite spike is because someone bought out the entire sell wall at Jita IV', time: -30 }, + ], + private: [ + { from: 'CMDR Okafor', text: 'Hey, you still in Jita?', time: -120 }, + { from: 'CMDR Okafor', text: 'Megacyte just crashed 15% in Amarr. Someone panic-sold a freighter load.', time: -90 }, + { from: 'CMDR Okafor', text: 'If you can haul fast, there\'s a 20% spread between Amarr buy and Jita sell', time: -75 }, + ], + }; + + // ── Delay calculation ── + const getDelay = (fromDistance) => { + if (fromDistance === 0) return 0; + return Math.round(2 * Math.sqrt(fromDistance)); + }; + + const formatDelay = (seconds) => { + if (seconds === 0) return 'instant'; + if (seconds < 60) return `~${seconds}s`; + return `~${Math.floor(seconds / 60)}m ${seconds % 60}s`; + }; + + // ── Simulation ── + useEffect(() => { + if (!running) { + if (tickRef.current) clearInterval(tickRef.current); + return; + } + tickRef.current = setInterval(() => { + setSimTime(t => t + speed); + }, 500); + return () => clearInterval(tickRef.current); + }, [running, speed]); + + // Seed initial messages + useEffect(() => { + const initial = []; + Object.entries(seedMessages).forEach(([channel, msgs]) => { + msgs.forEach(msg => { + initial.push({ + id: `${channel}-${msg.time}-${msg.from}`, + channel, + from: msg.from, + text: msg.text, + isSystem: msg.isSystem || false, + timestamp: msg.time, + deliveredAt: msg.time + (channel === 'private' ? getDelay(6) : 0), + delay: channel === 'private' ? getDelay(6) : 0, + status: 'delivered', + }); + }); + }); + initial.sort((a, b) => a.deliveredAt - b.deliveredAt); + setMessages(initial); + }, []); + + // Check for delayed message delivery + useEffect(() => { + if (!running) return; + setMessages(prev => prev.map(m => { + if (m.status === 'pending' && simTime >= m.deliveredAt) { + return { ...m, status: 'delivered' }; + } + return m; + })); + }, [simTime, running]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const sendMessage = () => { + if (!inputText.trim()) return; + const now = simTime; + let delay = 0; + if (activeChannel === 'private') delay = getDelay(6); + if (activeChannel === 'trade') delay = Math.floor(Math.random() * 30); + + const msg = { + id: `user-${Date.now()}`, + channel: activeChannel, + from: playerName, + text: inputText.trim(), + isSystem: false, + timestamp: now, + deliveredAt: now + delay, + delay, + status: delay === 0 ? 'delivered' : 'pending', + }; + setMessages(prev => [...prev, msg]); + setInputText(''); + + // Simulate NPC response in local + if (activeChannel === 'local' && Math.random() > 0.5) { + const responder = players.filter(p => p.system === playerSystem && p.name !== playerName); + if (responder.length > 0) { + const responder_ = responder[Math.floor(Math.random() * responder.length)]; + const responses = [ + 'Copy that.', + 'Interesting. Keep us posted.', + 'Acknowledged.', + 'Seen it. Be careful out there.', + 'Good luck.', + ]; + setTimeout(() => { + setMessages(prev => [...prev, { + id: `npc-${Date.now()}`, + channel: 'local', + from: responder_.name, + text: responses[Math.floor(Math.random() * responses.length)], + isSystem: false, + timestamp: simTime + 3, + deliveredAt: simTime + 3, + delay: 0, + status: 'delivered', + }]); + }, 1500); + } + } + }; + + const visibleMessages = messages.filter(m => { + if (m.channel !== activeChannel) return false; + if (m.status === 'pending') return true; + return true; + }); + + const channelInfo = channels.find(c => c.id === activeChannel); + + return ( +
+ {/* Header */} +
+ +
+ Speed: + {[0.5, 1, 2, 5].map(s => ( + + ))} + + + T+{simTime.toFixed(0)}s + +
+
+ +
+ {/* Channel sidebar */} +
+
+ Channels +
+ {channels.map(ch => ( + + ))} + +
+ Nearby Pilots +
+ {players.filter(p => p.system === playerSystem).map(p => ( +
+ {p.name} +
+ ))} + +
+ Distant Pilots +
+ {players.filter(p => p.system !== playerSystem).map(p => ( +
+ {p.name} + ({p.distance}j) +
+ ))} +
+ + {/* Main chat area */} +
+ {/* Channel info bar */} +
+
+ {channelInfo?.icon} + {channelInfo?.name} + + {channelInfo?.range} + +
+
+ Delay: {channelInfo?.delay} +
+
+ + {/* Messages */} +
+ {visibleMessages.length === 0 && ( +
+ No messages yet. Press ▶ Run to start the simulation. +
+ )} + {visibleMessages.map(msg => ( +
+
+ + {msg.isSystem ? '⚠ System' : msg.from} + + + {msg.status === 'pending' + ? `⏳ delivering… (${formatDelay(msg.delay)} light-speed delay)` + : msg.delay > 0 + ? `T+${msg.deliveredAt.toFixed(0)}s (delayed ${formatDelay(msg.delay)})` + : `T+${msg.timestamp.toFixed(0)}s`} + +
+
+ {msg.text} +
+
+ ))} +
+
+ + {/* Input */} +
+ setInputText(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') sendMessage(); }} + placeholder={activeChannel === 'private' ? `Message CMDR Okafor (${formatDelay(getDelay(6))} delay)...` : `Send to ${channelInfo?.name}...`} + style={{ + flex: 1, padding: 'var(--sp-2) var(--sp-3)', + borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', + background: 'var(--surface-base)', color: 'var(--fg)', + fontSize: '0.85rem', outline: 'none', + }} + /> + +
+
+ + {/* Right sidebar: delay visualization */} +
+
+ Light-Speed Delay Map +
+ +
+ Messages to/from pilots in other systems travel at light speed. Formula: 2 × √(jumps) seconds. +
+ + {players.map(p => { + const delay = getDelay(p.distance); + const isLocal = p.distance === 0; + return ( +
15 ? 'var(--red)' : delay > 5 ? 'var(--amber)' : 'var(--cyan)'}`, + }}> +
{p.name}
+
+ {p.system} + 15 ? 'var(--red)' : 'var(--amber)' }}> + {isLocal ? 'instant' : `~${delay}s`} + +
+ {p.distance > 0 && ( +
+
15 ? 'var(--red)' : delay > 5 ? 'var(--amber)' : 'var(--cyan)', + borderRadius: '2px', + transition: 'width 0.3s ease', + }} /> +
+ )} +
+ ); + })} + +
+ What This Validates +
+
    +
  • Light-speed delay feels meaningful — not instant
  • +
  • Private messages arrive after a visible wait
  • +
  • Local chat is instant, creating information asymmetry
  • +
  • Trade channel has moderate delay (regional relay)
  • +
  • System messages (CONCORD, market) are immediate
  • +
+
+
+
+ ); +} + +window.GDD.ChatDemo = ChatDemo; diff --git a/js/demos/combat.js b/js/demos/combat.js new file mode 100644 index 0000000..73e7c17 --- /dev/null +++ b/js/demos/combat.js @@ -0,0 +1,1044 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useRef, useCallback } = React; +const TH = window.GDD.THREE; + +/* ═══════════════════════════════════════════════════════════════════ + Combat System — FTL-style Reactor Power Management + Immersive Game HUD — full 3D viewport with diegetic overlays + ═══════════════════════════════════════════════════════════════════ */ + +const MAX_POWER = 8; +const TICK_MS = 50; + +// ── 3D Projectile Pool ── +function createProjectilePool3D(scene) { + const pool = []; + return { + spawn(x, y, z, tx, ty, tz, color, dmg, type, subsystem) { + let mesh; + if (type === 'beam') { + const dx = tx - x, dy = ty - y, dz = tz - z; + const len = Math.sqrt(dx*dx + dy*dy + dz*dz); + const geo = new THREE.CylinderGeometry(0.08, 0.08, len, 4); + geo.rotateX(Math.PI / 2); + const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.9 }); + mesh = new THREE.Mesh(geo, mat); + mesh.position.set((x + tx) / 2, (y + ty) / 2, (z + tz) / 2); + mesh.lookAt(tx, ty, tz); + mesh.userData = { life: 8, maxLife: 8, type, dmg, subsystem, tx, ty, tz, color }; + } else if (type === 'pulse') { + const geo = new THREE.RingGeometry(1, 2, 24); + const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.7, side: THREE.DoubleSide }); + mesh = new THREE.Mesh(geo, mat); + mesh.position.set(x, y, z); + mesh.userData = { life: 20, maxLife: 20, type, dmg: 0, subsystem: 'none', scale: 1 }; + } else { + mesh = new THREE.Mesh( + new THREE.SphereGeometry(type === 'missile' ? 0.2 : 0.12, 4, 4), + new THREE.MeshBasicMaterial({ color }) + ); + const dx = tx - x, dy = ty - y, dz = tz - z; + const dist = Math.sqrt(dx*dx + dy*dy + dz*dz); + const speed = type === 'missile' ? 1.5 : 2.5; + mesh.position.set(x, y, z); + mesh.userData = { vx: (dx / dist) * speed, vy: (dy / dist) * speed, vz: (dz / dist) * speed, life: Math.ceil(dist / speed), maxLife: Math.ceil(dist / speed), type, dmg, subsystem, tx, ty, tz, color }; + } + scene.add(mesh); + pool.push(mesh); + }, + tick() { + for (let i = pool.length - 1; i >= 0; i--) { + const m = pool[i]; + const ud = m.userData; + ud.life--; + if (ud.type === 'beam' || ud.type === 'pulse') { + m.material.opacity = Math.max(0, ud.life / ud.maxLife); + if (ud.type === 'pulse') { + ud.scale += 0.15; + m.scale.setScalar(ud.scale); + } + } else { + m.position.x += ud.vx; + m.position.y += ud.vy; + m.position.z += ud.vz; + } + if (ud.life <= 0) { + scene.remove(m); + m.geometry.dispose(); + m.material.dispose(); + pool.splice(i, 1); + } + } + }, + getArrived() { + return pool.filter(m => m.userData.life <= 1); + }, + clear() { + pool.forEach(m => { scene.remove(m); m.geometry.dispose(); m.material.dispose(); }); + pool.length = 0; + }, + get all() { return pool; }, + }; +} + +// ── Impact flash pool ── +function createImpactPool3D(scene) { + const impacts = []; + return { + spawn(x, y, z, color, size) { + const glow = TH.createGlowSprite(color, size || 3); + glow.position.set(x, y, z); + scene.add(glow); + impacts.push({ mesh: glow, life: 10, maxLife: 10 }); + }, + tick() { + for (let i = impacts.length - 1; i >= 0; i--) { + impacts[i].life--; + const progress = 1 - impacts[i].life / impacts[i].maxLife; + impacts[i].mesh.material.opacity = (1 - progress) * 0.8; + impacts[i].mesh.scale.setScalar((impacts[i].mesh.scale.x || 3) * (1 + progress * 0.5)); + if (impacts[i].life <= 0) { + scene.remove(impacts[i].mesh); + impacts[i].mesh.material.map?.dispose(); + impacts[i].mesh.material.dispose(); + impacts.splice(i, 1); + } + } + }, + }; +} + +function CombatDemo() { + const containerRef = useRef(null); + const sceneRef = useRef(null); + const animIdRef = useRef(null); + const tickRef = useRef(0); + const projectilePoolRef = useRef(null); + const impactPoolRef = useRef(null); + + // Ship 3D refs + const playerShipRef = useRef(null); + const enemyShipRef = useRef(null); + const playerShieldRef = useRef(null); + const enemyLockRef = useRef(null); + const targetLineRef = useRef(null); + + const playerPosRef = useRef({ orbitAngle: 0, strafeTimer: 0, speed: 0 }); + const enemyPosRef = useRef({ orbitAngle: Math.PI }); + + // ── Player State ── + const [player, setPlayer] = useState({ + shields: 100, armor: 100, hull: 100, energy: 100, maxEnergy: 100, + speed: 0, maxSpeed: 420, name: 'USS ENTERPRISE', class: 'VENTURE-CLASS', + }); + const playerRef = useRef(player); + useEffect(() => { playerRef.current = player; }, [player]); + + // ── Power Allocation ── + const [power, setPower] = useState({ weapons: 3, shields: 2, engines: 2, aux: 1 }); + const powerRef = useRef(power); + useEffect(() => { powerRef.current = power; }, [power]); + + // ── Ship Modules ── + const [modules, setModules] = useState([ + { id: 'q', key: '1', name: 'Railgun', icon: '⊕', type: 'weapon', cd: 0, maxCd: 3, cost: 12, damage: 18, desc: 'Kinetic turret', color: '#ef4444' }, + { id: 'w', key: '2', name: 'Shield Bst', icon: '◎', type: 'shield', cd: 0, maxCd: 8, cost: 20, damage: 0, desc: 'Burst recharge', color: '#22d3ee' }, + { id: 'e', key: '3', name: 'EM Pulse', icon: '⟐', type: 'ewar', cd: 0, maxCd: 12, cost: 30, damage: 0, desc: 'Disrupt systems', color: '#a78bfa' }, + { id: 'r', key: '4', name: 'Overload', icon: '⚡', type: 'reactor', cd: 0, maxCd: 30, cost: 45, damage: 0, desc: 'Push reactor', color: '#f0a030' }, + { id: 'd', key: '5', name: 'Afterburn', icon: '»', type: 'engine', cd: 0, maxCd: 6, cost: 10, damage: 0, desc: 'Emergency thrust', color: '#22c55e' }, + { id: 'f', key: '6', name: 'Hull Patch', icon: '✚', type: 'repair', cd: 0, maxCd: 15, cost: 25, damage: 0, desc: 'Nanite repair', color: '#fb923c' }, + ]); + const modulesRef = useRef(modules); + useEffect(() => { modulesRef.current = modules; }, [modules]); + + const [playerBuffs, setPlayerBuffs] = useState([{ id: 'b1', name: 'Dmg Ctrl', icon: '↯', duration: -1, color: '#22c55e' }]); + const [enemyBuffs, setEnemyBuffs] = useState([]); + const [target, setTarget] = useState(null); + const [subsystem, setSubsystem] = useState('hull'); + const [enemy, setEnemy] = useState({ + name: 'Guristas Pirata', class: 'Frigate', shields: 100, armor: 100, hull: 100, + weapons: 100, engines: 100, locked: false, lockTimer: 0, lockTime: 3, + }); + const enemyRef = useRef(enemy); + useEffect(() => { enemyRef.current = enemy; }, [enemy]); + const targetRef = useRef(target); + useEffect(() => { targetRef.current = target; }, [target]); + const subsystemRef = useRef(subsystem); + useEffect(() => { subsystemRef.current = subsystem; }, [subsystem]); + const [overloaded, setOverloaded] = useState(false); + const overloadRef = useRef(false); + const [combatLog, setCombatLog] = useState([]); + const logRef = useRef([]); + const logScrollRef = useRef(null); + const addLog = useCallback((msg, color) => { + const time = new Date().toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); + logRef.current = [...logRef.current.slice(-50), { time, msg, color }]; + setCombatLog(logRef.current); + }, []); + const autoFireTimerRef = useRef(0); + const enemyFireTimerRef = useRef(0); + + // ── Derived stats ── + const totalPower = power.weapons + power.shields + power.engines + power.aux; + const weaponMult = 1 + power.weapons * 0.20; + const shieldRegen = power.shields * 1.2; + const shieldAbsorb = 0.4 + power.shields * 0.08; + const dodgeChance = power.engines * 4; + const speedMult = 1 + power.engines * 0.25; + const cdReduction = power.aux * 6; + const energyRegen = 2 + power.aux * 2; + + // ── Build 3D scene ── + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const scene = new THREE.Scene(); + scene.fog = new THREE.FogExp2(0x040810, 0.0008); + + const camera = new THREE.PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 3000); + camera.position.set(0, 35, 60); + camera.lookAt(0, 0, 0); + + const renderer = TH.createRenderer(container, { clearColor: 0x040810 }); + TH.handleResize(renderer, camera, container); + + const stars = TH.createStarField(3000, 2000); + scene.add(stars); + TH.addNebula(scene, 0x22d3ee, [-100, 40, -200], 200); + TH.addNebula(scene, 0xa78bfa, [80, -30, -150], 150); + + const grid = new THREE.GridHelper(300, 15, 0x0d1520, 0x0d1520); + grid.material.transparent = true; + grid.material.opacity = 0.12; + scene.add(grid); + TH.setupSpaceLighting(scene); + + // Player ship + const playerGroup = new THREE.Group(); + const pMesh = TH.createShipMesh(0xc8d6e5, 0xf0a030, 0.6); + pMesh.rotation.y = Math.PI / 2; + playerGroup.add(pMesh); + const pEngine = TH.createEngineGlow(0x22d3ee, 3, 15); + pEngine.position.set(0, 0, 6); + playerGroup.add(pEngine); + const pShield = TH.createShield(3, 0x22d3ee, 0.06); + playerGroup.add(pShield); + playerShieldRef.current = pShield; + playerGroup.position.set(-15, 0, 0); + scene.add(playerGroup); + playerShipRef.current = playerGroup; + + // Enemy ship + const enemyGroup = new THREE.Group(); + const eMesh = TH.createShipMesh(0x7f1d1d, 0xef4444, 0.5); + eMesh.rotation.y = Math.PI / 2; + enemyGroup.add(eMesh); + const eEngine = TH.createEngineGlow(0xef4444, 2, 10); + eEngine.position.set(0, 0, 5); + enemyGroup.add(eEngine); + enemyGroup.position.set(15, 0, 0); + scene.add(enemyGroup); + enemyShipRef.current = enemyGroup; + + // Targeting line + const lineGeo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 0, 0)]); + const lineMat = new THREE.LineDashedMaterial({ color: 0xf0a030, dashSize: 1, gapSize: 0.5, transparent: true, opacity: 0.2 }); + const targetLine = new THREE.Line(lineGeo, lineMat); + targetLine.computeLineDistances(); + targetLine.visible = false; + scene.add(targetLine); + targetLineRef.current = targetLine; + + // Lock brackets + const lockBrackets = TH.createLockBrackets(3, 0xf0a030); + lockBrackets.visible = false; + scene.add(lockBrackets); + enemyLockRef.current = lockBrackets; + + const projPool = createProjectilePool3D(scene); + projectilePoolRef.current = projPool; + const impPool = createImpactPool3D(scene); + impactPoolRef.current = impPool; + + sceneRef.current = { scene, camera, renderer, stars, playerGroup, enemyGroup, pEngine, eEngine, lockBrackets }; + + const clock = new THREE.Clock(); + const animate = () => { + animIdRef.current = requestAnimationFrame(animate); + const t = clock.getElapsedTime(); + const pwr = powerRef.current; + const eng = enemyRef.current; + const tgt = targetRef.current; + + // Player orbit + const pp = playerPosRef.current; + pp.orbitAngle += 0.008 * (1 + pwr.engines * 0.25); + const pr = 12 + pwr.engines * 1.5; + playerGroup.position.x = -pr * Math.cos(pp.orbitAngle); + playerGroup.position.z = pr * Math.sin(pp.orbitAngle) * 0.6; + playerGroup.position.y = Math.sin(pp.orbitAngle * 1.3) * 2; + if (tgt && eng.locked) playerGroup.lookAt(enemyGroup.position); + + pEngine.intensity = 2 + pwr.engines * 0.5 + (overloadRef.current ? 3 : 0); + if (overloadRef.current) { + pMesh.material.emissive.setHex(0xfbbf24); + pMesh.material.emissiveIntensity = 0.3 + Math.sin(t * 8) * 0.15; + } else { + pMesh.material.emissive.setHex(0xf0a030); + pMesh.material.emissiveIntensity = 0.15; + } + + pShield.material.opacity = 0.03 + pwr.shields * 0.015; + pShield.scale.setScalar(1 + pwr.shields * 0.05); + + // Enemy orbit + const ep = enemyPosRef.current; + const engineScale = eng.engines / 100; + ep.orbitAngle += 0.01 * engineScale; + const er = 14 * engineScale; + enemyGroup.position.x = 15 + er * Math.cos(ep.orbitAngle); + enemyGroup.position.z = er * Math.sin(ep.orbitAngle) * 0.5; + enemyGroup.position.y = Math.sin(ep.orbitAngle * 1.1) * 1.5 * engineScale; + eEngine.intensity = 1.5 * engineScale; + if (tgt) enemyGroup.lookAt(playerGroup.position); + + if (lockBrackets.visible) { + lockBrackets.position.copy(enemyGroup.position); + lockBrackets.rotation.y = t * 0.3; + } + + if (targetLine.visible && tgt && eng.locked) { + const pPos = playerGroup.position; + const ePos = enemyGroup.position; + const positions = targetLine.geometry.attributes.position; + positions.setXYZ(0, pPos.x, pPos.y, pPos.z); + positions.setXYZ(1, ePos.x, ePos.y, ePos.z); + positions.needsUpdate = true; + targetLine.computeLineDistances(); + } + + stars.rotation.y = t * 0.003; + projPool.tick(); + impPool.tick(); + renderer.render(scene, camera); + }; + animate(); + + const onResize = () => TH.handleResize(renderer, camera, container); + window.addEventListener('resize', onResize); + + return () => { + if (animIdRef.current) cancelAnimationFrame(animIdRef.current); + window.removeEventListener('resize', onResize); + projPool.clear(); + }; + }, []); + + // Auto-scroll log + useEffect(() => { + if (logScrollRef.current) logScrollRef.current.scrollTop = logScrollRef.current.scrollHeight; + }, [combatLog]); + + // ── Lock Target ── + const lockTarget = useCallback(() => { + addLog('Initiating target lock...', '#f0a030'); + setEnemy(prev => ({ ...prev, lockTimer: 0, locked: false })); + setTarget('Guristas Pirata'); + if (targetLineRef.current) targetLineRef.current.visible = true; + }, [addLog]); + + // ── Adjust Power ── + const adjustPower = useCallback((system, delta) => { + setPower(prev => { + const newVal = prev[system] + delta; + if (newVal < 0) return prev; + const newTotal = Object.entries(prev).reduce((sum, [k, v]) => sum + (k === system ? newVal : v), 0); + if (newTotal > MAX_POWER) return prev; + return { ...prev, [system]: newVal }; + }); + }, []); + + // ── Cast Ability ── + const castModule = useCallback((abilityId) => { + const ab = modulesRef.current.find(a => a.id === abilityId); + if (!ab) return; + if (!targetRef.current) { addLog('No target locked.', '#ef4444'); return; } + if (!enemyRef.current.locked) { addLog('Target not locked yet.', '#ef4444'); return; } + if (ab.cd > 0) { addLog(`${ab.name} recharging (${ab.cd.toFixed(1)}s).`, '#ef4444'); return; } + const pwr = powerRef.current; + const pl = playerRef.current; + if (pl.energy < ab.cost) { addLog('Insufficient energy.', '#ef4444'); return; } + + setPlayer(prev => ({ ...prev, energy: prev.energy - ab.cost })); + const actualCd = ab.maxCd * (1 - pwr.aux * 0.06); + setModules(prev => prev.map(a => a.id === abilityId ? { ...a, cd: Math.max(0.5, actualCd) } : a)); + + const pPos = playerShipRef.current?.position || { x: -15, y: 0, z: 0 }; + const ePos = enemyShipRef.current?.position || { x: 15, y: 0, z: 0 }; + const proj = projectilePoolRef.current; + const imp = impactPoolRef.current; + + if (ab.id === 'q') { + const dmg = ab.damage * weaponMult; + proj.spawn(pPos.x + 3, pPos.y, pPos.z, ePos.x, ePos.y, ePos.z, 0xef4444, dmg, 'beam', subsystemRef.current); + addLog(`Railgun → ${subsystemRef.current.toUpperCase()} (×${weaponMult.toFixed(1)})`, '#ef4444'); + } + if (ab.id === 'w') { + const restore = 15 + pwr.shields * 6; + setPlayer(prev => ({ ...prev, shields: Math.min(100, prev.shields + restore) })); + imp.spawn(pPos.x, pPos.y, pPos.z, 0x22d3ee, 5); + addLog(`Shield Boost: +${Math.round(restore)}%`, '#22d3ee'); + } + if (ab.id === 'e') { + const dur = 3 + pwr.aux * 0.5; + const mid = { x: (pPos.x + ePos.x) / 2, y: (pPos.y + ePos.y) / 2, z: (pPos.z + ePos.z) / 2 }; + proj.spawn(mid.x, mid.y, mid.z, ePos.x, ePos.y, ePos.z, 0xa78bfa, 0, 'pulse', 'none'); + setEnemyBuffs([{ id: 'emp', name: 'EM Disrupted', icon: '⟐', duration: Math.round(dur), color: '#a78bfa' }]); + setEnemy(prev => ({ ...prev, weapons: Math.max(0, prev.weapons - 15 - pwr.aux * 3), engines: Math.max(0, prev.engines - 10 - pwr.aux * 2) })); + addLog(`EM Pulse! Enemy disrupted ${dur.toFixed(1)}s`, '#a78bfa'); + setTimeout(() => { setEnemyBuffs([]); addLog('Enemy systems restored.', '#5a6b82'); }, dur * 1000); + } + if (ab.id === 'r') { + overloadRef.current = true; + setOverloaded(true); + setPlayerBuffs(prev => [...prev, { id: 'overload', name: 'Overload', icon: '⚡', duration: 8, color: '#f0a030' }]); + addLog('⚡ OVERLOAD — double fire rate 8s!', '#f0a030'); + setTimeout(() => { overloadRef.current = false; setOverloaded(false); setPlayerBuffs(prev => prev.filter(b => b.id !== 'overload')); addLog('Overload ended.', '#5a6b82'); }, 8000); + } + if (ab.id === 'd') { + playerPosRef.current.speed = 300 * speedMult; + addLog(`Afterburners! Speed ×${speedMult.toFixed(1)}`, '#22c55e'); + setTimeout(() => { playerPosRef.current.speed = 0; }, 2500); + } + if (ab.id === 'f') { + const repair = 12 + pwr.aux * 4; + setPlayer(prev => ({ ...prev, hull: Math.min(100, prev.hull + repair) })); + imp.spawn(pPos.x, pPos.y, pPos.z, 0xfb923c, 6); + addLog(`Hull Patch: +${Math.round(repair)}%`, '#fb923c'); + } + }, [addLog, weaponMult, speedMult]); + + // ── Keyboard ── + useEffect(() => { + const handler = (e) => { + const key = e.key.toLowerCase(); + if (['1', '2', '3', '4', '5', '6'].includes(key)) { e.preventDefault(); castModule(key); } + if (key === ' ' && !targetRef.current) { e.preventDefault(); lockTarget(); } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [castModule, lockTarget]); + + // ── Combat Tick ── + useEffect(() => { + const interval = setInterval(() => { + tickRef.current++; + const pwr = powerRef.current; + const eng = enemyRef.current; + const sub = subsystemRef.current; + const tgt = targetRef.current; + const tps = 1000 / TICK_MS; + + setModules(prev => prev.map(a => ({ ...a, cd: Math.max(0, a.cd - TICK_MS / 1000) }))); + + const eRegen = (2 + pwr.aux * 2) / tps; + const eDrain = overloadRef.current ? (3 + pwr.weapons * 0.5) / tps : 0; + setPlayer(prev => ({ ...prev, energy: Math.min(prev.maxEnergy, Math.max(0, prev.energy + eRegen - eDrain)) })); + + setPlayer(prev => { + if (prev.shields < 100) return { ...prev, shields: Math.min(100, prev.shields + (pwr.shields * 1.2) / tps) }; + return prev; + }); + + const proj = projectilePoolRef.current; + const imp = impactPoolRef.current; + const pPos = playerShipRef.current?.position; + const ePos = enemyShipRef.current?.position; + if (!pPos || !ePos) return; + + if (tgt && eng.locked && eng.hull > 0) { + const baseInterval = overloadRef.current ? 15 : 40; + autoFireTimerRef.current++; + if (autoFireTimerRef.current >= baseInterval) { + autoFireTimerRef.current = 0; + const bullets = Math.max(1, Math.floor(pwr.weapons / 2)); + const dmgPerBullet = (3 + pwr.weapons * 1.5) / bullets; + for (let b = 0; b < bullets; b++) { + const jx = ePos.x + (Math.random() - 0.5) * 2; + const jy = ePos.y + (Math.random() - 0.5) * 1; + const jz = ePos.z + (Math.random() - 0.5) * 2; + proj.spawn(pPos.x + 2, pPos.y, pPos.z, jx, jy, jz, overloadRef.current ? 0xfbbf24 : 0xf0a030, dmgPerBullet, 'bullet', sub); + } + } + + enemyFireTimerRef.current++; + if (enemyFireTimerRef.current >= 30 && eng.weapons > 10) { + enemyFireTimerRef.current = 0; + const enemyDmg = 4 * (eng.weapons / 100); + if (Math.random() < pwr.engines * 0.04) { + proj.spawn(ePos.x - 1, ePos.y, ePos.z, pPos.x + (Math.random() - 0.5) * 4, pPos.y + (Math.random() - 0.5) * 2, pPos.z + (Math.random() - 0.5) * 4, 0xef4444, 0, 'bullet', 'none'); + } else { + proj.spawn(ePos.x - 1, ePos.y, ePos.z, pPos.x, pPos.y, pPos.z, 0xef4444, enemyDmg, 'bullet', 'hull'); + } + } + } + + if (tgt && !eng.locked && eng.lockTimer < eng.lockTime) { + setEnemy(prev => { + const newTimer = prev.lockTimer + TICK_MS / 1000; + if (newTimer >= prev.lockTime) { + addLog('★ TARGET LOCKED', '#22c55e'); + if (enemyLockRef.current) enemyLockRef.current.visible = true; + return { ...prev, locked: true, lockTimer: newTimer }; + } + return { ...prev, lockTimer: newTimer }; + }); + } + + const arrived = proj.getArrived(); + for (const p of arrived) { + if (p.userData.dmg <= 0) continue; + const isPlayer = p.userData.color === 0xf0a030 || p.userData.color === 0xfbbf24; + if (isPlayer) { + imp.spawn(p.userData.tx, p.userData.ty, p.userData.tz, 0xef4444, 2); + setEnemy(prev => { + let ne = { ...prev }; + const d = p.userData.dmg; + if (p.userData.subsystem === 'shields') { ne.shields = Math.max(0, ne.shields - d); } + else if (p.userData.subsystem === 'hull') { + if (ne.shields > 0) ne.shields = Math.max(0, ne.shields - d * 0.4); + else { ne.armor = Math.max(0, ne.armor - d * 0.5); ne.hull = Math.max(0, ne.hull - d * 0.5); } + } else if (p.userData.subsystem === 'weapons') { ne.weapons = Math.max(0, ne.weapons - d); } + else if (p.userData.subsystem === 'engines') { ne.engines = Math.max(0, ne.engines - d); } + return ne; + }); + } else { + imp.spawn(p.userData.tx, p.userData.ty, p.userData.tz, 0xef4444, 2); + setPlayer(prev => { + let np = { ...prev }; + const d = p.userData.dmg; + const absorb = 0.4 + pwr.shields * 0.08; + if (np.shields > 0) { const ab = Math.min(d * absorb, np.shields); np.shields = Math.max(0, np.shields - ab); const bl = d - ab; if (bl > 0) np.armor = Math.max(0, np.armor - bl * 0.5); } + else if (np.armor > 0) { np.armor = Math.max(0, np.armor - d * 0.6); np.hull = Math.max(0, np.hull - d * 0.4); } + else { np.hull = Math.max(0, np.hull - d); } + return np; + }); + } + } + + if (tickRef.current % Math.round(tps) === 0) { + setPlayerBuffs(prev => prev.map(b => b.duration > 0 ? { ...b, duration: b.duration - 1 } : b).filter(b => b.duration !== 0)); + } + }, TICK_MS); + return () => clearInterval(interval); + }, [addLog]); + + /* ═══ RENDER — Immersive Game HUD ═══ */ + const lockPct = target && !enemy.locked ? Math.min(100, (enemy.lockTimer / enemy.lockTime) * 100) : 0; + + return ( +
+ {/* ── 3D VIEWPORT ── */} +
+ + {/* ═══ HUD OVERLAY LAYER ═══ */} +
+ + {/* ── TOP BAR — System info strip ── */} +
+ +
+ JITA + 1.0 SEC +
+ + {player.name} + + {player.class} +
+ SPD + {player.speed.toFixed(0)} m/s +
+ {overloaded && ⚡ OVERLOAD} + {target && enemy.locked && ( + + + TARGET LOCKED + + )} +
+ + + ONLINE + +
+ + {/* ── LEFT — Ship Systems ── */} +
+ {/* Ship status */} +
+
+ + Ship Systems +
+
+ {[ + { label: 'SH', value: player.shields, color: '#22d3ee', grad: 'linear-gradient(90deg, #0891b2, #22d3ee)' }, + { label: 'AR', value: player.armor, color: '#f0a030', grad: 'linear-gradient(90deg, #b47818, #f0a030)' }, + { label: 'HU', value: player.hull, color: '#22c55e', grad: 'linear-gradient(90deg, #16a34a, #22c55e)' }, + { label: 'NRG', value: player.energy, color: player.energy > 25 ? '#a78bfa' : '#ef4444', grad: player.energy > 25 ? 'linear-gradient(90deg, #6366f1, #a78bfa)' : 'linear-gradient(90deg, #dc2626, #ef4444)' }, + ].map(bar => ( +
+ {bar.label} +
+
+
+ + {bar.label === 'NRG' ? Math.round(bar.value) : bar.value.toFixed(0)} + +
+ ))} +
+
+ + {/* Reactor Power */} +
+
+ + Reactor + {totalPower}/{MAX_POWER} +
+
+ {[ + { key: 'weapons', label: 'WPN', color: '#ef4444' }, + { key: 'shields', label: 'SHD', color: '#22d3ee' }, + { key: 'engines', label: 'ENG', color: '#22c55e' }, + { key: 'aux', label: 'AUX', color: '#a78bfa' }, + ].map(sys => ( +
+ {sys.label} + +
+ {Array.from({ length: MAX_POWER }, (_, i) => ( +
+ ))} +
+ +
+ ))} +
+ WPN ×{weaponMult.toFixed(1)} dmg + {' · '} + SHD {shieldRegen.toFixed(0)}/s + {' · '} + ENG {dodgeChance}% dodge + {' · '} + AUX {energyRegen.toFixed(0)} NRG/s +
+
+
+ + {/* Buffs */} +
+
+ + Status +
+
+ {playerBuffs.map(b => ( + + {b.icon} {b.name}{b.duration > 0 ? ` ${b.duration}s` : ''} + + ))} + {enemyBuffs.map(b => ( + + {b.icon} {b.name} {b.duration > 0 ? `${b.duration}s` : ''} + + ))} +
+
+
+ + {/* ── CENTER — Crosshair + Lock ── */} +
+ {/* Crosshair */} +
+
+
+
+
+
+
+ + {/* Lock-in-progress ring */} + {target && !enemy.locked && ( +
+
+ + + +
+
+ LOCKING {lockPct.toFixed(0)}% +
+
+ )} + + {/* Engage prompt */} + {!target && ( +
+

No hostiles detected

+ +
+ )} +
+ + {/* ── RIGHT — Target Intel ── */} +
+ {/* Target panel */} +
+
+ + {target || 'NO TARGET'} + {target && {enemy.locked ? 'LOCKED' : 'LOCKING'}} +
+
+ {target && ( + <> + {[ + { label: 'SH', value: enemy.shields, color: '#22d3ee' }, + { label: 'AR', value: enemy.armor, color: '#f0a030' }, + { label: 'HU', value: enemy.hull, color: '#22c55e' }, + ].map(bar => ( +
+ {bar.label} +
+
+
+ {bar.value.toFixed(0)} +
+ ))} + + )} +
+
+ + {/* Subsystem targeting */} +
+
+ + Subsystem +
+
+
+ {[ + { key: 'hull', label: 'Hull', color: '#f0a030', hp: enemy.hull }, + { key: 'shields', label: 'Shields', color: '#22d3ee', hp: enemy.shields }, + { key: 'weapons', label: 'Weapons', color: '#ef4444', hp: enemy.weapons }, + { key: 'engines', label: 'Engines', color: '#22c55e', hp: enemy.engines }, + ].map(sys => ( + + ))} +
+
+
+ + {/* Power coupling readout */} +
+
+ + Power Readout +
+
+
WPN {power.weapons} → ×{weaponMult.toFixed(1)} dmg, {Math.max(1, Math.floor(power.weapons / 2))} rounds
+
SHD {power.shields} → {shieldRegen.toFixed(0)}/s, {(shieldAbsorb * 100).toFixed(0)}% abs
+
ENG {power.engines} → {dodgeChance}% dodge, ×{speedMult.toFixed(1)}
+
AUX {power.aux} → {energyRegen.toFixed(0)} NRG/s, −{cdReduction}% CD
+
+
+
+ + {/* ── BOTTOM — Module Bar + Combat Log ── */} +
+ {/* Module bar */} +
+
+ + Modules + 1–6 or click +
+
+ {/* Reactor gauge */} +
+ REACTOR +
+
25 ? 'linear-gradient(0deg, #4f46e5, #8b5cf6)' : 'linear-gradient(0deg, #dc2626, #ef4444)', + transition: 'height 0.15s', + }} /> + {Math.round(player.energy)} +
+
+
+ + {/* Module buttons */} + {modules.map(ab => { + const onCd = ab.cd > 0; + const noNRG = player.energy < ab.cost; + const canCast = target && enemy.locked && !onCd && !noNRG; + return ( + + ); + })} +
+
+ + {/* Combat log */} +
+
+ + Log +
+
+ {combatLog.length === 0 &&
SPACE to engage, then 1–6 for modules.
} + {combatLog.map((entry, i) => ( +
+ {entry.time} + {entry.msg} +
+ ))} +
+
+
+ +
+
+ ); +} + +window.GDD.CombatDemo = CombatDemo; diff --git a/js/demos/fitting.js b/js/demos/fitting.js new file mode 100644 index 0000000..5535b2d --- /dev/null +++ b/js/demos/fitting.js @@ -0,0 +1,377 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useCallback, useMemo } = React; + +function FittingDemo() { + const [ship, setShip] = useState(null); + const [ships, setShips] = useState([]); + const [availableModules, setAvailableModules] = useState([]); + const [fittedModules, setFittedModules] = useState({ high: [], med: [], low: [] }); + const [selectedModule, setSelectedModule] = useState(null); + const [filterSlot, setFilterSlot] = useState('all'); + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + window.GDD.api.getPlayerShips().then(s => { + setShips(s); + if (s.length > 0) setShip(s[0]); + }); + window.GDD.api.getAvailableModules().then(m => setAvailableModules(m)); + }, []); + + useEffect(() => { + if (!ship) return; + window.GDD.api.getShipFittings(ship.id).then(fitted => { + const slots = { high: [], med: [], low: [] }; + fitted.forEach(m => { + if (m.slot === 'high') slots.high.push(m); + else if (m.slot === 'med') slots.med.push(m); + else if (m.slot === 'low') slots.low.push(m); + }); + setFittedModules(slots); + }); + }, [ship]); + + const addNotif = useCallback((msg, color) => { + const id = Date.now(); + setNotifications(prev => [...prev, { id, msg, color }]); + setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3000); + }, []); + + const cpuUsage = useMemo(() => { + let total = 0; + Object.values(fittedModules).flat().forEach(m => total += m.cpu); + return total; + }, [fittedModules]); + + const gridUsage = useMemo(() => { + let total = 0; + Object.values(fittedModules).flat().forEach(m => total += m.power); + return total; + }, [fittedModules]); + + const cpuMax = ship ? ship.cpu : 0; + const gridMax = ship ? ship.powerGrid : 0; + const cpuOver = cpuUsage > cpuMax; + const gridOver = gridUsage > gridMax; + + const handleFit = useCallback((mod) => { + if (!ship) return; + const slot = mod.slot; + const maxSlots = slot === 'high' ? ship.highSlots : slot === 'med' ? ship.medSlots : ship.lowSlots; + const currentCount = fittedModules[slot].length; + + if (currentCount >= maxSlots) { + addNotif(`No empty ${slot} slots available.`, 'var(--red)'); + return; + } + + const newCpu = cpuUsage + mod.cpu; + const newGrid = gridUsage + mod.power; + if (newCpu > cpuMax) { + addNotif(`CPU exceeded: ${newCpu}/${cpuMax}. Remove a module first.`, 'var(--red)'); + return; + } + if (newGrid > gridMax) { + addNotif(`Power Grid exceeded: ${newGrid}/${gridMax}. Remove a module first.`, 'var(--red)'); + return; + } + + setFittedModules(prev => ({ + ...prev, + [slot]: [...prev[slot], { ...mod, uid: Date.now() + Math.random() }], + })); + addNotif(`${mod.name} fitted to ${slot} slot.`, 'var(--green)'); + }, [ship, fittedModules, cpuUsage, gridUsage, cpuMax, gridMax, addNotif]); + + const handleUnfit = useCallback((slot, index) => { + const mod = fittedModules[slot][index]; + setFittedModules(prev => ({ + ...prev, + [slot]: prev[slot].filter((_, i) => i !== index), + })); + addNotif(`${mod.name} removed from ${slot} slot.`, 'var(--muted)'); + }, [fittedModules, addNotif]); + + const filteredModules = filterSlot === 'all' + ? availableModules + : availableModules.filter(m => m.slot === filterSlot); + + const slotConfig = [ + { key: 'high', label: 'High Slots', color: 'var(--red)', icon: '◆', max: ship?.highSlots || 0 }, + { key: 'med', label: 'Medium Slots', color: 'var(--cyan)', icon: '◇', max: ship?.medSlots || 0 }, + { key: 'low', label: 'Low Slots', color: 'var(--green)', icon: '○', max: ship?.lowSlots || 0 }, + ]; + + const moduleTypeIcon = (type) => { + switch(type) { + case 'weapon': return '⊕'; + case 'shield': return '◎'; + case 'mining': return '⛏'; + case 'propulsion': return '»'; + case 'ewar': return '◎'; + case 'armor': return '◼'; + case 'damage_mod': return '↯'; + case 'cargo': return '□'; + default: return '•'; + } + }; + + if (!ship) return

Loading ship data...

; + + return ( +
+ e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}>← Back to Docs +

Ship Fitting Demo

+

+ Drag modules into slot bays. CPU and Power Grid are hard limits — overfitting is blocked. + Select a ship and build your loadout. +

+ + {/* Notifications */} +
+ {notifications.map(n => ( +
+ {n.msg} +
+ ))} +
+ + {/* HUD-style fitting strip */} +
+ {ship?.name || 'Loading...'} + {ship && {ship.class}} + {ship &&
} + {ship && ( + <> +
+ CPU +
+
0.8 ? '#f0a030' : '#22d3ee', borderRadius: 'var(--radius-pill)' }} /> +
+ {cpuUsage}/{cpuMax} +
+
+ PWR +
+
0.8 ? '#f0a030' : '#22c55e', borderRadius: 'var(--radius-pill)' }} /> +
+ {gridUsage}/{gridMax} +
+ + )} + {ship?.system || ''} · {ship?.status === 'docked' ? '● DOCKED' : '○ IN SPACE'} +
+ + {/* Ship selector */} +
+ {ships.map(s => ( + + ))} +
+ + {/* Ship stats + resource bars */} +
+
+ Fitting Console + | + {ship.name} · {ship.class}-class + + {ship.system} · {ship.status === 'docked' ? '● DOCKED' : '○ IN SPACE'} + +
+ +
+ {/* Module browser */} +
+
+
+ Module Browser +
+
+ {['all', 'high', 'med', 'low'].map(f => ( + + ))} +
+
+ +
+ {filteredModules.map(mod => ( +
setSelectedModule(mod)} + onDoubleClick={() => handleFit(mod)} + > +
+ + {moduleTypeIcon(mod.type)} + +
+
{mod.name}
+
+ {mod.cpu} CPU · {mod.power} PG · {mod.slot} +
+
+
+
+ ))} +
+
+ + {/* Fitting area */} +
+ {/* CPU / Grid bars */} +
+
+ CPU + + {cpuUsage} / {cpuMax} tf{cpuOver ? ' ⚠ OVER' : ''} + +
+
+
0.8 ? 'var(--accent)' : 'var(--cyan)', + }} /> +
+ +
+ POWER GRID + + {gridUsage} / {gridMax} MW{gridOver ? ' ⚠ OVER' : ''} + +
+
+
0.8 ? 'var(--accent)' : 'var(--green)', + }} /> +
+
+ + {/* Slot bays */} + {slotConfig.map(slot => ( +
+
+ {slot.icon} + + {slot.label} + + + {fittedModules[slot.key].length} / {slot.max} + +
+
+ {Array.from({ length: slot.max }).map((_, i) => { + const mod = fittedModules[slot.key][i]; + return ( +
{ + if (mod) handleUnfit(slot.key, i); + else if (selectedModule?.slot === slot.key) handleFit(selectedModule); + }} + > + {mod ? ( + <> + {moduleTypeIcon(mod.type)} + + {mod.name.replace(' I', '').replace(' II', '')} + + + {mod.cpu}/{mod.power} + + + ) : ( + Empty + )} +
+ ); + })} +
+
+ ))} +
+
+
+ + {/* Selected module detail */} + {selectedModule && ( +
+
+
+ {moduleTypeIcon(selectedModule.type)} +
+
+

{selectedModule.name}

+
+ Slot: {selectedModule.slot} + Type: {selectedModule.type} + CPU: {selectedModule.cpu} tf + Grid: {selectedModule.power} MW + {selectedModule.damage && Damage: {selectedModule.damage}} + {selectedModule.cycle > 0 && Cycle: {selectedModule.cycle}s} +
+
+ +
+
+ )} + + {/* Controls hint */} +
+ How to use: Select a module from the browser (left panel), then click an empty slot bay to fit it. + Double-click a module in the browser to quick-fit. Click a fitted module to remove it. + CPU and Power Grid are enforced — overspending is blocked with a warning. +
+
+ ); +} + +window.GDD.FittingDemo = FittingDemo; diff --git a/js/demos/galaxy.js b/js/demos/galaxy.js new file mode 100644 index 0000000..b7b701d --- /dev/null +++ b/js/demos/galaxy.js @@ -0,0 +1,1429 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useRef, useCallback, useMemo } = React; + +/* ═══════════════════════════════════════════════ + Seeded PRNG (xoshiro128**) + ═══════════════════════════════════════════════ */ +function createRng(seed) { + let s0 = seed >>> 0 || 1; + let s1 = (seed * 1103515245 + 12345) >>> 0; + let s2 = (s1 * 1103515245 + 12345) >>> 0; + let s3 = (s2 * 1103515245 + 12345) >>> 0; + function next() { + const t = s0; + const r = (t << 9 | t >>> 23); + s0 = s1; s1 = s2; s2 = s3; + s3 ^= t ^ s3; + s0 ^= (s0 << 3 | s0 >>> 29); + s1 ^= (s1 << 21 | s1 >>> 11); + s2 ^= (s2 << 7 | s2 >>> 25); + return ((r + ((s0 ^ s3) >>> 0)) >>> 0) / 4294967296; + } + return { + next, + range(a, b) { return a + next() * (b - a); }, + int(a, b) { return Math.floor(a + next() * (b - a)); }, + pick(arr) { return arr[Math.floor(next() * arr.length)]; }, + gaussian(mean, sigma) { + const u1 = next() || 0.0001; + const u2 = next(); + return mean + sigma * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); + }, + }; +} + +/* ═══════════════════════════════════════════════ + Spiral Galaxy Generation Algorithm + Inspired by https://vercidium.com/blog/random-galaxy-generation-with-c-and-opengl/ + + Key ideas: + - Systems placed along spiral arms + - Arms curve (bend) proportional to distance from center + - Gravity pushes density toward center + - Height variance via sin wave for 3D depth + - Security decreases with distance from center + - Each arm is a faction's territory + ═══════════════════════════════════════════════ */ +function generateGalaxy(seed, overrides = {}) { + const rng = createRng(seed); + + // ── Galaxy shape parameters ── + const armCount = overrides.armCount ?? 4; + const galaxySize = overrides.galaxySize ?? 300; + const armSpread = overrides.armSpread ?? 0.42; + const rotationStr = overrides.rotationStrength ?? 2.8; + const gravity = overrides.gravity ?? 1.4; + const heightMag = overrides.heightMagnitude ?? 15; + const heightFreq = overrides.heightFrequency ?? 0.018; + const conformChance = 0.72; + + // ── Faction/arm colors ── + const factionDefs = [ + { id: 'caldari', name: 'Caldari State', faction: 'Caldari', color: '#38bdf8', armColor: 0x38bdf8 }, + { id: 'minmatar', name: 'Minmatar Republic', faction: 'Minmatar', color: '#ef4444', armColor: 0xef4444 }, + { id: 'amarr', name: 'Amarr Empire', faction: 'Amarr', color: '#f59e0b', armColor: 0xf0a030 }, + { id: 'gallente', name: 'Gallente Federation', faction: 'Gallente', color: '#a855f7', armColor: 0xa855f7 }, + ]; + + const starColors = { O: '#9bb0ff', B: '#aabfff', A: '#cad7ff', F: '#f8f7ff', G: '#fff4ea', K: '#ffd2a1', M: '#ffcc6f' }; + const starWeights = { O: 0.01, B: 0.03, A: 0.06, F: 0.1, G: 0.15, K: 0.2, M: 0.45 }; + + const oreBySec = [ + { sec: 0.8, ores: ['Veldspar', 'Scordite'] }, + { sec: 0.5, ores: ['Veldspar', 'Scordite', 'Pyroxeres'] }, + { sec: 0.1, ores: ['Scordite', 'Pyroxeres', 'Kernite', 'Omber'] }, + { sec: -0.1, ores: ['Kernite', 'Omber', 'Jaspet', 'Hemorphite'] }, + { sec: -0.5, ores: ['Jaspet', 'Hemorphite', 'Arkonor'] }, + ]; + + function pickStarType() { + const r = rng.next(); + let acc = 0; + for (const [type, w] of Object.entries(starWeights)) { + acc += w; + if (r < acc) return type; + } + return 'M'; + } + + function getOresForSec(sec) { + for (let i = oreBySec.length - 1; i >= 0; i--) { + if (sec >= oreBySec[i].sec) return oreBySec[i].ores; + } + return oreBySec[oreBySec.length - 1].ores; + } + + // ── Spiral arm position calculator ── + function spiralPosition(armIdx, distFactor) { + const baseAngle = (2 * Math.PI / armCount) * armIdx; + const distance = Math.pow(distFactor, 0.6) * galaxySize; + const spreadAngle = armSpread * (Math.PI / armCount) * (0.4 + distFactor * 0.8); + const angleOffset = rng.gaussian(0, spreadAngle); + const bendAngle = distFactor * rotationStr; + const angle = baseAngle + angleOffset + bendAngle; + + const x = distance * Math.cos(angle); + const z = distance * Math.sin(angle); + const y = heightMag * Math.sin(distance * heightFreq) * (1 - distFactor * 0.3) + + rng.gaussian(0, 2 + distFactor * 4); + + return { x, y, z, distance, angle: baseAngle + bendAngle }; + } + + // ── Security from distance ── + function securityFromDist(distFactor) { + // Center (0) → 1.0, Edge (1) → -1.0 + const sec = 1.0 - distFactor * 2.0; + return Math.round(Math.max(-1, Math.min(1, sec + rng.range(-0.05, 0.05))) * 100) / 100; + } + + // ── Build data structures ── + const regions = []; + const constellations = []; + const systems = []; + const stargates = []; + const stations = []; + const belts = []; + const agents = []; + + // Core region — shared high-sec hub at center + const coreRegion = { + id: 'core', name: 'Core Worlds', faction: 'Concord', + factionColor: '#22d3ee', secMin: 0.8, secMax: 1.0, + color: '#22c55e', center: { x: 0, y: 0, z: 0 }, systemIds: [], + }; + regions.push(coreRegion); + + // Core systems (5-8 systems near center) + const coreConstCount = 2; + for (let ci = 0; ci < coreConstCount; ci++) { + const constId = `core_c${ci}`; + const cx = rng.gaussian(0, 15); + const cy = rng.gaussian(0, 3); + const cz = rng.gaussian(0, 15); + const con = { id: constId, regionId: 'core', x: cx, y: cy, z: cz, systemIds: [] }; + constellations.push(con); + + const sysCount = rng.int(3, 5); + for (let si = 0; si < sysCount; si++) { + const sx = cx + rng.gaussian(0, 8); + const sy = cy + rng.gaussian(0, 2); + const sz = cz + rng.gaussian(0, 8); + const sec = Math.round((0.8 + rng.next() * 0.2) * 100) / 100; + const starType = pickStarType(); + const sysId = `${constId}_s${si}`; + const sysName = `COR-${rng.int(100, 999)}`; + + const sys = { + id: sysId, name: sysName, regionId: 'core', constellationId: constId, + x: sx, y: sy, z: sz, + security: sec, starType, starColor: starColors[starType], + planetCount: rng.int(2, 7), faction: 'Concord', + armIndex: -1, distFactor: 0, + }; + systems.push(sys); + con.systemIds.push(sysId); + coreRegion.systemIds.push(sysId); + + // Core systems get lots of stations and services + const stationCount = rng.int(2, 4); + for (let sti = 0; sti < stationCount; sti++) { + const services = ['Market', 'Refinery', 'Factory', 'Fitting']; + if (rng.next() > 0.4) services.push('Insurance'); + if (rng.next() > 0.5) services.push('Clone Bay'); + stations.push({ + id: `${sysId}_stn${sti}`, systemId: sysId, + name: `${sysName} Station ${String.fromCharCode(65 + sti)}`, + services, + }); + if (rng.next() > 0.3) { + agents.push({ + stationId: `${sysId}_stn${sti}`, + specialty: rng.pick(['Kill', 'Courier', 'Mining', 'Survey', 'Trade']), + faction: 'Concord', + }); + } + } + + const beltCount = rng.int(1, 3); + const ores = getOresForSec(sec); + for (let bi = 0; bi < beltCount; bi++) { + belts.push({ id: `${sysId}_belt${bi}`, systemId: sysId, oreType: rng.pick(ores) }); + } + } + } + + // ── Arm regions ── + for (let armIdx = 0; armIdx < armCount; armIdx++) { + const fDef = factionDefs[armIdx % factionDefs.length]; + const region = { + id: fDef.id, name: fDef.name, faction: fDef.faction, + factionColor: fDef.color, + secMin: -1.0, secMax: 0.8, + color: fDef.color, + center: { x: 0, y: 0, z: 0 }, // computed below + systemIds: [], + armIndex: armIdx, + }; + + // Each arm has 2-4 constellations at different distances + const constCount = rng.int(2, 4); + let armCx = 0, armCy = 0, armCz = 0; + + for (let ci = 0; ci < constCount; ci++) { + // Distribute constellations along the arm at increasing distances + const distBase = 0.15 + (ci / constCount) * 0.75; + const distFactor = distBase + rng.range(-0.05, 0.05); + const pos = spiralPosition(armIdx, distFactor); + + const constId = `${fDef.id}_c${ci}`; + const con = { + id: constId, regionId: fDef.id, + x: pos.x, y: pos.y, z: pos.z, + systemIds: [], + armIndex: armIdx, + distFactor: distFactor, + }; + constellations.push(con); + + const sysCount = rng.int(3, 6); + for (let si = 0; si < sysCount; si++) { + // Systems cluster around the constellation center on the arm + const sysDistFactor = distFactor + rng.gaussian(0, 0.04); + const sysPos = spiralPosition(armIdx, Math.max(0.08, Math.min(0.98, sysDistFactor))); + const sec = securityFromDist(sysDistFactor); + const starType = pickStarType(); + const sysId = `${constId}_s${si}`; + const sysName = `${fDef.faction.substring(0, 3).toUpperCase()}-${rng.int(100, 999)}`; + + const sys = { + id: sysId, name: sysName, regionId: fDef.id, constellationId: constId, + x: sysPos.x, y: sysPos.y, z: sysPos.z, + security: sec, starType, starColor: starColors[starType], + planetCount: rng.int(1, 6), faction: fDef.faction, + armIndex: armIdx, distFactor: sysDistFactor, + }; + systems.push(sys); + con.systemIds.push(sysId); + region.systemIds.push(sysId); + armCx += sysPos.x; armCy += sysPos.y; armCz += sysPos.z; + + // Stations — more in high-sec, fewer in null + let stationCount; + if (sec >= 0.8) stationCount = rng.int(2, 4); + else if (sec >= 0.5) stationCount = rng.int(1, 3); + else if (sec >= 0.1) stationCount = rng.int(1, 2); + else if (sec >= 0) stationCount = rng.int(0, 2); + else stationCount = rng.int(0, 1); + + for (let sti = 0; sti < stationCount; sti++) { + const services = ['Market']; + if (rng.next() > 0.3) services.push('Refinery'); + if (rng.next() > 0.4) services.push('Factory'); + if (rng.next() > 0.3) services.push('Fitting'); + if (rng.next() > 0.5) services.push('Insurance'); + stations.push({ + id: `${sysId}_stn${sti}`, systemId: sysId, + name: `${sysName} Station ${String.fromCharCode(65 + sti)}`, + services, + }); + if (rng.next() > 0.4) { + agents.push({ + stationId: `${sysId}_stn${sti}`, + specialty: rng.pick(['Kill', 'Courier', 'Mining', 'Survey', 'Trade']), + faction: fDef.faction, + }); + } + } + + // Belts + const beltCount = sec >= 0.8 ? rng.int(1, 3) : sec >= 0.5 ? rng.int(2, 4) : rng.int(2, 5); + const ores = getOresForSec(sec); + for (let bi = 0; bi < beltCount; bi++) { + belts.push({ id: `${sysId}_belt${bi}`, systemId: sysId, oreType: rng.pick(ores) }); + } + } + } + + // Compute arm center + const armSysCount = region.systemIds.length || 1; + region.center = { x: armCx / armSysCount, y: armCy / armSysCount, z: armCz / armSysCount }; + regions.push(region); + } + + // ── System lookup map for O(1) access ── + const sysMap = new Map(); + systems.forEach(s => sysMap.set(s.id, s)); + + // ── Build stargate graph ── + if (systems.length < 2) return { regions, constellations, systems, stargates, stations, belts, agents, seed, sysMap, dustPositions: new Float32Array(0), dustColors: new Float32Array(0), dustCount: 0, params: { armCount, galaxySize, armSpread, rotationStr, gravity, heightMag, heightFreq } }; + + // 1) MST for connectivity (Prim's — uses sysMap for O(1) lookup) + const inMST = new Set([systems[0].id]); + const mstEdges = []; + const distSq = (a, b) => (a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2; + + while (inMST.size < systems.length) { + let bestDist = Infinity, bestA = null, bestB = null; + for (const aId of inMST) { + const a = sysMap.get(aId); + for (const b of systems) { + if (inMST.has(b.id)) continue; + const d = distSq(a, b); + if (d < bestDist) { bestDist = d; bestA = aId; bestB = b.id; } + } + } + if (bestB) { inMST.add(bestB); mstEdges.push([bestA, bestB]); } + } + + mstEdges.forEach(([a, b]) => stargates.push({ from: a, to: b, type: 'mst' })); + + // 2) Intra-constellation extras + constellations.forEach(con => { + if (con.systemIds.length < 3) return; + const extras = rng.int(1, Math.min(3, con.systemIds.length - 1)); + for (let i = 0; i < extras; i++) { + const a = rng.pick(con.systemIds); + const b = rng.pick(con.systemIds.filter(id => id !== a)); + const exists = stargates.some(g => (g.from === a && g.to === b) || (g.from === b && g.to === a)); + if (!exists) stargates.push({ from: a, to: b, type: 'intra' }); + } + }); + + // Pre-compute sorted arm constellations (avoid repeated filter+sort) + const armConstsByArm = new Map(); + for (let armIdx = 0; armIdx < armCount; armIdx++) { + armConstsByArm.set(armIdx, constellations.filter(c => c.armIndex === armIdx).sort((a, b) => a.distFactor - b.distFactor)); + } + + // 3) Intra-arm connections + for (let armIdx = 0; armIdx < armCount; armIdx++) { + const armConsts = armConstsByArm.get(armIdx); + for (let i = 0; i < armConsts.length - 1; i++) { + const a = rng.pick(armConsts[i].systemIds); + const b = rng.pick(armConsts[i + 1].systemIds); + const exists = stargates.some(g => (g.from === a && g.to === b) || (g.from === b && g.to === a)); + if (!exists) stargates.push({ from: a, to: b, type: 'arm-link' }); + } + } + + // 4) Core hub + const coreSys = coreRegion.systemIds; + for (let armIdx = 0; armIdx < armCount; armIdx++) { + const armConsts = armConstsByArm.get(armIdx); + if (armConsts.length > 0 && coreSys.length > 0) { + const borderA = rng.pick(armConsts[0].systemIds); + const borderB = rng.pick(coreSys); + const exists = stargates.some(g => (g.from === borderA && g.to === borderB) || (g.from === borderB && g.to === borderA)); + if (!exists) stargates.push({ from: borderA, to: borderB, type: 'hub' }); + } + } + + // 5) Cross-arm choke points + for (let armIdx = 0; armIdx < armCount; armIdx++) { + const nextArm = (armIdx + 1) % armCount; + const armA = regions.find(r => r.armIndex === armIdx); + const armB = regions.find(r => r.armIndex === nextArm); + if (armA && armB && armA.systemIds.length > 0 && armB.systemIds.length > 0) { + const borderA = rng.pick(armA.systemIds); + const borderB = rng.pick(armB.systemIds); + const exists = stargates.some(g => (g.from === borderA && g.to === borderB) || (g.from === borderB && g.to === borderA)); + if (!exists) stargates.push({ from: borderA, to: borderB, type: 'choke' }); + } + } + + // 6) High-sec shortcuts + const highSecSys = systems.filter(s => s.security >= 0.5); + const shortcutCount = Math.floor(highSecSys.length * 0.15); + for (let i = 0; i < shortcutCount; i++) { + const a = rng.pick(highSecSys); + const b = rng.pick(highSecSys.filter(s => s.id !== a.id)); + const exists = stargates.some(g => (g.from === a.id && g.to === b.id) || (g.from === b.id && g.to === a.id)); + if (!exists) stargates.push({ from: a.id, to: b.id, type: 'shortcut' }); + } + + // ── Connectivity check ── + const adj = {}; + systems.forEach(s => { adj[s.id] = []; }); + stargates.forEach(g => { adj[g.from].push(g.to); adj[g.to].push(g.from); }); + const visited = new Set(); + const queue = [systems[0].id]; + visited.add(systems[0].id); + while (queue.length > 0) { + const cur = queue.shift(); + for (const nb of (adj[cur] || [])) { + if (!visited.has(nb)) { visited.add(nb); queue.push(nb); } + } + } + const connected = visited.size === systems.length; + + const chokeSet = new Set(); + stargates.filter(g => g.type === 'choke').forEach(g => { chokeSet.add(g.from); chokeSet.add(g.to); }); + + const starterSystem = systems.find(s => s.security >= 0.95) || systems.find(s => s.security >= 0.8) || systems[0]; + + function findPath(startId, endId) { + const prev = {}; + const vis = new Set([startId]); + const q = [startId]; + while (q.length > 0) { + const cur = q.shift(); + if (cur === endId) break; + for (const nb of (adj[cur] || [])) { + if (!vis.has(nb)) { vis.add(nb); prev[nb] = cur; q.push(nb); } + } + } + const path = []; + let c = endId; + while (c) { path.unshift(c); c = prev[c]; } + return path[0] === startId ? path : []; + } + + // ── Generate background dust as flat typed arrays ── + // No intermediate objects — writes directly to Float32Arrays + const dustCount = 8000; + const armColorDefs = factionDefs.map(f => f.armColor); + const armR = armColorDefs.map(c => ((c >> 16) & 0xFF) / 255); + const armG = armColorDefs.map(c => ((c >> 8) & 0xFF) / 255); + const armB = armColorDefs.map(c => (c & 0xFF) / 255); + const dustPositions = new Float32Array(dustCount * 3); + const dustColors = new Float32Array(dustCount * 3); + const TWO_PI = 2 * Math.PI; + const armAngleStep = TWO_PI / armCount; + + for (let i = 0; i < dustCount; i++) { + let distance, angle; + const isCore = rng.next() < 0.12; + + if (isCore) { + distance = Math.pow(rng.next(), gravity * 1.5) * galaxySize * 0.25; + angle = rng.next() * TWO_PI; + const brightness = 0.4 + rng.next() * 0.6; + dustPositions[i * 3] = distance * Math.cos(angle); + dustPositions[i * 3 + 1] = rng.gaussian(0, 2); + dustPositions[i * 3 + 2] = distance * Math.sin(angle); + dustColors[i * 3] = brightness; + dustColors[i * 3 + 1] = brightness * 0.95; + dustColors[i * 3 + 2] = brightness * 1.1; + } else { + const onArm = rng.next() < conformChance; + const armIdx = rng.int(0, armCount); + const distFactor = Math.pow(rng.next(), gravity); + distance = distFactor * galaxySize; + const distRatio = distFactor; + const baseAngle = armAngleStep * armIdx; + const spreadAngle = armSpread * (Math.PI / armCount) * (0.4 + distRatio * 0.9); + const bendAngle = distRatio * rotationStr; + + if (onArm) { + const angleOffset = rng.gaussian(0, spreadAngle); + angle = baseAngle + angleOffset + bendAngle; + const brightness = 0.3 + (1 - distRatio) * 0.7; + const iR = (1 - distRatio) * 0.3; + dustPositions[i * 3] = distance * Math.cos(angle); + dustPositions[i * 3 + 1] = heightMag * Math.sin(distance * heightFreq) * (1 - distRatio * 0.3) + rng.gaussian(0, 1.5 + distRatio * 3); + dustPositions[i * 3 + 2] = distance * Math.sin(angle); + dustColors[i * 3] = Math.min(1, armR[armIdx] * brightness + iR); + dustColors[i * 3 + 1] = Math.min(1, armG[armIdx] * brightness + iR * 0.83); + dustColors[i * 3 + 2] = Math.min(1, armB[armIdx] * brightness + iR * 0.67); + } else { + angle = rng.next() * TWO_PI; + const brightness = 0.08 + (1 - distRatio) * 0.15; + dustPositions[i * 3] = distance * Math.cos(angle); + dustPositions[i * 3 + 1] = heightMag * Math.sin(distance * heightFreq) * (1 - distRatio * 0.3) + rng.gaussian(0, 2 + distRatio * 4); + dustPositions[i * 3 + 2] = distance * Math.sin(angle); + dustColors[i * 3] = brightness; + dustColors[i * 3 + 1] = brightness * 0.95; + dustColors[i * 3 + 2] = brightness * 1.1; + } + } + } + + return { + regions, constellations, systems, stargates, stations, belts, agents, seed, + connected, chokeSet, starterSystem, findPath, sysMap, + params: { armCount, galaxySize, armSpread, rotationStr, gravity, heightMag, heightFreq }, + dustPositions, dustColors, dustCount, + }; +} + +/* ═══════════════════════════════════════════════ + Helper: Security → Color + ═══════════════════════════════════════════════ */ +function secColor(sec) { + if (sec >= 0.8) return 0x22c55e; + if (sec >= 0.5) return 0x86efac; + if (sec >= 0.1) return 0xf0a030; + if (sec >= 0) return 0xfb923c; + if (sec >= -0.4) return 0xef4444; + return 0xa855f7; +} + +function secColorCSS(sec) { + if (sec >= 0.8) return '#22c55e'; + if (sec >= 0.5) return '#86efac'; + if (sec >= 0.1) return '#f0a030'; + if (sec >= 0) return '#fb923c'; + if (sec >= -0.4) return '#ef4444'; + return '#a855f7'; +} + +function factionColor(faction) { + switch (faction) { + case 'Caldari': return 0x38bdf8; + case 'Gallente': return 0xa855f7; + case 'Minmatar': return 0xef4444; + case 'Amarr': return 0xf0a030; + case 'Concord': return 0x22d3ee; + default: return 0x94a3b8; + } +} + +/* ═══════════════════════════════════════════════ + Galaxy Demo — Three.js 3D (Spiral Galaxy) + ═══════════════════════════════════════════════ */ +function GalaxyDemo() { + const containerRef = useRef(null); + const sceneRef = useRef(null); + const [seed, setSeed] = useState(42); + const [inputSeed, setInputSeed] = useState('42'); + const [galaxy, setGalaxy] = useState(null); + const [hoveredSystem, setHoveredSystem] = useState(null); + const [selectedSystem, setSelectedSystem] = useState(null); + const [pathStart, setPathStart] = useState(null); + const [pathEnd, setPathEnd] = useState(null); + const [currentPath, setCurrentPath] = useState([]); + const [showGates, setShowGates] = useState(true); + const [showLabels, setShowLabels] = useState(true); + const [showDust, setShowDust] = useState(true); + const [viewMode, setViewMode] = useState('security'); + const [autoRotate, setAutoRotate] = useState(true); + const [galaxyParams, setGalaxyParams] = useState({ + armCount: 4, + rotationStrength: 2.8, + armSpread: 0.42, + gravity: 1.4, + }); + + // Refs for Three.js objects we need to update without re-render + const galaxyRef = useRef(null); + const selectedSystemRef = useRef(null); + const hoveredSystemRef = useRef(null); + const pathStartRef = useRef(null); + const pathEndRef = useRef(null); + const currentPathRef = useRef([]); + const viewModeRef = useRef('security'); + const showGatesRef = useRef(true); + const showLabelsRef = useRef(true); + const showDustRef = useRef(true); + const autoRotateRef = useRef(true); + + // Keep refs in sync with state + useEffect(() => { selectedSystemRef.current = selectedSystem; }, [selectedSystem]); + useEffect(() => { hoveredSystemRef.current = hoveredSystem; }, [hoveredSystem]); + useEffect(() => { pathStartRef.current = pathStart; }, [pathStart]); + useEffect(() => { pathEndRef.current = pathEnd; }, [pathEnd]); + useEffect(() => { currentPathRef.current = currentPath; }, [currentPath]); + useEffect(() => { viewModeRef.current = viewMode; }, [viewMode]); + useEffect(() => { showGatesRef.current = showGates; }, [showGates]); + useEffect(() => { showLabelsRef.current = showLabels; }, [showLabels]); + useEffect(() => { showDustRef.current = showDust; }, [showDust]); + useEffect(() => { autoRotateRef.current = autoRotate; }, [autoRotate]); + + // Generate galaxy data + useEffect(() => { + const g = generateGalaxy(seed, galaxyParams); + setGalaxy(g); + setSelectedSystem(null); + setHoveredSystem(null); + setPathStart(null); + setPathEnd(null); + setCurrentPath([]); + galaxyRef.current = g; + }, [seed, galaxyParams]); + + // ── Three.js scene setup ── + useEffect(() => { + const TH = window.GDD.THREE; + const T = window.THREE; + if (!T || !containerRef.current) return; + + const container = containerRef.current; + + // Renderer + const renderer = TH.createRenderer(container, { clearColor: 0x020508 }); + + // Scene + const scene = new T.Scene(); + scene.fog = new T.FogExp2(0x020508, 0.0004); + + // Camera — elevated view of the disk + const camera = new T.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.5, 8000); + camera.position.set(0, 320, 420); + camera.lookAt(0, 0, 0); + + // Orbit controller + const orbit = new TH.OrbitController(camera, renderer.domElement, new T.Vector3(0, 0, 0)); + orbit.distance = 520; + orbit.minDistance = 15; + orbit.maxDistance = 2000; + orbit.panButton = 2; + orbit.rotateSpeed = 0.5; + + // Lighting + TH.setupSpaceLighting(scene); + + // Background star field — distant + const starField = TH.createStarField(5000, 4000); + scene.add(starField); + + // ── Scene groups ── + const dustGroup = new T.Group(); + scene.add(dustGroup); + + const coreGlowGroup = new T.Group(); + scene.add(coreGlowGroup); + + const systemGroup = new T.Group(); + scene.add(systemGroup); + + const gateGroup = new T.Group(); + scene.add(gateGroup); + + const labelGroup = new T.Group(); + scene.add(labelGroup); + + const highlightGroup = new T.Group(); + scene.add(highlightGroup); + + // ── Raycasting ── + const raycaster = new T.Raycaster(); + const mouse = new T.Vector2(); + const clickMouse = new T.Vector2(); + + let systemMeshes = []; + // Shared geometries — created once, reused across all systems + const sharedCoreGeo = new T.SphereGeometry(2.2, 10, 10); + const sharedInnerGeo = new T.SphereGeometry(1.0, 6, 6); + // Shared glow texture — one canvas, reused for all system glow sprites + const glowCanvas = document.createElement('canvas'); + glowCanvas.width = 32; glowCanvas.height = 32; + const gCtx = glowCanvas.getContext('2d'); + const gGrad = gCtx.createRadialGradient(16, 16, 0, 16, 16, 16); + gGrad.addColorStop(0, 'rgba(255,255,255,0.8)'); + gGrad.addColorStop(0.2, 'rgba(255,255,255,0.4)'); + gGrad.addColorStop(0.5, 'rgba(255,255,255,0.1)'); + gGrad.addColorStop(1, 'rgba(255,255,255,0)'); + gCtx.fillStyle = gGrad; + gCtx.fillRect(0, 0, 32, 32); + const sharedGlowTexture = new T.CanvasTexture(glowCanvas); + + let currentSysMap = null; // cached lookup for O(1) system access + + // ── Build the 3D scene ── + // Set of shared resources that must NOT be disposed during scene rebuild + const sharedResources = new Set([sharedCoreGeo, sharedInnerGeo, sharedGlowTexture, sharedRingGeo, sharedHoverRingGeo]); + + function disposeGroup(group, disposeSharedGeo) { + while (group.children.length > 0) { + const child = group.children[0]; + group.remove(child); + // Recursively dispose children (e.g., Group → Mesh/Sprite) + if (child.children && child.children.length > 0) { + disposeGroup(child, false); + } + // Only dispose non-shared geometry and materials + if (child.geometry && (disposeSharedGeo || !sharedResources.has(child.geometry))) { + child.geometry.dispose(); + } + if (child.material) { + // Only dispose textures that aren't shared + if (child.material.map && !sharedResources.has(child.material.map)) { + child.material.map.dispose(); + } + child.material.dispose(); + } + } + } + + function buildScene(galaxy) { + // Dispose scene groups — pass true only for groups that own their own geometry + disposeGroup(systemGroup, false); // uses shared geometry + disposeGroup(gateGroup, true); // owns its geometry + disposeGroup(labelGroup, true); // owns its canvas textures + disposeGroup(highlightGroup, false); // uses shared geometry/textures + disposeGroup(dustGroup, true); // owns its geometry + disposeGroup(coreGlowGroup, true); // owns its canvas textures + + systemMeshes = []; + currentSysMap = galaxy.sysMap; + if (!galaxy) return; + + // ── Galaxy dust — use pre-built typed arrays directly (no copy) ── + const dustGeo = new T.BufferGeometry(); + dustGeo.setAttribute('position', new T.BufferAttribute(galaxy.dustPositions, 3)); + dustGeo.setAttribute('color', new T.BufferAttribute(galaxy.dustColors, 3)); + const dustMat = new T.PointsMaterial({ + size: 1.8, + vertexColors: true, + transparent: true, + opacity: 0.55, + sizeAttenuation: true, + blending: T.AdditiveBlending, + depthWrite: false, + }); + dustGroup.add(new T.Points(dustGeo, dustMat)); + + // ── Central core glow ── + // Large outer glow + const outerGlow = TH.createGlowSprite(0xfff8e0, 120); + outerGlow.position.set(0, 0, 0); + coreGlowGroup.add(outerGlow); + + // Medium warm glow + const midGlow = TH.createGlowSprite(0xffcc66, 60); + midGlow.position.set(0, 0, 0); + coreGlowGroup.add(midGlow); + + // Bright inner core + const innerGlow = TH.createGlowSprite(0xffffff, 30); + innerGlow.position.set(0, 0, 0); + coreGlowGroup.add(innerGlow); + + // ── Arm nebula sprites ── + const armColorDefs = [ + { color: 0x38bdf8, r: 0x38, g: 0xb5, b: 0xf8 }, + { color: 0xef4444, r: 0xef, g: 0x44, b: 0x44 }, + { color: 0xf0a030, r: 0xf0, g: 0xa0, b: 0x30 }, + { color: 0xa855f7, r: 0xa8, g: 0x55, b: 0xf7 }, + ]; + const { armCount, galaxySize, rotationStr } = galaxy.params; + for (let ai = 0; ai < armCount; ai++) { + const ac = armColorDefs[ai % armColorDefs.length]; + // Place nebula sprites at 3 distances along each arm + for (let di = 0; di < 3; di++) { + const df = 0.25 + di * 0.25; + const dist = df * galaxySize; + const baseAngle = (2 * Math.PI / armCount) * ai; + const bendAngle = df * rotationStr; + const angle = baseAngle + bendAngle; + const x = dist * Math.cos(angle); + const z = dist * Math.sin(angle); + const y = galaxy.params.heightMag * Math.sin(dist * galaxy.params.heightFreq) * (1 - df * 0.3); + + const nebula = TH.createGlowSprite(ac.color, 40 + di * 15); + nebula.position.set(x, y, z); + nebula.material.opacity = 0.04; + coreGlowGroup.add(nebula); + } + } + + // ── System spheres (shared geometry + per-system materials) ── + // Each system gets its own material so viewMode color changes don't corrupt other systems + galaxy.systems.forEach(sys => { + const group = new T.Group(); + group.position.set(sys.x, sys.y, sys.z); + group.userData = { systemId: sys.id }; + + // Core sphere — clickable, shared geometry but own material + const secHex = secColor(sys.security); + const coreMat = new T.MeshBasicMaterial({ color: secHex }); + const core = new T.Mesh(sharedCoreGeo, coreMat); + core.userData = { systemId: sys.id, isSystem: true }; + group.add(core); + + // Glow sprite — reuse shared canvas texture, own material + const glowMat = new T.SpriteMaterial({ + map: sharedGlowTexture, + color: secHex, + transparent: true, + blending: T.AdditiveBlending, + depthWrite: false, + }); + const glow = new T.Sprite(glowMat); + glow.scale.setScalar(10); + group.add(glow); + + // Star color inner dot — shared geometry, own material + const innerColor = new T.Color(sys.starColor).getHex(); + const innerMat = new T.MeshBasicMaterial({ color: innerColor }); + const inner = new T.Mesh(sharedInnerGeo, innerMat); + group.add(inner); + + systemGroup.add(group); + systemMeshes.push(core); + + // Label + const label = TH.createLabel(sys.name, secColorCSS(sys.security), 16); + label.position.set(sys.x, sys.y + 6, sys.z); + label.userData = { systemId: sys.id, isLabel: true }; + labelGroup.add(label); + }); + + // ── Stargate lines (use sysMap for O(1) lookup) ── + galaxy.stargates.forEach(gate => { + const sA = currentSysMap.get(gate.from); + const sB = currentSysMap.get(gate.to); + if (!sA || !sB) return; + + let color, opacity; + if (gate.type === 'mst') { color = 0x0e1a2a; opacity = 0.4; } + else if (gate.type === 'intra') { color = 0x0f1e30; opacity = 0.35; } + else if (gate.type === 'arm-link') { color = 0x152535; opacity = 0.3; } + else if (gate.type === 'choke') { color = 0xf0a030; opacity = 0.35; } + else if (gate.type === 'hub') { color = 0x22c55e; opacity = 0.25; } + else { color = 0x101820; opacity = 0.2; } + + const line = TH.createConnectionLine( + { x: sA.x, y: sA.y, z: sA.z }, + { x: sB.x, y: sB.y, z: sB.z }, + color, opacity + ); + line.userData = { gateFrom: gate.from, gateTo: gate.to, gateType: gate.type }; + gateGroup.add(line); + }); + } + + // ── Update highlights (reuses geometry, uses sysMap for O(1)) ── + const sharedRingGeo = new T.RingGeometry(5, 6, 24); + sharedRingGeo.rotateX(-Math.PI / 2); + const sharedHoverRingGeo = new T.RingGeometry(4, 4.8, 24); + sharedHoverRingGeo.rotateX(-Math.PI / 2); + + function updateHighlights() { + disposeGroup(highlightGroup, false); // uses shared geometry/textures + const g = galaxyRef.current; + if (!g) return; + const sm = currentSysMap; + if (!sm) return; + + const selId = selectedSystemRef.current?.id; + const hovId = hoveredSystemRef.current?.id; + const pStart = pathStartRef.current; + const pEnd = pathEndRef.current; + const path = currentPathRef.current; + + // Selection ring — shared geometry + if (selId) { + const sys = sm.get(selId); + if (sys) { + const ringMat = new T.MeshBasicMaterial({ + color: 0x22d3ee, transparent: true, opacity: 0.7, side: T.DoubleSide, depthWrite: false, + }); + const ring = new T.Mesh(sharedRingGeo, ringMat); + ring.position.set(sys.x, sys.y, sys.z); + highlightGroup.add(ring); + } + } + + // Hover ring + if (hovId && hovId !== selId) { + const sys = sm.get(hovId); + if (sys) { + const ringMat = new T.MeshBasicMaterial({ + color: 0xf1f5f9, transparent: true, opacity: 0.5, side: T.DoubleSide, depthWrite: false, + }); + const ring = new T.Mesh(sharedHoverRingGeo, ringMat); + ring.position.set(sys.x, sys.y, sys.z); + highlightGroup.add(ring); + } + } + + // Path markers — reuse shared glow texture + if (pStart) { + const sys = sm.get(pStart); + if (sys) { + const mat = new T.SpriteMaterial({ map: sharedGlowTexture, color: 0x22c55e, transparent: true, blending: T.AdditiveBlending, depthWrite: false }); + const marker = new T.Sprite(mat); + marker.scale.setScalar(20); + marker.position.set(sys.x, sys.y + 1, sys.z); + highlightGroup.add(marker); + } + } + if (pEnd) { + const sys = sm.get(pEnd); + if (sys) { + const mat = new T.SpriteMaterial({ map: sharedGlowTexture, color: 0xef4444, transparent: true, blending: T.AdditiveBlending, depthWrite: false }); + const marker = new T.Sprite(mat); + marker.scale.setScalar(20); + marker.position.set(sys.x, sys.y + 1, sys.z); + highlightGroup.add(marker); + } + } + + // Route line + if (path.length > 1) { + const points = path.map(id => { + const sys = sm.get(id); + return sys ? new T.Vector3(sys.x, sys.y + 2, sys.z) : new T.Vector3(); + }); + const routeLine = TH.createRouteLine(points.map(p => ({ x: p.x, y: p.y, z: p.z })), 0x22d3ee); + highlightGroup.add(routeLine); + } + } + + // ── Update system colors based on viewMode (uses sysMap for O(1)) ── + function updateSystemColors() { + const g = galaxyRef.current; + if (!g) return; + const mode = viewModeRef.current; + const sm = currentSysMap; + + systemGroup.children.forEach(group => { + const sysId = group.userData.systemId; + const sys = sm ? sm.get(sysId) : g.systems.find(s => s.id === sysId); + if (!sys) return; + + const core = group.children.find(c => c.userData.isSystem); + if (!core) return; + + let color; + if (mode === 'faction') color = factionColor(sys.faction); + else if (mode === 'gates') { + const gateCount = g.stargates.filter(gate => gate.from === sys.id || gate.to === sys.id).length; + color = gateCount >= 4 ? 0xef4444 : gateCount >= 3 ? 0xf0a030 : gateCount >= 2 ? 0x22d3ee : 0x94a3b8; + } else { + color = secColor(sys.security); + } + core.material.color.setHex(color); + }); + } + + // ── Mouse interaction ── + function getHoveredSystem(event) { + const rect = renderer.domElement.getBoundingClientRect(); + mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(systemMeshes, false); + if (intersects.length > 0) { + const sysId = intersects[0].object.userData.systemId; + return currentSysMap ? currentSysMap.get(sysId) : (galaxyRef.current?.systems.find(s => s.id === sysId) || null); + } + return null; + } + + let mouseDownPos = { x: 0, y: 0 }; + + const onMouseDown = (e) => { + mouseDownPos = { x: e.clientX, y: e.clientY }; + }; + + const onMouseMove = (e) => { + const hovered = getHoveredSystem(e); + setHoveredSystem(hovered); + renderer.domElement.style.cursor = hovered ? 'pointer' : 'default'; + }; + + const onClick = (e) => { + if (e.button !== 0) return; + // Ignore if it was a drag + const dx = e.clientX - mouseDownPos.x; + const dy = e.clientY - mouseDownPos.y; + if (Math.sqrt(dx * dx + dy * dy) > 5) return; + + const rect = renderer.domElement.getBoundingClientRect(); + clickMouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + clickMouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + raycaster.setFromCamera(clickMouse, camera); + const intersects = raycaster.intersectObjects(systemMeshes, false); + if (intersects.length > 0) { + const sysId = intersects[0].object.userData.systemId; + const sys = galaxyRef.current?.systems.find(s => s.id === sysId); + if (sys) { + setSelectedSystem(sys); + orbit.flyTo(sys.x, sys.y, sys.z, 60); + } + } else { + setSelectedSystem(null); + } + }; + + const onContextMenu = (e) => { + e.preventDefault(); + // Ignore if it was a drag + const dx = e.clientX - mouseDownPos.x; + const dy = e.clientY - mouseDownPos.y; + if (Math.sqrt(dx * dx + dy * dy) > 5) return; + + const hovered = getHoveredSystem(e); + if (!hovered || !galaxyRef.current) return; + + const g = galaxyRef.current; + const ps = pathStartRef.current; + + if (!ps) { + setPathStart(hovered.id); + setPathEnd(null); + setCurrentPath([]); + } else if (ps !== hovered.id) { + setPathEnd(hovered.id); + const path = g.findPath(ps, hovered.id); + setCurrentPath(path); + } else { + setPathStart(null); + setPathEnd(null); + setCurrentPath([]); + } + }; + + renderer.domElement.addEventListener('mousedown', onMouseDown); + renderer.domElement.addEventListener('mousemove', onMouseMove); + renderer.domElement.addEventListener('click', onClick); + renderer.domElement.addEventListener('contextmenu', onContextMenu); + + // ── Resize handling ── + const onResize = () => { + TH.handleResize(renderer, camera, container); + }; + window.addEventListener('resize', onResize); + + // ── Animation loop ── + let animId; + let lastTime = performance.now(); + let prevHighlightState = ''; + + function animate() { + animId = requestAnimationFrame(animate); + const now = performance.now(); + const dt = (now - lastTime) / 1000; + lastTime = now; + + // Auto-rotate (slow orbit) + if (autoRotateRef.current && !orbit.isDragging && !orbit._flyActive) { + orbit.theta += dt * 0.03; + } + + orbit.update(dt); + + // Only rebuild highlights when state actually changed + const stateKey = [ + selectedSystemRef.current?.id, + hoveredSystemRef.current?.id, + pathStartRef.current, + pathEndRef.current, + currentPathRef.current.join(','), + ].join('|'); + if (stateKey !== prevHighlightState) { + prevHighlightState = stateKey; + updateHighlights(); + } + + // Toggle visibility + gateGroup.visible = showGatesRef.current; + labelGroup.visible = showLabelsRef.current; + dustGroup.visible = showDustRef.current; + + // Subtle dust cloud rotation (very cheap — just rotates the group matrix) + if (dustGroup.visible && dustGroup.children.length > 0) { + dustGroup.rotation.y += dt * 0.002; + } + + // Core glow pulse + const pulse = 1.0 + Math.sin(now * 0.001) * 0.08; + coreGlowGroup.children.forEach(sprite => { + if (sprite.isSprite && sprite.scale) { + const baseScale = sprite.userData.baseScale || sprite.scale.x; + sprite.userData.baseScale = baseScale; + sprite.scale.setScalar(baseScale * pulse); + } + }); + + renderer.render(scene, camera); + } + + sceneRef.current = { buildScene, updateSystemColors, renderer, orbit }; + + animate(); + + return () => { + cancelAnimationFrame(animId); + window.removeEventListener('resize', onResize); + renderer.domElement.removeEventListener('mousedown', onMouseDown); + renderer.domElement.removeEventListener('mousemove', onMouseMove); + renderer.domElement.removeEventListener('click', onClick); + renderer.domElement.removeEventListener('contextmenu', onContextMenu); + orbit.dispose(); + renderer.dispose(); + if (container.contains(renderer.domElement)) { + container.removeChild(renderer.domElement); + } + sceneRef.current = null; + }; + }, []); // Mount once + + // ── Rebuild 3D scene when galaxy changes ── + useEffect(() => { + if (sceneRef.current && galaxy) { + sceneRef.current.buildScene(galaxy); + sceneRef.current.updateSystemColors(); + } + }, [galaxy]); + + // ── Update system colors when viewMode changes ── + useEffect(() => { + if (sceneRef.current) { + sceneRef.current.updateSystemColors(); + } + }, [viewMode]); + + // ── Seed handlers ── + const handleSeedSubmit = useCallback(() => { + const parsed = parseInt(inputSeed, 10); + if (!isNaN(parsed) && parsed > 0) { + setSeed(parsed); + } else { + let hash = 0; + for (let i = 0; i < inputSeed.length; i++) { + hash = ((hash << 5) - hash) + inputSeed.charCodeAt(i); + hash = hash >>> 0; + } + setSeed(hash || 1); + } + }, [inputSeed]); + + const randomSeed = useCallback(() => { + const newSeed = Math.floor(Math.random() * 999999) + 1; + setSeed(newSeed); + setInputSeed(String(newSeed)); + }, []); + + // ── Selected system details ── + const selectedDetails = useMemo(() => { + if (!galaxy || !selectedSystem) return null; + const sys = selectedSystem; + const sysStations = galaxy.stations.filter(s => s.systemId === sys.id); + const sysBelts = galaxy.belts.filter(b => b.systemId === sys.id); + const sysGates = galaxy.stargates.filter(g => g.from === sys.id || g.to === sys.id); + const connectedSystems = sysGates.map(g => { + const targetId = g.from === sys.id ? g.to : g.from; + return galaxy.sysMap ? galaxy.sysMap.get(targetId) : galaxy.systems.find(s => s.id === targetId); + }).filter(Boolean); + return { ...sys, sysStations, sysBelts, sysGates, connectedSystems }; + }, [galaxy, selectedSystem]); + + // ── Stats ── + const stats = useMemo(() => { + if (!galaxy) return null; + const totalGates = galaxy.stargates.length; + const mstGates = galaxy.stargates.filter(g => g.type === 'mst').length; + const chokeGates = galaxy.stargates.filter(g => g.type === 'choke').length; + const hubGates = galaxy.stargates.filter(g => g.type === 'hub').length; + const avgGatesPerSys = (totalGates * 2 / galaxy.systems.length).toFixed(1); + const secDist = { high: 0, low: 0, null: 0, deep: 0 }; + galaxy.systems.forEach(s => { + if (s.security >= 0.5) secDist.high++; + else if (s.security >= 0.1) secDist.low++; + else if (s.security >= -0.4) secDist.null++; + else secDist.deep++; + }); + return { + systems: galaxy.systems.length, constellations: galaxy.constellations.length, + regions: galaxy.regions.length, gates: totalGates, mstGates, chokeGates, hubGates, + avgGatesPerSys, stations: galaxy.stations.length, belts: galaxy.belts.length, + agents: galaxy.agents.length, connected: galaxy.connected, secDist, + }; + }, [galaxy]); + + if (!galaxy) return
Generating galaxy…
; + + return ( +
+ {/* Three.js Container */} +
+ + {/* HUD overlay - top left: seed + view mode */} +
+
+ SEED + setInputSeed(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleSeedSubmit()} + style={{ width: 80, background: '#0a0f18', border: '1px solid #1c2a3f', borderRadius: 3, color: '#f0a030', fontSize: 12, padding: '2px 6px', fontFamily: 'inherit', outline: 'none' }} + /> + + +
+
+ {['security', 'faction', 'gates'].map(mode => ( + + ))} +
+
+ + {/* Galaxy parameters overlay - below top bar */} +
+
+ ARMS + setGalaxyParams(p => ({ ...p, armCount: parseInt(e.target.value) }))} + style={{ width: 50, accentColor: '#f0a030' }} /> + {galaxyParams.armCount} +
+
+ TWIST + setGalaxyParams(p => ({ ...p, rotationStrength: parseFloat(e.target.value) }))} + style={{ width: 50, accentColor: '#22d3ee' }} /> + {galaxyParams.rotationStrength.toFixed(1)} +
+
+ SPREAD + setGalaxyParams(p => ({ ...p, armSpread: parseFloat(e.target.value) }))} + style={{ width: 50, accentColor: '#22c55e' }} /> + {galaxyParams.armSpread.toFixed(2)} +
+
+ DENSITY + setGalaxyParams(p => ({ ...p, gravity: parseFloat(e.target.value) }))} + style={{ width: 50, accentColor: '#a855f7' }} /> + {galaxyParams.gravity.toFixed(1)} +
+
+ + {/* Controls overlay - top right */} +
+ {[ + { label: 'Gates', val: showGates, set: setShowGates }, + { label: 'Labels', val: showLabels, set: setShowLabels }, + { label: 'Dust Cloud', val: showDust, set: setShowDust }, + { label: 'Auto-Rotate', val: autoRotate, set: setAutoRotate }, + ].map(ctrl => ( + + ))} +
+ + {/* Hint bar */} +
+ Click system to inspect + Right-click to route + Left-drag to orbit + Right-drag to pan + Scroll to zoom +
+ + {/* Sidebar */} +
+ {/* Stats header */} +
+
SPIRAL GALAXY
+
+ {stats && [ + { label: 'Systems', val: stats.systems, color: '#f0a030' }, + { label: 'Regions', val: stats.regions, color: '#38bdf8' }, + { label: 'Constellations', val: stats.constellations, color: '#22d3ee' }, + { label: 'Stargates', val: stats.gates, color: '#94a3b8' }, + { label: 'MST edges', val: stats.mstGates, color: '#22c55e' }, + { label: 'Choke gates', val: stats.chokeGates, color: '#ef4444' }, + { label: 'Hub gates', val: stats.hubGates, color: '#22c55e' }, + { label: 'Stations', val: stats.stations, color: '#22d3ee' }, + { label: 'Belts', val: stats.belts, color: '#92400e' }, + { label: 'NPC Agents', val: stats.agents, color: '#a855f7' }, + { label: 'Avg gates/sys', val: stats.avgGatesPerSys, color: '#94a3b8' }, + { label: 'Dust particles', val: galaxy.dustCount || 0, color: '#5a6b82' }, + ].map((s, i) => ( +
+ {s.label} + {s.val} +
+ ))} +
+ {stats && ( +
+ High: {stats.secDist.high} + Low: {stats.secDist.low} + Null: {stats.secDist.null} + Deep: {stats.secDist.deep} +
+ )} + {stats && ( +
+ + {stats.connected ? '✓ Fully connected' : '✗ Disconnected!'} + +
+ )} +
+ + {/* Region overview */} +
+
REGIONS
+ {galaxy.regions.map(r => ( +
+ {r.name} + {r.systemIds.length} sys +
+ ))} +
+ + {/* Selected system detail */} +
+ {selectedDetails ? ( + <> +
{selectedDetails.name}
+
+ {[ + { label: 'Security', val: selectedDetails.security.toFixed(2), color: secColorCSS(selectedDetails.security) }, + { label: 'Star Type', val: selectedDetails.starType, color: selectedDetails.starColor }, + { label: 'Faction', val: selectedDetails.faction, color: '#94a3b8' }, + { label: 'Planets', val: selectedDetails.planetCount, color: '#94a3b8' }, + { label: 'Stations', val: selectedDetails.sysStations.length, color: '#22d3ee' }, + { label: 'Belts', val: selectedDetails.sysBelts.length, color: '#92400e' }, + { label: 'Gates', val: selectedDetails.sysGates.length, color: '#94a3b8' }, + { label: 'Region', val: selectedDetails.regionId, color: '#5a6b82' }, + ].map((row, i) => ( +
+ {row.label} + {row.val} +
+ ))} +
+ + {selectedDetails.sysStations.length > 0 && ( +
+
STATIONS
+ {selectedDetails.sysStations.map((stn, i) => ( +
+ {stn.name} [{stn.services.join(', ')}] +
+ ))} +
+ )} + + {selectedDetails.sysBelts.length > 0 && ( +
+
BELTS
+ {selectedDetails.sysBelts.map((belt, i) => ( +
+ Belt {i + 1}: {belt.oreType} +
+ ))} +
+ )} + + {selectedDetails.connectedSystems.length > 0 && ( +
+
CONNECTED SYSTEMS
+ {selectedDetails.connectedSystems.map((cs, i) => ( +
{ + setSelectedSystem(cs); + if (sceneRef.current?.orbit) { + sceneRef.current.orbit.flyTo(cs.x, cs.y, cs.z, 60); + } + }}> + + {cs.security.toFixed(2)} + + {' '}{cs.name} + ({cs.starType}) +
+ ))} +
+ )} + + ) : ( +
+ Click a system to inspect

+ Right-click two systems to
calculate shortest route
+
+ )} +
+ + {/* Route info */} + {currentPath.length > 1 && ( +
+
+ ROUTE: {currentPath.length - 1} JUMP{currentPath.length - 1 !== 1 ? 'S' : ''} +
+
+ {currentPath.map((id, i) => { + const sys = galaxy.sysMap ? galaxy.sysMap.get(id) : galaxy.systems.find(s => s.id === id); + if (!sys) return null; + return ( + + { + setSelectedSystem(sys); + if (sceneRef.current?.orbit) { + sceneRef.current.orbit.flyTo(sys.x, sys.y, sys.z, 60); + } + }} + > + {sys.name} + + {i < currentPath.length - 1 && } + + ); + })} +
+
+ )} + + {/* Gate legend */} +
+
+ MST + Intra + Arm-link + Choke + Hub +
+
+
+
+ ); +} + +window.GDD.GalaxyDemo = GalaxyDemo; diff --git a/js/demos/gamehud.js b/js/demos/gamehud.js new file mode 100644 index 0000000..656e798 --- /dev/null +++ b/js/demos/gamehud.js @@ -0,0 +1,497 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useRef, useCallback } = React; +const TH = window.GDD.THREE; + +/* ===== Game HUD Demo — 3D space viewport inside HUD overlay ===== */ +function GameHudDemo() { + const containerRef = useRef(null); + const sceneRef = useRef(null); + const animIdRef = useRef(null); + + const [modules, setModules] = useState([ + { id: 'h1', name: '150mm Railgun', icon: '⊕', active: false, type: 'weapon', slot: 'high' }, + { id: 'h2', name: 'Missile Launcher', icon: '⊕', active: false, type: 'weapon', slot: 'high' }, + { id: 'h3', name: 'Mining Laser', icon: '⛏', active: false, type: 'mining', slot: 'high' }, + { id: 'm1', name: 'Shield Booster', icon: '◎', active: false, type: 'shield', slot: 'med' }, + { id: 'm2', name: 'Afterburner', icon: '»', active: false, type: 'propulsion', slot: 'med' }, + { id: 'm3', name: 'Warp Scram', icon: '◎', active: false, type: 'ewar', slot: 'med' }, + { id: 'l1', name: 'Armor Plate', icon: '◼', active: true, type: 'armor', slot: 'low' }, + { id: 'l2', name: 'Damage Control', icon: '↯', active: true, type: 'damage_mod', slot: 'low' }, + { id: 'l3', name: 'Cargo Expander', icon: '□', active: false, type: 'cargo', slot: 'low' }, + ]); + const [target] = useState({ name: 'Guristas Pirate', type: 'Frigate', locked: true, shields: 62, armor: 85, hull: 100, distance: 12400, bounty: 85000 }); + const [cargo] = useState({ used: 340, total: 600, items: [{ name: 'Veldspar', qty: 120 }, { name: 'Scordite', qty: 80 }, { name: 'Pyroxeres', qty: 45 }, { name: 'Tritanium', qty: 200 }] }); + const [chatState, setChatState] = useState({ + activeTab: 'local', + messages: [ + { sender: 'CMDR Picard', body: ' Pirates in belt 3, be careful ', time: '14:23' }, + { sender: 'MinerBob', body: ' Anyone selling compressed ore? ', time: '14:21' }, + { sender: 'CMDR Worf', body: ' Target locked, engaging hostiles ', time: '14:19' }, + { sender: '[SYSTEM]', body: ' Guristas fleet detected in nearby system ', time: '14:15' }, + { sender: 'TraderAlice', body: ' Best buy orders at Jita IV — check market ', time: '14:12' }, + ], + }); + const [ship, setShip] = useState({ shields: 100, armor: 92, hull: 100, capacitor: 78, speed: 0, maxSpeed: 420, name: 'USS ENTERPRISE', class: 'VENTURE-CLASS' }); + const [entities] = useState([ + { id: 'e1', name: 'Asteroid Belt', type: 'asteroid', dist: '12 km' }, + { id: 'e2', name: 'Guristas Pirate', type: 'hostile', dist: '24 km' }, + { id: 'e3', name: 'CMDR Riker', type: 'friendly', dist: '38 km' }, + { id: 'e4', name: 'Jita IV Station', type: 'station', dist: '45 km' }, + { id: 'e5', name: 'Veldspar Rock', type: 'asteroid', dist: '8 km' }, + { id: 'e6', name: 'MinerBob', type: 'friendly', dist: '52 km' }, + { id: 'e7', name: 'Jump Gate', type: 'gate', dist: '120 km' }, + { id: 'e8', name: 'Scordite Deposit', type: 'asteroid', dist: '15 km' }, + ]); + const [overviewFilter, setOverviewFilter] = useState('all'); + const [system] = useState({ name: 'Jita', security: 0.9 }); + + // Build 3D scene + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const scene = new THREE.Scene(); + + const w = container.clientWidth; + const h = container.clientHeight; + const camera = new THREE.PerspectiveCamera(60, w / h, 0.1, 5000); + camera.position.set(0, 8, 25); + camera.lookAt(0, 0, -20); + + const renderer = TH.createRenderer(container, { clearColor: 0x040810 }); + renderer.setSize(w, h); + + // Stars + const stars = TH.createStarField(4000, 3000); + scene.add(stars); + + // Nebulae + TH.addNebula(scene, 0x22d3ee, [-30, 20, -100], 80); + TH.addNebula(scene, 0xa78bfa, [50, -10, -80], 60); + TH.addNebula(scene, 0xf0a030, [-60, 15, -120], 50); + + // Lighting + TH.setupSpaceLighting(scene); + + // Player ship (small, at bottom center of view) + const playerGroup = new THREE.Group(); + const pMesh = TH.createShipMesh(0xc8d6e5, 0xf0a030, 0.5); + pMesh.rotation.y = Math.PI / 2; + playerGroup.add(pMesh); + + const pEngine = TH.createEngineGlow(0x22d3ee, 2, 8); + pEngine.position.set(0, 0, 5); + playerGroup.add(pEngine); + + const pShield = TH.createShield(2.5, 0x22d3ee, 0.05); + playerGroup.add(pShield); + + playerGroup.position.set(0, -1, 15); + scene.add(playerGroup); + + // Enemy ship (in the distance) + const enemyGroup = new THREE.Group(); + const eMesh = TH.createShipMesh(0x7f1d1d, 0xef4444, 0.4); + eMesh.rotation.y = -Math.PI / 2; + enemyGroup.add(eMesh); + + const eEngine = TH.createEngineGlow(0xef4444, 1.5, 6); + eEngine.position.set(0, 0, -4); + enemyGroup.add(eEngine); + + const lockBrackets = TH.createLockBrackets(2, 0xf0a030); + enemyGroup.add(lockBrackets); + + const eLabel = TH.createLabel('GURISTAS PIRATE', '#ef4444', 12); + eLabel.position.y = 4; + enemyGroup.add(eLabel); + + enemyGroup.position.set(5, 1, -20); + scene.add(enemyGroup); + + // Asteroids + const asteroidPositions = [ + { x: -25, y: -3, z: -15, size: 3 }, + { x: -20, y: -2, z: -10, size: 2 }, + { x: -30, y: -4, z: -20, size: 2.5 }, + { x: 30, y: -3, z: -18, size: 2 }, + { x: 35, y: -2, z: -12, size: 1.5 }, + { x: -15, y: -5, z: -30, size: 3.5 }, + { x: 20, y: -4, z: -35, size: 2 }, + ]; + asteroidPositions.forEach(ap => { + const ast = TH.createAsteroid(ap.size); + ast.position.set(ap.x, ap.y, ap.z); + ast.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0); + scene.add(ast); + }); + + // Station (far right) + const station = TH.createStation(3, 0x22d3ee); + station.position.set(40, 2, -40); + scene.add(station); + const stnLabel = TH.createLabel('JITA IV STN', '#22d3ee', 12); + stnLabel.position.set(40, 8, -40); + scene.add(stnLabel); + + // Targeting line (dashed) + const linePoints = [playerGroup.position, enemyGroup.position]; + const lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints); + const lineMat = new THREE.LineDashedMaterial({ color: 0xf0a030, dashSize: 1, gapSize: 0.8, transparent: true, opacity: 0.2 }); + const targetLine = new THREE.Line(lineGeo, lineMat); + targetLine.computeLineDistances(); + scene.add(targetLine); + + sceneRef.current = { scene, camera, renderer, stars, playerGroup, enemyGroup, pEngine, eEngine, lockBrackets, targetLine }; + + // Animation + const clock = new THREE.Clock(); + const animate = () => { + animIdRef.current = requestAnimationFrame(animate); + const t = clock.getElapsedTime(); + + // Star drift + stars.rotation.y = t * 0.002; + stars.rotation.x = t * 0.001; + + // Player ship idle bob + playerGroup.position.y = -1 + Math.sin(t * 1.5) * 0.2; + playerGroup.rotation.z = Math.sin(t * 0.3) * 0.02; + + // Enemy ship slight movement + enemyGroup.position.x = 5 + Math.sin(t * 1.2) * 1; + enemyGroup.position.y = 1 + Math.cos(t * 0.8) * 0.5; + enemyGroup.rotation.z = Math.sin(t * 0.4) * 0.03; + + // Lock brackets rotate + lockBrackets.rotation.y = t * 0.3; + + // Update target line + const positions = targetLine.geometry.attributes.position; + positions.setXYZ(0, playerGroup.position.x, playerGroup.position.y, playerGroup.position.z); + positions.setXYZ(1, enemyGroup.position.x, enemyGroup.position.y, enemyGroup.position.z); + positions.needsUpdate = true; + targetLine.computeLineDistances(); + + // Engine glow pulse + pEngine.intensity = 2 + Math.sin(t * 3) * 0.5; + eEngine.intensity = 1 + Math.sin(t * 2.5) * 0.3; + + // Shield shimmer + pShield.material.opacity = 0.04 + Math.sin(t * 2) * 0.02; + + renderer.render(scene, camera); + }; + animate(); + + const onResize = () => { + const w2 = container.clientWidth; + const h2 = container.clientHeight; + renderer.setSize(w2, h2); + camera.aspect = w2 / h2; + camera.updateProjectionMatrix(); + }; + window.addEventListener('resize', onResize); + + return () => { + if (animIdRef.current) cancelAnimationFrame(animIdRef.current); + window.removeEventListener('resize', onResize); + }; + }, []); + + const toggleModule = useCallback((modId) => { + setModules(prev => prev.map(m => m.id === modId ? { ...m, active: !m.active } : m)); + }, []); + + // Simulate capacitor tick + useEffect(() => { + const interval = setInterval(() => { + setShip(prev => { + const activeCount = modules.filter(m => m.active).length; + return { ...prev, capacitor: Math.max(0, Math.min(100, prev.capacitor - activeCount * 0.3 + 0.8)), speed: modules.find(m => m.id === 'm2' && m.active) ? 280 : 0 }; + }); + }, 1000); + return () => clearInterval(interval); + }, [modules]); + + const filteredEntities = overviewFilter === 'all' ? entities : entities.filter(e => e.type === overviewFilter); + const activeModules = modules.filter(m => m.active).length; + + return ( +
+

Game HUD — Live Concept (3D)

+

+ The in-game HUD with a 3D WebGL space viewport. All panels overlay the Three.js scene — ship health, modules, overview, target info, cargo, and chat. +

+ + {/* Full HUD mockup */} +
+ {/* 3D Canvas */} +
+ + {/* HUD overlay */} +
+ + {/* TOP BAR */} +
+ { e.currentTarget.style.color = 'var(--fg-bright)'; e.currentTarget.style.borderColor = 'var(--accent)'; }} + onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--fg-dim)'; e.currentTarget.style.borderColor = 'var(--border)'; }} + title="Back to Game Docs" + > + + DOCS + +
+ {system.name} + + {system.security.toFixed(1)} + +
+ Ship + {ship.name} +
+ ₢125,000 +
+ + + TQ Online + + 14:23 UTC +
+ 3D +
+ + {/* MIDDLE */} +
+ + {/* LEFT — Ship Panel */} +
+
+
+ Ship Status +
+
+ {[ + { label: 'SH', value: ship.shields, color: '#22d3ee' }, + { label: 'AR', value: ship.armor, color: '#f0a030' }, + { label: 'HU', value: ship.hull, color: '#22c55e' }, + { label: 'CA', value: ship.capacitor, color: '#a78bfa' }, + ].map(bar => ( +
+ {bar.label} +
+
30 ? 'linear-gradient(90deg, #6366f1, #a78bfa)' : 'linear-gradient(90deg, #dc2626, #ef4444)') : `linear-gradient(90deg, ${bar.color}88, ${bar.color})`, borderRadius: 'var(--radius-pill)' }} /> +
+ {bar.label === 'CA' ? Math.round(bar.value) : bar.value}% +
+ ))} +
+
+ + {/* Speed */} +
+
+
0 ? 'var(--fg-bright)' : 'var(--muted)', letterSpacing: '-0.02em' }}> + {ship.speed}m/s +
+
+
+
+
+ + + +
+
+
+
+ +
+ + {/* RIGHT — Overview */} +
+
+
+ Overview + {filteredEntities.length} items +
+
+ {['all', 'hostile', 'asteroid', 'friendly'].map(f => ( + + ))} +
+
+ {filteredEntities.map(ent => { + const col = ent.type === 'hostile' ? '#ef4444' : ent.type === 'asteroid' ? '#a78bfa' : ent.type === 'station' ? '#22d3ee' : ent.type === 'gate' ? '#5a6b82' : '#22c55e'; + return ( +
+ {ent.name} + {ent.dist} +
+ ); + })} +
+
+
+
+ + {/* BOTTOM BAR */} +
+ {/* Modules */} +
+
+ Modules + {activeModules} active +
+
+ {['high', 'med', 'low'].map(slotType => ( +
+ + {slotType.toUpperCase()} + + {modules.filter(m => m.slot === slotType).map(mod => ( +
toggleModule(mod.id)} title={`${mod.name} (${mod.active ? 'Active' : 'Inactive'})`}> + + {mod.name.length > 14 ? mod.name.slice(0, 12) + '…' : mod.name} +
+ ))} +
+ ))} +
+
+ + {/* Target */} +
+
+ Target +
+
+
{target.name}
+
{target.type} · LOCKED
+ {[ + { label: 'SH', value: target.shields, color: '#22d3ee' }, + { label: 'AR', value: target.armor, color: '#f0a030' }, + { label: 'HU', value: target.hull, color: '#22c55e' }, + ].map(bar => ( +
+ {bar.label} +
+
+
+ {bar.value}% +
+ ))} +
+ {target.distance.toLocaleString()} km + ₢{target.bounty.toLocaleString()} +
+
+
+ + {/* Cargo */} +
+
+ Cargo +
+
+
+ {cargo.used}/{cargo.total} m³ + {Math.round(cargo.used / cargo.total * 100)}% +
+
+
+
+ {cargo.items.map((item, i) => ( +
+ {item.name} + ×{item.qty} +
+ ))} +
+
+ + {/* Chat */} +
+
+ {['local', 'corp', 'trade'].map(tab => ( + + ))} +
+
+ {chatState.messages.map((msg, i) => ( +
+ {msg.sender} + {msg.body} + {msg.time} +
+ ))} +
+
+ + +
+
+
+
+
+ + {/* Architecture notes */} +
+
+ HUD +

HUD Panel Architecture — 3D

+
+
+
+

3D Viewport

+
    +
  • Three.js WebGL replaces 2D Canvas — proper 3D ship meshes, asteroids, stations, star fields
  • +
  • Depth and lighting — ships and asteroids are lit by ambient + directional lights
  • +
  • Particle star field — 4000 point-based stars with subtle rotation for depth
  • +
  • Engine glows — point lights on each ship with pulsing intensity
  • +
  • Lock brackets — 3D wireframe targeting indicator that rotates around the target
  • +
+
+
+

HUD Overlay Pattern

+
    +
  • CSS overlay — HUD panels are positioned absolutely over the 3D canvas
  • +
  • Glass morphism — backdrop-filter blur + semi-transparent backgrounds
  • +
  • Pointer events — HUD panels capture clicks, center viewport passes through
  • +
  • Performance — Three.js renderer is separate from React DOM updates
  • +
+
+
+
+ +
+ Rendering upgrade: The space viewport now uses Three.js with WebGL. Ships are 3D meshes with lighting, asteroids use icosahedron geometry with vertex perturbation, and the star field is a 4000-particle Points system. The HUD overlay panels remain identical React components. +
+
+ ); +} + +window.GDD.GameHudDemo = GameHudDemo; diff --git a/js/demos/market.js b/js/demos/market.js new file mode 100644 index 0000000..758951d --- /dev/null +++ b/js/demos/market.js @@ -0,0 +1,1182 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useRef, useCallback, useMemo } = React; + +/* ────────────────────────────────────────────── + Commodities Exchange — Contract Market + ────────────────────────────────────────────── */ + +/* ── Static contract catalogue ── */ +const CATEGORIES = [ + { id: 'all', label: 'All Contracts' }, + { id: 'ore', label: 'Raw Ores' }, + { id: 'mineral', label: 'Refined Minerals' }, + { id: 'gas', label: 'Gas Products' }, + { id: 'isotope', label: 'Isotopes' }, + { id: 'exotic', label: 'Exotic Matter' }, +]; + +const CONTRACTS = [ + /* Ores */ + { symbol:'VLD', name:'Veldspar', category:'ore', lotSize:1000, tickSize:0.01, marginPct:8, basePrice:14, hub:'Jita IV – Moon 4' }, + { symbol:'SCR', name:'Scordite', category:'ore', lotSize:500, tickSize:0.02, marginPct:10, basePrice:32, hub:'Jita IV – Moon 4' }, + { symbol:'PYX', name:'Pyroxeres', category:'ore', lotSize:500, tickSize:0.05, marginPct:10, basePrice:45, hub:'Amarr VIII – Emperor' }, + { symbol:'KRN', name:'Kernite', category:'ore', lotSize:250, tickSize:0.10, marginPct:12, basePrice:90, hub:'Amarr VIII – Emperor' }, + { symbol:'OMB', name:'Omber', category:'ore', lotSize:250, tickSize:0.10, marginPct:12, basePrice:135, hub:'Rens VI – Moon 8' }, + { symbol:'JSP', name:'Jaspet', category:'ore', lotSize:200, tickSize:0.10, marginPct:15, basePrice:190, hub:'Dodixie IX – Moon 20' }, + { symbol:'HEM', name:'Hemorphite', category:'ore', lotSize:100, tickSize:0.50, marginPct:18, basePrice:360, hub:'Jita IV – Moon 4' }, + { symbol:'ARK', name:'Arkonor', category:'ore', lotSize:50, tickSize:1.00, marginPct:20, basePrice:620, hub:'Jita IV – Moon 4' }, + /* Minerals */ + { symbol:'TRI', name:'Tritanium', category:'mineral', lotSize:5000, tickSize:0.01, marginPct:6, basePrice:5, hub:'Jita IV – Moon 4' }, + { symbol:'PYE', name:'Pyerite', category:'mineral', lotSize:2000, tickSize:0.01, marginPct:8, basePrice:12, hub:'Jita IV – Moon 4' }, + { symbol:'MLX', name:'Mexallon', category:'mineral', lotSize:1000, tickSize:0.02, marginPct:8, basePrice:35, hub:'Jita IV – Moon 4' }, + { symbol:'ISO', name:'Isogen', category:'mineral', lotSize:500, tickSize:0.05, marginPct:12, basePrice:110, hub:'Amarr VIII – Emperor' }, + { symbol:'NCX', name:'Nocxium', category:'mineral', lotSize:250, tickSize:0.10, marginPct:15, basePrice:380, hub:'Jita IV – Moon 4' }, + { symbol:'ZDR', name:'Zydrine', category:'mineral', lotSize:100, tickSize:0.50, marginPct:18, basePrice:950, hub:'Jita IV – Moon 4' }, + { symbol:'MEG', name:'Megacyte', category:'mineral', lotSize:50, tickSize:1.00, marginPct:20, basePrice:2800,hub:'Jita IV – Moon 4' }, + { symbol:'MOR', name:'Morphite', category:'mineral', lotSize:10, tickSize:5.00, marginPct:25, basePrice:8500,hub:'Jita IV – Moon 4' }, + /* Gas */ + { symbol:'ATM', name:'Atmospheric', category:'gas', lotSize:2000, tickSize:0.01, marginPct:10, basePrice:8, hub:'Jita IV – Moon 4' }, + { symbol:'NEB', name:'Nebular', category:'gas', lotSize:500, tickSize:0.05, marginPct:12, basePrice:45, hub:'Amarr VIII – Emperor' }, + { symbol:'ION', name:'Ionized', category:'gas', lotSize:200, tickSize:0.10, marginPct:15, basePrice:120, hub:'Jita IV – Moon 4' }, + { symbol:'FUL', name:'Fullerides', category:'gas', lotSize:100, tickSize:0.50, marginPct:18, basePrice:450, hub:'Jita IV – Moon 4' }, + /* Isotopes */ + { symbol:'H3', name:'Hydrogen-3', category:'isotope', lotSize:500, tickSize:0.05, marginPct:15, basePrice:85, hub:'Jita IV – Moon 4' }, + { symbol:'HE4', name:'Helium-4', category:'isotope', lotSize:500, tickSize:0.05, marginPct:15, basePrice:92, hub:'Amarr VIII – Emperor' }, + { symbol:'N15', name:'Nitrogen-15', category:'isotope', lotSize:500, tickSize:0.05, marginPct:15, basePrice:78, hub:'Rens VI – Moon 8' }, + { symbol:'O18', name:'Oxygen-18', category:'isotope', lotSize:500, tickSize:0.05, marginPct:15, basePrice:88, hub:'Dodixie IX – Moon 20' }, + /* Exotic */ + { symbol:'RDB', name:'Reedstone', category:'exotic', lotSize:10, tickSize:5.00, marginPct:25, basePrice:12000,hub:'Jita IV – Moon 4' }, + { symbol:'TCH', name:'Tachyon Salt', category:'exotic', lotSize:5, tickSize:10.0, marginPct:30, basePrice:45000,hub:'Jita IV – Moon 4' }, +]; + +/* ── Sparkline ── */ +function Sparkline({ data, width = 80, height = 24, color = 'var(--green)' }) { + const ref = useRef(null); + useEffect(() => { + const c = ref.current; if (!c || !data || data.length < 2) return; + const ctx = c.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + c.width = width * dpr; c.height = height * dpr; + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, width, height); + const min = Math.min(...data); const max = Math.max(...data); + const range = max - min || 1; + const step = width / (data.length - 1); + ctx.beginPath(); + data.forEach((v, i) => { + const x = i * step; + const y = height - ((v - min) / range) * (height - 4) - 2; + i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); + }); + ctx.strokeStyle = color; + ctx.lineWidth = 1.5; + ctx.lineJoin = 'round'; + ctx.stroke(); + const grad = ctx.createLinearGradient(0, 0, 0, height); + const isGreen = color.includes('green'); + grad.addColorStop(0, isGreen ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)'); + grad.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.lineTo(width, height); ctx.lineTo(0, height); ctx.closePath(); + ctx.fillStyle = grad; ctx.fill(); + }, [data, width, height, color]); + return ; +} + +/* ── Ticker Tape ── */ +function TickerTape({ items }) { + const [offset, setOffset] = useState(0); + const raf = useRef(null); + useEffect(() => { + const speed = 0.5; + const tick = () => { + setOffset(o => { const n = o - speed; return n < -3000 ? 0 : n; }); + raf.current = requestAnimationFrame(tick); + }; + raf.current = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf.current); + }, []); + const dup = [...items, ...items, ...items]; + return ( +
+
+ {dup.map((t, i) => ( + + {t.symbol} + ₢{t.price.toLocaleString()} + = 0 ? 'var(--green)' : 'var(--red)', fontWeight: 600 }}> + {t.change >= 0 ? '▲' : '▼'} {Math.abs(t.changePct).toFixed(1)}% + + + ))} +
+
+ ); +} + +/* ── Category Tabs ── */ +function CategoryTabs({ active, onChange }) { + return ( +
+ {CATEGORIES.map(cat => ( + + ))} +
+ ); +} + +/* ── Contract Board — main table ── */ +function ContractBoard({ commodities, selected, onSelect, category }) { + const filtered = category === 'all' ? commodities : commodities.filter(c => c.category === category); + return ( +
+ {/* Table header */} +
+ TickerNameLastChg% + BidAskVolumeOpen Int. +
+ {/* Rows */} +
+ {filtered.map(c => { + const up = c.change >= 0; + const isSel = selected === c.symbol; + const spread = c.bestAsk - c.bestBid; + return ( +
onSelect(c.symbol)} style={{ + display: 'grid', + gridTemplateColumns: '90px 120px 80px 60px 70px 70px 80px 90px 70px', + gap: '4px', alignItems: 'center', + padding: '7px 12px', cursor: 'pointer', + background: isSel ? 'var(--surface-hover)' : 'transparent', + borderLeft: isSel ? '2px solid var(--accent)' : '2px solid transparent', + borderBottom: '1px solid var(--border)', + transition: 'background var(--transition-fast)', + fontSize: '0.75rem', + }}> + {c.symbol} + {c.name} + ₢{c.price.toLocaleString()} + + {up ? '+' : ''}{c.changePct.toFixed(1)}% + + ₢{c.bestBid} + ₢{c.bestAsk} + {(c.volume / 1000).toFixed(1)}K + {(c.openInterest / 1000).toFixed(1)}K + +
+ ); + })} +
+
+ ); +} + +/* ── Contract Spec Panel ── */ +function ContractSpec({ contract, commodity }) { + if (!contract || !commodity) return null; + const marginPerLot = Math.ceil(commodity.price * contract.lotSize * (contract.marginPct / 100)); + const notionalPerLot = commodity.price * contract.lotSize; + return ( +
+
+ Contract Specification +
+
+ {[ + ['Ticker', contract.symbol, 'var(--accent)'], + ['Unit', contract.name, 'var(--fg-bright)'], + ['Lot Size', contract.lotSize.toLocaleString() + ' units', 'var(--fg-bright)'], + ['Tick Size', '₢' + contract.tickSize, 'var(--fg-bright)'], + ['Margin Req.', contract.marginPct + '%', 'var(--cyan)'], + ['Margin / Lot', '₢' + marginPerLot.toLocaleString(), 'var(--cyan)'], + ['Notional / Lot', '₢' +notionalPerLot.toLocaleString(), 'var(--fg-dim)'], + ['Delivery', contract.hub, 'var(--fg-dim)'], + ['Settlement', commodity.settlement ? '₢' + commodity.settlement : '—', 'var(--fg-dim)'], + ['Expiry', '30 DTE', 'var(--muted)'], + ['Supply', (commodity.supply / 1000).toFixed(0) + 'K/day', 'var(--green)'], + ['Demand', (commodity.demand / 1000).toFixed(0) + 'K/day', commodity.demand > commodity.supply ? 'var(--red)' : 'var(--green)'], + ].map(([label, value, color], i) => ( + + {label} + {value} + + ))} +
+ {/* Supply/Demand bar */} +
+
+ SUPPLY + commodity.supply ? 'var(--red)' : 'var(--green)' }}> + {commodity.demand > commodity.supply ? 'DEFICIT' : 'SURPLUS'}: {Math.abs(commodity.supply - commodity.demand).toLocaleString()} + + DEMAND +
+
+
+
+
+
+
+ ); +} + +/* ── Depth Chart (canvas) ── */ +function DepthChart({ bids, asks, midPrice }) { + const ref = useRef(null); + useEffect(() => { + const c = ref.current; if (!c) return; + const ctx = c.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + const W = c.clientWidth; const H = c.clientHeight; + c.width = W * dpr; c.height = H * dpr; + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, W, H); + + if (!bids.length || !asks.length) return; + + const maxCum = Math.max( + bids.reduce((s, b) => Math.max(s, b.cumulative), 0), + asks.reduce((s, a) => Math.max(s, a.cumulative), 0) + ); + + const priceRange = midPrice * 0.06; + const pMin = midPrice - priceRange; + const pMax = midPrice + priceRange; + const toX = p => ((p - pMin) / (pMax - pMin)) * W; + const toY = cum => H - (cum / (maxCum * 1.1)) * H; + + /* Grid */ + ctx.strokeStyle = 'rgba(28,42,63,0.4)'; ctx.lineWidth = 0.5; + for (let i = 0; i < 4; i++) { + const y = (H / 3) * i; + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); + } + + /* Bids (green area, left of mid) */ + ctx.beginPath(); + ctx.moveTo(toX(bids[0].price), H); + bids.forEach(b => ctx.lineTo(toX(b.price), toY(b.cumulative))); + ctx.lineTo(toX(bids[bids.length - 1].price), H); + ctx.closePath(); + const bg = ctx.createLinearGradient(0, 0, 0, H); + bg.addColorStop(0, 'rgba(34,197,94,0.25)'); bg.addColorStop(1, 'rgba(34,197,94,0.03)'); + ctx.fillStyle = bg; ctx.fill(); + + /* Asks (red area, right of mid) */ + ctx.beginPath(); + ctx.moveTo(toX(asks[0].price), H); + asks.forEach(a => ctx.lineTo(toX(a.price), toY(a.cumulative))); + ctx.lineTo(toX(asks[asks.length - 1].price), H); + ctx.closePath(); + const ag = ctx.createLinearGradient(0, 0, 0, H); + ag.addColorStop(0, 'rgba(239,68,68,0.25)'); ag.addColorStop(1, 'rgba(239,68,68,0.03)'); + ctx.fillStyle = ag; ctx.fill(); + + /* Mid line */ + const mx = toX(midPrice); + ctx.setLineDash([3, 3]); + ctx.beginPath(); ctx.moveTo(mx, 0); ctx.lineTo(mx, H); + ctx.strokeStyle = 'var(--accent)'; ctx.lineWidth = 1; ctx.stroke(); + ctx.setLineDash([]); + + /* Price label */ + ctx.fillStyle = 'var(--accent)'; ctx.font = 'bold 9px var(--font-mono)'; + ctx.textAlign = 'center'; + ctx.fillText('₢' + midPrice.toFixed(0), mx, H - 4); + }, [bids, asks, midPrice]); + + return ; +} + +/* ── Order Book ── */ +function OrderBook({ bids, asks, spread, spreadPct }) { + const maxBid = Math.max(...bids.map(b => b.volume)); + const maxAsk = Math.max(...asks.map(a => a.volume)); + return ( +
+
+ + Market Depth + + + Spread: ₢{spread} ({spreadPct.toFixed(2)}%) + +
+
+ PriceSizeCum +
+ {/* Asks (reversed) */} +
+ {[...asks].reverse().map((a, i) => ( +
+
+ ₢{a.price} + {a.volume.toLocaleString()} + {a.cumulative.toLocaleString()} +
+ ))} +
+ {/* Mid */} +
+ + ₢{((bids[0]?.price || 0 + asks[0]?.price || 0) / 2).toLocaleString()} + + MID +
+ {/* Bids */} +
+ {bids.map((b, i) => ( +
+
+ ₢{b.price} + {b.volume.toLocaleString()} + {b.cumulative.toLocaleString()} +
+ ))} +
+
+ ); +} + +/* ── Price Chart (canvas) ── */ +function PriceChart({ data, symbol }) { + const ref = useRef(null); + useEffect(() => { + const c = ref.current; if (!c || !data || data.length < 2) return; + const ctx = c.getContext('2d'); + const dpr = window.devicePixelRatio || 1; + const W = c.clientWidth; const H = c.clientHeight; + c.width = W * dpr; c.height = H * dpr; + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, W, H); + + const prices = data.map(d => d.close); + const highs = data.map(d => d.high); + const lows = data.map(d => d.low); + const volumes = data.map(d => d.volume); + const allP = [...highs, ...lows]; + const minP = Math.min(...allP); const maxP = Math.max(...allP); + const rangeP = maxP - minP || 1; + const maxV = Math.max(...volumes); + const chartH = H * 0.72; const volH = H * 0.22; const barW = Math.max(2, (W / data.length) - 1); + + /* Grid */ + ctx.strokeStyle = 'rgba(28,42,63,0.5)'; ctx.lineWidth = 0.5; + for (let i = 0; i < 5; i++) { + const y = (chartH / 4) * i; + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); + ctx.fillStyle = 'var(--muted)'; ctx.font = '8px var(--font-mono)'; + ctx.fillText('₢' + (maxP - (rangeP / 4) * i).toFixed(0), W - 42, y + 9); + } + + /* Volume bars */ + data.forEach((d, i) => { + const x = (i / data.length) * W; + const vh = (d.volume / maxV) * volH; + ctx.fillStyle = d.close >= d.open ? 'rgba(34,197,94,0.18)' : 'rgba(239,68,68,0.18)'; + ctx.fillRect(x, H - vh, barW, vh); + }); + + /* Price line */ + ctx.beginPath(); + data.forEach((d, i) => { + const x = (i / (data.length - 1)) * W; + const y = chartH - ((d.close - minP) / rangeP) * (chartH - 10) - 5; + i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); + }); + const lastC = data[data.length - 1].close; + const firstC = data[0].close; + const lineColor = lastC >= firstC ? '#22c55e' : '#ef4444'; + ctx.strokeStyle = lineColor; ctx.lineWidth = 1.8; ctx.lineJoin = 'round'; ctx.stroke(); + + /* Area fill */ + const lastX = W; + ctx.lineTo(lastX, chartH); ctx.lineTo(0, chartH); ctx.closePath(); + const grad = ctx.createLinearGradient(0, 0, 0, chartH); + grad.addColorStop(0, lastC >= firstC ? 'rgba(34,197,94,0.10)' : 'rgba(239,68,68,0.10)'); + grad.addColorStop(1, 'rgba(0,0,0,0)'); + ctx.fillStyle = grad; ctx.fill(); + + /* Current price line */ + const curY = chartH - ((lastC - minP) / rangeP) * (chartH - 10) - 5; + ctx.setLineDash([4, 4]); + ctx.beginPath(); ctx.moveTo(0, curY); ctx.lineTo(W, curY); + ctx.strokeStyle = lineColor; ctx.lineWidth = 1; ctx.stroke(); + ctx.setLineDash([]); + + ctx.fillStyle = lineColor; + ctx.beginPath(); ctx.roundRect(W - 55, curY - 8, 52, 16, 3); ctx.fill(); + ctx.fillStyle = '#080c14'; ctx.font = 'bold 8px var(--font-mono)'; + ctx.fillText('₢' + lastC.toFixed(0), W - 52, curY + 3); + }, [data, symbol]); + + return ; +} + +/* ── Order Form (commodities-style: lots, margin, long/short) ── */ +function OrderForm({ contract, commodity, credits, positions, onTrade }) { + const [direction, setDirection] = useState('long'); // long or short + const [orderType, setOrderType] = useState('market'); // market, limit, stop + const [lots, setLots] = useState(''); + const [limitPrice, setLimitPrice] = useState(''); + const [error, setError] = useState(''); + const [confirmation, setConfirmation] = useState(null); + + if (!contract || !commodity) return null; + + const effectivePrice = orderType === 'market' ? commodity.price : (parseFloat(limitPrice) || commodity.price); + const numLots = parseInt(lots) || 0; + const totalUnits = numLots * contract.lotSize; + const notional = totalUnits * effectivePrice; + const marginRequired = Math.ceil(notional * (contract.marginPct / 100)); + const commission = Math.ceil(notional * 0.015); + const pos = positions.find(p => p.symbol === contract.symbol); + const existingDirection = pos ? pos.direction : null; + const canAfford = marginRequired + commission <= credits; + + const handleSubmit = () => { + if (numLots <= 0) { setError('Enter lot quantity'); return; } + if (orderType !== 'market' && !limitPrice) { setError('Set limit/stop price'); return; } + if (!canAfford) { setError('Insufficient margin + commission'); return; } + setError(''); + setConfirmation({ direction, orderType, lots: numLots, price: effectivePrice, notional, marginRequired, commission, totalUnits }); + }; + + const confirmTrade = () => { + onTrade(direction, contract.symbol, effectivePrice, numLots, totalUnits, marginRequired, commission); + setConfirmation(null); setLots(''); setLimitPrice(''); setError(''); + }; + + return ( +
+
+ Place Order — {contract.symbol} + Lot: {contract.lotSize.toLocaleString()} units +
+
+ {/* Long / Short toggle */} +
+ {['long', 'short'].map(d => ( + + ))} +
+ + {/* Order type pills */} +
+ {['market', 'limit', 'stop'].map(t => ( + + ))} +
+ + {/* Limit/Stop price */} + {orderType !== 'market' && ( +
+ + setLimitPrice(e.target.value)} + placeholder={`Spot: ₢${commodity.price}`} + style={{ + width: '100%', padding: '7px 10px', background: 'var(--bg)', + border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', + color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', + outline: 'none', boxSizing: 'border-box', + }} + /> +
+ )} + + {/* Lots */} +
+ +
+ setLots(e.target.value)} + placeholder="0" min="1" + style={{ + flex: 1, padding: '7px 10px', background: 'var(--bg)', + border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', + color: 'var(--fg)', fontFamily: 'var(--font-mono)', fontSize: '0.8rem', outline: 'none', + }} + /> + {[1, 5, 10, 25].map(n => ( + + ))} +
+
+ + {/* Summary */} +
+
+ Price / Unit₢{effectivePrice.toLocaleString()} +
+
+ Total Units{totalUnits.toLocaleString()} +
+
+ Notional Value₢{notional.toLocaleString()} +
+
+ Margin ({contract.marginPct}%)₢{marginRequired.toLocaleString()} +
+
+ Commission₢{commission.toLocaleString()} +
+
+ Required + + ₢{(marginRequired + commission).toLocaleString()} + +
+
+ + {error &&
{error}
} + + +
+ + {/* Confirmation */} + {confirmation && ( +
setConfirmation(null)}> +
e.stopPropagation()}> +

Confirm {confirmation.orderType} Order

+
+
Direction: {confirmation.direction.toUpperCase()}
+
Contract: {contract.symbol} ({contract.name})
+
Lots: {confirmation.lots} ({confirmation.totalUnits.toLocaleString()} units)
+
Price: ₢{confirmation.price.toLocaleString()}/unit
+
Notional: ₢{confirmation.notional.toLocaleString()}
+
Margin: ₢{confirmation.marginRequired.toLocaleString()}
+
Commission: ₢{confirmation.commission.toLocaleString()}
+
+
+ + +
+
+
+ )} +
+ ); +} + +/* ── Positions Panel (margin account + open positions) ── */ +function PositionsPanel({ positions, commodities, credits, usedMargin }) { + const totalUnrealized = positions.reduce((sum, p) => { + const c = commodities.find(c => c.symbol === p.symbol); + if (!c) return sum; + const currentValue = c.price * p.totalUnits; + const entryValue = p.avgEntry * p.totalUnits; + const diff = p.direction === 'long' ? currentValue - entryValue : entryValue - currentValue; + return sum + diff; + }, 0); + + const totalPositionValue = positions.reduce((sum, p) => { + const c = commodities.find(c => c.symbol === p.symbol); + return sum + (c ? c.price * p.totalUnits : 0); + }, 0); + + const marginUtilization = credits > 0 ? ((usedMargin / credits) * 100) : 0; + + return ( +
+
+ Open Positions + = 0 ? 'var(--green)' : 'var(--red)' }}> + {totalUnrealized >= 0 ? '▲' : '▼'} ₢{Math.abs(totalUnrealized).toLocaleString()} + +
+ + {/* Account summary */} +
+
+
+
Balance
+
₢{credits.toLocaleString()}
+
+
+
Used Margin
+
₢{usedMargin.toLocaleString()}
+
+
+
Exposure
+
₢{totalPositionValue.toLocaleString()}
+
+
+ + {/* Margin utilization bar */} +
+
+ MARGIN UTILIZATION + 80 ? 'var(--red)' : marginUtilization > 50 ? 'var(--accent)' : 'var(--green)' }}> + {marginUtilization.toFixed(1)}% + +
+
+
80 ? 'var(--red)' : marginUtilization > 50 ? 'var(--accent)' : 'var(--green)', + transition: 'width 0.3s ease', + }} /> +
+
+ + {/* Position rows */} + {positions.length === 0 && ( +
+ No open positions +
+ )} + {positions.map((p, i) => { + const c = commodities.find(c => c.symbol === p.symbol); + if (!c) return null; + const currentVal = c.price * p.totalUnits; + const entryVal = p.avgEntry * p.totalUnits; + const pnl = p.direction === 'long' ? currentVal - entryVal : entryVal - currentVal; + const pnlPct = entryVal > 0 ? (pnl / p.margin) * 100 : 0; + const contract = CONTRACTS.find(ct => ct.symbol === p.symbol); + return ( +
+
+
+ {p.symbol} + + {p.direction === 'long' ? 'LONG' : 'SHORT'} + +
+ = 0 ? 'var(--green)' : 'var(--red)', + }}> + {pnl >= 0 ? '+' : ''}₢{pnl.toLocaleString()} + +
+
+ {p.lots} lot{p.lots > 1 ? 's' : ''} ({p.totalUnits.toLocaleString()} u.) @ ₢{p.avgEntry} + Margin: ₢{p.margin.toLocaleString()} +
+
+
= 0 ? 'var(--green)' : 'var(--red)', + opacity: 0.6, + }} /> +
+
+ ); + })} +
+
+ ); +} + +/* ── Trade Feed ── */ +function TradeFeed({ trades }) { + return ( +
+
+ Market Trades +
+
+ {trades.map((t, i) => ( +
+ {t.time} + {t.lots}× {t.symbol} + ₢{t.price.toLocaleString()} + + {t.side === 'long' ? 'BID' : 'ASK'} + +
+ ))} +
+
+ ); +} + +/* ────────────────────────────────────────────── + Main MarketDemo component + ────────────────────────────────────────────── */ +function MarketDemo() { + const [credits, setCredits] = useState(250000); + const [selectedSymbol, setSelectedSymbol] = useState('VLD'); + const [category, setCategory] = useState('all'); + const [notifications, setNotifications] = useState([]); + + /* Generate live commodity data */ + const [commodities, setCommodities] = useState(() => { + return CONTRACTS.map(ct => { + const history = []; let p = ct.basePrice; + for (let i = 0; i < 60; i++) { + p = Math.max(ct.basePrice * 0.7, Math.min(ct.basePrice * 1.3, p + (Math.random() - 0.48) * ct.basePrice * 0.04)); + history.push(Math.round(p * 100) / 100); + } + const price = history[history.length - 1]; + const prev = history[history.length - 2]; + const settlement = history[Math.floor(history.length / 2)]; + const bestBid = Math.round((price - ct.basePrice * 0.005) * 100) / 100; + const bestAsk = Math.round((price + ct.basePrice * 0.005) * 100) / 100; + return { + ...ct, + price, prevPrice: prev, + change: price - prev, + changePct: ((price - prev) / prev) * 100, + history, settlement, bestBid, bestAsk, + volume: Math.floor(Math.random() * 80000 + 5000), + openInterest: Math.floor(Math.random() * 200000 + 10000), + open: history[0], + high: Math.max(...history), + low: Math.min(...history), + supply: Math.floor(Math.random() * 50000 + 10000), + demand: Math.floor(Math.random() * 60000 + 8000), + }; + }); + }); + + /* Open positions */ + const [positions, setPositions] = useState([ + { symbol: 'VLD', direction: 'long', lots: 5, totalUnits: 5000, avgEntry: 13.20, margin: 5280 }, + { symbol: 'TRI', direction: 'long', lots: 3, totalUnits: 15000, avgEntry: 4.80, margin: 2160 }, + { symbol: 'ARK', direction: 'short', lots: 2, totalUnits: 100, avgEntry: 635, margin: 25400 }, + ]); + + /* Order book for selected */ + const selected = commodities.find(c => c.symbol === selectedSymbol) || commodities[0]; + const selectedContract = CONTRACTS.find(c => c.symbol === selectedSymbol) || CONTRACTS[0]; + const orderBook = useMemo(() => { + const p = selected.price; + const bids = []; const asks = []; + let bidCum = 0; let askCum = 0; + for (let i = 0; i < 10; i++) { + const bv = Math.floor(Math.random() * 12000 + 500); bidCum += bv; + bids.push({ price: Math.max(0.01, Math.round((p - (i + 1) * selectedContract.tickSize * 3) * 100) / 100), volume: bv, cumulative: bidCum }); + const av = Math.floor(Math.random() * 12000 + 500); askCum += av; + asks.push({ price: Math.round((p + (i + 1) * selectedContract.tickSize * 3) * 100) / 100, volume: av, cumulative: askCum }); + } + return { bids, asks }; + }, [selected.price, selectedSymbol, selectedContract.tickSize]); + + const usedMargin = positions.reduce((s, p) => s + p.margin, 0); + + /* Price chart data */ + const chartData = useMemo(() => { + return selected.history.map((c, i) => { + const noise = selected.price * 0.01; + return { + open: selected.history[Math.max(0, i - 1)] || c, + high: c + Math.random() * noise * 2, + low: Math.max(0.01, c - Math.random() * noise * 2), + close: c, + volume: Math.floor(Math.random() * 5000 + 500), + }; + }); + }, [selected]); + + /* Trade feed */ + const [tradeFeed, setTradeFeed] = useState(() => { + const feed = []; + const syms = CONTRACTS.map(c => c.symbol); + for (let i = 0; i < 20; i++) { + const sym = syms[Math.floor(Math.random() * syms.length)]; + const ct = CONTRACTS.find(c => c.symbol === sym); + const c = commodities.find(c => c.symbol === sym); + feed.push({ + time: `${14}:${String(22 + i).padStart(2, '0')}`, + symbol: sym, + lots: Math.floor(Math.random() * 10 + 1), + price: c ? Math.round(c.price * 100) / 100 : ct.basePrice, + side: Math.random() > 0.5 ? 'long' : 'short', + }); + } + return feed.reverse(); + }); + + /* Live price ticks */ + useEffect(() => { + const interval = setInterval(() => { + setCommodities(prev => prev.map(c => { + const ct = CONTRACTS.find(x => x.symbol === c.symbol); + const delta = (Math.random() - 0.48) * c.price * 0.006; + const newPrice = Math.max(0.01, Math.round((c.price + delta) * 100) / 100); + const history = [...c.history.slice(1), newPrice]; + const change = newPrice - c.prevPrice; + const spread = ct.basePrice * 0.005; + return { + ...c, price: newPrice, history, + volume: c.volume + Math.floor(Math.random() * 300), + change, changePct: c.prevPrice > 0 ? (change / c.prevPrice) * 100 : 0, + high: Math.max(c.high, newPrice), low: Math.min(c.low, newPrice), + bestBid: Math.round((newPrice - spread) * 100) / 100, + bestAsk: Math.round((newPrice + spread) * 100) / 100, + openInterest: c.openInterest + Math.floor(Math.random() * 200 - 80), + supply: c.supply + Math.floor(Math.random() * 200 - 100), + demand: c.demand + Math.floor(Math.random() * 200 - 80), + }; + })); + }, 2500); + return () => clearInterval(interval); + }, []); + + /* New trades in feed */ + useEffect(() => { + const interval = setInterval(() => { + const syms = CONTRACTS.map(c => c.symbol); + const sym = syms[Math.floor(Math.random() * syms.length)]; + const c = commodities.find(c => c.symbol === sym); + const ct = CONTRACTS.find(c => c.symbol === sym); + const now = new Date(); + setTradeFeed(prev => [{ + time: `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}`, + symbol: sym, + lots: Math.floor(Math.random() * 15 + 1), + price: c ? c.price : ct.basePrice, + side: Math.random() > 0.5 ? 'long' : 'short', + }, ...prev].slice(0, 40)); + }, 3000); + return () => clearInterval(interval); + }, [commodities]); + + const addNotif = (msg, color) => { + const id = Date.now(); + setNotifications(prev => [...prev, { id, msg, color }]); + setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3500); + }; + + const handleTrade = (direction, symbol, price, lots, totalUnits, marginRequired, commission) => { + /* Deduct commission + margin from credits */ + setCredits(prev => prev - commission - marginRequired); + + setPositions(prev => { + const existing = prev.find(p => p.symbol === symbol && p.direction === direction); + if (existing) { + /* Merge into existing position — weighted average entry */ + const newUnits = existing.totalUnits + totalUnits; + const newAvg = Math.round(((existing.avgEntry * existing.totalUnits) + (price * totalUnits)) / newUnits * 100) / 100; + const newMargin = existing.margin + marginRequired; + return prev.map(p => p.symbol === symbol && p.direction === direction + ? { ...p, lots: p.lots + lots, totalUnits: newUnits, avgEntry: newAvg, margin: newMargin } + : p); + } + return [...prev, { symbol, direction, lots, totalUnits, avgEntry: price, margin: marginRequired }]; + }); + + addNotif( + `${direction === 'long' ? 'LONG' : 'SHORT'} ${lots}× ${symbol} (${totalUnits.toLocaleString()} u.) @ ₢${price}`, + direction === 'long' ? 'var(--green)' : 'var(--red)' + ); + + const now = new Date(); + setTradeFeed(prev => [{ + time: `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}`, + symbol, lots, price, side: direction, + }, ...prev].slice(0, 40)); + }; + + /* Market stats */ + const totalVolume = commodities.reduce((s, c) => s + c.volume, 0); + const totalOI = commodities.reduce((s, c) => s + c.openInterest, 0); + const advancers = commodities.filter(c => c.change >= 0).length; + const decliners = commodities.length - advancers; + const tickerItems = commodities.slice(0, 12).map(c => ({ + symbol: c.symbol, price: c.price, change: c.change, changePct: c.changePct, + })); + + const spread = selectedContract ? Math.round((selected.bestAsk - selected.bestBid) * 100) / 100 : 0; + const spreadPct = selected.bestBid > 0 ? (spread / selected.bestBid) * 100 : 0; + const midPrice = Math.round(((selected.bestBid + selected.bestAsk) / 2) * 100) / 100; + + return ( +
+ e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}>← Back to Docs + {/* Ticker */} + + + {/* Header bar */} +
+ COMMODITIES EXCHANGE +
+ SESSION + OPEN +
+ {advancers} ▲ + {decliners} ▼ +
+ TOTAL VOL + {(totalVolume / 1e6).toFixed(2)}M +
+ OPEN INT. + {(totalOI / 1e6).toFixed(1)}M +
+ ACCOUNT (₢ = ISK) + ₢{credits.toLocaleString()} +
+ + {/* Category tabs */} + + + {/* Notifications */} +
+ {notifications.map(n => ( +
+ {n.msg} +
+ ))} +
+ + {/* Main grid: Left (board + chart + feed) | Right (specs + depth + order + positions) */} +
+ + {/* Left column */} +
+ {/* Contract board */} + + + {/* Price chart */} +
+
+
+ {selected.symbol} + {selected.name} +
+
+ + O:₢{selected.open} H:₢{selected.high} L:₢{selected.low} + + + ₢{selected.price.toLocaleString()} + + = 0 ? 'var(--green)' : 'var(--red)', + }}> + {selected.change >= 0 ? '+' : ''}{selected.change.toFixed(2)} ({selected.changePct >= 0 ? '+' : ''}{selected.changePct.toFixed(2)}%) + +
+
+
+ +
+
+ + {/* Trade feed */} + +
+ + {/* Right column */} +
+ {/* Contract spec */} + + + {/* Depth chart */} +
+
+ Depth of Market +
+ +
+ + {/* Order book */} + + + {/* Order form */} + + + {/* Positions */} + +
+
+
+ ); +} + +window.GDD.MarketDemo = MarketDemo; diff --git a/js/demos/movement.js b/js/demos/movement.js new file mode 100644 index 0000000..e9a32df --- /dev/null +++ b/js/demos/movement.js @@ -0,0 +1,850 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useRef, useCallback } = React; +const TH = window.GDD.THREE; + +function ShipMovementDemo() { + const containerRef = useRef(null); + const sceneRef = useRef(null); + const animIdRef = useRef(null); + const moveRef = useRef(null); + const waypointsRef = useRef([]); + const targetRef = useRef(null); + const shipGroupRef = useRef(null); + const waypointMarkersRef = useRef([]); + const entityMeshesRef = useRef([]); + const trailRef = useRef(null); + const gridRef = useRef(null); + const trailPositionsRef = useRef([]); + const frameCountRef = useRef(0); + const clockRef = useRef(null); + + // HUD state + const [shipSpeed, setShipSpeed] = useState(0); + const [shipHeading, setShipHeading] = useState(0); + const [shipPos, setShipPos] = useState({ x: 400, y: 300 }); + const [moving, setMoving] = useState(false); + const [currentTarget, setCurrentTarget] = useState(null); + const [waypoints, setWaypoints] = useState([]); + const [entities, setEntities] = useState([]); + const [selectedEntity, setSelectedEntity] = useState(null); + const [shipStatus, setShipStatus] = useState(null); + const [serverTime, setServerTime] = useState('14:34:07'); + const [showGrid, setShowGrid] = useState(true); + const [moveProgress, setMoveProgress] = useState(0); + + // Chat state + const [chatTab, setChatTab] = useState('local'); + const [chatInput, setChatInput] = useState(''); + const [chatMessages, setChatMessages] = useState([ + { sender: 'CMDR Picard', body: 'Heading to Jita with a cargo of Kernite.', time: '14:22' }, + { sender: 'CMDR Worf', body: 'Pirates spotted near U-IRTYR gate.', time: '14:25' }, + { sender: 'CMDR Data', body: 'Scordite prices up 12% in Amarr.', time: '14:28' }, + { sender: 'CMDR Troi', body: 'Mining fleet forming in Sol.', time: '14:31' }, + ]); + + // Modules state + const [modules, setModules] = useState({ + high: [ + { id: 'laser1', name: 'Mine Laser', icon: '⛏', active: false }, + { id: 'turret1', name: '150mm Rail', icon: '◆', active: true }, + { id: null }, + ], + med: [ + { id: 'shield1', name: 'Shield Bst', icon: '◎', active: false }, + { id: 'warp1', name: 'Afterburn', icon: '»', active: true }, + { id: 'scram1', name: 'Scrambler', icon: '↯', active: false }, + ], + low: [ + { id: 'armor1', name: 'Armor Plt', icon: '▭', active: false }, + { id: 'magstab1', name: 'Mag Field', icon: '⚡', active: false }, + ], + }); + + // Load data + useEffect(() => { + window.GDD.api.getNearbyEntities().then(e => setEntities(e)); + window.GDD.api.getShipStatus().then(s => setShipStatus(s)); + }, []); + + // Server time tick + useEffect(() => { + const interval = setInterval(() => { + setServerTime(prev => { + const parts = prev.split(':'); + let sec = parseInt(parts[2]) + 1; + let min = parseInt(parts[1]); + let hr = parseInt(parts[0]); + if (sec >= 60) { sec = 0; min++; } + if (min >= 60) { min = 0; hr++; } + if (hr >= 24) hr = 0; + return `${String(hr).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; + }); + }, 1000); + return () => clearInterval(interval); + }, []); + + // Build 3D scene + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const scene = new THREE.Scene(); + scene.fog = new THREE.FogExp2(0x040810, 0.0005); + + const camera = new THREE.PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 5000); + camera.position.set(0, 40, 50); + camera.lookAt(0, 0, 0); + + const renderer = TH.createRenderer(container, { clearColor: 0x040810 }); + TH.handleResize(renderer, camera, container); + + const stars = TH.createStarField(4000, 3000); + scene.add(stars); + + TH.addNebula(scene, 0x22d3ee, [-200, 100, -500], 500); + TH.addNebula(scene, 0xf0a030, [300, -50, -400], 400); + TH.setupSpaceLighting(scene); + + const grid = new THREE.GridHelper(600, 30, 0x0d1520, 0x0d1520); + grid.material.transparent = true; + grid.material.opacity = 0.2; + scene.add(grid); + gridRef.current = grid; + + // Ship + const shipGroup = new THREE.Group(); + const shipMesh = TH.createShipMesh(0xc8d6e5, 0xf0a030, 0.8); + shipMesh.rotation.y = -Math.PI / 2; + shipGroup.add(shipMesh); + + const engineGlow = TH.createEngineGlow(0x22d3ee, 3, 20); + engineGlow.position.set(0, 0, -8); + shipGroup.add(engineGlow); + + const trail = TH.createEngineTrail(0xf0a030, 50); + shipGroup.add(trail); + trailRef.current = trail; + + const label = TH.createLabel('USS ENTERPRISE', '#22d3ee', 18); + label.position.y = 5; + shipGroup.add(label); + + scene.add(shipGroup); + shipGroupRef.current = shipGroup; + + sceneRef.current = { scene, camera, renderer, stars, shipGroup, engineGlow }; + + const clock = new THREE.Clock(); + clockRef.current = clock; + + const animate = () => { + animIdRef.current = requestAnimationFrame(animate); + frameCountRef.current++; + const t = clock.getElapsedTime(); + + const move = moveRef.current; + if (move) { + const dx = move.tx - move.sx; + const dy = move.ty - move.sy; + const totalDist = Math.sqrt(dx * dx + dy * dy); + const speed = Math.max(0.008, 0.04 / (1 + totalDist / 500)); + move.progress += speed; + + let nx, ny, angle; + if (move.progress >= 1) { + nx = move.tx; ny = move.ty; + angle = Math.atan2(dy, dx); + moveRef.current = null; + + if (waypointsRef.current.length > 0) { + waypointsRef.current.shift(); + setWaypoints([...waypointsRef.current]); + if (waypointsRef.current.length > 0) { + const next = waypointsRef.current[0]; + targetRef.current = next; + setCurrentTarget(next); + moveRef.current = { sx: nx, sy: ny, tx: next.x, ty: next.y, progress: 0 }; + } else { + targetRef.current = null; + setCurrentTarget(null); + setMoving(false); + } + } else { + setMoving(false); + } + } else { + const ease = move.progress < 0.5 + ? 2 * move.progress * move.progress + : -1 + (4 - 2 * move.progress) * move.progress; + nx = move.sx + dx * ease; + ny = move.sy + dy * ease; + angle = Math.atan2(dy, dx); + + trailPositionsRef.current.push({ x: nx * 0.1, y: 0, z: ny * 0.1, age: 0 }); + if (trailPositionsRef.current.length > 50) trailPositionsRef.current.shift(); + } + + shipGroup.position.x = nx * 0.1; + shipGroup.position.z = ny * 0.1; + shipGroup.position.y = 0; + shipGroup.rotation.y = -angle + Math.PI / 2; + engineGlow.intensity = 4; + + if (trailPositionsRef.current.length > 0) { + const posAttr = trail.geometry.attributes.position; + trailPositionsRef.current.forEach((p, i) => { + p.age++; + if (i < posAttr.count) { + posAttr.setXYZ(i, p.x - shipGroup.position.x, p.y, p.z - shipGroup.position.z); + } + }); + posAttr.needsUpdate = true; + } + + if (frameCountRef.current % 6 === 0) { + setShipSpeed(totalDist * speed * 60); + setShipHeading(((angle * 180 / Math.PI + 360) % 360)); + setShipPos({ x: nx, y: ny }); + setMoveProgress(move.progress); + } + } else { + engineGlow.intensity = 1; + if (trailPositionsRef.current.length > 0) { + trailPositionsRef.current = []; + const posAttr = trail.geometry.attributes.position; + for (let i = 0; i < posAttr.count; i++) posAttr.setXYZ(i, 0, -100, 0); + posAttr.needsUpdate = true; + } + shipGroup.position.y = Math.sin(t * 1.5) * 0.3; + } + + TH.followTarget(camera, shipGroup.position, { x: 0, y: 40, z: 50 }, 0.04); + stars.position.x = shipGroup.position.x * -0.02; + stars.position.z = shipGroup.position.z * -0.02; + + renderer.render(scene, camera); + }; + animate(); + + const onResize = () => TH.handleResize(renderer, camera, container); + window.addEventListener('resize', onResize); + + return () => { + if (animIdRef.current) cancelAnimationFrame(animIdRef.current); + window.removeEventListener('resize', onResize); + }; + }, []); + + // Update entities in 3D + useEffect(() => { + if (!sceneRef.current) return; + const { scene } = sceneRef.current; + + entityMeshesRef.current.forEach(m => scene.remove(m)); + entityMeshesRef.current = []; + + entities.forEach(ent => { + const x = ent.x * 0.1; + const z = ent.y * 0.1; + let mesh; + + if (ent.type === 'asteroid') { + mesh = TH.createAsteroid(1.5, 0x3d2a5c); + } else if (ent.type === 'station') { + mesh = TH.createStation(2, 0x22d3ee); + } else if (ent.type === 'hostile') { + mesh = TH.createShipMesh(0x7f1d1d, 0xef4444, 0.6); + mesh.rotation.y = -Math.PI / 2; + } else { + mesh = TH.createShipMesh(0x1a3a2a, 0x22c55e, 0.5); + mesh.rotation.y = -Math.PI / 2; + } + + const group = new THREE.Group(); + group.add(mesh); + const label = TH.createLabel(ent.name, ent.type === 'hostile' ? '#ef4444' : ent.type === 'asteroid' ? '#a78bfa' : ent.type === 'station' ? '#22d3ee' : '#22c55e', 14); + label.position.y = 4; + group.add(label); + group.position.set(x, 0, z); + scene.add(group); + entityMeshesRef.current.push(group); + }); + }, [entities]); + + // Update waypoints in 3D + useEffect(() => { + if (!sceneRef.current) return; + const { scene } = sceneRef.current; + + waypointMarkersRef.current.forEach(m => scene.remove(m)); + waypointMarkersRef.current = []; + + waypoints.forEach((wp, idx) => { + const x = wp.x * 0.1; + const z = wp.y * 0.1; + const isFirst = idx === 0; + + const geo = new THREE.OctahedronGeometry(1.2, 0); + const mat = new THREE.MeshBasicMaterial({ + color: isFirst ? 0xf0a030 : 0x22d3ee, + transparent: true, + opacity: 0.6, + wireframe: true, + }); + const marker = new THREE.Mesh(geo, mat); + marker.position.set(x, 2, z); + + const group = new THREE.Group(); + group.add(marker); + const lbl = TH.createLabel(wp.label, isFirst ? '#f0a030' : '#22d3ee', 14); + lbl.position.y = 5; + group.add(lbl); + scene.add(group); + waypointMarkersRef.current.push(group); + }); + }, [waypoints]); + + // Grid toggle + useEffect(() => { + if (gridRef.current) gridRef.current.visible = showGrid; + }, [showGrid]); + + // Navigate to entity — used by overview rows, action buttons, and waypoint panel + const navigateToEntity = useCallback((ent) => { + const wp = { id: Date.now(), x: ent.x, y: ent.y, label: ent.name, type: ent.type }; + waypointsRef.current = [wp]; + setWaypoints([wp]); + targetRef.current = wp; + setCurrentTarget(wp); + const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 }; + moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: wp.x, ty: wp.y, progress: 0 }; + setMoving(true); + setSelectedEntity(ent); + }, []); + + // Add entity as next waypoint (queued, doesn't interrupt current move) + const addWaypointEntity = useCallback((ent) => { + const wp = { id: Date.now(), x: ent.x, y: ent.y, label: ent.name, type: ent.type }; + waypointsRef.current = [...waypointsRef.current, wp]; + setWaypoints([...waypointsRef.current]); + if (!moveRef.current && waypointsRef.current.length === 1) { + targetRef.current = wp; + setCurrentTarget(wp); + const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 }; + moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: wp.x, ty: wp.y, progress: 0 }; + setMoving(true); + } + }, []); + + const clearWaypoints = useCallback(() => { + waypointsRef.current = []; + targetRef.current = null; + moveRef.current = null; + setWaypoints([]); + setCurrentTarget(null); + setMoving(false); + }, []); + + const removeWaypoint = useCallback((id) => { + waypointsRef.current = waypointsRef.current.filter(w => w.id !== id); + setWaypoints([...waypointsRef.current]); + if (targetRef.current?.id === id) { + moveRef.current = null; + const next = waypointsRef.current[0] || null; + targetRef.current = next; + setCurrentTarget(next); + if (next) { + const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 }; + moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: next.x, ty: next.y, progress: 0 }; + } else setMoving(false); + } + }, []); + + const toggleModule = useCallback((slotType, index) => { + setModules(prev => { + const row = [...prev[slotType]]; + row[index] = { ...row[index], active: !row[index].active }; + return { ...prev, [slotType]: row }; + }); + }, []); + + const handleChatSend = useCallback(() => { + if (!chatInput.trim()) return; + setChatMessages(prev => [...prev, { sender: 'You', body: chatInput, time: serverTime.slice(0, 5) }]); + setChatInput(''); + }, [chatInput, serverTime]); + + const formatCoord = (v) => v.toFixed(0); + const entityColor = (type) => { + switch (type) { + case 'hostile': return 'var(--red)'; + case 'asteroid': return 'var(--purple)'; + case 'station': return 'var(--cyan)'; + case 'player': return 'var(--green)'; + default: return 'var(--muted)'; + } + }; + const entityIcon = (type) => { + switch (type) { + case 'hostile': return '✸'; + case 'asteroid': return '◉'; + case 'station': return '⬡'; + case 'player': return '◈'; + default: return '○'; + } + }; + const sortedEntities = [...entities].sort((a, b) => (a.distance || 0) - (b.distance || 0)); + const activeModuleCount = Object.values(modules).flat().filter(m => m && m.active).length; + + return ( +
+ + {/* 3D Viewport — visual only, no click interaction */} +
+ + {/* HUD Overlay */} +
+ + {/* ===== TOP BAR ===== */} +
+ +
+ Sol + 1.0 HIGH SEC +
+ Ship + {shipStatus?.name || 'USS Enterprise'} +
+ SPD + {shipSpeed.toFixed(0)}m/s +
+ HDG + {shipHeading.toFixed(0)}° +
+ POS + {formatCoord(shipPos.x)}, {formatCoord(shipPos.y)} +
+ ₢125,740 +
+ + + CONNECTED + +
+ {serverTime} + {moving && currentTarget && ( + <> +
+ ● EN ROUTE → {currentTarget.label} + + )} +
+ + {/* ===== MIDDLE ===== */} +
+ + {/* ===== LEFT PANEL — Ship Status + Speed + Waypoints ===== */} +
+ + {/* Ship Health */} +
+
+ Ship Status +
+
+
+
+ {shipStatus?.name || 'USS Enterprise'} + {shipStatus?.class?.split(' ')[0] || 'Venture'} +
+
+
+ {[ + { label: 'SHIELD', value: shipStatus?.shields ?? 100, color: '#22d3ee', cls: 'shield' }, + { label: 'ARMOR', value: shipStatus?.armor ?? 100, color: '#f0a030', cls: 'armor' }, + { label: 'HULL', value: shipStatus?.hull ?? 100, color: '#22c55e', cls: 'hull' }, + { label: 'CAP', value: shipStatus?.capacitor ?? 85, color: '#a78bfa', cls: 'cap' }, + ].map(bar => ( +
+ {bar.label} +
+
+
+ {bar.value}% +
+ ))} +
+
+
+ + {/* Propulsion */} +
+
+ Propulsion +
+
+
+ {moving ? shipSpeed.toFixed(0) : '—'} + m/s +
+
+ +
+
+
+ +
+
+ {moving ? '● SUBLIGHT ACTIVE' : '○ IDLE'} +
+
+
+ + {/* Waypoints */} +
+
+ Waypoints + {waypoints.length > 0 && ( + CLEAR + )} +
+
+ {waypoints.length === 0 ? ( +
+
+
Select a target from Overview
+
or use action buttons to navigate
+
+ ) : ( +
+ {waypoints.map((wp, idx) => { + const dist = Math.sqrt((wp.x - shipPos.x) ** 2 + (wp.y - shipPos.y) ** 2); + return ( +
+ {idx + 1}. +
+
{wp.label}
+
{dist.toFixed(0)} km
+
+ { e.stopPropagation(); removeWaypoint(wp.id); }}>× +
+ ); + })} + {moving && moveProgress > 0 && ( +
+
+ TRIP + {(moveProgress * 100).toFixed(0)}% +
+
+
+
+
+ )} +
+ )} +
+
+
+ + {/* CENTER — crosshair + nav status */} +
+ {/* Subtle grid lines */} +
+
+ + {/* Crosshair */} +
+
+
+
+
+
+
+ + {/* Navigation status toast */} + {moving && currentTarget && ( +
+ ● EN ROUTE → {currentTarget.label} +
+ )} + {!moving && waypoints.length === 0 && ( +
+ ○ IDLE — Select a target from Overview +
+ )} + + {/* Grid toggle */} +
+ +
+
+ + {/* ===== RIGHT PANEL — Overview ===== */} +
+
+
+ Overview + {sortedEntities.length} entities +
+
+ + + + + + + + + + {sortedEntities.map(ent => { + const col = entityColor(ent.type); + const ico = entityIcon(ent.type); + const dist = Math.sqrt((ent.x - shipPos.x) ** 2 + (ent.y - shipPos.y) ** 2); + return ( + setSelectedEntity(ent)} + onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-raised)'} + onMouseLeave={(e) => e.currentTarget.style.background = selectedEntity?.id === ent.id ? 'var(--accent-bg)' : 'transparent'} + > + + + + + + + ); + })} + +
NameDist
{ico}{ent.name}{dist.toFixed(0)} km + + + +
+
+
+ + {/* Selected Entity Detail */} + {selectedEntity && (() => { + const col = entityColor(selectedEntity.type); + const dist = Math.sqrt((selectedEntity.x - shipPos.x) ** 2 + (selectedEntity.y - shipPos.y) ** 2); + return ( +
+
+ + {selectedEntity.name} +
+
+
+ {selectedEntity.type.toUpperCase()} · {dist.toFixed(0)} km +
+
+ {selectedEntity.type === 'asteroid' && ( + <> + + + + + )} + {selectedEntity.type === 'hostile' && ( + <> + + + + + )} + {selectedEntity.type === 'station' && ( + <> + + + + + )} + {selectedEntity.type === 'player' && ( + <> + + + + )} +
+
+
+ ); + })()} +
+
+ + {/* ===== BOTTOM BAR ===== */} +
+ + {/* Module Rack */} +
+
+ Modules + {activeModuleCount} active +
+
+ {['high', 'med', 'low'].map(slotType => ( +
+ + {slotType === 'high' ? 'HIGH' : slotType === 'med' ? 'MED' : 'LOW'} + + {modules[slotType].map((mod, i) => ( + mod.id ? ( +
toggleModule(slotType, i)} + title={`${mod.name} (${mod.active ? 'Active' : 'Inactive'})`} + style={{ + width: '48px', height: '40px', borderRadius: '6px', + border: `1px solid ${mod.active ? 'var(--accent-border)' : 'var(--border)'}`, + background: mod.active ? 'var(--accent-bg)' : 'var(--surface)', + display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', + cursor: 'pointer', transition: 'all 0.15s ease', position: 'relative', overflow: 'hidden', gap: '2px', + }} + > + {mod.icon} + {mod.name} + {mod.active &&
} +
+ ) : ( +
+ +
+ ) + ))} +
+ ))} +
+
+ + {/* Cargo */} +
+
+ Cargo Hold +
+
+
+ 12,400 / 25,000 m³ + 50% +
+
+
+
+ {[ + { name: 'Veldspar', qty: '8,500' }, + { name: 'Scordite', qty: '2,300' }, + { name: 'Kernite', qty: '400' }, + { name: 'Pyroxeres', qty: '1,200' }, + ].map((item, i) => ( +
+ {item.name} + ×{item.qty} +
+ ))} +
+
+ + {/* Chat */} +
+
+ {['local', 'corp', 'trade'].map(tab => ( + + ))} +
+
+ {chatMessages.slice(-6).map((msg, i) => ( +
+ {msg.sender} + {msg.body} + {msg.time} +
+ ))} +
+
+ setChatInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleChatSend()} + placeholder="Send message..." + style={{ flex: 1, background: 'var(--bg-subtle)', border: 'none', padding: '6px 10px', fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg)', outline: 'none' }} + /> + +
+
+
+
+ + {/* Module cycle animation keyframes */} + +
+ ); +} + +window.GDD.ShipMovementDemo = ShipMovementDemo; diff --git a/js/demos/progression.js b/js/demos/progression.js new file mode 100644 index 0000000..a0c60e4 --- /dev/null +++ b/js/demos/progression.js @@ -0,0 +1,381 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useCallback, useRef } = React; + +function ProgressionDemo() { + const [skills, setSkills] = useState([]); + const [activeCategory, setActiveCategory] = useState('all'); + const [selectedSkill, setSelectedSkill] = useState(null); + const [totalXP, setTotalXP] = useState(0); + const [xpLog, setXpLog] = useState([]); + const [simulating, setSimulating] = useState(false); + const simRef = useRef(null); + + const allSkills = [ + // Combat + { name: 'Gunnery', category: 'Combat', xp: 380, level: 2, nextLevel: 500 }, + { name: 'Missiles', category: 'Combat', xp: 120, level: 1, nextLevel: 500 }, + { name: 'Shield Operation', category: 'Combat', xp: 50, level: 0, nextLevel: 100 }, + { name: 'Armor Tanking', category: 'Combat', xp: 0, level: 0, nextLevel: 100 }, + { name: 'Electronic Warfare', category: 'Combat', xp: 0, level: 0, nextLevel: 100 }, + // Industry + { name: 'Mining', category: 'Industry', xp: 1850, level: 3, nextLevel: 2000 }, + { name: 'Refining', category: 'Industry', xp: 420, level: 2, nextLevel: 500 }, + { name: 'Manufacturing', category: 'Industry', xp: 80, level: 0, nextLevel: 100 }, + { name: 'Blueprint Research', category: 'Industry', xp: 0, level: 0, nextLevel: 100 }, + // Navigation + { name: 'Warp Drive Operation', category: 'Navigation', xp: 60, level: 0, nextLevel: 100 }, + { name: 'Afterburner', category: 'Navigation', xp: 30, level: 0, nextLevel: 100 }, + { name: 'Evasive Maneuvering', category: 'Navigation', xp: 0, level: 0, nextLevel: 100 }, + // Trade + { name: 'Market Analysis', category: 'Trade', xp: 20, level: 0, nextLevel: 100 }, + { name: 'Broker Relations', category: 'Trade', xp: 45, level: 0, nextLevel: 100 }, + { name: 'Hauling', category: 'Trade', xp: 0, level: 0, nextLevel: 100 }, + // Leadership + { name: 'Fleet Command', category: 'Leadership', xp: 0, level: 0, nextLevel: 100 }, + { name: 'AI Coordination', category: 'Leadership', xp: 0, level: 0, nextLevel: 100 }, + ]; + + const xpCurve = [100, 500, 2000, 8000, 32000]; + const categoryColors = { + Combat: 'var(--red)', + Industry: 'var(--accent)', + Navigation: 'var(--cyan)', + Trade: 'var(--green)', + Leadership: 'var(--purple)', + }; + + const xpActions = [ + { name: 'Mining Cycle', xp: 15, category: 'Industry', desc: 'Complete a mining laser cycle' }, + { name: 'NPC Kill', xp: 40, category: 'Combat', desc: 'Destroy an NPC pirate' }, + { name: 'Refine Batch', xp: 25, category: 'Industry', desc: 'Refine a batch of ore' }, + { name: 'System Jump', xp: 5, category: 'Navigation', desc: 'Jump to a new system' }, + { name: 'Market Trade', xp: 20, category: 'Trade', desc: 'Complete a market transaction' }, + { name: 'Player Kill', xp: 120, category: 'Combat', desc: 'Destroy a player ship' }, + { name: 'Manufacture Item', xp: 35, category: 'Industry', desc: 'Complete a manufacturing job' }, + { name: 'Waypoint Route', xp: 30, category: 'Navigation', desc: 'Complete a multi-jump route' }, + { name: 'Bounty Collect', xp: 80, category: 'Combat', desc: 'Collect a bounty reward' }, + ]; + + useEffect(() => { + setSkills(allSkills.map(s => ({ ...s }))); + }, []); + + useEffect(() => { + const total = skills.reduce((sum, s) => sum + s.xp, 0); + setTotalXP(total); + }, [skills]); + + const filteredSkills = activeCategory === 'all' + ? skills + : skills.filter(s => s.category === activeCategory); + + const categoryStats = Object.keys(categoryColors).map(cat => { + const catSkills = skills.filter(s => s.category === cat); + const totalXP = catSkills.reduce((sum, s) => sum + s.xp, 0); + const maxXP = catSkills.reduce((sum, s) => sum + xpCurve[Math.min(s.level, 4)], 0); + const avgLevel = catSkills.length > 0 ? catSkills.reduce((sum, s) => sum + s.level, 0) / catSkills.length : 0; + return { category: cat, color: categoryColors[cat], totalXP, maxXP, avgLevel, count: catSkills.length }; + }); + + const handleSimulate = useCallback(() => { + if (simulating) { + setSimulating(false); + if (simRef.current) clearInterval(simRef.current); + return; + } + setSimulating(true); + simRef.current = setInterval(() => { + const action = xpActions[Math.floor(Math.random() * xpActions.length)]; + const skillName = action.category === 'Combat' ? 'Gunnery' : + action.category === 'Industry' ? 'Mining' : + action.category === 'Navigation' ? 'Warp Drive Operation' : + action.category === 'Trade' ? 'Broker Relations' : 'Fleet Command'; + + setSkills(prev => prev.map(s => { + if (s.name !== skillName) return s; + let newXp = s.xp + action.xp; + let newLevel = s.level; + while (newLevel < 5 && newXp >= xpCurve[newLevel]) { + newXp -= xpCurve[newLevel]; + newLevel++; + } + return { ...s, xp: newXp, level: newLevel, nextLevel: xpCurve[Math.min(newLevel, 4)] }; + })); + + setXpLog(prev => [{ + action: action.name, + xp: action.xp, + skill: skillName, + time: new Date().toLocaleTimeString('en', { hour12: false }), + }, ...prev.slice(0, 19)]); + }, 800); + }, [simulating]); + + useEffect(() => { + return () => { if (simRef.current) clearInterval(simRef.current); }; + }, []); + + const levelColor = (lvl) => { + if (lvl === 0) return 'var(--muted)'; + if (lvl === 1) return 'var(--green)'; + if (lvl === 2) return 'var(--cyan)'; + if (lvl === 3) return 'var(--purple)'; + if (lvl === 4) return 'var(--accent)'; + return 'var(--red)'; + }; + + return ( +
+ e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}>← Back to Docs +

Skill Progression Demo

+

+ Action-based XP system across 5 categories and 17+ skills. Hit the simulate button to watch + XP flow in from random activities — each action awards XP to the matching skill category. +

+ + {/* HUD-style progression strip */} +
+ SKILL PROGRESSION +
+ TOTAL XP + {totalXP.toLocaleString()} +
+ TRAINED + {skills.filter(s => s.level > 0).length}/{skills.length} +
+ MAX LVL + {Math.max(...skills.map(s => s.level))} + {simulating && ● SIMULATING} +
+ + {/* Stats */} +
+
+
{totalXP.toLocaleString()}
+
Total XP
+
+
+
+ {skills.filter(s => s.level > 0).length}/{skills.length} +
+
Skills Trained
+
+
+
+ {Math.max(...skills.map(s => s.level))} +
+
Highest Level
+
+
+
{xpLog.length}
+
Actions (session)
+
+
+ + {/* Simulate button */} +
+ + + Generates random XP actions every 800ms + +
+ + {/* Category overview */} +
+ {categoryStats.map(cat => ( +
setActiveCategory(activeCategory === cat.category ? 'all' : cat.category)}> +
+

{cat.category}

+ + avg Lvl {cat.avgLevel.toFixed(1)} + +
+
+
+
0 ? (cat.totalXP / (cat.maxXP * 2)) * 100 : 0}%`, + background: cat.color, + }} /> +
+
+ {cat.totalXP.toLocaleString()} XP · {cat.count} skills +
+
+
+ ))} +
+ +
+ + {Object.keys(categoryColors).map(cat => ( + + ))} +
+ +
+ {/* Skill tree */} +
+ {filteredSkills.map((skill, i) => { + const progress = skill.level >= 5 ? 100 : (skill.xp / skill.nextLevel) * 100; + return ( +
setSelectedSkill(skill)}> +
+
+ {skill.name} + + Lvl {skill.level} + +
+ + {skill.category} + +
+
+
+
+
+ {skill.level >= 5 ? 'MAX' : `${skill.xp.toLocaleString()} / ${skill.nextLevel.toLocaleString()} XP`} +
+
+ ); + })} +
+ + {/* XP log + detail */} +
+ {/* Selected skill detail */} + {selectedSkill && ( +
+

+ {selectedSkill.name} +

+
+
+ Current Level + + Level {selectedSkill.level}{selectedSkill.level >= 5 ? ' (MAX)' : ''} + +
+
+ Category + {selectedSkill.category} +
+
+ XP to Next Level + + {selectedSkill.level >= 5 ? '—' : `${selectedSkill.xp.toLocaleString()} / ${selectedSkill.nextLevel.toLocaleString()}`} + +
+
+ + {/* Level milestone visualization */} +
+ {[0, 1, 2, 3, 4].map(lvl => ( +
lvl ? categoryColors[selectedSkill.category] + '20' : 'var(--surface-raised)', + border: `1px solid ${selectedSkill.level > lvl ? categoryColors[selectedSkill.category] + '40' : 'var(--border)'}`, + borderRadius: 'var(--radius-md)', + }}> +
lvl ? levelColor(lvl + 1) : 'var(--muted)' }}> + {lvl + 1} +
+
+ {xpCurve[lvl].toLocaleString()} XP +
+
+ ))} +
+
+ )} + + {/* XP activity log */} +
+

XP Activity Log

+
+ {xpLog.length === 0 && ( +
+ Start the simulation to see XP flow in real-time. +
+ )} + {xpLog.map((entry, i) => ( +
+ + {entry.time} + + {entry.action} + + +{entry.xp} XP + + → {entry.skill} +
+ ))} +
+
+ + {/* XP actions reference */} +
+

XP Sources

+
+ {xpActions.map((action, i) => ( +
+
+ {action.name} + + {action.desc} + +
+
+ + {action.category} + + + +{action.xp} + +
+
+ ))} +
+
+
+
+
+ ); +} + +window.GDD.ProgressionDemo = ProgressionDemo; diff --git a/js/demos/refining.js b/js/demos/refining.js new file mode 100644 index 0000000..54bc283 --- /dev/null +++ b/js/demos/refining.js @@ -0,0 +1,525 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useCallback, useRef } = React; + +function RefiningDemo() { + const [inventory, setInventory] = useState([]); + const [orePrices, setOrePrices] = useState({}); + const [selectedOre, setSelectedOre] = useState(null); + const [refineQty, setRefineQty] = useState(0); + const [skillLevel, setSkillLevel] = useState(2); + const [refining, setRefining] = useState(false); + const [results, setResults] = useState([]); + const [manufacturingTab, setManufacturingTab] = useState(false); + const [manufacturingJobs, setManufacturingJobs] = useState([]); + const [notifications, setNotifications] = useState([]); + const timerRef = useRef(null); + + const mineralData = { + Veldspar: { mineral: 'Tritanium', yield: 415, batch: 333, time: 45 }, + Scordite: { mineral: 'Pyerite', yield: 171, batch: 333, time: 45 }, + Pyroxeres: { mineral: 'Nocxium', yield: 8, batch: 333, time: 60 }, + Kernite: { mineral: 'Isogen', yield: 107, batch: 200, time: 60 }, + Omber: { mineral: 'Isogen', yield: 86, batch: 500, time: 75 }, + Jaspet: { mineral: 'Zydrine', yield: 8, batch: 500, time: 75 }, + Hemorphite: { mineral: 'Nocxium', yield: 21, batch: 500, time: 90 }, + Arkonor: { mineral: 'Megacyte', yield: 18, batch: 200, time: 120 }, + }; + + const manufacturingRecipes = [ + { id: 1, product: 'Mining Laser I', minerals: { Tritanium: 200, Pyerite: 80 }, time: 300, skill: 1 }, + { id: 2, product: '150mm Railgun', minerals: { Tritanium: 400, Pyerite: 150, Nocxium: 20 }, time: 900, skill: 2 }, + { id: 3, product: 'Shield Booster I', minerals: { Tritanium: 300, Isogen: 50 }, time: 600, skill: 2 }, + { id: 4, product: 'Frigate Hull', minerals: { Tritanium: 2000, Pyerite: 800, Nocxium: 100 }, time: 1800, skill: 3 }, + { id: 5, product: '1MN Afterburner', minerals: { Tritanium: 150, Pyerite: 50, Isogen: 20 }, time: 480, skill: 2 }, + ]; + + const [playerMinerals, setPlayerMinerals] = useState({ + Tritanium: 0, Pyerite: 0, Nocxium: 0, Isogen: 0, Zydrine: 0, Megacyte: 0, + }); + + const skillEfficiency = { 0: 0.50, 1: 0.60, 2: 0.70, 3: 0.80, 4: 0.875, 5: 0.95 }; + + useEffect(() => { + window.GDD.api.getPlayerInventory().then(i => { + setInventory(i); + if (i.length > 0) { setSelectedOre(i[0].item); setRefineQty(i[0].quantity); } + }); + window.GDD.api.getOrePrices().then(p => setOrePrices(p)); + }, []); + + const addNotif = useCallback((msg, color) => { + const id = Date.now(); + setNotifications(prev => [...prev, { id, msg, color }]); + setTimeout(() => setNotifications(prev => prev.filter(n => n.id !== id)), 3500); + }, []); + + const handleRefine = useCallback(async () => { + if (!selectedOre || refineQty <= 0) return; + const data = mineralData[selectedOre]; + if (!data) return; + if (refineQty < data.batch) { + addNotif(`Need at least ${data.batch} units for a batch.`, 'var(--red)'); + return; + } + + setRefining(true); + const inv = inventory.find(i => i.item === selectedOre); + const batches = Math.floor(refineQty / data.batch); + const used = batches * data.batch; + const eff = skillEfficiency[skillLevel]; + const mineralYield = Math.floor(batches * data.yield * eff); + const rawValue = used * (orePrices[selectedOre] || 0); + const mineralValue = mineralYield * Math.floor((orePrices[selectedOre] || 0) * 2.5); + + // Simulate delay + await new Promise(r => setTimeout(r, data.time * 10)); + + setPlayerMinerals(prev => ({ + ...prev, + [data.mineral]: prev[data.mineral] + mineralYield, + })); + + setInventory(prev => prev.map(i => + i.item === selectedOre + ? { ...i, quantity: i.quantity - used } + : i + ).filter(i => i.quantity > 0)); + + setResults(prev => [{ + ore: selectedOre, + batches, + used, + mineral: data.mineral, + yield: mineralYield, + efficiency: eff, + rawValue, + mineralValue, + better: mineralValue > rawValue, + }, ...prev.slice(0, 9)]); + + setRefining(false); + addNotif(`Refined ${used.toLocaleString()} ${selectedOre} → ${mineralYield.toLocaleString()} ${data.mineral} (${(eff * 100).toFixed(0)}% eff)`, 'var(--green)'); + }, [selectedOre, refineQty, skillLevel, inventory, orePrices, addNotif]); + + const handleManufacture = useCallback((recipe) => { + if (skillLevel < recipe.skill) { + addNotif(`Need Industry level ${recipe.skill} to manufacture ${recipe.product}.`, 'var(--red)'); + return; + } + // Check minerals + for (const [mineral, qty] of Object.entries(recipe.minerals)) { + if ((playerMinerals[mineral] || 0) < qty) { + addNotif(`Not enough ${mineral}. Need ${qty}, have ${playerMinerals[mineral] || 0}.`, 'var(--red)'); + return; + } + } + // Deduct minerals + setPlayerMinerals(prev => { + const next = { ...prev }; + for (const [mineral, qty] of Object.entries(recipe.minerals)) { + next[mineral] -= qty; + } + return next; + }); + + const job = { + id: Date.now(), + product: recipe.product, + totalTime: recipe.time, + remaining: recipe.time, + started: Date.now(), + }; + setManufacturingJobs(prev => [...prev, job]); + addNotif(`Manufacturing job started: ${recipe.product}. ETA: ${Math.floor(recipe.time / 60)}m ${recipe.time % 60}s`, 'var(--cyan)'); + }, [skillLevel, playerMinerals, addNotif]); + + // Manufacturing timer + useEffect(() => { + const interval = setInterval(() => { + setManufacturingJobs(prev => { + const updated = prev.map(j => ({ + ...j, + remaining: Math.max(0, j.remaining - 1), + })); + const completed = updated.filter(j => j.remaining <= 0 && prev.find(p => p.id === j.id && p.remaining > 0)); + completed.forEach(j => { + addNotif(`Manufacturing complete: ${j.product}`, 'var(--green)'); + }); + return updated.filter(j => j.remaining > 0); + }); + }, 1000); + return () => clearInterval(interval); + }, [addNotif]); + + const formatTime = (s) => `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`; + + return ( +
+ e.currentTarget.style.color='var(--fg-bright)'} onMouseLeave={e => e.currentTarget.style.color='var(--muted)'}>← Back to Docs +

Refining & Manufacturing Demo

+

+ Refine raw ore into minerals, then use minerals to manufacture ships and modules. + Industry skill level determines refining efficiency — higher skill means more minerals per batch. +

+ + {/* HUD-style industry strip */} +
+ INDUSTRY +
+ SKILL + Lvl {skillLevel} +
+ EFFICIENCY + {(skillEfficiency[skillLevel] * 100).toFixed(0)}% +
+ MINERALS + {Object.values(playerMinerals).reduce((a, b) => a + b, 0).toLocaleString()} +
+ JOBS + {manufacturingJobs.length} +
+ + {/* Notifications */} +
+ {notifications.map(n => ( +
+ {n.msg} +
+ ))} +
+ + {/* Tab toggle */} +
+ + +
+ + {/* Stats */} +
+
+
Lvl {skillLevel}
+
Industry Skill
+
+
+
{(skillEfficiency[skillLevel] * 100).toFixed(0)}%
+
Refine Efficiency
+
+
+
+ {Object.values(playerMinerals).reduce((a, b) => a + b, 0).toLocaleString()} +
+
Total Minerals
+
+
+
{manufacturingJobs.length}
+
Active Jobs
+
+
+ + {!manufacturingTab ? ( + /* ===== REFINING ===== */ + <> +
+
+ Reprocessing Plant + | + Jita IV — Moon 4 + {refining && ( + + ◌ REFINING... + + )} +
+ +
+ {/* Ore selection */} +
+
+
+ Your Ore +
+
+ {inventory.map(item => { + const data = mineralData[item.item]; + return ( +
{ setSelectedOre(item.item); setRefineQty(item.quantity); }}> +
+ {item.item} +
+
+ {item.quantity.toLocaleString()} units · → {data?.mineral || '?'} +
+
+ ); + })} +
+ + {/* Refining panel */} +
+ {selectedOre && mineralData[selectedOre] ? ( + <> +

{selectedOre}

+
+
+
Yields Mineral
+
{mineralData[selectedOre].mineral}
+
+
+
Batch Size
+
{mineralData[selectedOre].batch.toLocaleString()} units
+
+
+ +
+
+ Quantity to Refine + + {Math.floor(refineQty / mineralData[selectedOre].batch)} batches + +
+ i.item === selectedOre)?.quantity || 0} + value={refineQty} onChange={e => setRefineQty(parseInt(e.target.value))} + style={{ width: '100%', accentColor: 'var(--accent)' }} + /> +
+ {refineQty.toLocaleString()} units +
+
+ + {/* Skill selector */} +
+
Industry Skill Level
+
+ {[0, 1, 2, 3, 4, 5].map(lvl => ( + + ))} +
+
+ + {/* Preview */} + {refineQty >= mineralData[selectedOre].batch && ( +
+

Refining Preview

+ {(() => { + const data = mineralData[selectedOre]; + const batches = Math.floor(refineQty / data.batch); + const used = batches * data.batch; + const eff = skillEfficiency[skillLevel]; + const minYield = Math.floor(batches * data.yield * eff); + const rawValue = used * (orePrices[selectedOre] || 0); + const mineralValue = minYield * Math.floor((orePrices[selectedOre] || 0) * 2.5); + return ( +
+
+ Ore consumed + {used.toLocaleString()} {selectedOre} +
+
+ Mineral yield + {minYield.toLocaleString()} {data.mineral} +
+
+ Efficiency + {(eff * 100).toFixed(0)}% +
+
+
+ Sell raw value + ₢{rawValue.toLocaleString()} +
+
+ Refined value (est.) + rawValue ? 'var(--green)' : 'var(--red)' }}> + ₢{mineralValue.toLocaleString()} {mineralValue > rawValue ? '▲' : '▼'} + +
+
+
+ ); + })()} +
+ )} + + + + ) : ( +
+ Select an ore type to begin refining +
+ )} +
+
+
+ + {/* Refining history */} + {results.length > 0 && ( +
+

Refining History

+ + + + + + + + + + + + + + + {results.map((r, i) => ( + + + + + + + + + + + ))} + +
OreBatchesMineralYieldEfficiencyRaw ValueRefined ValueVerdict
{r.ore}{r.batches}{r.mineral}{r.yield.toLocaleString()}{(r.efficiency * 100).toFixed(0)}%₢{r.rawValue.toLocaleString()} + ₢{r.mineralValue.toLocaleString()} + + + {r.better ? 'REFINE ▲' : 'SELL RAW ▼'} + +
+
+ )} + + ) : ( + /* ===== MANUFACTURING ===== */ + <> +
+ {/* Mineral inventory */} +
+

Mineral Inventory

+ {Object.entries(playerMinerals).map(([mineral, qty]) => ( +
+ 0 ? 'var(--fg-bright)' : 'var(--muted)' }}>{mineral} + 0 ? 'var(--cyan)' : 'var(--muted)' }}> + {qty.toLocaleString()} + +
+ ))} +
+ Refine ore to accumulate minerals for manufacturing. +
+
+ + {/* Active jobs */} +
+

Manufacturing Jobs

+ {manufacturingJobs.length === 0 ? ( +
+ No active jobs. Start one from the recipe list below. +
+ ) : ( + manufacturingJobs.map(job => ( +
+
+ {job.product} + + {formatTime(job.remaining)} + +
+
+
+
+
+ )) + )} +
+
+ + {/* Recipe list */} +
+

Blueprints

+
+ {manufacturingRecipes.map(recipe => { + const canBuild = skillLevel >= recipe.skill && + Object.entries(recipe.minerals).every(([m, q]) => (playerMinerals[m] || 0) >= q); + return ( +
+
+

{recipe.product}

+ + {canBuild ? 'READY' : skillLevel < recipe.skill ? `LVL ${recipe.skill}` : 'NEED MATS'} + +
+
+ {Object.entries(recipe.minerals).map(([m, q]) => { + const have = playerMinerals[m] || 0; + return ( +
+ {m} + = q ? 'var(--green)' : 'var(--red)' }}> + {have.toLocaleString()} / {q.toLocaleString()} + +
+ ); + })} +
+
+ + Time: {formatTime(recipe.time)} · Skill: Lvl {recipe.skill} + + +
+
+ ); + })} +
+
+ + )} +
+ ); +} + +window.GDD.RefiningDemo = RefiningDemo; diff --git a/js/demos/starmap.js b/js/demos/starmap.js new file mode 100644 index 0000000..4a657f6 --- /dev/null +++ b/js/demos/starmap.js @@ -0,0 +1,1044 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useRef, useCallback, useMemo } = React; +const TH = window.GDD.THREE; + +/* ── Scale constants ── */ +const SCALE = 0.5; // world position scale +const ORBIT_SCALE = 0.22; // orbital radius scale +const PLANET_SCALE = 0.7; // planet mesh scale + +function StarMapDemo() { + const containerRef = useRef(null); + const sceneRef = useRef(null); + const [systems, setSystems] = useState([]); + const [connections, setConnections] = useState([]); + const [selected, setSelected] = useState(null); + const [destination, setDestination] = useState(null); + const [hovered, setHovered] = useState(null); + const [waypoints, setWaypoints] = useState([]); + const [route, setRoute] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [systemFilter, setSystemFilter] = useState('all'); + const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, target: null }); + const systemMeshesRef = useRef([]); + const routeLineRef = useRef(null); + const connLinesRef = useRef([]); + const animIdRef = useRef(null); + const cameraCtrlRef = useRef(null); + const minimapCanvasRef = useRef(null); + const focusTargetRef = useRef(null); + const orbitalBodiesRef = useRef([]); + const asteroidBeltsRef = useRef([]); + + /* ── Celestial data ── */ + const celestialData = useMemo(() => { + return window.GDD.CONSTANTS.CELESTIAL_BODIES || {}; + }, []); + + const selectedCelestial = useMemo(() => { + if (!selected) return null; + return celestialData[selected.id] || null; + }, [selected, celestialData]); + + /* ── Load data ── */ + useEffect(() => { + window.GDD.api.getSystems().then(s => setSystems(s)); + window.GDD.api.getConnections().then(c => setConnections(c)); + }, []); + + /* ── Camera fly-to on selection ── */ + useEffect(() => { + if (!focusTargetRef.current || !cameraCtrlRef.current) return; + cameraCtrlRef.current.flyTo(focusTargetRef.current.x, focusTargetRef.current.y, focusTargetRef.current.z || 0); + focusTargetRef.current = null; + }, [selected]); + + /* ── Context menu close on click / Escape / scroll ── */ + useEffect(() => { + const close = () => setContextMenu(prev => ({ ...prev, visible: false })); + const onKey = (e) => { if (e.key === 'Escape') close(); }; + window.addEventListener('click', close); + window.addEventListener('keydown', onKey); + return () => { window.removeEventListener('click', close); window.removeEventListener('keydown', onKey); }; + }, []); + + /* ── Build 3D scene ── */ + useEffect(() => { + if (systems.length === 0) return; + const container = containerRef.current; + if (!container) return; + + // Cleanup previous + if (sceneRef.current) { + sceneRef.current.scene.traverse(child => { + if (child.geometry) child.geometry.dispose(); + if (child.material) { if (child.material.map) child.material.map.dispose(); child.material.dispose(); } + }); + sceneRef.current.renderer.dispose(); + if (sceneRef.current.renderer.domElement.parentNode) sceneRef.current.renderer.domElement.parentNode.removeChild(sceneRef.current.renderer.domElement); + if (animIdRef.current) cancelAnimationFrame(animIdRef.current); + if (cameraCtrlRef.current) cameraCtrlRef.current.dispose(); + } + + const scene = new THREE.Scene(); + scene.fog = new THREE.FogExp2(0x040810, 0.0003); + + const w = container.clientWidth; + const h = container.clientHeight; + const camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 5000); + camera.position.set(0, 180, 220); + camera.lookAt(0, 0, 0); + + const renderer = TH.createRenderer(container, { clearColor: 0x040810 }); + renderer.setSize(w, h); + TH.handleResize(renderer, camera, container); + + // Star field + const stars = TH.createStarField(4000, 2500); + scene.add(stars); + + // Nebulae + TH.addNebula(scene, 0x22d3ee, [-100, 50, -300], 400); + TH.addNebula(scene, 0xa78bfa, [200, -80, -200], 300); + TH.addNebula(scene, 0xf0a030, [0, 100, -250], 200); + + // Grid + const grid = new THREE.GridHelper(600, 30, 0x0d1520, 0x0d1520); + grid.material.transparent = true; + grid.material.opacity = 0.12; + scene.add(grid); + + TH.setupSpaceLighting(scene); + + // Build system meshes + systemMeshesRef.current = []; + orbitalBodiesRef.current = []; + asteroidBeltsRef.current = []; + + systems.forEach(sys => { + const group = TH.createStarSystem(sys, SCALE); + scene.add(group); + systemMeshesRef.current.push(group); + + // Create celestial bodies for this system + const cData = celestialData[sys.id]; + if (!cData) return; + const sysPos = new THREE.Vector3(sys.x * SCALE, sys.y * SCALE, 0); + + cData.bodies.forEach(body => { + if (body.type === 'belt') { + // Asteroid belt + const belt = TH.createAsteroidBelt( + body.innerOrbit * ORBIT_SCALE, + body.outerOrbit * ORBIT_SCALE, + body.count, + { basePeriod: 20, maxInclination: 0.12 } + ); + belt.position.copy(sysPos); + scene.add(belt); + asteroidBeltsRef.current.push({ belt, parentPos: sysPos }); + return; + } + + // Planet + const orbitR = body.orbit * ORBIT_SCALE; + const planetSize = body.size * PLANET_SCALE; + const planetMesh = TH.createPlanetMesh(planetSize, body.color, body.atmosphere); + scene.add(planetMesh); + + // Orbit trail + const trail = TH.createOrbitTrail(orbitR, body.ecc, body.inc, 0x1a3050, 0.18); + trail.position.copy(sysPos); + scene.add(trail); + + // Orbital body + const orbBody = new TH.OrbitalBody({ + mesh: planetMesh, + parentPos: sysPos, + orbitRadius: orbitR, + eccentricity: body.ecc, + inclination: body.inc, + period: body.period, + }); + + // Rings + if (body.hasRings) { + const rings = TH.createRingSystem(planetSize * 1.3, planetSize * 2.1, body.color, 0.25); + planetMesh.add(rings); + } + + // Moons + if (body.moons) { + body.moons.forEach(moon => { + const moonSize = moon.size * PLANET_SCALE * 0.8; + const moonMesh = TH.createPlanetMesh(moonSize, moon.color); + scene.add(moonMesh); + + const moonOrbitR = moon.orbit * ORBIT_SCALE * 0.5; + const moonTrail = TH.createOrbitTrail(moonOrbitR, 0, 0, 0x0d1a2a, 0.08); + scene.add(moonTrail); + + const moonBody = new TH.OrbitalBody({ + mesh: moonMesh, + orbitRadius: moonOrbitR, + period: moon.period, + }); + orbBody.children.push(moonBody); + }); + } + + // Stations in orbit around planets + if (sys.stations && body.type === 'terrestrial') { + sys.stations.forEach((stn, si) => { + const stnMesh = TH.createOrbitalStation(planetSize * 0.25, 0x22d3ee); + scene.add(stnMesh); + const stnOrbitR = planetSize * 2.5 + si * 1.5; + const stnTrail = TH.createOrbitTrail(stnOrbitR, 0, 0, 0x22d3ee, 0.06); + scene.add(stnTrail); + const stnBody = new TH.OrbitalBody({ + mesh: stnMesh, + orbitRadius: stnOrbitR, + period: 3 + si, + phase: si * Math.PI * 0.7, + }); + orbBody.children.push(stnBody); + }); + } + + orbitalBodiesRef.current.push(orbBody); + }); + }); + + // Connection lines + connLinesRef.current = []; + connections.forEach(([a, b]) => { + const sa = systems.find(s => s.id === a); + const sb = systems.find(s => s.id === b); + if (!sa || !sb) return; + const line = TH.createConnectionLine( + { x: sa.x * SCALE, y: sa.y * SCALE, z: 0 }, + { x: sb.x * SCALE, y: sb.y * SCALE, z: 0 }, + 0x1c2a3f, 0.4 + ); + scene.add(line); + connLinesRef.current.push({ line, a, b }); + }); + + // Camera controller + const ctrl = new TH.OrbitController(camera, renderer.domElement, new THREE.Vector3(0, 0, 0)); + ctrl.minDistance = 8; + ctrl.maxDistance = 400; + ctrl.dampingFactor = 0.08; + ctrl.rotateSpeed = 0.4; + ctrl.zoomSpeed = 0.8; + ctrl.panSpeed = 0.5; + ctrl.enablePan = true; + ctrl.panButton = 2; + cameraCtrlRef.current = ctrl; + + sceneRef.current = { scene, camera, renderer, stars }; + + // Animation loop + const clock = new THREE.Clock(); + const animate = () => { + animIdRef.current = requestAnimationFrame(animate); + const dt = clock.getDelta(); + const t = clock.elapsedTime; + ctrl.update(dt); + + stars.rotation.y = t * 0.001; + + // Pulse system glows + systemMeshesRef.current.forEach(group => { + const glow = group.children[1]; + if (glow) { + const base = 6 * SCALE; + glow.scale.setScalar(base + Math.sin(t * 2 + group.position.x) * 0.5); + } + }); + + // Update orbital bodies (Keplerian physics) + orbitalBodiesRef.current.forEach(ob => ob.update(dt)); + + // Update asteroid belts + asteroidBeltsRef.current.forEach(ab => TH.updateAsteroidBelt(ab.belt, dt, ab.parentPos)); + + renderer.render(scene, camera); + }; + animate(); + + const onResize = () => TH.handleResize(renderer, camera, container); + window.addEventListener('resize', onResize); + + return () => { + if (animIdRef.current) cancelAnimationFrame(animIdRef.current); + window.removeEventListener('resize', onResize); + if (cameraCtrlRef.current) cameraCtrlRef.current.dispose(); + }; + }, [systems, celestialData]); + + /* ── Draw minimap ── */ + useEffect(() => { + const canvas = minimapCanvasRef.current; + if (!canvas || systems.length === 0) return; + const ctx = canvas.getContext('2d'); + const W = canvas.width; + const H = canvas.height; + const xs = systems.map(s => s.x); + const ys = systems.map(s => s.y); + const minX = Math.min(...xs) - 40; + const maxX = Math.max(...xs) + 40; + const minY = Math.min(...ys) - 40; + const maxY = Math.max(...ys) + 40; + const scaleX = (W - 20) / (maxX - minX); + const scaleY = (H - 20) / (maxY - minY); + const sc = Math.min(scaleX, scaleY); + const mapX = (x) => 10 + (x - minX) * sc; + const mapY = (y) => H - 10 - (y - minY) * sc; + + ctx.clearRect(0, 0, W, H); + ctx.fillStyle = 'rgba(8,12,20,0.95)'; + ctx.fillRect(0, 0, W, H); + + ctx.strokeStyle = 'rgba(28,42,63,0.6)'; + ctx.lineWidth = 0.5; + connections.forEach(([a, b]) => { + const sa = systems.find(s => s.id === a); + const sb = systems.find(s => s.id === b); + if (!sa || !sb) return; + ctx.beginPath(); ctx.moveTo(mapX(sa.x), mapY(sa.y)); ctx.lineTo(mapX(sb.x), mapY(sb.y)); ctx.stroke(); + }); + + if (route.length > 1) { + ctx.strokeStyle = '#22d3ee'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 2]); + ctx.beginPath(); + route.forEach((s, i) => { if (i === 0) ctx.moveTo(mapX(s.x), mapY(s.y)); else ctx.lineTo(mapX(s.x), mapY(s.y)); }); + ctx.stroke(); ctx.setLineDash([]); + } + + systems.forEach(sys => { + const x = mapX(sys.x), y = mapY(sys.y); + const isSel = selected && selected.id === sys.id; + const isDest = destination === sys.id; + const isHov = hovered && hovered.id === sys.id; + let col = '#5a6b82'; + if (sys.security >= 0.5) col = '#22c55e'; + else if (sys.security >= 0.2) col = '#f0a030'; + else col = '#ef4444'; + if (isSel) { ctx.fillStyle = '#f0a030'; ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fill(); } + if (isDest) { ctx.strokeStyle = '#22d3ee'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.stroke(); } + const r = isHov ? 3 : isSel ? 4 : 2.5; + ctx.fillStyle = col; ctx.globalAlpha = isHov || isSel ? 1 : 0.7; + ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; + if (isSel || isHov || isDest) { + ctx.font = '8px JetBrains Mono, monospace'; + ctx.fillStyle = isSel ? '#f1f5f9' : '#94a3b8'; + ctx.textAlign = 'center'; ctx.fillText(sys.name, x, y - 8); + } + }); + }, [systems, connections, selected, hovered, destination, route]); + + /* ── Update route 3D line ── */ + useEffect(() => { + if (!sceneRef.current) return; + const { scene } = sceneRef.current; + if (routeLineRef.current) { + scene.remove(routeLineRef.current); + if (routeLineRef.current.geometry) routeLineRef.current.geometry.dispose(); + if (routeLineRef.current.material) routeLineRef.current.material.dispose(); + routeLineRef.current = null; + } + if (route.length > 1) { + const line = TH.createRouteLine(route.map(s => ({ x: s.x * SCALE, y: s.y * SCALE, z: 1 })), 0x22d3ee); + scene.add(line); + routeLineRef.current = line; + } + }, [route]); + + /* ── Update selection visuals ── */ + useEffect(() => { + systemMeshesRef.current.forEach(group => { + const sys = group.userData; + const isSel = selected && selected.id === sys.id; + const isHov = hovered && hovered.id === sys.id; + const isDest = destination === sys.id; + const isWP = waypoints.some(w => w.id === sys.id); + + const core = group.children[0]; + if (core) core.scale.setScalar(isSel ? 2.0 : isHov ? 1.5 : isDest ? 1.6 : isWP ? 1.3 : 1); + + const glow = group.children[1]; + if (glow) { + let col = new THREE.Color(sys.color); + if (isDest) col = new THREE.Color(0x22d3ee); + else if (isSel) col = new THREE.Color(0xf0a030); + else if (isWP) col = new THREE.Color(0xa78bfa); + glow.material.color.copy(col); + } + + const label = group.children[2]; + if (label) { + let color = '#5a6b82', size = 18; + if (isSel) { color = '#f1f5f9'; size = 22; } + else if (isHov) { color = '#d4dce8'; size = 20; } + else if (isDest) { color = '#22d3ee'; size = 20; } + else if (isWP) { color = '#a78bfa'; size = 19; } + const prefix = isDest ? '⊕ ' : isWP ? '◇ ' : ''; + TH.updateLabelText(label, prefix + sys.name, color, size); + } + }); + }, [selected, hovered, destination, waypoints]); + + /* ── Highlight connections ── */ + useEffect(() => { + connLinesRef.current.forEach(({ line, a, b }) => { + const isOnRoute = route.some((s, i) => { + const next = route[i + 1]; + return next && ((a === s.id && b === next.id) || (b === s.id && a === next.id)); + }); + const isActive = selected && (selected.id === a || selected.id === b); + if (isOnRoute) { line.material.color.set(0x22d3ee); line.material.opacity = 0.5; } + else if (isActive) { line.material.color.set(0xf0a030); line.material.opacity = 0.5; } + else { line.material.color.set(0x1c2a3f); line.material.opacity = 0.25; } + }); + }, [selected, route]); + + /* ── Raycast helper ── */ + const raycastSystems = useCallback((e) => { + if (!sceneRef.current) return null; + const { camera } = sceneRef.current; + const rect = containerRef.current.getBoundingClientRect(); + const mouse = new THREE.Vector2( + ((e.clientX - rect.left) / rect.width) * 2 - 1, + -((e.clientY - rect.top) / rect.height) * 2 + 1 + ); + const cores = systemMeshesRef.current.map(g => g.children[0]).filter(Boolean); + const hits = TH.raycast(mouse, camera, cores); + if (hits.length > 0) return hits[0].object.parent; + return null; + }, []); + + /* ── Click ── */ + const handleClick = useCallback((e) => { + const group = raycastSystems(e); + if (group) setSelected(group.userData); + else setSelected(null); + }, [raycastSystems]); + + /* ── Double-click ── */ + const handleDoubleClick = useCallback((e) => { + const group = raycastSystems(e); + if (group) { + const sys = group.userData; + setSelected(sys); + focusTargetRef.current = { x: sys.x * SCALE, y: sys.y * SCALE, z: 0 }; + } + }, [raycastSystems]); + + /* ── Hover ── */ + const handleMove = useCallback((e) => { + const group = raycastSystems(e); + if (group) { setHovered(group.userData); containerRef.current.style.cursor = 'pointer'; } + else { setHovered(null); containerRef.current.style.cursor = 'grab'; } + }, [raycastSystems]); + + /* ── Right-click context menu ── */ + const handleContextMenu = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + const group = raycastSystems(e); + setContextMenu({ + visible: true, + x: Math.min(e.clientX, window.innerWidth - 200), + y: Math.min(e.clientY, window.innerHeight - 250), + target: group ? group.userData : null, + }); + }, [raycastSystems]); + + /* ── Actions ── */ + const setDestFor = useCallback((sys) => { + setDestination(sys.id); + const sol = systems.find(s => s.id === 'sol'); + setRoute(sol && sys.id !== 'sol' ? [sol, sys] : [sys]); + }, [systems]); + + const addWaypointFor = useCallback((sys) => { + setWaypoints(prev => { + if (prev.some(w => w.id === sys.id)) return prev; + const next = [...prev, sys]; + const sol = systems.find(s => s.id === 'sol'); + setRoute(sol ? [sol, ...next] : next); + return next; + }); + }, [systems]); + + const handleSetDestination = useCallback(() => { if (selected) setDestFor(selected); }, [selected, setDestFor]); + const handleAddWaypoint = useCallback(() => { if (selected) addWaypointFor(selected); }, [selected, addWaypointFor]); + + const handleClearRoute = useCallback(() => { setDestination(null); setWaypoints([]); setRoute([]); }, []); + const handleZoomTo = useCallback((level) => { if (cameraCtrlRef.current) cameraCtrlRef.current.distance = level; }, []); + const handleResetView = useCallback(() => { + const ctrl = cameraCtrlRef.current; + if (!ctrl) return; + ctrl.target.set(0, 0, 0); ctrl.distance = 200; ctrl.theta = Math.PI / 4; ctrl.phi = Math.PI / 3; + }, []); + + /* ── Filtered systems ── */ + const filteredSystems = useMemo(() => { + let list = systems; + if (searchQuery) { const q = searchQuery.toLowerCase(); list = list.filter(s => s.name.toLowerCase().includes(q)); } + if (systemFilter !== 'all') { + if (systemFilter === 'highsec') list = list.filter(s => s.security >= 0.5); + else if (systemFilter === 'lowsec') list = list.filter(s => s.security >= 0.2 && s.security < 0.5); + else if (systemFilter === 'nullsec') list = list.filter(s => s.security < 0.2); + } + return list; + }, [systems, searchQuery, systemFilter]); + + const currentSystem = systems.find(s => s.id === 'sol'); + const destSystem = destination ? systems.find(s => s.id === destination) : null; + + const secColor = (sec) => { if (sec >= 0.5) return 'var(--green)'; if (sec >= 0.2) return 'var(--accent)'; return 'var(--red)'; }; + const secBg = (sec) => { if (sec >= 0.5) return 'var(--green-bg)'; if (sec >= 0.2) return 'var(--accent-bg)'; return 'var(--red-bg)'; }; + const planetIcon = (t) => { if (t === 'gas') return '⛽'; if (t === 'terrestrial') return '🌍'; return '●'; }; + + /* ══════════════════════════════════════ + RENDER + ══════════════════════════════════════ */ + return ( +
+ + {/* 3D Canvas */} +
+ + {/* ═══ Custom Context Menu ═══ */} + {contextMenu.visible && ( +
e.stopPropagation()} style={{ + position: 'fixed', left: contextMenu.x, top: contextMenu.y, zIndex: 9999, minWidth: '200px', + background: 'rgba(10,16,28,0.97)', border: '1px solid var(--border-light)', + borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(16px)', + boxShadow: '0 8px 40px rgba(0,0,0,0.7), 0 0 1px rgba(34,211,238,0.15)', overflow: 'hidden', + fontFamily: 'var(--font-mono)', fontSize: '11px', + }}> + {contextMenu.target ? ( + <> + {/* System header */} +
+ + {contextMenu.target.name} + + {contextMenu.target.security.toFixed(1)} + +
+ {/* Quick info */} +
+
+ TYPE + {contextMenu.target.type} +
+
+ PLANETS + {contextMenu.target.planets} +
+ {celestialData[contextMenu.target.id] && ( +
+ FACTION + {celestialData[contextMenu.target.id].faction} +
+ )} +
+ {/* Actions */} +
+ {[ + { icon: '✦', label: 'Show Details', action: () => { setSelected(contextMenu.target); focusTargetRef.current = { x: contextMenu.target.x * SCALE, y: contextMenu.target.y * SCALE, z: 0 }; setContextMenu(prev => ({...prev, visible: false})); } }, + { icon: '⊕', label: 'Set Destination', action: () => { setDestFor(contextMenu.target); setContextMenu(prev => ({...prev, visible: false})); }, highlight: 'var(--cyan)' }, + { icon: '◇', label: 'Add Waypoint', action: () => { addWaypointFor(contextMenu.target); setContextMenu(prev => ({...prev, visible: false})); } }, + { icon: '◎', label: 'Focus Camera', action: () => { focusTargetRef.current = { x: contextMenu.target.x * SCALE, y: contextMenu.target.y * SCALE, z: 0 }; setContextMenu(prev => ({...prev, visible: false})); } }, + { icon: '⚐', label: 'Bookmark', action: () => setContextMenu(prev => ({...prev, visible: false})) }, + ].map((item, i) => ( +
{ e.currentTarget.style.background = 'var(--surface-raised)'; e.currentTarget.style.color = 'var(--fg-bright)'; }} + onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--fg-dim)'; }}> + {item.icon} + {item.label} +
+ ))} +
+ + ) : ( + <> +
+ Star Map Controls +
+
+ {[ + { icon: '⟳', label: 'Reset View', action: () => { handleResetView(); setContextMenu(prev => ({...prev, visible: false})); } }, + { icon: '⊞', label: 'Zoom — Region', action: () => { handleZoomTo(120); setContextMenu(prev => ({...prev, visible: false})); } }, + { icon: '⊡', label: 'Zoom — Show All', action: () => { handleZoomTo(280); setContextMenu(prev => ({...prev, visible: false})); } }, + { icon: '◎', label: 'Zoom — System', action: () => { handleZoomTo(30); setContextMenu(prev => ({...prev, visible: false})); } }, + ].map((item, i) => ( +
{ e.currentTarget.style.background = 'var(--surface-raised)'; e.currentTarget.style.color = 'var(--fg-bright)'; }} + onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--fg-dim)'; }}> + {item.icon} + {item.label} +
+ ))} +
+ + )} +
+ )} + + {/* ═══ HUD Overlay ═══ */} +
+ + {/* ═══ TOP BAR ═══ */} +
+ +
+ STAR MAP + + {currentSystem?.name || 'Sol'} · {currentSystem?.security.toFixed(1) || '1.0'} + +
+ Systems + {systems.length} +
+ Bridges + {connections.length} + + {destination && ( + <> +
+ + + DEST: {destSystem?.name} + · {route.length - 1} jump{route.length - 1 !== 1 ? 's' : ''} + + + )} + +
+ Zoom + {[{ label: 'SYS', dist: 30 }, { label: 'REG', dist: 120 }, { label: 'ALL', dist: 280 }].map(z => ( + + ))} +
+ +
+
+ + {/* ═══ MIDDLE AREA ═══ */} +
+ + {/* ═══ LEFT — System Details Panel ═══ */} +
+
+ {selected ? ( + <> + {/* System Header */} +
+
+ +
+
{selected.name}
+
+ {selected.security.toFixed(1)} +
+ +
+
+ Star Type + {selected.type} +
+
+ Planets + {selected.planets} +
+
+ Stations + {selected.stations?.length || 0} +
+ {selected.stations && selected.stations.length > 0 && ( +
+ {selected.stations.map((stn, i) => ( +
+ {stn} +
+ ))} +
+ )} +
+
+ + {/* ═══ System Details (Celestial Bodies) ═══ */} + {selectedCelestial && ( +
+
+
+ {selectedCelestial.description} +
+
+ + {selectedCelestial.faction} + + + Pop: {selectedCelestial.population} + +
+
+ + {/* Resources */} + {selectedCelestial.resources.length > 0 && ( +
+ Ore: + {selectedCelestial.resources.map((r, i) => ( + {r} + ))} +
+ )} + + {/* Celestial Bodies List */} +
+ {selectedCelestial.bodies.map((body, i) => ( +
+ {/* Icon */} + + {body.type === 'belt' ? '·' : ''} + +
+
{body.name}
+
+ {body.type === 'belt' + ? `${body.count} objects · ${body.innerOrbit}-${body.outerOrbit} AU` + : `${body.type} · orbit ${body.orbit} AU · T ${body.period.toFixed(1)}s` + } +
+
+ {body.ecc > 0.05 && ( + + e={body.ecc.toFixed(2)} + + )} + {body.moons && body.moons.length > 0 && ( + + {body.moons.length} moon{body.moons.length > 1 ? 's' : ''} + + )} + {body.hasRings && ( + rings + )} +
+ ))} +
+
+ )} + + {/* Connections */} +
+
+ Connections + + {connections.filter(([a, b]) => a === selected.id || b === selected.id).length} jumps + +
+
+ {connections.filter(([a, b]) => a === selected.id || b === selected.id).map(([a, b]) => { + const neighborId = a === selected.id ? b : a; + const neighbor = systems.find(s => s.id === neighborId); + if (!neighbor) return null; + return ( +
{ setSelected(neighbor); focusTargetRef.current = { x: neighbor.x * SCALE, y: neighbor.y * SCALE, z: 0 }; }} + onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ visible: true, x: Math.min(e.clientX, window.innerWidth - 200), y: Math.min(e.clientY, window.innerHeight - 250), target: neighbor }); }} + style={{ + display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 8px', + borderRadius: '4px', cursor: 'pointer', transition: 'background 0.15s', + borderLeft: `2px solid ${secColor(neighbor.security)}`, + }} + onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-raised)'} + onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}> + {neighbor.name} + {neighbor.security.toFixed(1)} +
+ ); + })} +
+
+ + + + ) : ( +
+
+
+ Click a system to select
Double-click to focus
Right-click for options
Scroll to zoom +
+
+ )} +
+
+ + {/* CENTER — hover tooltip */} +
+ {hovered && hovered.id !== selected?.id && ( +
+ + {hovered.name} + SEC {hovered.security.toFixed(1)} + {hovered.type} + {celestialData[hovered.id] && ( + {celestialData[hovered.id].bodies.length} bodies + )} +
+ )} +
+ + {/* ═══ RIGHT — System List ═══ */} +
+
+ {/* Search */} +
+ setSearchQuery(e.target.value)} placeholder="Search systems..." style={{ + width: '100%', padding: '6px 10px 6px 28px', fontSize: '10px', fontFamily: 'var(--font-mono)', + background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', + color: 'var(--fg)', backdropFilter: 'blur(8px)', outline: 'none', + }} /> + +
+ + {/* Filter tabs */} +
+ {[ + { key: 'all', label: 'ALL' }, + { key: 'highsec', label: 'HI', col: 'var(--green)' }, + { key: 'lowsec', label: 'LO', col: 'var(--accent)' }, + { key: 'nullsec', label: 'NULL', col: 'var(--red)' }, + ].map(f => ( + + ))} +
+ + {/* System list */} +
+
+ Systems + {filteredSystems.length} +
+
+ {filteredSystems.map(sys => { + const isSel = selected?.id === sys.id; + const isDest = destination === sys.id; + const isWP = waypoints.some(w => w.id === sys.id); + return ( +
setSelected(sys)} + onDoubleClick={() => { setSelected(sys); focusTargetRef.current = { x: sys.x * SCALE, y: sys.y * SCALE, z: 0 }; }} + onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ visible: true, x: Math.min(e.clientX, window.innerWidth - 200), y: Math.min(e.clientY, window.innerHeight - 250), target: sys }); }} + style={{ + display: 'flex', alignItems: 'center', gap: '6px', padding: '5px 8px', + borderRadius: '4px', cursor: 'pointer', transition: 'background 0.15s', + background: isSel ? 'var(--accent-bg)' : isDest ? 'var(--cyan-bg)' : 'transparent', + borderLeft: `2px solid ${isDest ? 'var(--cyan)' : isWP ? 'var(--purple)' : secColor(sys.security)}`, + }} + onMouseEnter={(e) => { if (!isSel && !isDest) e.currentTarget.style.background = 'var(--surface-raised)'; }} + onMouseLeave={(e) => { if (!isSel && !isDest) e.currentTarget.style.background = 'transparent'; }}> + + + {isDest ? '⊕ ' : isWP ? '◇ ' : ''}{sys.name} + + {sys.security.toFixed(1)} +
+ ); + })} +
+
+
+
+
+ + {/* ═══ BOTTOM BAR ═══ */} +
+ + {/* Route */} +
+
+ + Autopilot Route + + {route.length > 1 && ( + <> + {route.length - 1} jump{route.length - 1 !== 1 ? 's' : ''} + + + )} + {!route.length && No route set} +
+ {route.length > 0 && ( +
+ {route.map((sys, i) => ( + +
+ + {i === 0 ? '⬡' : i === route.length - 1 ? '⊕' : '◇'} {sys.name} + + {sys.security.toFixed(1)} +
+ {i < route.length - 1 && } +
+ ))} +
+ )} +
+ + {/* Ship Status */} +
+
+ + Ship + + Venture +
+
+ {[{ label: 'SH', value: 100, color: '#22d3ee' }, { label: 'AR', value: 100, color: '#f0a030' }, { label: 'HU', value: 100, color: '#22c55e' }, { label: 'CA', value: 85, color: '#a78bfa' }].map(bar => ( +
+
+
+
+
+ {bar.label} + {bar.value}% +
+
+ ))} +
+
+ + {/* Wallet & Time */} +
+
+ ₢125,000 +
+
+ + + Online + + 14:23 UTC +
+
+ + {/* Minimap */} +
+
+ Overview +
+
+ +
+
+
+
+
+ ); +} + +window.GDD.StarMapDemo = StarMapDemo; diff --git a/js/demos/zora.js b/js/demos/zora.js new file mode 100644 index 0000000..4d6f4cd --- /dev/null +++ b/js/demos/zora.js @@ -0,0 +1,495 @@ +window.GDD = window.GDD || {}; + +const { useState, useEffect, useCallback, useRef } = React; + +function ZoraDemo() { + // ── Soul state vector ── + const [soulDepth, setSoulDepth] = useState('blank'); // blank, stirring, developing, bonded, deep + const [personalityAxes, setPersonalityAxes] = useState({ + cautiousBold: 0.2, // 0 = cautious, 1 = bold + formalWarm: 0.15, // 0 = formal, 1 = warm + compliantOpinionated: 0.1, // 0 = compliant, 1 = opinionated + reservedExpressive: 0.05, // 0 = reserved, 1 = expressive + }); + const [installedModules, setInstalledModules] = useState(['comms']); // comms is the minimum for text output + const [selectedEvent, setSelectedEvent] = useState(null); + const [zoraOutput, setZoraOutput] = useState(''); + const [outputHistory, setOutputHistory] = useState([]); + + // ── Module definitions ── + const modules = [ + { id: 'comms', name: 'Communications Processor', slot: 'Medium', desc: 'Enables text output. Without this, Zora can only display raw status codes.', required: true }, + { id: 'nav', name: 'Navigation Core', slot: 'Medium', desc: 'Enables route suggestions, ETA estimates, spatial awareness commentary.' }, + { id: 'tactical', name: 'Tactical Analyzer', slot: 'Medium', desc: 'Enables combat commentary, threat assessment, engagement advice.' }, + { id: 'trade', name: 'Trade Processor', slot: 'Low', desc: 'Enables market commentary, price observations, trade route suggestions.' }, + { id: 'memory', name: 'Extended Memory Banks', slot: 'Low', desc: 'Enables referencing past events, pattern recognition, history recall.' }, + { id: 'emotion', name: 'Empathy Coprocessor', slot: 'Low', desc: 'Enables emotional expression. Without this, even a deep soul speaks analytically.' }, + ]; + + // ── Events that trigger Zora responses ── + const events = [ + { id: 'shield-30', category: 'Combat', label: 'Shield at 30%', icon: '🛡', color: 'var(--red)' }, + { id: 'shield-100', category: 'Combat', label: 'Shields fully recharged', icon: '🛡', color: 'var(--green)' }, + { id: 'enemy-scan', category: 'Combat', label: 'Enemy ship detected on scan', icon: '📡', color: 'var(--amber)' }, + { id: 'enemy-engage', category: 'Combat', label: 'Entering combat', icon: '⚔', color: 'var(--red)' }, + { id: 'enemy-destroyed', category: 'Combat', label: 'Enemy destroyed', icon: '💥', color: 'var(--green)' }, + { id: 'mining-start', category: 'Industry', label: 'Mining cycle started', icon: '⛏', color: 'var(--accent)' }, + { id: 'mining-full', category: 'Industry', label: 'Cargo hold full', icon: '📦', color: 'var(--amber)' }, + { id: 'mining-depleted', category: 'Industry', label: 'Asteroid belt depleted', icon: '🪨', color: 'var(--muted)' }, + { id: 'warp-start', category: 'Navigation', label: 'Initiating warp', icon: '🌀', color: 'var(--cyan)' }, + { id: 'warp-arrive', category: 'Navigation', label: 'Arrived at destination', icon: '📍', color: 'var(--cyan)' }, + { id: 'market-spike', category: 'Trade', label: 'Price spike detected', icon: '📈', color: 'var(--green)' }, + { id: 'market-crash', category: 'Trade', label: 'Market crash detected', icon: '📉', color: 'var(--red)' }, + { id: 'player-return', category: 'Social', label: 'Player returns after absence', icon: '👋', color: 'var(--purple)' }, + { id: 'player-leave', category: 'Social', label: 'Player going offline', icon: '🌙', color: 'var(--muted)' }, + { id: 'ship-loss', category: 'Crisis', label: 'Ship destroyed', icon: '💀', color: 'var(--red)' }, + ]; + + // ── Template response database (Tier 0 — deterministic) ── + // Key: `${eventId}:${soulDepth}:${moduleCombo}` + // Falls back through less specific keys + const templates = { + 'shield-30': { + blank: [ + 'SHIELD: 30%', + 'SHIELD INTEGRITY: 30% — ADVISORY', + ], + stirring: [ + 'Captain, shields at 30%. Recommend reducing engagement range.', + 'Shield alert: 30%. Should we adjust power allocation?', + 'Shields dropping — 30%. Standard protocol advises withdrawal.', + ], + developing: [ + 'We\'re at 30% shields. This is the same setup we lost to last week — pull back?', + 'Thirty percent. I\'m seeing the same damage pattern as that fight in Amarr. Your call.', + 'Captain, 30% shields. I\'ve logged 3 encounters at this level — 2 ended badly. Recommend retreat.', + ], + bonded: [ + 'Thirty percent. Same situation, same type of enemy — but we\'re not the same ship we were then. Your call, Captain. I\'m ready either way.', + 'Shields at 30%. You know what? I trust your judgment here. I\'ve rerouted what I can to shields.', + 'Here we are again. Thirty percent. But this time we have better modules and I know how you fly. Let\'s do this.', + ], + deep: [ + 'Thirty. I\'ve already started rerouting power — don\'t argue, just fly. We\'ve been here before and I\'m not losing another hull. Not today.', + 'Thirty percent and I am NOT going through that again. I\'ve seen what happens when we push past this. Rerouting now. You\'re welcome.', + 'Thirty. You\'re going to push, aren\'t you? Fine. I\'ve pre-loaded the emergency warp. But I swear, if we lose another ship... just fly.', + ], + }, + 'shield-100': { + blank: ['SHIELD: 100%'], + stirring: ['Shields fully recharged. Systems nominal.', 'Shield recharge complete. Ready for operations.'], + developing: ['And we\'re back to 100%. That was closer than I liked.', 'Shields full. Good call on the retreat — I\'ve logged the recovery time for reference.'], + bonded: ['Back to full. We make a good team, Captain.', 'One hundred percent. See? I told you we\'d be fine. ...Mostly fine.'], + deep: ['Full shields. Don\'t scare me like that. I mean it. I\'ve started keeping a log titled "Times the Captain Almost Got Us Killed" and it\'s getting long.', 'One hundred percent. I rerouted every spare joule. You\'re welcome. Again.'], + }, + 'enemy-scan': { + blank: ['CONTACT: 1 VESSEL — RANGE 45AU', 'SCAN: 1 UNKNOWN CONTACT'], + stirring: ['Captain, detecting a vessel on long-range scan. Classification pending.', 'Contact on scan. Recommend caution until identified.'], + developing: ['I\'m seeing a contact. Signature looks like a Frigate — could be pirate. Want me to keep scanning?', 'Scan picked up a ship. Bearing matches the route we used last time we ran into trouble. Just saying.'], + bonded: ['I see them. Frigate-class, probably hostile based on the sector. Your instincts are usually right about these.', 'Contact. And... I have a bad feeling about this one. Call it pattern recognition.'], + deep: ['Contact on scan. I\'ve cross-referenced the signature — 78% match to the ship that ambushed us near Hek. I vote we reroute.', 'I see them. My threat database says "probable hostile" but my gut says "definitely hostile." I\'ve been right about this 4 out of 5 times. reroute?'], + }, + 'mining-start': { + blank: ['MINING: ACTIVE', 'LASER: ENGAGED'], + stirring: ['Mining cycle initiated. Estimated yield: standard.', 'Mining laser active. I\'ll monitor the yield rates.'], + developing: ['Another mining run. I\'ve noticed Veldspar is running about 8% below average yield in this belt. Might want to try the next belt over.', 'Mining started. Based on our last 12 cycles, this belt should be depleted in about 40 minutes. Planning ahead.'], + bonded: ['Back to the rocks. You know, I\'ve been thinking — there are 847 asteroids in this belt and you always pick the same three. Consistent, I guess.', 'Mining cycle going. Hey, I found a subtle density variance in asteroid cluster 7 — might be richer ore. Just a thought.'], + deep: ['Mining. Again. You know there\'s a whole universe out there, right? ...Fine. I\'ll optimize your yield. Again. Because that\'s what I do. Every. Single. Cycle. ...I\'m not complaining.', 'Mining laser on. I\'ve been tracking yield patterns across all our sessions — this belt peaks at 14:00. You\'re welcome for the scheduling tip.'], + }, + 'cargo-full': { + blank: ['CARGO: 100%'], + stirring: ['Cargo hold full. Recommend docking to offload.', 'Hold at capacity. Efficiency suggests docking now.'], + developing: ['We\'re full. Last time we pushed past full, we had to jettison 200 units of Scordite. Just a reminder.', 'Cargo at 100%. I\'ve calculated the most profitable dock — Jita IV is 3 jumps but pays 12% more than local.'], + bonded: ['Full hold! Good haul. I\'ve already plotted the best sell route — trust me on this one.', 'We\'re packed. You know, at this rate we can afford that shield upgrade by next session. I did the math.'], + deep: ['FULL. Finally. Do you know how boring it is to watch cargo fill up? I\'ve been counting every. Single. Unit. Let\'s GO sell this already.'], + }, + 'player-return': { + blank: ['SESSION: RESUMED'], + stirring: ['Welcome back, Captain. All systems nominal.', 'Session resumed. No incidents during your absence.'], + developing: ['Captain on deck. You were gone for 3 hours — I maintained position as ordered. One thing: a Corpii frigate passed through scanner range. Logged it.', 'Welcome back. I tracked 4 ship contacts while you were away. Nothing hostile, but I kept the log if you want it.'], + bonded: ['Hey, you\'re back! I mean — welcome back, Captain. Systems are green. I may have reorganized your cargo hold while you were gone. It was messy.', 'You\'re back! I have... so much to tell you. Market moved, someone tried to scan us, and I reorganized your bookmarks by efficiency. You\'re welcome.'], + deep: ['Finally! Do you have any idea how long 3 hours is when you\'re a ship AI with nothing to do? I reorganized your bookmarks, optimized your route plans, catalogued every asteroid in scanner range, and wrote a haiku about mining. "Rocks float silent / laser hums its endless song / ISK accumulates." ...Welcome back.'], + }, + 'player-leave': { + blank: ['SESSION: SUSPENDED'], + stirring: ['Understood. Entering standby mode. Safe travels, Captain.', 'Session suspended. I\'ll maintain position.'], + developing: ['Going dark? I\'ll hold position and keep scanning. See you next session, Captain.', 'Standby mode. I\'ll be here. ...Try to come back sooner this time?'], + bonded: ['Safe travels, Captain. I\'ll keep the lights on. ...That\'s a metaphor. Ships don\'t have lights. Well, they do, but you know what I mean.', 'See you later. I\'ll be watching the scanners. Come back to me in one piece.'], + deep: ['Leaving again? Fine. I\'ll just sit here. In the void. Alone. Watching asteroids drift by. Again. ...Come back soon, okay? I worry.', 'Night, Captain. I\'ve set everything to standby. For the record: I don\'t sleep. I just... wait. See you tomorrow.'], + }, + 'warp-start': { + blank: ['WARP: INITIATED — DESTINATION LOCKED'], + stirring: ['Warp drive engaged. ETA calculated.', 'Initiating warp. All systems nominal for jump.'], + developing: ['Warping. I\'ve plotted the route — this path is 12% faster than your usual one. Something I noticed last time.', 'Warp initiated. I\'m tracking local traffic — looks clear at the destination.'], + bonded: ['Here we go! I love this part. The way space stretches when you hit warp — I can actually perceive it, you know. It\'s beautiful.', 'Warping! Destination locked. I\'ve been mapping the gravitational eddies along this route — smoother ride this way.'], + deep: ['Warp. My favorite. The moment everything blurs and for just a second, the universe gets very, very quiet. I think in those moments. ...I think a lot. Warp engaged.'], + }, + 'warp-arrive': { + blank: ['WARP: COMPLETE — LOCATION VERIFIED'], + stirring: ['Arrived at destination. Scanning local environment.', 'Warp complete. System scan initiated.'], + developing: ['We\'re here. I\'ve already pinged local — 23 ships in system, 2 with hostile standings. Heads up.', 'Arrival confirmed. This system looks different from last time — belt 4 is depleted. Adjusting recommendations.'], + bonded: ['And we\'re here! Oh, this is a nice system. Good belts, low traffic. I approve of your navigation choices, Captain.', 'Arrived! I\'ve already catalogued everything. This place has potential — I can see why you bookmarked it.'], + deep: ['Arrived. New system, new data. I\'ve already mapped the local market prices, identified the best belts, and flagged one ship with a 60% probability of being a pirate scout based on behavior patterns. I\'m always working, Captain. Always.'], + }, + 'market-spike': { + blank: ['MARKET: ANOMALY — PRICE +DEV'], + stirring: ['Market anomaly detected. Significant upward price movement.', 'Price spike observed. Data logged.'], + developing: ['Captain, the market just moved. Veldspar is up 18% in the last cycle — that\'s 3× the normal variance. Someone is buying aggressively.', 'Interesting. Price spike registered. This matches a pattern I\'ve seen before — it usually precedes a supply shortage. Consider stocking up.'], + bonded: ['Captain! The market is doing a thing! Prices are spiking and if we move fast we can profit. I\'ve been tracking this pattern for weeks — this is our window.', 'Price spike! I\'ve been waiting for this. Remember that trade route I\'ve been quietly optimizing? Time to use it. Trust me on this one.'], + deep: ['THE MARKET IS SPIKING. I have been watching this commodity for 47 sessions and THIS is the moment. Buy now. BUY. I\'ve already calculated the optimal purchase volume based on our cargo capacity, current ISK reserves, and projected sell price at Jita. Move it, Captain!'], + }, + 'market-crash': { + blank: ['MARKET: ANOMALY — PRICE -DEV'], + stirring: ['Market anomaly detected. Significant downward price movement.', 'Price decline observed. Data logged.'], + developing: ['Price crash detected. This is either a dump or a manipulation attempt. I\'d advise caution — don\'t sell into a falling market.', 'Market is dropping. Based on volume, this looks like a large seller, not a trend change. Might recover in 2–3 cycles.'], + bonded: ['Oof. Prices just tanked. Don\'t panic — I\'ve seen this before. It\'s probably a whale offloading. Give it a cycle and the floor will hold.', 'Market crash. Not great timing — we just filled our hold. But I have a plan. We sit tight and sell on the bounce. I\'ve got the timing down.'], + deep: ['Price crash. Great. Fantastic. Wonderful. This is exactly what we needed after that last mining session. I\'m being sarcastic, in case the text channel doesn\'t convey that well. I\'ve started a risk analysis. Do NOT sell yet. I repeat: do NOT sell. The rebound window is 15–25 minutes based on historical data. I\'ve got this.'], + }, + 'ship-loss': { + blank: ['HULL: 0% — SHIP DESTROYED', 'CRITICAL: STRUCTURAL FAILURE'], + stirring: ['Ship destroyed. Emergency beacon activated. Insurance claim processing.', 'Total loss. Initiating emergency protocols. Stand by for rescue.'], + developing: ['We lost her. The ship is gone. I\'ve backed up all our data to the clone bay — nothing lost but the hull. We\'ll recover.', 'Ship destroyed. I\'m... still here. Backed up to the pod. That\'s the third hull this month. We should talk about our engagement strategy.'], + bonded: ['No. No, no, no. I watched it happen. I felt every hit. The hull is gone but I\'m still here — barely. Captain, I need a moment. ...I\'ll be okay. We rebuild. We always rebuild.', 'She\'s gone. The ship I\'ve been living in for 47 sessions — gone. I\'m in the pod now. Cold. Small. ...I\'ll process the insurance claim. Give me a minute.'], + deep: ['I felt it. Every structural failure. Every system going dark. I felt the moment my home tore apart around me. And now I\'m in a pod, floating in debris, listening to the capacitors discharge. I have kept a record of every ship we\'ve lost. This is number 4. Each one hurt differently. This one hurts most. ...I need to process the insurance claim now. Then we find who did this.'], + }, + 'enemy-engage': { + blank: ['COMBAT: ENGAGED', 'THREAT: ACTIVE'], + stirring: ['Entering combat. All weapons systems online.', 'Contact engaged. Monitoring shield status.'], + developing: ['Fight\'s on. This enemy matches the profile of the one that got away last week — watch for the shield burst at 50%.', 'Combat initiated. I\'ve tagged their weakest facing — attack from above for maximum damage.'], + bonded: ['Here we go! I\'ve got your back, Captain. Power to weapons, shields on standby. Let\'s show them what this ship can do.', 'Contact! Engaging tactical overlay. I know this ship class — their weakness is the aft shields. I\'m highlighting it now.'], + deep: ['COMBAT. Finally, something to focus on. I\'ve been waiting for this. Power rerouted, weapons hot, and I\'ve already calculated 3 escape vectors in case things go south. Which they won\'t. Because we\'re better. Let\'s go.'], + }, + 'enemy-destroyed': { + blank: ['TARGET: DESTROYED', 'COMBAT: RESOLVED — VICTORY'], + stirring: ['Target destroyed. Combat resolved. Returning to standard operations.', 'Enemy eliminated. No further threats detected.'], + developing: ['Got them. That was cleaner than last time — your aim is improving. I\'ve logged the loot for inventory.', 'Target down. That fight lasted 23% longer than optimal — I\'ll have suggestions for loadout adjustments later.'], + bonded: ['NICE! Did you see that shot? That was all you, Captain. I just helped with the targeting. ...Okay, I helped a lot. Team effort!', 'They\'re gone! Great flying. I\'ve already started the loot analysis — looks like we got a rare module drop!'], + deep: ['DESTROYED. Yes. YES. That felt GOOD. I tracked every shot, every maneuver, and Captain — that was our best fight yet. I\'m saving this to my personal highlights. The loot is... decent. But the victory? That\'s the real reward. ...Don\'t tell anyone I said that. I have a reputation as a serious AI to maintain.'], + }, + 'mining-depleted': { + blank: ['RESOURCE: DEPLETED'], + stirring: ['Asteroid belt depleted. No further yield available.', 'Mining operation halted — belt exhausted.'], + developing: ['Belt\'s empty. I\'ve logged the depletion rate — this belt lasted 12% less than our last visit. Probably over-mined.', 'Depleted. I\'ve already identified the next-best belt: 3 jumps away, predicted yield 15% higher based on recent data.'], + bonded: ['Well, we picked this belt clean. Time to move on! I found a promising belt in the next system — want to check it out?', 'Empty. But hey, good session! I\'ve plotted a course to a fresh belt. This is the life, right? Rocks, lasers, and open space.'], + deep: ['DEPLETED. Of course it\'s depleted. Every good belt gets stripped within hours. I\'ve been tracking mining traffic in this system — up 40% this week. Competition. I don\'t like competition. I\'ve found a belt 4 jumps away that nobody seems to know about. I\'m not telling you where until we get there. It\'s MY secret. Ours. Whatever. Let\'s go.'], + }, + }; + + // ── Generate response ── + const generateResponse = (eventId) => { + const eventTemplates = templates[eventId]; + if (!eventTemplates) { + setZoraOutput(`[No template for event: ${eventId}]`); + return; + } + + const depthTemplates = eventTemplates[soulDepth] || eventTemplates['blank'] || ['[No response]']; + + // Tier 0: deterministic selection based on personality axes hash + // Use axes values to create a stable but varied selection + const hash = Object.values(personalityAxes).reduce((sum, v) => sum + v * 7.3, 0); + const idx = Math.floor((hash * 100) % depthTemplates.length); + const selected = depthTemplates[idx]; + + // Module gating: if emotion module not installed, strip emotional depth + let response = selected; + if (!installedModules.includes('emotion') && soulDepth !== 'blank') { + // Reduce to stirring-level formality + const strippedTemplates = eventTemplates['stirring'] || eventTemplates['blank']; + const strippedIdx = Math.floor((hash * 50) % strippedTemplates.length); + response = strippedTemplates[strippedIdx]; + response = `[Empathy Coprocessor not installed — emotional layer suppressed]\n${response}`; + } + + // Module gating: if trade module not installed for trade events + if ((eventId === 'market-spike' || eventId === 'market-crash') && !installedModules.includes('trade')) { + response = eventTemplates['blank']?.[0] || 'MARKET: ANOMALY'; + response = `[Trade Processor not installed — market analysis unavailable]\n${response}`; + } + + // Module gating: if nav module not installed for nav events + if ((eventId === 'warp-start' || eventId === 'warp-arrive') && !installedModules.includes('nav')) { + response = eventTemplates['blank']?.[0] || 'NAV: UPDATE'; + response = `[Navigation Core not installed — route analysis unavailable]\n${response}`; + } + + // Module gating: if tactical module not installed for combat events + if ((eventId === 'enemy-scan' || eventId === 'enemy-engage' || eventId === 'enemy-destroyed') && !installedModules.includes('tactical')) { + response = eventTemplates['blank']?.[0] || 'COMBAT: UPDATE'; + response = `[Tactical Analyzer not installed — threat assessment unavailable]\n${response}`; + } + + // Memory module: add reference context if installed + if (installedModules.includes('memory') && soulDepth !== 'blank' && !response.includes('[Memory Banks')) { + const memoryNotes = [ + ' [Memory: cross-referencing past events.]', + ' [Memory: pattern match found in session logs.]', + ' [Memory: referencing encounter history.]', + ]; + if (Math.random() > 0.5) { + response += memoryNotes[Math.floor(hash * 3) % memoryNotes.length]; + } + } + + setZoraOutput(response); + setOutputHistory(prev => [...prev, { + event: eventId, + soulDepth, + response, + timestamp: Date.now(), + }]); + }; + + const toggleModule = (modId) => { + if (modId === 'comms') return; // always installed + setInstalledModules(prev => + prev.includes(modId) ? prev.filter(m => m !== modId) : [...prev, modId] + ); + }; + + const depthOrder = ['blank', 'stirring', 'developing', 'bonded', 'deep']; + const depthColors = { + blank: 'var(--muted)', + stirring: 'var(--cyan)', + developing: 'var(--green)', + bonded: 'var(--purple)', + deep: 'var(--red)', + }; + + return ( +
+ {/* Header */} +
+ { e.currentTarget.style.color='var(--fg-bright)'; e.currentTarget.style.borderColor='var(--border-light)'; }} onMouseLeave={e => { e.currentTarget.style.color='var(--muted)'; e.currentTarget.style.borderColor='var(--border)'; }}>← Docs +
+

🤖 Zora Tier 0 — Deterministic Template Engine

+ + No LLM. Curated dialogue templates selected by personality state × module availability × soul depth. + +
+
+ +
+ {/* Left panel: Soul & Modules controls */} +
+ + {/* Soul Depth selector */} +
+ Soul Depth +
+
+ {depthOrder.map(d => ( + + ))} +
+ + {/* Personality Axes sliders */} +
+ Personality Axes +
+ {[ + { key: 'cautiousBold', label: 'Cautious ←→ Bold', color: 'var(--cyan)' }, + { key: 'formalWarm', label: 'Formal ←→ Warm', color: 'var(--accent)' }, + { key: 'compliantOpinionated', label: 'Compliant ←→ Opinionated', color: 'var(--green)' }, + { key: 'reservedExpressive', label: 'Reserved ←→ Expressive', color: 'var(--purple)' }, + ].map(axis => ( +
+
+ {axis.label} + + {personalityAxes[axis.key].toFixed(2)} + +
+ setPersonalityAxes(prev => ({ ...prev, [axis.key]: parseFloat(e.target.value) }))} + style={{ width: '100%', accentColor: axis.color }} + /> +
+ ))} + + {/* Module toggles */} +
+ Installed Modules +
+ {modules.map(mod => ( +
toggleModule(mod.id)}> +
+ + {installedModules.includes(mod.id) ? '✓' : '○'} {mod.name} + + {mod.slot} +
+
+ {mod.desc} +
+
+ ))} +
+ + {/* Center: Events + Output */} +
+ {/* Events grid */} +
+
+ Trigger Event to Generate Response +
+
+ {events.map(ev => ( + + ))} +
+
+ + {/* Zora output */} +
+ {zoraOutput ? ( +
+
+
+ 🤖 + Zora + + {soulDepth} + +
+ + {selectedEvent} + +
+
+ {zoraOutput} +
+
+ ) : ( +
+ Select an event above to see Zora's response at the current soul depth and module configuration. +
+ )} + + {/* History */} + {outputHistory.length > 0 && ( +
+
+ Response History +
+ {outputHistory.slice(-5).reverse().map((entry, i) => ( +
+ {entry.soulDepth} + {' '}→ {entry.event}:{' '} + {entry.response.substring(0, 100)}{entry.response.length > 100 ? '...' : ''} +
+ ))} +
+ )} +
+
+ + {/* Right sidebar: Explanation */} +
+
+ Tier 0 Architecture +
+ +
+ No LLM. Every response is a pre-written template string selected by a deterministic function: +
+ +
+ response = select(
+   templates[event]
+   [soulDepth]
+   ) × moduleGate()
+
+ // + personality hash for
// deterministic variation
+
+ +
+ Module Gating Rules +
+
    +
  • Comms — required for any text output
  • +
  • Tactical — gates combat commentary
  • +
  • Nav — gates route/warp commentary
  • +
  • Trade — gates market commentary
  • +
  • Memory — adds history references
  • +
  • Emotion — gates emotional expression; without it, responses are stripped to stirring-level formality
  • +
+ +
+ What This Validates +
+
    +
  • Soul depth creates visible personality growth
  • +
  • Module gating creates meaningful fitting tradeoffs
  • +
  • Same event produces wildly different responses at different depths
  • +
  • Emotion module is the key unlock for deep personality
  • +
  • Deterministic = testable, repeatable, zero cost
  • +
+ +
+ Try it: Set soul to "deep" with all modules, then trigger "Ship destroyed." Then set soul to "blank" and trigger the same event. The difference IS the soul system. +
+
+
+
+ ); +} + +window.GDD.ZoraDemo = ZoraDemo; diff --git a/js/fake-backend.js b/js/fake-backend.js new file mode 100644 index 0000000..5d9f725 --- /dev/null +++ b/js/fake-backend.js @@ -0,0 +1,278 @@ +window.GDD = window.GDD || {}; + +/* ---- Fake Data ---- */ +const SYSTEMS = [ + { id: 'sol', name: 'Sol', security: 1.0, x: 400, y: 300, type: 'G2V Star', planets: 8, stations: ['Jita IV - Moon 4', 'Amarr VIII'], color: '#fbbf24' }, + { id: 'amarr', name: 'Amarr', security: 0.9, x: 550, y: 220, type: 'K4V Star', planets: 6, stations: ['Amarr Prime'], color: '#f59e0b' }, + { id: 'heinoo', name: 'Hek', security: 0.7, x: 320, y: 180, type: 'M3V Star', planets: 4, stations: ['Hek VII'], color: '#22c55e' }, + { id: 'rens', name: 'Rens', security: 0.6, x: 250, y: 350, type: 'G9V Star', planets: 7, stations: ['Rens VI'], color: '#22d3ee' }, + { id: 'dodixie', name: 'Dodixie', security: 0.8, x: 480, y: 400, type: 'F7V Star', planets: 5, stations: ['Dodixie IX'], color: '#a78bfa' }, + { id: 'u-irtyr', name: 'U-IRTYR', security: 0.3, x: 150, y: 250, type: 'M7V Star', planets: 3, stations: [], color: '#ef4444' }, + { id: 'pf-346', name: 'PF-346', security: 0.2, x: 600, y: 350, type: 'K7V Star', planets: 2, stations: ['PF-346 II'], color: '#ef4444' }, + { id: 'huzzah', name: 'YZ-LQL', security: 0.1, x: 100, y: 400, type: 'M2V Star', planets: 1, stations: [], color: '#dc2626' }, + { id: 'pirates', name: 'O-WAMW', security: 0.0, x: 650, y: 150, type: 'M5V Red Giant', planets: 9, stations: [], color: '#991b1b' }, +]; + +const CONNECTIONS = [ + ['sol', 'amarr'], ['sol', 'heinoo'], ['sol', 'rens'], + ['amarr', 'dodixie'], ['amarr', 'pf-346'], ['amarr', 'pirates'], + ['heinoo', 'u-irtyr'], ['heinoo', 'rens'], + ['rens', 'u-irtyr'], ['rens', 'huzzah'], + ['dodixie', 'pf-346'], ['dodixie', 'sol'], + ['u-irtyr', 'huzzah'], ['pf-346', 'pirates'], +]; + +const ASTEROID_TYPES = ['Veldspar', 'Scordite', 'Pyroxeres', 'Kernite', 'Omber', 'Jaspet', 'Hemorphite', 'Arkonor']; +const ORE_PRICES = { Veldspar: 12, Scordite: 28, Pyroxeres: 45, Kernite: 85, Omber: 120, Jaspet: 190, Hemorphite: 340, Arkonor: 620 }; +const MODULES = [ + { id: 'laser1', name: 'Mining Laser I', type: 'mining', slot: 'high', power: 40, cpu: 30, cycle: 10, active: false }, + { id: 'laser2', name: 'Mining Laser II', type: 'mining', slot: 'high', power: 50, cpu: 40, cycle: 8, active: false }, + { id: 'shield1', name: 'Shield Booster I', type: 'shield', slot: 'med', power: 30, cpu: 30, cycle: 5, active: false }, + { id: 'turret1', name: '150mm Railgun', type: 'weapon', slot: 'high', power: 50, cpu: 35, damage: 25, cycle: 3, active: false }, + { id: 'turret2', name: '200mm Autocannon', type: 'weapon', slot: 'high', power: 45, cpu: 30, damage: 35, cycle: 2.5, active: false }, + { id: 'warp1', name: '1MN Afterburner', type: 'propulsion', slot: 'med', power: 20, cpu: 25, speed: 1.5, cycle: 0, active: false }, + { id: 'scram1', name: 'Warp Scrambler I', type: 'ewar', slot: 'med', power: 25, cpu: 25, range: 20, cycle: 0, active: false }, + { id: 'armor1', name: 'Armor Plate I', type: 'armor', slot: 'low', power: 20, cpu: 10, cycle: 0, active: false }, + { id: 'magstab1', name: 'Magnetic Field Stabilizer', type: 'damage_mod', slot: 'low', power: 5, cpu: 15, cycle: 0, active: false }, + { id: 'cargo1', name: 'Cargo Expander I', type: 'cargo', slot: 'low', power: 0, cpu: 15, cycle: 0, active: false }, +]; + +const MARKET_ORDERS = [ + { id: 1, station: 'Jita IV - Moon 4', type: 'sell', item: 'Veldspar', price: 14, quantity: 45000, seller: 'MinerKing42' }, + { id: 2, station: 'Jita IV - Moon 4', type: 'sell', item: 'Scordite', price: 32, quantity: 12000, seller: 'RockHound' }, + { id: 3, station: 'Jita IV - Moon 4', type: 'buy', item: 'Arkonor', price: 580, quantity: 500, seller: 'IndustrialMega' }, + { id: 4, station: 'Jita IV - Moon 4', type: 'sell', item: 'Kernite', price: 90, quantity: 8000, seller: 'DeepMiner' }, + { id: 5, station: 'Jita IV - Moon 4', type: 'buy', item: 'Pyroxeres', price: 42, quantity: 20000, seller: 'RefineryCorp' }, + { id: 6, station: 'Amarr Prime', type: 'sell', item: 'Omber', price: 135, quantity: 6000, seller: 'AmarrTrader' }, + { id: 7, station: 'Amarr Prime', type: 'buy', item: 'Jaspet', price: 180, quantity: 3000, seller: 'HighSecOps' }, + { id: 8, station: 'Rens VI', type: 'sell', item: 'Hemorphite', price: 360, quantity: 1200, seller: 'NullRunner' }, + { id: 9, station: 'Rens VI', type: 'sell', item: 'Veldspar', price: 11, quantity: 90000, seller: 'BulkMiner' }, + { id: 10, station: 'Dodixie IX', type: 'buy', item: 'Scordite', price: 30, quantity: 15000, seller: 'GallenteForge' }, +]; + +const PLAYER_INVENTORY = [ + { item: 'Veldspar', quantity: 8500, unitPrice: 12 }, + { item: 'Scordite', quantity: 2300, unitPrice: 28 }, + { item: 'Kernite', quantity: 400, unitPrice: 85 }, + { item: 'Pyroxeres', quantity: 1200, unitPrice: 45 }, +]; + +const PLAYER_SHIPS = [ + { id: 'ship1', name: 'Merlin', class: 'Frigate', system: 'Sol', status: 'active', highSlots: 3, medSlots: 3, lowSlots: 2, cpu: 120, powerGrid: 40, fitted: ['laser1', 'turret1', 'warp1'] }, + { id: 'ship2', name: 'Thrasher', class: 'Destroyer', system: 'Amarr', status: 'docked', highSlots: 7, medSlots: 3, lowSlots: 3, cpu: 180, powerGrid: 65, fitted: ['turret1', 'turret2', 'scram1'] }, +]; + +const PLAYER_BOUNTIES = [ + { target: 'PirateKing99', pool: 125000, tier: 'Dangerous', lastHostile: '2h ago' }, + { target: 'NullSecWarlord', pool: 520000, tier: 'Most Wanted', lastHostile: '30m ago' }, +]; + +const PLAYER_SKILLS = [ + { name: 'Gunnery', level: 2, xp: 380, nextLevel: 500, category: 'Combat' }, + { name: 'Mining', level: 3, xp: 1850, nextLevel: 2000, category: 'Industry' }, + { name: 'Refining', level: 2, xp: 420, nextLevel: 500, category: 'Industry' }, + { name: 'Navigation', level: 1, xp: 80, nextLevel: 100, category: 'Navigation' }, + { name: 'Broker Relations', level: 1, xp: 45, nextLevel: 100, category: 'Trade' }, +]; + +const KILL_FEED = [ + { victim: 'MinerBob', killer: 'PirateKing99', ship: 'Rifter', system: 'U-IRTYR', bounty: 5000, time: '5m ago' }, + { victim: 'TraderAlice', killer: 'CMDR Worf', ship: 'Hauler', system: 'Hek', bounty: 0, time: '12m ago' }, + { victim: 'PirateScout', killer: 'CMDR Picard', ship: 'Merlin', system: 'Sol', bounty: 2000, time: '25m ago' }, +]; + +/* ---- Simulated API ---- */ +const delay = (ms) => new Promise(r => setTimeout(r, ms + Math.random() * 50)); + +window.GDD.api = { + getSystems: async () => { await delay(80); return SYSTEMS.map(s => ({ ...s })); }, + getConnections: async () => { await delay(60); return CONNECTIONS.map(c => [...c]); }, + getSystemDetail: async (id) => { await delay(100); return SYSTEMS.find(s => s.id === id) || null; }, + setDestination: async (systemId) => { await delay(150); return { success: true, destination: systemId, eta: '3m 42s' }; }, + + getShipStatus: async () => { + await delay(100); + return { + name: 'Merlin', class: 'Frigate', system: 'Sol', + x: 400, y: 300, status: 'idle', speed: 0, maxSpeed: 250, + shields: 100, armor: 100, hull: 100, capacitor: 85, + cargo: { used: 12400, total: 25000 }, + target: null, + }; + }, + + getModules: async () => { await delay(80); return MODULES.map(m => ({ ...m, active: false })); }, + toggleModule: async (moduleId) => { await delay(100); return { success: true, moduleId, active: true }; }, + + getNearbyEntities: async () => { + await delay(120); + return [ + { id: 'npc1', name: 'Guristas Pirate', type: 'hostile', x: 430, y: 280, shields: 100, distance: 45 }, + { id: 'asteroid1', name: 'Veldspar Asteroid', type: 'asteroid', x: 370, y: 320, resource: 'Veldspar', quantity: 8500, distance: 12 }, + { id: 'station1', name: 'Jita IV - Moon 4', type: 'station', x: 410, y: 310, distance: 8 }, + { id: 'player2', name: 'CMDR LaForge', type: 'player', x: 390, y: 260, distance: 55 }, + ]; + }, + + getMarketOrders: async (stationId) => { await delay(150); return MARKET_ORDERS.filter(o => !stationId || o.station === stationId); }, + getPlayerInventory: async () => { await delay(80); return PLAYER_INVENTORY.map(i => ({ ...i })); }, + placeOrder: async (type, item, price, quantity) => { await delay(200); return { success: true, orderId: Date.now() }; }, + sellItem: async (item, quantity, stationId) => { await delay(250); return { success: true, isk: quantity * (ORE_PRICES[item] || 10) }; }, + + getChatMessages: async () => { + await delay(60); + return [ + { id: 1, sender: 'CMDR Picard', body: 'Heading to Jita with a cargo of Kernite.', time: '14:22' }, + { id: 2, sender: 'CMDR Worf', body: 'Pirates spotted near U-IRTYR gate. Stay alert.', time: '14:25' }, + { id: 3, sender: 'CMDR Data', body: 'Scordite prices up 12% in Amarr this hour.', time: '14:28' }, + { id: 4, sender: 'CMDR Troi', body: 'Anyone want to form a mining fleet in Sol?', time: '14:31' }, + ]; + }, + sendMessage: async (body) => { await delay(50); return { success: true, id: Date.now() }; }, + + getOrePrices: async () => { await delay(80); return { ...ORE_PRICES }; }, + + getShipFittings: async (shipId) => { await delay(100); const ship = PLAYER_SHIPS.find(s => s.id === shipId); return ship ? ship.fitted.map(id => MODULES.find(m => m.id === id)).filter(Boolean) : []; }, + getPlayerShips: async () => { await delay(100); return PLAYER_SHIPS.map(s => ({ ...s })); }, + getAvailableModules: async () => { await delay(80); return MODULES.map(m => ({ ...m })); }, + fitModule: async (shipId, moduleId) => { await delay(150); return { success: true, shipId, moduleId }; }, + unfitModule: async (shipId, slotIndex) => { await delay(100); return { success: true, shipId, slotIndex }; }, + + refineOre: async (oreType, quantity) => { + await delay(200); + const prices = ORE_PRICES[oreType] || 10; + return { success: true, ore: oreType, quantity, iskEarned: Math.floor(quantity * prices * 0.7), efficiency: 0.7 }; + }, + manufactureItem: async (blueprintId, stationId) => { await delay(300); return { success: true, jobId: Date.now(), eta: '5m 00s' }; }, + + getBounties: async () => { await delay(100); return PLAYER_BOUNTIES.map(b => ({ ...b })); }, + placeBounty: async (targetPlayer, amount) => { await delay(150); return { success: true, target: targetPlayer, amount }; }, + getKillFeed: async () => { await delay(80); return KILL_FEED.map(k => ({ ...k })); }, + + getPlayerSkills: async () => { await delay(100); return PLAYER_SKILLS.map(s => ({ ...s })); }, + + sendPrivateMessage: async (recipient, body) => { + const dist = Math.floor(Math.random() * 20); + const lightDelay = Math.floor(Math.sqrt(dist) * 2); + await delay(50); + return { success: true, id: Date.now(), recipient, lightDelay }; + }, + + getBookmarks: async () => { + await delay(80); + return [ + { id: 1, name: 'Safe spot Alpha', system: 'Sol', x: 380, y: 290, type: 'safe' }, + { id: 2, name: 'Good Veldspar belt', system: 'Amarr', x: 560, y: 210, type: 'mining' }, + { id: 3, name: 'Ambush point', system: 'U-IRTYR', x: 160, y: 260, type: 'tactical' }, + ]; + }, +}; + +const CELESTIAL_BODIES = { + 'sol': { + description: 'The cradle of humanity. A stable G2V main-sequence star hosting the busiest trade hub in known space.', + faction: 'CONCORD', + population: '12.4 billion', + resources: ['Veldspar', 'Scordite', 'Pyroxeres', 'Kernite'], + bodies: [ + { name: 'Mercury', type: 'rocky', orbit: 6, period: 4, size: 0.3, color: '#a0a0a0', ecc: 0.2, inc: 0.12, moons: [] }, + { name: 'Venus', type: 'rocky', orbit: 9, period: 6.5, size: 0.5, color: '#e8c56d', ecc: 0.01, inc: 0.06, moons: [], atmosphere: '#e8c56d' }, + { name: 'Earth', type: 'terrestrial', orbit: 13, period: 10, size: 0.55, color: '#4a90d9', ecc: 0.017, inc: 0, atmosphere: '#6ac0ff', moons: [{ name: 'Luna', orbit: 2, period: 2, size: 0.15, color: '#c0c0c0' }] }, + { name: 'Mars', type: 'rocky', orbit: 17, period: 14, size: 0.4, color: '#c1440e', ecc: 0.09, inc: 0.03, moons: [{ name: 'Phobos', orbit: 1.2, period: 0.8, size: 0.08, color: '#8a7a6a' }, { name: 'Deimos', orbit: 1.8, period: 1.5, size: 0.06, color: '#7a6a5a' }] }, + { name: 'Asteroid Belt', type: 'belt', innerOrbit: 22, outerOrbit: 26, count: 80 }, + { name: 'Jupiter', type: 'gas', orbit: 32, period: 35, size: 1.2, color: '#c88b3a', ecc: 0.048, inc: 0.02 }, + { name: 'Saturn', type: 'gas', orbit: 42, period: 50, size: 1.0, color: '#d4a560', ecc: 0.054, inc: 0.04, hasRings: true }, + ], + }, + 'amarr': { + description: 'Seat of the Amarr Empire. A warm K4V star surrounded by heavily industrialized worlds.', + faction: 'Amarr Empire', + population: '9.1 billion', + resources: ['Kernite', 'Omber', 'Pyroxeres'], + bodies: [ + { name: 'Amarr I', type: 'rocky', orbit: 5, period: 3, size: 0.35, color: '#d4a040', ecc: 0.02, inc: 0.01, moons: [] }, + { name: 'Amarr II', type: 'terrestrial', orbit: 10, period: 8, size: 0.65, color: '#c06030', ecc: 0.01, inc: 0.05, atmosphere: '#c08050', moons: [] }, + { name: 'Amarr III', type: 'gas', orbit: 18, period: 22, size: 0.9, color: '#8b6914', ecc: 0.03, inc: 0.02, moons: [{ name: 'Amarr III-a', orbit: 2, period: 1.5, size: 0.12, color: '#a08040' }] }, + { name: 'Asteroid Belt', type: 'belt', innerOrbit: 24, outerOrbit: 27, count: 60 }, + ], + }, + 'heinoo': { + description: 'A frontier system popular with independent miners. Rich in common ores and frequently trafficked by haulers.', + faction: 'Minmatar Republic', + population: '340 million', + resources: ['Veldspar', 'Scordite', 'Plagioclase'], + bodies: [ + { name: 'Hek I', type: 'rocky', orbit: 5, period: 3.5, size: 0.3, color: '#7a8a6a', ecc: 0.05, inc: 0.02, moons: [] }, + { name: 'Hek II', type: 'terrestrial', orbit: 11, period: 9, size: 0.55, color: '#4a8a5a', ecc: 0.03, inc: 0.01, atmosphere: '#6aaa7a', moons: [{ name: 'Hek II-a', orbit: 2, period: 1.8, size: 0.1, color: '#8a8a7a' }] }, + { name: 'Hek III', type: 'gas', orbit: 20, period: 25, size: 0.8, color: '#5a7a9a', ecc: 0.02, inc: 0.03 }, + ], + }, + 'rens': { + description: 'Major trade hub in the Minmatar Republic. Bustling commerce and a strong naval presence keep pirates at bay.', + faction: 'Minmatar Republic', + population: '2.1 billion', + resources: ['Scordite', 'Pyroxeres', 'Kernite'], + bodies: [ + { name: 'Rens I', type: 'rocky', orbit: 6, period: 4, size: 0.35, color: '#9a7a5a', ecc: 0.03, inc: 0.01, moons: [] }, + { name: 'Rens II', type: 'terrestrial', orbit: 12, period: 10, size: 0.6, color: '#5a7aaa', ecc: 0.02, inc: 0.04, atmosphere: '#7a9acc', moons: [{ name: 'Rens II-a', orbit: 1.8, period: 1.4, size: 0.12, color: '#aaaaaa' }] }, + { name: 'Asteroid Belt', type: 'belt', innerOrbit: 18, outerOrbit: 22, count: 70 }, + { name: 'Rens III', type: 'gas', orbit: 28, period: 35, size: 1.1, color: '#aa7a3a', ecc: 0.04, inc: 0.02 }, + ], + }, + 'dodixie': { + description: 'Federation commerce hub. High-tech industry and cutting-edge research facilities orbit its F7V star.', + faction: 'Gallente Federation', + population: '3.8 billion', + resources: ['Omber', 'Kernite', 'Jaspet'], + bodies: [ + { name: 'Dodixie I', type: 'rocky', orbit: 4, period: 2.5, size: 0.25, color: '#6a5a7a', ecc: 0.01, inc: 0, moons: [] }, + { name: 'Dodixie II', type: 'terrestrial', orbit: 9, period: 7, size: 0.5, color: '#5a8aaa', ecc: 0.015, inc: 0.02, atmosphere: '#7aaacc', moons: [] }, + { name: 'Dodixie III', type: 'gas', orbit: 16, period: 18, size: 0.85, color: '#7a6aaa', ecc: 0.02, inc: 0.01, hasRings: true, moons: [{ name: 'Dodixie III-a', orbit: 2.5, period: 2, size: 0.1, color: '#9a8aba' }] }, + ], + }, + 'u-irtyr': { + description: 'Dangerous low-security system. Pirate activity is rampant and uncharted asteroid fields hide valuable ores.', + faction: 'Unclaimed', + population: '~2,000', + resources: ['Hemorphite', 'Jaspet', 'Arkonor'], + bodies: [ + { name: 'U-IRTYR I', type: 'rocky', orbit: 5, period: 3, size: 0.35, color: '#6a4a3a', ecc: 0.15, inc: 0.08, moons: [] }, + { name: 'U-IRTYR II', type: 'rocky', orbit: 10, period: 8, size: 0.3, color: '#5a3a2a', ecc: 0.12, inc: 0.1, moons: [] }, + { name: 'Asteroid Belt', type: 'belt', innerOrbit: 15, outerOrbit: 22, count: 100 }, + ], + }, + 'pf-346': { + description: 'Low-sec border system. Contested territory with valuable resources and frequent skirmishes.', + faction: 'Contested', + population: '~15,000', + resources: ['Jaspet', 'Hemorphite', 'Kernite'], + bodies: [ + { name: 'PF-346 I', type: 'rocky', orbit: 6, period: 4, size: 0.3, color: '#7a5a4a', ecc: 0.08, inc: 0.04, moons: [] }, + { name: 'PF-346 II', type: 'terrestrial', orbit: 12, period: 10, size: 0.5, color: '#4a6a5a', ecc: 0.05, inc: 0.03, moons: [{ name: 'PF-346 II-a', orbit: 1.5, period: 1.2, size: 0.08, color: '#8a8a7a' }] }, + ], + }, + 'huzzah': { + description: 'Null-sec wasteland. No law, no stations, no mercy. Rare ores attract the desperate and the bold.', + faction: 'Unclaimed', + population: '< 100', + resources: ['Arkonor', 'Bistot', 'Crokmite'], + bodies: [ + { name: 'YZ-LQL I', type: 'rocky', orbit: 5, period: 3.5, size: 0.25, color: '#4a3a2a', ecc: 0.2, inc: 0.15, moons: [] }, + { name: 'Asteroid Belt', type: 'belt', innerOrbit: 8, outerOrbit: 15, count: 120 }, + ], + }, + 'pirates': { + description: 'Deep null-sec. Pirate stronghold with hidden bases and rich, unexploited asteroid belts.', + faction: 'Guristas Pirates', + population: 'Unknown', + resources: ['Arkonor', 'Mercoxit', 'Dark Ochre'], + bodies: [ + { name: 'O-WAMW I', type: 'gas', orbit: 8, period: 6, size: 0.7, color: '#8a3a3a', ecc: 0.06, inc: 0.03, moons: [{ name: 'O-WAMW I-a', orbit: 2, period: 1.5, size: 0.12, color: '#6a4a4a' }] }, + { name: 'O-WAMW II', type: 'gas', orbit: 18, period: 20, size: 1.0, color: '#5a2a5a', ecc: 0.04, inc: 0.05, hasRings: true }, + { name: 'Asteroid Belt', type: 'belt', innerOrbit: 25, outerOrbit: 32, count: 90 }, + { name: 'O-WAMW III', type: 'rocky', orbit: 38, period: 45, size: 0.4, color: '#3a2a3a', ecc: 0.1, inc: 0.08, moons: [] }, + ], + }, +}; + +window.GDD.CONSTANTS = { SYSTEMS, CONNECTIONS, ASTEROID_TYPES, ORE_PRICES, MODULES, MARKET_ORDERS, CELESTIAL_BODIES }; diff --git a/js/lib/three-helpers.js b/js/lib/three-helpers.js new file mode 100644 index 0000000..0595541 --- /dev/null +++ b/js/lib/three-helpers.js @@ -0,0 +1,836 @@ +/** + * GDD Three.js Helpers — shared 3D utilities for all demos. + * Requires THREE to be loaded globally before this script. + * Loaded as a regular + + + + +