chore: sync codebase remediation, gameplay systems, and docs

Security & infrastructure:
- Remove unused services/ (auth, spacetimedb) and auth.db
- Add .env.example template, expand .gitignore for env/db files
- Add GitHub Actions CI + commitlint config and workflows
- Add manual vendor chunking and source maps to docs/site vite configs

Shared UI & docs app:
- Add ARIA props and focus-visible rings to Button/Panel
- Add ButtonAsLink primitive; use shared Button in NotFound
- Wire @void-nav/ui into docs app; refresh content pages
- Replace Todo page with Kanban board

Gameplay (Bevy):
- Add ai module (behavior, faction, navigation, perception, spawning, states)
- Add narrative module (events, history, synthesis, ui)
- Refine galaxy contents and in-system flight/scene systems
This commit is contained in:
2026-06-16 11:49:13 -04:00
parent 98c2ba59df
commit 57633addfe
60 changed files with 5084 additions and 2473 deletions

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@react-three/drei": "^9.122.0",
"@react-three/fiber": "^8.17.10",
"@void-nav/ui": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.1",

View File

@@ -17,7 +17,7 @@ import { DemoGalleryPage } from "./pages/docs/DemoGalleryPage";
import { GapAnalysisPage } from "./pages/docs/GapAnalysisPage";
import { VerticalSliceEvaluationPage } from "./pages/docs/VerticalSliceEvaluationPage";
import { DesignDocPage } from "./pages/docs/DesignDocPage";
import { TodoPage } from "./pages/docs/TodoPage";
import { KanbanBoardPage } from "./pages/docs/KanbanBoardPage";
import { StarMapDemo } from "./prototypes/existing-demos/StarMapDemo";
import { ShipMovementDemo } from "./prototypes/existing-demos/ShipMovementDemo";
import { WarpTravelDemo } from "./prototypes/existing-demos/WarpTravelDemo";
@@ -57,7 +57,7 @@ export function App() {
<Route path="gap-analysis" element={<GapAnalysisPage />} />
<Route path="vertical-slice-evaluation" element={<VerticalSliceEvaluationPage />} />
<Route path="design-doc" element={<DesignDocPage />} />
<Route path="todo" element={<TodoPage />} />
<Route path="kanban-board" element={<KanbanBoardPage />} />
<Route path="demos/starmap" element={<StarMapDemo />} />
<Route path="demos/game-loop" element={<GameLoopSliceDemo />} />

View File

@@ -1,11 +1,11 @@
import { Link } from "react-router-dom";
import { ButtonAsLink } from "@void-nav/ui";
export function NotFound() {
return (
<div className="mx-auto max-w-content">
<h1>Page Not Found</h1>
<p className="text-fg-dim">The requested route does not exist.</p>
<Link className="inline-flex items-center gap-2 rounded-lg border border-accent bg-accent px-4 py-2 text-[0.8rem] font-semibold text-bg transition-all duration-150 hover:bg-accent-hover" to="/docs">Back to documentation</Link>
<ButtonAsLink to="/docs" tone="primary">Back to documentation</ButtonAsLink>
</div>
);
}

View File

@@ -24,7 +24,7 @@ export const navSections: NavSection[] = [
{ path: "/docs/social", icon: "✧", label: "Progression & World" },
{ path: "/docs/ship-ai", icon: "◈", label: "Ship AI - Zora" },
{ path: "/docs/roadmap", icon: "⊞", label: "Roadmap" },
{ path: "/docs/todo", icon: "", label: "Implementation Status" },
{ path: "/docs/kanban-board", icon: "", label: "Implementation Board" },
{ path: "/docs/risks", icon: "◬", label: "Risks & Questions" },
{ path: "/docs/gap-analysis", icon: "□", label: "Gap Analysis" },
{ path: "/docs/vertical-slice-evaluation", icon: "▤", label: "Slice Evaluation" },

View File

@@ -6,9 +6,9 @@ export function ArchitecturePage() {
<div className="mx-auto max-w-content">
<h1 style={{ marginBottom: '8px' }}>Architecture Overview</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '680px' }}>
<strong>Key principle:</strong> SpacetimeDB owns
authoritative game state. Everything else derives from it. There is no localStorage persistence is always through
SpacetimeDB, even in the single-player core game where SpacetimeDB runs locally.
<strong>Key principle:</strong> Local-first architecture for single-player narrative experience.
The game client manages all state locally with persistent storage. Your story lives on your machine
no cloud required, no always-online, no subscriptions.
</p>
{/* Architecture diagram */}
@@ -20,15 +20,16 @@ export function ArchitecturePage() {
</div>
<div style={{ padding: 'var(--sp-5)' }}>
{[
{ name: 'React UI', color: 'var(--cyan)', desc: 'Inventory, market, chat, station screens, ship status, debug panel. Owns UI layout and user workflow.' },
{ name: 'React UI', color: 'var(--cyan)', desc: 'Inventory, market, station screens, ship status, debug panel. Owns UI layout and user workflow.' },
{ name: 'Local Stores', color: 'var(--green)', desc: 'Selected entity, active panels, camera preferences, filters, sorting. Zustand/React state.' },
{ name: 'Renderer Adapter', color: 'var(--purple)', desc: 'Receives view models and emits events: select, move, mine, dock. Boundary that keeps R3F replaceable.' },
{ name: 'R3F Scene', color: 'var(--accent)', desc: 'Ships, stations, asteroids, anomalies, fauna, camera, targeting lines, world event effects. Visual layer only.' },
{ name: 'Ship AI (Zora)', color: 'var(--purple)', desc: 'Companion AI system with soul state, module gates, and autonomous agent behavior. See the Ship AI page for full design.' },
{ name: 'SpacetimeDB SDK', color: 'var(--cyan)', desc: 'Reducer calls and subscriptions. Client bridge to backend.' },
{ name: 'SpacetimeDB Module', color: 'var(--red)', desc: 'Tables, reducers, validation, persistence, authoritative game state. Source of truth.' },
{ name: 'R3F Scene', color: 'var(--accent)', desc: 'Ships, stations, asteroids, anomalies, camera, targeting lines. Visual layer only.' },
{ name: 'AI Story Director', color: 'var(--purple)', desc: 'Narrative engine that tracks player actions and generates story events. Weaves choices into saga.' },
{ name: 'Ship AI (Zora)', color: 'var(--purple)', desc: 'Companion AI system with soul state, module gates, and autonomous behavior. See Ship AI page.' },
{ name: 'Game Client', color: 'var(--red)', desc: 'Core game logic, state management, and local persistence. Source of truth.' },
{ name: 'Local Storage', color: 'var(--cyan)', desc: 'Persistent state survives restart. Ships, inventory, story log, faction relations.' },
].map((layer, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'stretch', gap: 'var(--sp-3)', marginBottom: i < 5 ? '2px' : 0 }}>
<div key={i} style={{ display: 'flex', alignItems: 'stretch', gap: 'var(--sp-3)', marginBottom: i < 6 ? '2px' : 0 }}>
<div style={{ width: '3px', background: layer.color, borderRadius: '2px', flexShrink: 0 }} />
<div style={{ padding: 'var(--sp-2) 0', flex: 1, display: 'flex', gap: 'var(--sp-4)', alignItems: 'baseline' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', color: layer.color, minWidth: '140px', fontWeight: 600 }}>
@@ -49,16 +50,16 @@ export function ArchitecturePage() {
<h3>React UI Responsibilities</h3>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.9rem', marginBottom: 'var(--sp-5)' }}>
<li>Global shell, HUD layout, docking/station screens, and route-like page states.</li>
<li>Inventory table, market orders table, chat, selected target panel, ship status, system overview.</li>
<li>User commands that call reducers: mine, dock, sell, place order, send chat.</li>
<li>Inventory table, market orders table, selected target panel, ship status, system overview.</li>
<li>User commands that trigger game actions: mine, dock, sell, place order.</li>
<li>Local-only concerns: panel layout, sorting, filters, tabs, keyboard shortcuts, tooltips.</li>
</ul>
<h3>R3F Responsibilities</h3>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.9rem', marginBottom: 'var(--sp-5)' }}>
<li>Render star-system scene with ships, stations, asteroids, anomalies, waypoints, fauna, and event effects.</li>
<li>Render star-system scene with ships, stations, asteroids, anomalies, and waypoints.</li>
<li>Camera controls, entity picking, hover states, click-to-move command creation.</li>
<li>Interpolated movement between authoritative state updates.</li>
<li>Interpolated movement between state updates.</li>
<li>Expose renderer events upward never push game logic downward into the renderer.</li>
</ul>
@@ -73,13 +74,13 @@ export function ArchitecturePage() {
</thead>
<tbody>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-red/25 bg-red/8 text-red">Authoritative</span></td>
<td>SpacetimeDB tables/subscriptions</td>
<td>Ship position, inventory, market orders, asteroid resources.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-red/25 bg-red/8 text-red">Persistent</span></td>
<td>Local storage</td>
<td>Ship position, inventory, market orders, story log, faction relations.</td>
</tr>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan">Derived view</span></td>
<td>Client game store/view models</td>
<td>Client game store</td>
<td>Selected ship details, rendered position, distance to target.</td>
</tr>
<tr>
@@ -95,165 +96,32 @@ export function ArchitecturePage() {
</tbody>
</table>
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]">
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]'" style={{ marginTop: 'var(--sp-8)' }}>
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-3</span>
<h2 style={{ margin: 0 }}>Renderer Replaceability</h2>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Key principle:</strong> Use React Three Fiber for speed now, but prevent it from becoming
the game architecture. The renderer consumes view models and emits input events.
</div>
<div className="mb-5 overflow-x-auto rounded-lg border border-border bg-surface px-5 py-4 [&_code]:bg-transparent [&_code]:p-0 [&_code]:font-mono [&_code]:text-[0.82rem] [&_code]:leading-[1.6] [&_code]:text-fg-dim">
<code>
<span className="text-muted italic">// Renderer interface — implementation-agnostic</span><br/>
<span className="text-purple">type</span> <span className="text-cyan">GameRendererInput</span> = &#123;<br/>
&nbsp;&nbsp;ships: <span className="text-cyan">ShipViewModel</span>[];<br/>
&nbsp;&nbsp;stations: <span className="text-cyan">StationViewModel</span>[];<br/>
&nbsp;&nbsp;asteroids: <span className="text-cyan">AsteroidViewModel</span>[];<br/>
&nbsp;&nbsp;anomalies: <span className="text-cyan">AnomalyViewModel</span>[];<br/>
&nbsp;&nbsp;worldEvents: <span className="text-cyan">WorldEventViewModel</span>[];<br/>
&nbsp;&nbsp;selectedEntityId: <span className="text-cyan">string</span> | <span className="text-purple">null</span>;<br/>
&#125;;<br/>
<br/>
<span className="text-purple">type</span> <span className="text-cyan">GameRendererEvents</span> = &#123;<br/>
&nbsp;&nbsp;<span className="text-accent">onSelectEntity</span>(entityId: <span className="text-cyan">string</span>): <span className="text-cyan">void</span>;<br/>
&nbsp;&nbsp;<span className="text-accent">onMoveCommand</span>(position: <span className="text-cyan">Vec3</span>): <span className="text-cyan">void</span>;<br/>
&nbsp;&nbsp;<span className="text-accent">onMineCommand</span>(asteroidId: <span className="text-cyan">string</span>): <span className="text-cyan">void</span>;<br/>
&nbsp;&nbsp;<span className="text-accent">onDockCommand</span>(stationId: <span className="text-cyan">string</span>): <span className="text-cyan">void</span>;<br/>
&#125;;
</code>
</div>
<div className="grid grid-cols-2 gap-5 max-[900px]:grid-cols-1" style={{ marginTop: 'var(--sp-5)' }}>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4 border-l-[3px] border-l-accent">
<h4 style={{ color: 'var(--green)' }}>Keep renderer-specific</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Meshes, materials, lights, particles</li>
<li>Camera controls</li>
<li>Raycasting and pointer interactions</li>
<li>Visual interpolation implementation</li>
</ul>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4 border-l-[3px] border-l-accent">
<h4 style={{ color: 'var(--cyan)' }}>Keep renderer-independent</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Game types, reducers, subscriptions</li>
<li>Domain rules: mining, docking, selling</li>
<li>Inventory logic and market validation</li>
<li>Shared movement math and view models</li>
</ul>
</div>
</div>
{/* ═══ RECONNECTION & ERROR HANDLING ═══ */}
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]" style={{ marginTop: 'var(--sp-8)' }}>
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-4</span>
<h2 style={{ margin: 0 }}>Error Handling & Reconnection</h2>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Players should never lose progress due to a disconnect.</strong>
SpacetimeDB is the source of truth and persists all authoritative state server-side.
A disconnected player's ship continues to exist in the world, subject to the same rules as every other ship.
Reconnection restores the player to their last authoritative state — nothing is lost.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Disconnection Scenarios</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead>
<tr><th>Scenario</th><th>Server Behavior</th><th>On Reconnect</th><th>Player Impact</th></tr>
</thead>
<tbody>
{[
{ scene: 'Idle in space', server: 'Ship remains in world. Continues orbit or holds position. No auto-actions.', reconnect: 'Full state restore. Ship where it was. No losses.', impact: 'None. Seamless.' },
{ scene: 'Mid-mining cycle', server: 'Active mining action continues to completion. Ore deposited to cargo.', reconnect: 'Mining cycle may have completed. Ore in cargo as expected. Partial cycles yield partial ore.', impact: 'Minimal. At worst, lost time on partial cycle.' },
{ scene: 'Mid-combat (PvE)', server: 'Ship continues with last known power allocation. Auto-defends with passive tank. If destroyed, insurance applies.', reconnect: 'If ship survived: full combat restore with current HP. If destroyed: respawn at home station, insurance payout queued.', impact: 'Ship may be destroyed. Insurance covers hull. Standard death penalty applies this is the risk of disconnecting in danger.' },
{ scene: 'Mid-warp', server: 'Warp completes normally. Ship arrives at destination.', reconnect: 'Ship at destination. No interruption to warp.', impact: 'None.' },
{ scene: 'Docked at station', server: 'No risk. Station is safe. Player remains docked.', reconnect: 'Restored to station. All panels and inventory intact.', impact: 'None.' },
{ scene: 'Mid-market order', server: 'If reducer was received, order is placed. If not, no order.', reconnect: 'Check market panel for order state. ISK and inventory reflect server truth.', impact: 'At worst, order was not placed. No double-spend possible (atomic reducers).' },
{ scene: 'Mid-combat (PvP co-op)', server: 'Ship continues with last power allocation. Enemy continues attacking.', reconnect: 'Same as PvE: ship may be destroyed. No special protection for PvP disconnect.', impact: 'Ship may be destroyed. Disconnecting during PvP carries the same risk as staying.' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--fg)' }}>{row.scene}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.server}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.reconnect}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.impact}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="grid grid-cols-2 gap-5 max-[900px]:grid-cols-1" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Reconnection Flow</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
1. <span style={{ color: 'var(--red)' }}>Detect disconnect</span> — WebSocket close event or heartbeat timeout (10s no response)<br/>
2. <span style={{ color: 'var(--accent)' }}>Show reconnect banner</span> — "Connection lost. Reconnecting..." with spinning indicator. UI freezes input but continues rendering last known state.<br/>
3. <span style={{ color: 'var(--green)' }}>Auto-retry</span> — Exponential backoff: 1s, 2s, 4s, 8s, max 30s. Up to 10 attempts over ~5 minutes.<br/>
4. <span style={{ color: 'var(--cyan)' }}>Re-establish subscription</span> — On reconnect, re-subscribe to all relevant SpacetimeDB tables. Server sends full state diff.<br/>
5. <span style={{ color: 'var(--purple)' }}>State reconciliation</span> — Client merges server state into local state. Visual positions snap to authoritative positions. Inventory, market, chat all refresh.<br/>
6. <span style={{ color: 'var(--green)' }}>Resume gameplay</span> — Banner disappears. All inputs re-enabled. Player is back in the game.
</div>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--red)' }}>
<h4 style={{ color: 'var(--red)' }}>Failed Reconnection</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
<p>If all 10 attempts fail (5+ minutes of no connection):</p>
<p>
<span style={{ color: 'var(--accent)' }}>{'Core game (local SpacetimeDB):'}</span>
{' This should never happen. If it does, it\'s a bug. Show error screen with "restart game" button. Local SpacetimeDB state is intact.'}
</p>
<p>
<span style={{ color: 'var(--cyan)' }}>{'Co-op server (remote SpacetimeDB):'}</span>
{' Show "Connection lost" screen with two options: [Retry Now] (immediate reconnect attempt) and [Return to Login]. Ship persists on server. No data lost.'}
</p>
<p>
<span style={{ color: 'var(--muted)' }}>{'The ship never vanishes on disconnect. It stays in the world, obeying server rules. This is intentional — on co-op servers, disconnecting to escape combat is not allowed.'}</span>
</p>
</div>
</div>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Anti-exploit: combat disconnect.</strong> On co-op servers, players who disconnect during PvP combat receive no special protection.
Their ship remains in the world, continuing to fight with last-known power allocation. If destroyed, normal death
penalties apply (loot drop, insurance). This prevents "combat logging" as an escape mechanism. Zora may send
a message on reconnect: "I sustained damage while you were away. Shields at 40%. The enemy disengaged."
</div>
{/* ═══ SESSION PERSISTENCE ═══ */}
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]">
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-5</span>
<h2 style={{ margin: 0 }}>Session Persistence & Save/Load</h2>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>There is no save button and no manual save/load.</strong>
SpacetimeDB persists all authoritative state continuously every reducer call, every tick update, every position change
is written to the database as it happens. "Saving" is not a player action; it is the natural consequence of playing the game.
Closing the browser and returning tomorrow restores the player to exactly where they left off.
The game client persists state continuously every action, position change, and story event is saved locally.
"Saving" is not a player action; it's the natural consequence of playing the game.
Closing the game and returning tomorrow restores you to exactly where you left off.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>What Gets Persisted</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead>
<tr><th>Category</th><th>Tables</th><th>Persistence Guarantee</th></tr>
<tr><th>Category</th><th>Data</th><th>Persistence Guarantee</th></tr>
</thead>
<tbody>
{[
{ cat: 'Player Identity', tables: 'players, player_skills, player_standing, player_loyalty_points', guarantee: 'Permanent. Never lost. Bound to SpacetimeDB identity.' },
{ cat: 'Ship State', tables: 'ships, ship_fittings, ship_ai_soul, ship_ai_modules, ship_ai_memory, ship_ai_directives', guarantee: 'Permanent. Ship position, fitting, AI state all persist. If destroyed, destroyed state persists until replaced.' },
{ cat: 'Economy', tables: 'inventory_items, market_orders, manufacturing_jobs, insurance_policies', guarantee: 'Permanent. Items, orders, and jobs survive restart. In-progress jobs continue server-side during offline time.' },
{ cat: 'Navigation', tables: 'bookmarks, waypoints', guarantee: 'Permanent. Saved locations and routes survive indefinitely.' },
{ cat: 'Social', tables: 'chat_messages (recent), bounties, kill_feed', guarantee: 'Messages expire after 30 days. Bounties persist until collected or decayed. Kill feed is permanent (story log).' },
{ cat: 'World State', tables: 'world_events, faction_relations, galaxy_story_log, anomalies, space_fauna', guarantee: 'Server-owned. Continues evolving while player is offline. Player returns to a changed galaxy.' },
{ cat: 'Player Identity', tables: 'Captain name, skills, standing, story progress', guarantee: 'Permanent. Never lost.' },
{ cat: 'Ship State', tables: 'Position, fitting, AI companion state, cargo', guarantee: 'Permanent. If destroyed, death state persists until respawn.' },
{ cat: 'Economy', tables: 'Inventory items, market orders, manufacturing jobs', guarantee: 'Permanent. Items and orders survive restart.' },
{ cat: 'Navigation', tables: 'Bookmarks, waypoints', guarantee: 'Permanent. Saved locations survive indefinitely.' },
{ cat: 'Story Log', tables: 'AI Story events, discoveries, choices, faction relations', guarantee: 'Permanent. Your saga is preserved.' },
{ cat: 'World State', tables: 'Factions, world events, galaxy state', guarantee: 'Saved with your campaign. Continues where you left off.' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--fg)' }}>{row.cat}</td>
@@ -266,339 +134,177 @@ export function ArchitecturePage() {
</div>
<div className="grid grid-cols-2 gap-5 max-[900px]:grid-cols-1" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Offline Progression</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
The server continues simulating the galaxy while the player is offline. Faction borders shift,
world events spawn and resolve, the economy adjusts. Manufacturing jobs placed before logging off
complete on schedule. Market orders can be filled while the player is away.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>
"Come back tomorrow" is always a valid answer your manufacturing job finishes whether you watch it or not.
</div>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>What Does NOT Persist</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>UI layout:</strong> Panel positions, sorting preferences, open tabs. These reset on reload. (Future: saved layout profiles.)</li>
<li><strong style={{ color: 'var(--fg)' }}>Camera state:</strong> Zoom level, orbit angle. Reset to default on reconnection.</li>
<li><strong style={{ color: 'var(--fg)' }}>In-progress inputs:</strong> Half-typed chat messages, unfinalized market orders. Lost on disconnect.</li>
<li><strong style={{ color: 'var(--fg)' }}>Transient effects:</strong> Active weapon animations, explosion particles, HUD flash effects. Visual only not gameplay state.</li>
<li><strong style={{ color: 'var(--fg)' }}>UI layout:</strong> Panel positions, sorting preferences, open tabs. These reset on reload.</li>
<li><strong style={{ color: 'var(--fg)' }}>Camera state:</strong> Zoom level, orbit angle. Reset to default on load.</li>
<li><strong style={{ color: 'var(--fg)' }}>In-progress inputs:</strong> Half-typed messages, unfinalized orders. Lost on exit.</li>
<li><strong style={{ color: 'var(--fg)' }}>Transient effects:</strong> Active weapon animations, explosion particles. Visual only.</li>
</ul>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--purple)' }}>
<h4 style={{ color: 'var(--purple)' }}>Story-Driven Persistence</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
Every choice you make feeds the AI Story Director. Your discoveries, alliances, conflicts, and triumphs
are woven into an ongoing narrative that persists across sessions. The story log is your personal
chronicle — preserved locally, always accessible, never lost.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>
"Come back tomorrow" continues your story where you left off.
</div>
</div>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Persistence model:</strong> The core game and co-op servers both use SpacetimeDB exclusively there is no localStorage.
In the core game, SpacetimeDB runs locally on the player's machine. "Persistence" means the local database file survives browser restart.
On co-op servers, SpacetimeDB runs on a remote server. Persistence is permanent and shared. The only difference is where the database
process runs; the persistence model is identical.
<strong>Future multiplayer considerations:</strong> If multiplayer servers are added post-V1, the architecture will
extend to support remote state synchronization. The single-player foundation ensures all systems work locally first —
multiplayer would be an optional enhancement, not a requirement.
</div>
{/* ═══ SOUND & AUDIO ═══ */}
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]">
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-6</span>
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-4</span>
<h2 style={{ margin: 0 }}>Sound & Audio Design</h2>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Audio reinforces the spreadsheet.</strong> This game is not a flight sim and audio should not pretend it is.
Sound design serves three purposes: (1) information delivery — alerts, status changes, notifications;
(2) atmosphere — ambient space sounds that make the galaxy feel alive; (3) feedback — clicks, confirmations,
and economic sounds that make spreadsheet actions feel satisfying. Audio is always optional — the game is fully
playable with sound muted, but richer with it.
<strong>Audio reinforces the narrative.</strong> Sound design serves three purposes: (1) information delivery — alerts,
status changes, notifications; (2) atmosphere — ambient space sounds that make the galaxy feel alive;
(3) feedback — clicks, confirmations, and sounds that make actions feel satisfying. Audio is always optional.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Audio Categories</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead>
<tr><th>Category</th><th>Purpose</th><th>Examples</th><th>Priority</th></tr>
<tr><th>Category</th><th>Purpose</th><th>Examples</th></tr>
</thead>
<tbody>
{[
{ cat: 'Alerts', purpose: 'Critical state changes that demand attention', examples: 'Red Alert klaxon (shields <25%), target lock acquired, CONCORD warning, incoming damage alarm, disconnect sound', priority: 'Critical always audible, respects master volume only' },
{ cat: 'UI Feedback', purpose: 'Confirm player actions in panels', examples: 'Order placed (cash register ding), module fitted (mechanical click), skill leveled up (ascending tone), ISK received (soft chime), insurance purchased (stamp sound)', priority: 'High respects UI volume slider' },
{ cat: 'Ambient', purpose: 'Atmosphere and spatial awareness', examples: 'Station hum (docked), solar wind (in space), mining laser drone, market chatter (station background), warp tunnel whoosh', priority: 'Medium respects ambient volume slider' },
{ cat: 'Combat', purpose: 'Combat state feedback', examples: 'Weapon firing (per type: beam hum, bolt crack, missile launch), shield hit (energy crackle), armor hit (metallic impact), hull hit (structural groan), capacitor warning (low power hum), weapon offline (power-down whine)', priority: 'High respects combat volume slider' },
{ cat: 'World Events', purpose: 'Environment storytelling', examples: 'Anomaly detection ping, faction broadcast (radio static + voice), fauna migration rumble, explosion (distant), wormhole opening', priority: 'Medium respects world volume slider' },
{ cat: 'Zora Voice', purpose: 'Ship AI spoken responses', examples: 'Status reports, combat warnings, market tips, tutorial hints. Voice synthesis via Voice Synthesizer module. See Ship AI Modules.', priority: 'High respects voice volume slider. Can be disabled entirely.' },
{ cat: 'Alerts', purpose: 'Critical state changes', examples: 'Shields low, target lock, incoming damage, story event notification' },
{ cat: 'UI Feedback', purpose: 'Confirm player actions', examples: 'Order placed, module fitted, skill leveled, ISK received' },
{ cat: 'Ambient', purpose: 'Atmosphere and immersion', examples: 'Station hum (docked), solar wind, mining laser, warp tunnel' },
{ cat: 'Combat', purpose: 'Combat state feedback', examples: 'Weapon firing, shield hit, armor hit, capacitor warning' },
{ cat: 'Story Events', purpose: 'Narrative moments', examples: 'Discovery ping, faction broadcast, AI Story Director narration' },
{ cat: 'Voice', purpose: 'Character dialogue', examples: 'Zora responses, NPC agent dialogue, story narration' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--fg)' }}>{row.cat}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.purpose}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.examples}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.priority}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="grid grid-cols-2 gap-5 max-[900px]:grid-cols-1" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Volume Controls</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Master:</strong> 0100%. Controls all audio. Default 80%.</li>
<li><strong style={{ color: 'var(--fg)' }}>UI:</strong> Panel sounds, market dings, fitting clicks. Default 70%.</li>
<li><strong style={{ color: 'var(--fg)' }}>Combat:</strong> Weapons, impacts, Red Alert. Default 80%.</li>
<li><strong style={{ color: 'var(--fg)' }}>Ambient:</strong> Background atmosphere. Default 50%.</li>
<li><strong style={{ color: 'var(--fg)' }}>World:</strong> Event sounds, fauna, anomalies. Default 60%.</li>
<li><strong style={{ color: 'var(--fg)' }}>Voice:</strong> Zora and NPC dialogue. Default 90%. Has separate mute toggle.</li>
</ul>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Spatial Audio Rules</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Combat sounds are directional — weapon fire comes from the direction of the source</li>
<li>Distant events are muffled (low-pass filter scales with distance)</li>
<li>Station ambient sounds only play when docked (cross-fade on dock/undock)</li>
<li>Warp tunnel audio fades in during acceleration, peaks at cruise, fades out on deceleration</li>
<li>Zora's voice always comes from "center" she's inside your head, not in space</li>
<li>Audio never provides exclusive gameplay information — everything audible has a visual equivalent</li>
</ul>
</div>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Implementation note:</strong> Audio is Phase 7 scope (Single-Player Polish). Phase 06 can ship with placeholder
sounds or no sounds at all. The audio system should be built on the Web Audio API with a thin abstraction layer
that maps game events to sound triggers. Sound assets can be procedurally generated or placeholder bleeps until
a proper sound design pass is done.
</div>
{/* ═══ LOCALIZATION ═══ */}
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]">
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-7</span>
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-5</span>
<h2 style={{ margin: 0 }}>Localization & Internationalization</h2>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>MVP is English-only. This section documents the decision and the architecture that makes future localization non-breaking.</strong>
Adding languages post-launch should require translators and asset work, not code changes. All user-facing strings
flow through a lookup layer from day one — even if that layer only returns English.
<strong>MVP is English-only with day-one i18n architecture.</strong> Adding languages post-launch should require translators
and asset work, not code changes. All user-facing strings flow through a lookup layer from day one.
</div>
<div className="grid grid-cols-2 gap-5 max-[900px]:grid-cols-1" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>What Ships in English Only (MVP)</h4>
<h4 style={{ color: 'var(--cyan)' }}>i18n Architecture</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>All UI labels, button text, and panel headers</li>
<li>Tutorial mission dialogue</li>
<li>Zora personality templates (Tier 0)</li>
<li>NPC agent dialogue</li>
<li>Error messages and status notifications</li>
<li>Item, module, and ship names</li>
<li><strong style={{ color: 'var(--fg)' }}>String keys:</strong> All user-facing text uses lookup keys: <code>t("market.order.placed")</code></li>
<li><strong style={{ color: 'var(--fg)' }}>Number formatting:</strong> ISK values use <code>Intl.NumberFormat</code></li>
<li><strong style={{ color: 'var(--fg)' }}>Date/time:</strong> Timestamps use <code>Intl.DateTimeFormat</code></li>
<li><strong style={{ color: 'var(--fg)' }}>Pluralization:</strong> ICU message format for count-aware strings</li>
<li><strong style={{ color: 'var(--fg)' }}>Layout:</strong> CSS flexbox/grid with logical properties for RTL readiness</li>
</ul>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>i18n Architecture (Day-One Foundation)</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>String keys:</strong> All user-facing text uses lookup keys, not hardcoded strings. <code>t("market.order.placed")</code> not <code>"Order placed"</code></li>
<li><strong style={{ color: 'var(--fg)' }}>Number formatting:</strong> ISK values, quantities, percentages use <code>Intl.NumberFormat</code> from day one</li>
<li><strong style={{ color: 'var(--fg)' }}>Date/time:</strong> Timestamps use <code>Intl.DateTimeFormat</code>. Relative time ("5 minutes ago") via <code>Intl.RelativeTimeFormat</code></li>
<li><strong style={{ color: 'var(--fg)' }}>Pluralization:</strong> Use ICU message format for count-aware strings ("1 item" vs "5 items")</li>
<li><strong style={{ color: 'var(--fg)' }}>Layout:</strong> UI components use CSS flexbox/grid. No hardcoded pixel widths that assume English string lengths</li>
<li><strong style={{ color: 'var(--fg)' }}>RTL ready:</strong> CSS logical properties (start/end, not left/right) for future Arabic/Hebrew support</li>
</ul>
<h4 style={{ color: 'var(--green)' }}>Post-MVP Priority</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
First languages after English determined by player population. Estimated effort per language:
12 weeks for translation + 23 days for QA with RTL layout testing if applicable.
</p>
</div>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Not localized (by design):</strong> Player names, chat messages, corporation names, and galaxy story log entries
are user-generated content that is never translated. Zora Tier 1+ (LLM-assisted) dialogue would need per-language
prompting — that's a Tier 2 scope concern, not MVP.
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Post-MVP language priority:</strong> The first languages after English would be determined by player population.
The i18n architecture supports adding a new language by dropping in a translation file no code changes.
Estimated effort per language: 12 weeks for translation + 23 days for QA with RTL layout testing if applicable.
</div>
{/* ═══ ACCESSIBILITY ═══ */}
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]">
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-8</span>
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-6</span>
<h2 style={{ margin: 0 }}>Accessibility</h2>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>A spreadsheet game should be the most accessible genre in the world.</strong>
The core gameplay involves reading tables, managing numbers, and clicking buttons activities that web browsers
already excel at supporting. The following accessibility targets are baseline requirements, not nice-to-haves.
Every feature listed here ships in Phase 7 (Single-Player Polish).
<strong>A narrative-driven game should be the most accessible genre.</strong>
Reading tables, managing numbers, and clicking buttonsactivities browsers excel at supporting.
The following accessibility targets are baseline requirements.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Accessibility Requirements</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead>
<tr><th>Area</th><th>Requirement</th><th>Implementation</th><th>Phase</th></tr>
<tr><th>Area</th><th>Requirement</th></tr>
</thead>
<tbody>
{[
{ area: 'Color Blindness', req: 'All color-coded information must have a secondary indicator (pattern, icon, label, or shape)', impl: 'Shield (cyan) → label "SHD". Armor (yellow) → label "ARM". Hull (red) → label "HUL". Security levels use text labels + icons, not just color. Market price changes use ▲/▼ arrows alongside green/red.', phase: '7' },
{ area: 'Keyboard Navigation', req: 'Every action reachable by keyboard. No mouse-only workflows.', impl: 'Tab order follows logical panel flow. Enter activates focused element. Arrow keys navigate table rows. Escape closes panels. Number keys for power allocation (1=weapons, 2=shields, 3=engines, 4=aux). F1-F8 for module activation.', phase: '7' },
{ area: 'Screen Reader', req: 'All panels and data tables announce state changes. Live regions for combat updates.', impl: 'ARIA labels on all interactive elements. role="grid" on data tables with aria-rowcount. aria-live="polite" on ISK balance, cargo capacity, skill XP. aria-live="assertive" on combat damage and Red Alert. Screen reader announcements for market order fills.', phase: '7' },
{ area: 'Text Scaling', req: 'UI remains usable at 200% browser zoom and with large font settings.', impl: 'All font sizes in rem. All layouts use CSS grid/flexbox with min/max sizing. Tables scroll horizontally rather than overflow. Panel widths are percentage-based, not fixed pixels.', phase: '7' },
{ area: 'Reduced Motion', req: 'Respect prefers-reduced-motion. No animations that could cause discomfort.', impl: 'CSS media query: disable particle effects, smooth scrolling, and HUD animations. Red Alert uses static red border instead of pulsing. Power allocation bars snap instead of animating. Mining cycle uses progress bar, not spinning animation.', phase: '7' },
{ area: 'Contrast', req: 'All text meets WCAG AA contrast ratio (4.5:1 for normal text, 3:1 for large text).', impl: 'Var(--fg) on var(--bg) already exceeds 7:1. Dim text (--fg-dim) checked to meet 4.5:1. Interactive elements have visible focus indicators with 3:1 contrast against adjacent colors. Red Alert border is high-contrast red (#ff0000) against dark background.', phase: '7' },
{ area: 'Cognitive Load', req: 'Information density is manageable. Players can hide complexity.', impl: 'Collapsible panels. Summary view vs. detail view toggle. Zora provides guided assistance. Red Alert collapses non-essential HUD elements. Tutorial hints can be disabled. Settings persist per player.', phase: '7' },
{ area: 'Input Timing', req: 'No time-critical inputs required for core gameplay. Combat is manageable at any APM.', impl: "Power allocation has no per-second requirements — the skill is in choosing distribution, not speed. Mining is click-and-wait. Market orders don't expire mid-interaction. Only exception: PvP combat on co-op servers, which is inherently competitive.", phase: '7' },
{ area: 'Color Blindness', req: 'All color-coded information has secondary indicator (icon, label, shape)' },
{ area: 'Keyboard Navigation', req: 'Every action reachable by keyboard. Tab order, Enter, Escape, arrow keys' },
{ area: 'Screen Reader', req: 'All panels and data tables announce state changes. ARIA labels on interactive elements' },
{ area: 'Text Scaling', req: 'UI remains usable at 200% browser zoom. All layouts use responsive sizing' },
{ area: 'Reduced Motion', req: 'Respect prefers-reduced-motion. Disable animations when requested' },
{ area: 'Contrast', req: 'All text meets WCAG AA contrast ratio (4.5:1 for normal text)' },
{ area: 'Cognitive Load', req: 'Collapsible panels. Summary vs detail view. Tutorial hints can be disabled' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--fg)' }}>{row.area}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.req}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.impl}</td>
<td className="font-mono tabular-nums">{row.phase}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>PvP accessibility note:</strong> PvP combat on co-op servers inherently involves time pressure rerouting power,
selecting targets, and reacting to damage. These cannot be fully de-timed without removing the competitive element.
Players with motor impairments can (1) stay in high-sec where PvP is punished, (2) focus on PvE, industry,
and market gameplay which are fully accessible, or (3) use fleet roles that require less real-time input
(logistics, scouting). The game should never require PvP to progress.
</div>
{/* ═══ MOVEMENT & COLLISION ═══ */}
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Testing:</strong> Accessibility validation is part of Gate 4 (Core Game Complete). The acceptance test is:
(1) navigate all panels via keyboard only, (2) complete a mining-sell cycle with screen reader enabled,
(3) verify all color-coded info has secondary indicators in grayscale. Automated: run axe-core or Lighthouse
accessibility audit as part of CI.
</div>
{/* ═══ CUSTOM MOVEMENT & COLLISION ═══ */}
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]" style={{ marginTop: 'var(--sp-8)' }}>
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-9</span>
<h2 style={{ margin: 0 }}>Custom Movement & Collision (No Physics Engine)</h2>
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]'" style={{ marginTop: 'var(--sp-8)' }}>
<span className="rounded-full bg-[rgba(240,160,48,0.08)] px-2 py-0.5 font-mono text-[0.7rem] text-accent">ARCH-7</span>
<h2 style={{ margin: 0 }}>Movement & Collision (No Physics Engine)</h2>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Decision:</strong> The Bevy game client uses a hand-rolled kinematic movement system and distance-based
collision detection. We do <strong>not</strong> integrate Rapier, Avian, XPBD, or any other rigid-body physics engine.
Raycasting and collision are implemented as pure geometry functions over circles in 2D.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Why No Physics Engine</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead>
<tr><th>Concern</th><th>Custom Kinematic</th><th>Physics Engine (Rapier/Avian)</th></tr>
</thead>
<tbody>
{[
{ c: 'Genre fit', custom: 'Native — FTL/Windward style is point-and-shoot, not rigid bodies.', engine: 'Overkill — solvers are for stacking, joints, real impulses.' },
{ c: 'Arcade feel', custom: 'Native — drag, snap turns, no inertia when undesired.', engine: 'Fight the solver — disabling bounce/friction is config hell.' },
{ c: 'Compile time', custom: '0 added', engine: '+3090s on every clean build' },
{ c: 'Binary size', custom: '0 added', engine: '+25 MB' },
{ c: 'Determinism', custom: 'Trivial — linear math, no solver iterations.', engine: 'Hard — solver iterations + float ops in unpredictable order.' },
{ c: 'Network prediction', custom: 'Linear extrapolation of owned values.', engine: 'Replaying the solver is a nightmare.' },
{ c: 'Bug surface', custom: '~50 lines you wrote.', engine: 'Tens of thousands of lines you didnt.' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--fg)' }}>{row.c}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.custom}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.engine}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>What "Physics" Means in This Game</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead>
<tr><th>Gameplay Need</th><th>Implementation</th><th>Math</th></tr>
</thead>
<tbody>
{[
{ need: 'Ship traversal', impl: 'Transform updates with Velocity, MaxSpeed, TurnRate, optional Drag.', math: 'pos += velocity * dt' },
{ need: 'Planet/station orbiting', impl: 'Orbit component on child entity of star system.', math: 'pos = center + r * (cos(θ), sin(θ))' },
{ need: 'Projectile vs ship hits', impl: 'Distance check each tick (no raycast needed for fast projectiles).', math: '‖a b‖ < r_a + r_b' },
{ need: 'Weapon targeting / LOS', impl: 'Ray-circle intersection, pick smallest t.', math: 'Quadratic: ‖o + t·d c‖² = r²' },
{ need: 'Ship-ship separation', impl: 'Push apart by overlap distance.', math: 'delta = (a b).normalize() * (r_a + r_b dist)' },
{ need: 'Docking / proximity triggers', impl: 'Distance check, fire event when crossing threshold.', math: '‖a b‖ < trigger_radius' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--fg)' }}>{row.need}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.impl}</td>
<td style={{ color: 'var(--cyan)', fontSize: '0.8rem', fontFamily: 'var(--font-mono)' }}>{row.math}</td>
</tr>
))}
</tbody>
</table>
<strong>Decision:</strong> The Bevy game client uses hand-rolled kinematic movement and distance-based collision.
We do <strong>not</strong> use a physics engine — movement is point-and-shoot, not rigid bodies.
</div>
<div className="grid grid-cols-2 gap-5 max-[900px]:grid-cols-1" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Movement Module Layout</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', color: 'var(--fg-dim)', lineHeight: 1.7 }}>
<span style={{ color: 'var(--purple)' }}>apps/game/src/gameplay/movement/</span><br/>
&nbsp;&nbsp;mod.rs&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style={{ color: 'var(--muted)' }}>// MovementPlugin</span><br/>
&nbsp;&nbsp;components.rs&nbsp;&nbsp;<span style={{ color: 'var(--muted)' }}>// Velocity, MaxSpeed, TurnRate, Drag</span><br/>
&nbsp;&nbsp;kinematic.rs&nbsp;&nbsp;&nbsp;<span style={{ color: 'var(--muted)' }}>// move + drag + clamp systems</span><br/>
&nbsp;&nbsp;orbit.rs&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style={{ color: 'var(--muted)' }}>// Orbit component + update_orbits</span>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 'var(--sp-3) 0 0 0' }}>
All systems run on Bevys <code>Time&lt;Fixed&gt;</code> schedule for stable, deterministic ticks that
align cleanly with SpacetimeDB updates.
</p>
<h4 style={{ color: 'var(--cyan)' }}>Why No Physics Engine</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Native to FTL/Windward style — point-and-shoot, not rigid bodies</li>
<li>Arcade feel — drag, snap turns, no unwanted inertia</li>
<li>Zero compile time and binary size overhead</li>
<li>Trivial determinism — linear math, no solver iterations</li>
<li>Smaller bug surface — ~50 lines you wrote vs tens of thousands you didn't</li>
</ul>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Physics Module Layout</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.8rem', color: 'var(--fg-dim)', lineHeight: 1.7 }}>
<span style={{ color: 'var(--purple)' }}>apps/game/src/gameplay/physics/</span><br/>
&nbsp;&nbsp;mod.rs&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style={{ color: 'var(--muted)' }}>// PhysicsPlugin</span><br/>
&nbsp;&nbsp;geometry.rs&nbsp;&nbsp;&nbsp;<span style={{ color: 'var(--muted)' }}>// ray_vs_circle, overlaps, separate</span><br/>
&nbsp;&nbsp;broad_phase.rs&nbsp;<span style={{ color: 'var(--muted)' }}>// (later) uniform grid</span><br/>
&nbsp;&nbsp;systems.rs&nbsp;&nbsp;&nbsp;&nbsp;<span style={{ color: 'var(--muted)' }}>// projectile_hits, ship_separation</span>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 'var(--sp-3) 0 0 0' }}>
Geometry functions are pure <code>pub fn</code>s. Systems are thin Bevy wrappers that query entities and
call them. No third-party deps.
</p>
<h4 style={{ color: 'var(--green)' }}>What Physics Means Here</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Ship traversal: <code>pos += velocity * dt</code></li>
<li>Projectiles vs ships: Distance check <code>a b &lt; r_a + r_b</code></li>
<li>Targeting: Ray-circle intersection</li>
<li>Separation: Push apart by overlap distance</li>
<li>Triggers: Distance check for docking/proximity</li>
</ul>
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Scaling Tiers</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead>
<tr><th>Concurrent Entities</th><th>Strategy</th><th>Estimated Cost</th></tr>
</thead>
<tbody>
<tr><td>{'< 500'}</td><td>Iterate all entities, linear scan.</td><td style={{ color: 'var(--fg-dim)' }}>Trivial no broad-phase needed.</td></tr>
<tr><td>500 10,000</td><td>Flat array of (Entity, Vec2, radius), still linear.</td><td style={{ color: 'var(--fg-dim)' }}>Cache-friendly, single microsecond per query.</td></tr>
<tr><td>{'> 10,000'}</td><td>Add uniform grid or quadtree (50 LOC).</td><td style={{ color: 'var(--fg-dim)' }}>Out of scope for FTL-style combat density.</td></tr>
</tbody>
</table>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Escape hatch when to reconsider:</strong> If destructible ship chunks that tumble, rotate, and stack
become a core visual, Rapier can be introduced <em>only for debris entities</em> while ships and projectiles
stay kinematic. This is additive: the custom movement/collision code does not need to be rewritten. The three
things a real solver would buy stacking, joints, continuous CCD are explicitly out of scope for arcade
space combat.
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Network determinism note:</strong> Because movement and collision are pure functions of position,
velocity, and dt, every client given the same inputs produces the same outputs. This makes client-side
prediction and rollback against SpacetimeDB straightforward a physics engines solver would make this
property very difficult to guarantee.
</div>
</div>
);
}

View File

@@ -7,10 +7,10 @@ export function BackendPage() {
return (
<div className="mx-auto max-w-content">
<h1 style={{ marginBottom: '8px' }}>SpacetimeDB Backend Model</h1>
<h1 style={{ marginBottom: '8px' }}>Game State & Data Model</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '680px' }}>
The backend holds persistent, authoritative state and exposes server-side reducers for game
actions. Clients subscribe to the rows they need and update reactively.
The game client maintains persistent, authoritative state locally. Data structures define
all game entities and their relationships. State updates are processed immediately and saved to local storage.
</p>
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-6)' }}>

View File

@@ -0,0 +1,958 @@
// @ts-nocheck
import * as React from 'react';
const LAST_UPDATED = '2025-06-16';
const STORAGE_KEY = 'void-nav-kanban-data';
type Column = 'done' | 'in-progress' | 'todo' | 'backlog';
interface FeatureCard {
id: string;
title: string;
description: string;
details: string;
category: string;
files: string;
notes?: string;
comments?: Comment[];
tags?: string[];
status?: string;
}
interface Comment {
id: string;
text: string;
timestamp: number;
author: string;
}
interface ColumnData {
title: string;
color: string;
bgColor: string;
items: FeatureCard[];
}
// Initial static data
const INITIAL_COLUMN_DATA: Record<Column, ColumnData> = {
done: {
title: 'DONE',
color: 'var(--green)',
bgColor: 'rgba(34,197,94,0.05)',
items: [
{
id: 'G1',
title: 'Galaxy Generation & Viewer',
description: 'Complete procedural spiral galaxy with core cluster, disk layers, and beam structures. Interactive 3D viewer with star selection, parameter controls, and system connections.',
details: 'Fully functional: generates star systems with faction distribution, nearest-neighbor connections, and POI contents. Control panel allows real-time parameter adjustment.',
category: 'Onboarding',
files: 'apps/game/src/gameplay/galaxy/mod.rs, apps/game/src/gameplay/galaxy/contents.rs',
},
{
id: 'G2',
title: 'Character Creation',
description: 'Bannerlord-style character generation with name selection, 4 origin archetypes, 3 ship choices, and 5-step backstory questionnaire with stat bonuses.',
details: 'Fully functional: complete UI with scrollable panels, stat calculation display, and character draft persistence for campaign.',
category: 'Onboarding',
files: 'apps/game/src/gameplay/character_creation/mod.rs',
},
{
id: 'G3',
title: 'Starting Base Selection',
description: 'System selection from outer-galaxy candidates with 3D visualization, POI display, and interactive camera controls.',
details: 'Fully functional: shows candidate systems from generated galaxy, with camera focus on selection and system contents preview.',
category: 'Onboarding',
files: 'apps/game/src/gameplay/starting_base/mod.rs',
},
{
id: 'S1',
title: 'Game State Management',
description: 'AppState with clean transitions: MainMenu, Galaxy, CharacterCreation, StartingBaseSelection, InGame, Options.',
details: 'Fully functional: state system works properly with OnEnter/OnExit/Update schedules for each game phase.',
category: 'Core',
files: 'apps/game/src/state.rs',
},
{
id: 'S2',
title: 'Campaign Persistence',
description: 'Galaxy draft, character draft, and starting base selection persisted through game flow.',
details: 'Fully functional: Resources carry data through state transitions. New game properly clears previous state.',
category: 'Core',
files: 'apps/game/src/gameplay/campaign.rs',
},
{
id: 'C1',
title: 'Camera System',
description: 'Three camera modes: Orbit (for galaxy inspection), Follow (for flight), Cinematic (for docked views).',
details: 'Fully functional: orbit camera with mouse controls, camera transitions between modes, proper target following.',
category: 'Core',
files: 'apps/game/src/camera.rs',
},
{
id: 'P1',
title: 'POI Data Structures',
description: 'Complete POI type system: 7 planet types, asteroid belts, stations, stargates, anomalies, gas clouds.',
details: 'Fully functional: POI generation with orbital mechanics, faction-based biasing, and system context awareness.',
category: 'Content',
files: 'apps/game/src/gameplay/galaxy/poi.rs, apps/game/src/gameplay/galaxy/contents.rs',
},
],
},
'in-progress': {
title: 'IN PROGRESS',
color: 'var(--accent)',
bgColor: 'rgba(240,160,48,0.05)',
items: [
{
id: 'I1',
title: 'In-System Scene Framework',
description: 'Scene setup with star, POIs, and player ship. Docked/Flight state management exists.',
details: 'Framework exists: scene spawns correctly, state transitions work. But UI is commented out and most gameplay features are stubs.',
category: 'In-System',
files: 'apps/game/src/gameplay/in_system/mod.rs, apps/game/src/gameplay/in_system/scene.rs',
notes: 'UI systems removed/commented. No flight HUD visible to player.',
},
{
id: 'I2',
title: 'Target Selection',
description: 'Click-to-select POIs (stations, asteroid belts) with selection tracking.',
details: 'Framework exists: target selection works, but no gameplay uses the target yet. No distance display or spatial grouping.',
category: 'In-System',
files: 'apps/game/src/gameplay/in_system/target.rs',
notes: 'Selection system works but no gameplay features depend on it yet.',
},
{
id: 'I3',
title: 'Operations Framework',
description: 'Timed operations system with undock (3s), travel (5s). Framework for docking (4s) and mining (8s).',
details: 'Partial: undock and travel work. Docking and mining are skeleton operations marked TODO. No actual gameplay consequences.',
category: 'In-System',
files: 'apps/game/src/gameplay/in_system/operations.rs, apps/game/src/gameplay/in_system/flight.rs',
notes: 'Only undock and approach are implemented. Docking and mining are TODO.',
},
{
id: 'M1',
title: 'Basic Movement',
description: 'Click-to-move navigation with kinematic steering and velocity integration.',
details: 'Partial: ship moves to target position, but no orbital mechanics, no physics-based movement, no collision avoidance.',
category: 'In-System',
files: 'apps/game/src/gameplay/movement/mod.rs',
notes: 'Basic steering works. No actual gameplay uses movement yet.',
},
{
id: 'U1',
title: 'Basic UI Shell',
description: 'Main menu, galaxy control panel, info panels. Foundation for UI exists.',
details: 'Partial: main menu and galaxy UI work. In-game flight UI is commented out. No station mode panel swap.',
category: 'UI',
files: 'apps/game/src/ui/',
notes: 'Flight HUD and station UI are removed/commented in code.',
},
],
},
todo: {
title: 'TODO',
color: 'var(--red)',
bgColor: 'rgba(239,68,68,0.05)',
items: [
{
id: 'I4',
title: 'Complete In-System Gameplay',
description: 'Docking operation, mining operation with output, cargo collection, station interactions.',
details: 'Missing: docking only undocks, mining has no resource output. No station services, no refueling, no repairs.',
category: 'In-System',
files: 'apps/game/src/gameplay/in_system/operations.rs',
notes: 'Docks and mines marked as TODO in operations.rs.',
},
{
id: 'U2',
title: 'Flight HUD / In-Game UI',
description: 'Target reticle, ship status display, operation progress bars, contextual action buttons.',
details: 'Missing: all in-game UI is commented out. Player has no visual feedback during flight.',
category: 'UI',
files: 'apps/game/src/gameplay/in_system/',
notes: 'UI setup and update systems are commented out in mod.rs.',
},
{
id: 'M2',
title: 'Star Map Plugin',
description: 'Galaxy-scale navigation showing all systems, hyperspace lanes, and current position.',
details: 'Missing: plugin structure exists but implementation is empty. No camera, no rendering, no interaction.',
category: 'Navigation',
files: 'apps/game/src/gameplay/star_map/mod.rs',
notes: 'File contains only TODO comments and empty plugin struct.',
},
{
id: 'P2',
title: 'Physics & Collision',
description: 'Mass-based interactions, collision detection between ships and objects, spatial queries.',
details: 'Missing: only basic geometry primitives exist. No collision system, no physics simulation.',
category: 'Core',
files: 'apps/game/src/gameplay/physics/mod.rs',
notes: 'Module exports marker components only. No actual physics systems.',
},
{
id: 'R1',
title: 'Resource System',
description: 'Ore types, cargo inventory, resource collection during mining.',
details: 'Missing: no resource types defined, no inventory system, no cargo hold.',
category: 'Economy',
files: 'N/A',
notes: 'No resource or inventory modules exist in codebase.',
},
{
id: 'E1',
title: 'Economy System',
description: 'Credits, market prices, trading, station markets.',
details: 'Missing: no economy simulation, no pricing, no trade mechanics.',
category: 'Economy',
files: 'N/A',
notes: 'No economy modules exist in codebase.',
},
{
id: 'C2',
title: 'Ship Components',
description: 'Shields, armor, hull, capacitor, power grid, CPU.',
details: 'Missing: ships have no stats beyond position. No component systems.',
category: 'Ships',
files: 'N/A',
notes: 'Ship is just a mesh with position. No stats or systems.',
},
],
},
backlog: {
title: 'BACKLOG',
color: 'var(--muted)',
bgColor: 'var(--surface-raised)',
items: [
{
id: 'C3',
title: 'Combat System',
description: 'Weapons, damage resolution, shield/armor/hull pools, combat AI, target locking.',
details: 'Not started: no weapon systems, no damage calculations, no combat state. FTL-style auto-combat planned.',
category: 'Combat',
files: 'N/A',
notes: 'No combat modules exist. Design specifies FTL-style power allocation combat.',
},
{
id: 'S3',
title: 'Multiple Ship Types',
description: 'Frigate, hauler, scout, warship, battleship classes with different stats.',
details: 'Not started: only generic starter ship mesh exists. No ship catalog or stat variations.',
category: 'Ships',
files: 'N/A',
notes: 'Single cone+box+sphere ship. No classes or variations.',
},
{
id: 'F1',
title: 'Module Fitting',
description: 'High/Med/Low slots, CPU/PG constraints, module catalog, fitting screen.',
details: 'Not started: no fitting system. Design specifies full fitting with drag-and-drop.',
category: 'Ships',
files: 'N/A',
notes: 'Reference: apps/docs/src/prototypes/existing-demos/FittingDemo.tsx',
},
{
id: 'N1',
title: 'NPC Ships & AI',
description: 'NPC spawning, behavior state machines, ambient traffic, hostile pirates.',
details: 'Not started: no NPC entities, no AI systems. Design describes full NPC AI.',
category: 'NPCs',
files: 'N/A',
notes: 'No NPC modules exist in codebase.',
},
{
id: 'Q1',
title: 'Quest & Mission System',
description: 'NPC agents, mission templates (kill, courier, mining), active mission tracking.',
details: 'Not started: no quest system. Design specifies 6 mission types with rewards.',
category: 'Content',
files: 'N/A',
notes: 'Reference: design docs describe faction quests and mission templates.',
},
{
id: 'A1',
title: 'Faction System',
description: 'Faction standing, loyalty points, reputation effects, faction-controlled space.',
details: 'Not started: factions are colors only. No standing or reputation tracking.',
category: 'Social',
files: 'N/A',
notes: 'Factions exist as labels only. No gameplay effects.',
},
{
id: 'W1',
title: 'Dynamic World Events',
description: 'Random events, anomalies, faction conflicts, world state changes.',
details: 'Not started: no event system. Design describes 6 event categories.',
category: 'Content',
files: 'N/A',
notes: 'No world event modules exist.',
},
{
id: 'PR1',
title: 'Skill & Progression',
description: 'XP awards, skill catalog (Mining, Industry, Trade, Gunnery, Navigation), level 0-5.',
details: 'Not started: no XP or skills. Design specifies 5 skills with tier bonuses.',
category: 'Progression',
files: 'N/A',
notes: 'Reference: apps/docs/src/prototypes/existing-demos/ProgressionDemo.tsx',
},
{
id: 'MF1',
title: 'Manufacturing',
description: 'Blueprints, production jobs, material requirements, output queues.',
details: 'Not started: no manufacturing. Design: Ore→Mineral→Component→Module→Ship chain.',
category: 'Industry',
files: 'N/A',
notes: 'Reference: apps/docs/src/prototypes/existing-demos/RefiningDemo.tsx',
},
{
id: 'Z1',
title: 'Zora Ship AI',
description: 'AI companion with personality, soul depth, tier progression (0-5).',
details: 'Not started: no Zora implementation. Design specifies full AI companion system.',
category: 'Ships',
files: 'N/A',
notes: 'Reference: apps/docs/src/prototypes/existing-demos/ZoraDemo.tsx',
},
{
id: 'ST1.1',
title: 'Event Logging System',
description: 'Track all player actions (mining, trading, combat, exploration) as structured events.',
details: 'Missing: no event logging exists. Need event types, event bus, persistence.',
category: 'Narrative',
files: 'apps/game/src/gameplay/narrative/events.rs',
notes: 'Foundation for narrative synthesis. Must capture all player actions.',
},
{
id: 'ST1.2',
title: 'Narrative Synthesis Engine',
description: 'LLM-based system that weaves events into coherent narrative text.',
details: 'Missing: no narrative generation. Need event aggregation, story templates, LLM integration.',
category: 'Narrative',
files: 'apps/game/src/gameplay/narrative/synthesis.rs',
notes: 'Core differentiator. Use Claude API for story generation.',
},
{
id: 'ST1.3',
title: 'Campaign History Tracking',
description: 'Persistent story log with chapter divisions and key moments.',
details: 'Missing: no campaign history. Need timeline, chapter detection, milestone tracking.',
category: 'Narrative',
files: 'apps/game/src/gameplay/narrative/history.rs',
notes: 'Player should access full campaign story at any time.',
},
{
id: 'ST1.4',
title: 'Story Log UI',
description: 'In-game interface for reading campaign narrative with chapter navigation.',
details: 'Missing: no UI for narrative. Need story viewer, chapter list, search.',
category: 'Narrative',
files: 'apps/game/src/gameplay/narrative/ui.rs',
notes: 'Integrates with existing UI system. Should support export/save.',
},
{
id: 'T1',
title: 'Tutorial & Onboarding',
description: 'Guided mission sequence, skip option, tutorial hints, objective tracking.',
details: 'Not started: no tutorial system. New players have no guidance.',
category: 'UX',
files: 'N/A',
notes: 'Design specifies 7-objective tutorial chain.',
},
{
id: 'MP1',
title: 'Multiplayer / Backend',
description: 'Server auth, multiplayer sessions, cloud persistence.',
details: 'Removed: multiplayer services deleted. Game is now single-player only with local persistence.',
category: 'Infrastructure',
files: 'Removed (services/spacetimedb, services/auth)',
notes: 'Multiplayer infrastructure has been removed to focus on single-player experience.',
},
],
},
};
const CATEGORIES = ['Onboarding', 'Core', 'Content', 'In-System', 'UI', 'Navigation', 'Economy', 'Ships', 'Combat', 'NPCs', 'Social', 'Progression', 'Industry', 'Narrative', 'UX', 'Infrastructure'];
export function KanbanBoardPage() {
// Load saved data from localStorage or use initial data
const [columnData, setColumnData] = React.useState<Record<Column, ColumnData>>(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
return JSON.parse(saved);
} catch (e) {
console.error('Failed to parse saved kanban data:', e);
return INITIAL_COLUMN_DATA;
}
}
return INITIAL_COLUMN_DATA;
});
// Save to localStorage whenever columnData changes
React.useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(columnData));
}, [columnData]);
// Expanded card state
const [expandedCard, setExpandedCard] = React.useState<string | null>(null);
// Comment input state
const [commentInputs, setCommentInputs] = React.useState<Record<string, string>>({});
// New tag input state
const [tagInputs, setTagInputs] = React.useState<Record<string, string>>({});
const counts = React.useMemo(() => {
return {
done: columnData.done.items.length,
'in-progress': columnData['in-progress'].items.length,
todo: columnData.todo.items.length,
backlog: columnData.backlog.items.length,
total: Object.values(columnData).reduce((sum, col) => sum + col.items.length, 0),
};
}, [columnData]);
const completionRate = React.useMemo(() => {
const weighted = counts.done + (counts['in-progress'] * 0.3);
return Math.round((weighted / counts.total) * 100);
}, [counts]);
// Move card to different column
const moveCard = (cardId: string, fromColumn: Column, toColumn: Column) => {
setColumnData(prev => {
const newPrev = { ...prev };
const cardIndex = newPrev[fromColumn].items.findIndex(c => c.id === cardId);
if (cardIndex === -1) return prev;
const [card] = newPrev[fromColumn].items.splice(cardIndex, 1);
newPrev[toColumn].items.push(card);
return newPrev;
});
};
// Add comment to card
const addComment = (cardId: string, text: string) => {
if (!text.trim()) return;
setColumnData(prev => {
const newPrev = { ...prev };
for (const col of Object.keys(newPrev) as Column[]) {
const card = newPrev[col].items.find(c => c.id === cardId);
if (card) {
if (!card.comments) card.comments = [];
card.comments.push({
id: `comment-${Date.now()}`,
text: text.trim(),
timestamp: Date.now(),
author: 'User',
});
break;
}
}
return newPrev;
});
setCommentInputs(prev => ({ ...prev, [cardId]: '' }));
};
// Add tag to card
const addTag = (cardId: string, tag: string) => {
if (!tag.trim()) return;
setColumnData(prev => {
const newPrev = { ...prev };
for (const col of Object.keys(newPrev) as Column[]) {
const card = newPrev[col].items.find(c => c.id === cardId);
if (card) {
if (!card.tags) card.tags = [];
if (!card.tags.includes(tag.trim())) {
card.tags.push(tag.trim());
}
break;
}
}
return newPrev;
});
setTagInputs(prev => ({ ...prev, [cardId]: '' }));
};
// Remove tag from card
const removeTag = (cardId: string, tagToRemove: string) => {
setColumnData(prev => {
const newPrev = { ...prev };
for (const col of Object.keys(newPrev) as Column[]) {
const card = newPrev[col].items.find(c => c.id === cardId);
if (card && card.tags) {
card.tags = card.tags.filter(t => t !== tagToRemove);
break;
}
}
return newPrev;
});
};
// Reset to initial data
const resetData = () => {
if (confirm('Reset all kanban data to defaults? This will delete all comments and tags.')) {
setColumnData(INITIAL_COLUMN_DATA);
localStorage.removeItem(STORAGE_KEY);
}
};
// Export data
const exportData = () => {
const dataStr = JSON.stringify(columnData, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `kanban-export-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="mx-auto max-w-content">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<h1 style={{ marginBottom: '0' }}>Implementation Board</h1>
<div style={{ display: 'flex', gap: 'var(--sp-2)' }}>
<button
onClick={exportData}
style={{
padding: '4px 12px',
fontSize: '0.8rem',
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
Export Data
</button>
<button
onClick={resetData}
style={{
padding: '4px 12px',
fontSize: '0.8rem',
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
Reset Data
</button>
</div>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '720px' }}>
Interactive kanban board. Updated on {LAST_UPDATED}.
Click cards to expand for comments and tags. Drag cards or use dropdowns to change status.
Data persists in localStorage.
</p>
{/* Stats overview */}
<div className="mb-6 grid grid-cols-[repeat(auto-fit,minmax(140px,1fr))] gap-4" style={{ marginTop: 'var(--sp-5)' }}>
<div className="rounded-xl border border-border bg-surface p-4 max-md:rounded-lg max-md:p-3" style={{ borderTop: '3px solid var(--green)' }}>
<div className="font-mono text-[1.4rem] font-bold tabular-nums" style={{ color: 'var(--green)' }}>{counts.done}</div>
<div className="mt-1 text-[0.7rem] uppercase tracking-[0.05em] text-muted">Done</div>
</div>
<div className="rounded-xl border border-border bg-surface p-4 max-md:rounded-lg max-md:p-3" style={{ borderTop: '3px solid var(--accent)' }}>
<div className="font-mono text-[1.4rem] font-bold tabular-nums" style={{ color: 'var(--accent)' }}>{counts['in-progress']}</div>
<div className="mt-1 text-[0.7rem] uppercase tracking-[0.05em] text-muted">In Progress</div>
</div>
<div className="rounded-xl border border-border bg-surface p-4 max-md:rounded-lg max-md:p-3" style={{ borderTop: '3px solid var(--red)' }}>
<div className="font-mono text-[1.4rem] font-bold tabular-nums" style={{ color: 'var(--red)' }}>{counts.todo}</div>
<div className="mt-1 text-[0.7rem] uppercase tracking-[0.05em] text-muted">Todo</div>
</div>
<div className="rounded-xl border border-border bg-surface p-4 max-md:rounded-lg max-md:p-3" style={{ borderTop: '3px solid var(--muted)' }}>
<div className="font-mono text-[1.4rem] font-bold tabular-nums" style={{ color: 'var(--muted)' }}>{counts.backlog}</div>
<div className="mt-1 text-[0.7rem] uppercase tracking-[0.05em] text-muted">Backlog</div>
</div>
</div>
{/* Overall progress */}
<div className="mb-6 rounded-lg border border-border bg-surface p-4 max-md:rounded-lg max-md:p-3">
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-2)' }}>
<span style={{ fontSize: '0.85rem', color: 'var(--fg)', fontWeight: 500 }}>Overall Implementation Progress</span>
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: '0.85rem', color: 'var(--fg-dim)' }}>
{completionRate}%
</span>
</div>
<div style={{
width: '100%',
height: '8px',
borderRadius: '4px',
background: 'var(--surface-raised)',
overflow: 'hidden',
}}>
<div style={{
width: `${completionRate}%`,
height: '100%',
borderRadius: '4px',
background: completionRate > 50 ? 'var(--green)' : completionRate > 25 ? 'var(--accent)' : 'var(--red)',
}} />
</div>
</div>
{/* Critical insight banner */}
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-amber/30 bg-amber/10 text-amber" style={{ maxWidth: '720px' }}>
<strong>Current State:</strong> The game has a complete <em>onboarding experience</em> (galaxy character base) but lacks a playable game loop.
The core systems (mining, trading, combat, economy) are frameworks without gameplay. Most in-System UI is commented out.
This reflects a focus on foundational architecture before gameplay implementation.
</div>
{/* Kanban board */}
<div style={{ marginTop: 'var(--sp-6)' }}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, minmax(280px, 1fr))',
gap: 'var(--sp-4)',
overflowX: 'auto',
paddingBottom: 'var(--sp-4)',
}}>
{(Object.keys(columnData) as Column[]).map((columnKey) => {
const column = columnData[columnKey];
return (
<div
key={columnKey}
style={{
background: column.bgColor,
borderRadius: 'var(--radius-md)',
padding: 'var(--sp-3)',
border: '1px solid var(--border)',
minHeight: '400px',
display: 'flex',
flexDirection: 'column',
}}
>
{/* Column header */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--sp-2)',
marginBottom: 'var(--sp-3)',
paddingBottom: 'var(--sp-2)',
borderBottom: '2px solid var(--border)',
}}>
<div style={{
width: '10px',
height: '10px',
borderRadius: '50%',
background: column.color,
boxShadow: `0 0 8px ${column.color}66`,
}} />
<h3 style={{ margin: 0, fontSize: '0.9rem', fontWeight: 600, color: 'var(--fg)' }}>
{column.title}
</h3>
<span style={{
marginLeft: 'auto',
fontFamily: 'var(--font-mono)',
fontSize: '0.75rem',
color: 'var(--muted)',
background: 'var(--surface)',
padding: '2px 8px',
borderRadius: 'var(--radius-pill)',
}}>
{column.items.length}
</span>
</div>
{/* Cards */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sp-2)', flex: 1 }}>
{column.items.map((item) => {
const isExpanded = expandedCard === item.id;
const hasComments = item.comments && item.comments.length > 0;
const hasTags = item.tags && item.tags.length > 0;
return (
<div
key={item.id}
style={{
background: 'var(--surface)',
border: `1px solid ${isExpanded ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 'var(--radius-md)',
padding: 'var(--sp-3)',
cursor: 'pointer',
transition: 'all 0.15s ease',
boxShadow: isExpanded ? '0 2px 8px rgba(0,0,0,0.1)' : 'none',
}}
onClick={() => setExpandedCard(isExpanded ? null : item.id)}
className="hover:shadow-sm"
>
{/* Card header */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--sp-2)', marginBottom: 'var(--sp-2)' }}>
<span style={{
fontFamily: 'var(--font-mono)',
fontSize: '0.65rem',
color: 'var(--muted)',
background: 'var(--surface-raised)',
padding: '2px 6px',
borderRadius: 'var(--radius-sm)',
}}>
{item.id}
</span>
<span style={{
fontSize: '0.7rem',
color: 'var(--fg-dim)',
background: 'rgba(100,100,100,0.1)',
padding: '2px 6px',
borderRadius: 'var(--radius-sm)',
}}>
{item.category}
</span>
<span style={{ marginLeft: 'auto', fontSize: '0.7rem' }}>
{hasComments && `💬 ${item.comments!.length} `}
{hasTags && `🏷️ ${item.tags!.length}`}
</span>
</div>
{/* Card title */}
<h4 style={{ margin: '0 0 var(--sp-1) 0', fontSize: '0.9rem', fontWeight: 500, color: 'var(--fg)' }}>
{item.title}
</h4>
{/* Card description */}
<p style={{ margin: '0 0 var(--sp-1) 0', fontSize: '0.8rem', color: 'var(--fg-dim)', lineHeight: '1.4' }}>
{item.description}
</p>
{/* Card details */}
<p style={{ margin: '0 0 var(--sp-2) 0', fontSize: '0.75rem', color: 'var(--fg-dim)', lineHeight: '1.4', fontStyle: 'italic' }}>
{item.details}
</p>
{/* Card footer */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sp-1)' }}>
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>
📁 {item.files}
</div>
{item.notes && (
<div style={{
fontSize: '0.7rem',
color: 'var(--accent)',
padding: '4px 8px',
background: 'rgba(240,160,48,0.08)',
borderRadius: 'var(--radius-sm)',
borderLeft: '2px solid var(--accent)',
}}>
💡 {item.notes}
</div>
)}
{/* Tags display */}
{hasTags && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '4px' }}>
{item.tags!.map(tag => (
<span
key={tag}
onClick={(e) => {
e.stopPropagation();
removeTag(item.id, tag);
}}
style={{
fontSize: '0.65rem',
padding: '2px 6px',
background: 'rgba(100,150,255,0.1)',
border: '1px solid rgba(100,150,255,0.3)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
title="Click to remove tag"
>
🏷 {tag}
</span>
))}
</div>
)}
</div>
{/* Expanded section */}
{isExpanded && (
<div style={{ marginTop: 'var(--sp-3)', paddingTop: 'var(--sp-3)', borderTop: '1px solid var(--border)' }}>
{/* Status change dropdown */}
<div style={{ marginBottom: 'var(--sp-3)' }}>
<label style={{ fontSize: '0.75rem', color: 'var(--fg-dim)', marginRight: '8px' }}>Move to:</label>
<select
onClick={(e) => e.stopPropagation()}
onChange={(e) => {
const newColumn = e.target.value as Column;
if (newColumn !== columnKey) {
moveCard(item.id, columnKey, newColumn);
}
}}
value={columnKey}
style={{
padding: '4px 8px',
fontSize: '0.8rem',
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
color: 'var(--fg)',
}}
>
<option value="done">Done</option>
<option value="in-progress">In Progress</option>
<option value="todo">Todo</option>
<option value="backlog">Backlog</option>
</select>
</div>
{/* Add tag */}
<div style={{ marginBottom: 'var(--sp-3)', display: 'flex', gap: '8px' }}>
<input
type="text"
placeholder="Add tag..."
value={tagInputs[item.id] || ''}
onChange={(e) => setTagInputs(prev => ({ ...prev, [item.id]: e.target.value }))}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.stopPropagation();
addTag(item.id, tagInputs[item.id] || '');
}
}}
onClick={(e) => e.stopPropagation()}
style={{
flex: 1,
padding: '4px 8px',
fontSize: '0.8rem',
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
color: 'var(--fg)',
}}
/>
<button
onClick={(e) => {
e.stopPropagation();
addTag(item.id, tagInputs[item.id] || '');
}}
style={{
padding: '4px 12px',
fontSize: '0.8rem',
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
}}
>
Add Tag
</button>
</div>
{/* Comments section */}
<div>
<h5 style={{ margin: '0 0 var(--sp-2) 0', fontSize: '0.85rem', color: 'var(--fg)' }}>Comments</h5>
{/* Existing comments */}
{hasComments ? (
<div style={{ marginBottom: 'var(--sp-2)', maxHeight: '200px', overflowY: 'auto' }}>
{item.comments!.map(comment => (
<div
key={comment.id}
style={{
padding: '8px',
marginBottom: '8px',
background: 'var(--surface-raised)',
borderRadius: 'var(--radius-sm)',
fontSize: '0.8rem',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<span style={{ fontWeight: 500, color: 'var(--fg)' }}>{comment.author}</span>
<span style={{ fontSize: '0.7rem', color: 'var(--muted)' }}>
{new Date(comment.timestamp).toLocaleString()}
</span>
</div>
<div style={{ color: 'var(--fg-dim)', lineHeight: '1.4' }}>{comment.text}</div>
</div>
))}
</div>
) : (
<p style={{ fontSize: '0.8rem', color: 'var(--muted)', fontStyle: 'italic' }}>No comments yet</p>
)}
{/* Add comment */}
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
placeholder="Add a comment..."
value={commentInputs[item.id] || ''}
onChange={(e) => setCommentInputs(prev => ({ ...prev, [item.id]: e.target.value }))}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.stopPropagation();
addComment(item.id, commentInputs[item.id] || '');
}
}}
onClick={(e) => e.stopPropagation()}
style={{
flex: 1,
padding: '8px',
fontSize: '0.8rem',
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
color: 'var(--fg)',
}}
/>
<button
onClick={(e) => {
e.stopPropagation();
addComment(item.id, commentInputs[item.id] || '');
}}
style={{
padding: '8px 16px',
fontSize: '0.8rem',
background: 'var(--accent)',
border: 'none',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
color: 'var(--surface)',
}}
>
Post
</button>
</div>
</div>
</div>
)}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
{/* Legend */}
<div style={{ marginTop: 'var(--sp-6)', padding: 'var(--sp-3)', background: 'var(--surface-raised)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border)' }}>
<h4 style={{ margin: '0 0 var(--sp-2) 0', fontSize: '0.85rem', color: 'var(--fg)' }}>Categories</h4>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--sp-2)', fontSize: '0.75rem', color: 'var(--fg-dim)' }}>
{CATEGORIES.map(cat => (
<span key={cat} style={{
padding: '2px 8px',
background: 'var(--surface)',
borderRadius: 'var(--radius-sm)',
border: '1px solid var(--border)',
}}>
{cat}
</span>
))}
</div>
</div>
{/* Notes */}
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginTop: 'var(--sp-6)', maxWidth: '720px' }}>
<strong>Implementation note:</strong> This board reflects code analysis of <code>apps/game/</code> as of {LAST_UPDATED}.
"Done" means fully functional. "In Progress" means framework exists but gameplay incomplete. "Todo" means next priority items.
See <a href="/docs/roadmap" style={{ color: 'var(--cyan)', textDecoration: 'underline' }}>Roadmap</a> for planned phases and
<a href="/docs/gameplay" style={{ color: 'var(--cyan)', textDecoration: 'underline' }}> Gameplay</a> for design details.
</div>
</div>
);
}

View File

@@ -4,36 +4,36 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
export function OverviewPage() {
return (
<div className="mx-auto max-w-content">
<h1 style={{ marginBottom: '8px' }}>Single-Player Space Exploration RPG</h1>
<h1 style={{ marginBottom: '8px' }}>Narrative-Driven Space Exploration Saga</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '1.1rem', maxWidth: '680px', marginBottom: 'var(--sp-6)' }}>
Trade, quest, fight, or simply drift and enjoy the ambiance. A charming <strong style={{ color: 'var(--fg-bright)' }}>single-player open-world space RPG</strong> inspired
by the freedom and charm of Windward Horizon set in a <strong style={{ color: 'var(--fg-bright)' }}>procedurally generated galaxy</strong> where
every campaign is unique. Explore sectors, trade between stations, take on faction quests, mine asteroids,
or simply cruise the void. Combat is <strong style={{ color: 'var(--fg-bright)' }}>FTL-style tactical resource management</strong>
no dogfighting. You are the captain who decides <em>what</em> to power and <em>when</em>, not the pilot who dodges.
The architecture supports optional co-op servers in the future, but the core experience is your solo adventure.
Every player becomes the author of their own space opera mini-series. A <strong style={{ color: 'var(--fg-bright)' }}>single-player narrative experience</strong>
inspired by the open-world freedom of <strong style={{ color: 'var(--fg-bright)' }}>Windward</strong>, the strategic depth of <strong style={{ color: 'var(--fg-bright)' }}>Stellaris</strong>,
and AI-generated storytelling reminiscent of <strong style={{ color: 'var(--fg-bright)' }}>The Templin Institute</strong>. Explore procedurally generated galaxies,
engage in diplomacy and commerce, wage wars or broker peace and watch as the AI Story Director weaves your choices
into an evolving narrative. V1 focuses on emergent storytelling, with future updates generating episodic mini-series
from your campaign history.
</p>
<div className="mb-6 grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-4">
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums text-fg-bright" style={{ color: 'var(--purple)' }}>Story</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">AI Director</div>
</div>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums text-fg-bright" style={{ color: 'var(--accent)' }}>Explore</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Core Pillar</div>
</div>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums text-fg-bright" style={{ color: 'var(--cyan)' }}>SpacetimeDB</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Backend</div>
<div className="font-mono text-[1.6rem] font-bold tabular-nums text-fg-bright" style={{ color: 'var(--cyan)' }}>Diplomacy</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Trade & Talk</div>
</div>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums text-fg-bright" style={{ color: 'var(--green)' }}>FTL Combat</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Combat Model</div>
</div>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums text-fg-bright" style={{ color: 'var(--purple)' }}>Dynamic</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">World Model</div>
<div className="font-mono text-[1.6rem] font-bold tabular-nums text-fg-bright" style={{ color: 'var(--green)' }}>Dynamic</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Living Galaxy</div>
</div>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums text-fg-bright" style={{ color: 'var(--red)' }}>Solo</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Adventure</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Your Saga</div>
</div>
</div>
@@ -43,10 +43,11 @@ export function OverviewPage() {
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan">
<strong>Primary inspiration:</strong> Windward Horizon's charm and freedom in a space setting. The game is an
<em> open-world exploration RPG</em> where the player charts their own path — trader, mercenary, explorer, or drifter.
<strong>Primary inspirations:</strong> Windward's open-world freedom, Stellaris's strategic diplomacy and commerce depth, and The Templin Institute's narrative-driven storytelling style.
The game is a <em>story-driven exploration saga</em> where the player authors their own mini-series — trader, diplomat, warmaker, or explorer.
The 3D scene is the primary viewport for spatial immersion. Panels and UI overlays handle station services, fitting, and market.
Combat uses FTL-style power management — no dogfighting. The galaxy is procedurally generated each campaign.
Combat, when it occurs, uses FTL-style power management — no dogfighting. The galaxy is procedurally generated each campaign.
The <strong>AI Story Director</strong> weaves your choices into an ongoing narrative.
</div>
<h3>Core Pillars</h3>
@@ -60,34 +61,39 @@ export function OverviewPage() {
</thead>
<tbody>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent">Open-world exploration</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Procedurally generated galaxy with unique sectors every campaign. Points of interest: stations, asteroid belts, anomalies, derelicts, signal sources. <strong style={{ color: 'var(--accent)' }}>Explore to discover resources, quests, and hidden stories.</strong> The map reveals as you travel — fog of war drives the urge to see what's out there.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Single system with POIs, procedural generation seed</span></td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-purple/25 bg-purple/8 text-purple">AI Story Director</span></td>
<td style={{ color: 'var(--fg-dim)' }}>NEW PRIMARY — Emergent narrative generation tracks your choices, alliances, conflicts, and discoveries. The AI weaves your actions into an ongoing saga. <strong style={{ color: 'var(--accent)' }}>In V1: real-time emergent storytelling. Future: episodic mini-series generation from campaign history.</strong> Every decision feeds the narrative engine.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Basic event tracking + narrative synthesis</span></td>
</tr>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan">Trade & commerce</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Buy low at one station, sell high at another. Regional price differences driven by supply/demand and faction needs. <strong style={{ color: 'var(--accent)' }}>Trade routes are the economic backbone.</strong> Towns need resources to grow supplying them leads to prosperity.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>NPC economy, regional prices, trade routes</span><br/><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan" style={{ fontSize: '0.65rem' }}>Future</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Optional player-to-player market on co-op servers</span></td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent">Exploration & Discovery</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Procedurally generated galaxy with fog of war. Discover anomalies, derelicts, new factions. <strong style={{ color: 'var(--accent)' }}>Every discovery becomes part of your story.</strong> The map reveals as you travel — fog of war drives the urge to see what's out there.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>POIs, procedural generation, discovery logging</span></td>
</tr>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">FTL-style combat</span></td>
<td style={{ color: 'var(--fg-dim)' }}>No dogfighting. Click hostile ship auto-engages manage <strong style={{ color: 'var(--fg)' }}>reactor power allocation</strong> (FTL-style) between Weapons/Shields/Engines/Aux. The skill is in <strong style={{ color: 'var(--fg)' }}>what</strong> you power, not <strong style={{ color: 'var(--fg)' }}>how fast</strong> you click. Combat creates consequences ship damage, loot, insurance but it's not the focus.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Core combat & power management</span></td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan">Trade & Diplomacy</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Stellaris-inspired economic and political systems. Supply factions, broker alliances, negotiate borders. <strong style={{ color: 'var(--accent)' }}>Commerce and conversation are equally viable paths.</strong> Trade routes and diplomatic relations shape your narrative.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Regional prices, faction standings, basic diplomacy</span><br/><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan" style={{ fontSize: '0.65rem' }}>Future</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Optional player-to-player market on co-op servers</span></td>
</tr>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-purple/25 bg-purple/8 text-purple">Ship customization</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Build the ship that suits your style — tough juggernaut that can take a beating, nimble scout that rains fire from afar, or cargo hauler keeping trade routes supplied. Swap modules to change role. <strong style={{ color: 'var(--fg)' }}>Multiple ship classes from nimble interceptors to powerful cruisers.</strong></td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-purple/25 bg-purple/8 text-purple">Ship Customization</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Your ship is your narrative vessel. Customize for trade, exploration, or conflict. Swap modules to change role. <strong style={{ color: 'var(--fg)' }}>Multiple ship classes from nimble interceptors to powerful cruisers.</strong></td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Basic fitting, multiple ship classes</span></td>
</tr>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent">Dynamic world & factions</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Stations and provinces need resources to grow. Supplying them leads to prosperity. Faction leaders involve you in their agendas — helping one may hinder another. <strong style={{ color: 'var(--fg)' }}>Enough support can let a faction leader take control of a province without a single shot fired.</strong> A living galaxy that evolves through player actions and world events.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Strategic Conflict</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Combat exists but is not the focus. When conflict occurs, it's FTL-style tactical resource management — a dramatic beat in your story, not the gameplay loop. Click hostile → ship auto-engages → manage <strong style={{ color: 'var(--fg)' }}>reactor power allocation</strong> (FTL-style) between Weapons/Shields/Engines/Aux. <strong style={{ color: 'var(--fg-dim)' }}>Secondary feature, not a core pillar.</strong></td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan">Secondary</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Basic combat system, minimized emphasis</span></td>
</tr>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-[rgba(240,160,48,0.25)] bg-[rgba(240,160,48,0.08)] text-accent">Dynamic Galaxy</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Your actions shape both the world AND the story. Stations and provinces need resources to grow. Faction leaders involve you in their agendas — helping one may hinder another. <strong style={{ color: 'var(--fg)' }}>Enough support can let a faction leader take control without a single shot fired.</strong> A living galaxy that evolves through player choices and world events.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Factions, NPC agents, quests, territory influence</span><br/><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan" style={{ fontSize: '0.65rem' }}>Future</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Living galaxy with world agents on co-op servers</span></td>
</tr>
<tr>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-red/25 bg-red/8 text-red">Solo Adventure</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Your personal galaxy to explore at your own pace. The game is <strong style={{ color: 'var(--fg)' }}>designed as a solo adventure first</strong> — every system works for one captain. The SpacetimeDB architecture naturally supports optional co-op servers later, but that's an enhancement, not the core.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Solo adventure with full game loop</span><br/><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan" style={{ fontSize: '0.65rem' }}>Future</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Optional co-op servers</span></td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-red/25 bg-red/8 text-red">Solo Saga</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Your personal galaxy to explore at your own pace. The game is <strong style={{ color: 'var(--fg)' }}>designed as a solo adventure first</strong> — every system works for one captain. <strong style={{ color: 'var(--fg)' }}>Your story, your pace.</strong> Future multiplayer servers will be an optional enhancement, not the core.</td>
<td><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-green/25 bg-green/8 text-green">Core</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Solo adventure with full narrative loop</span><br/><span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2.5 py-0.5 font-mono text-[0.7rem] border border-cyan/25 bg-cyan/8 text-cyan" style={{ fontSize: '0.65rem' }}>Future</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Optional co-op servers</span></td>
</tr>
</tbody>
</table>
@@ -98,35 +104,49 @@ export function OverviewPage() {
</div>
<div className="grid grid-cols-2 gap-5 max-[900px]:grid-cols-1">
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--purple)' }}>
<h4 style={{ color: 'var(--purple)' }}>Story-driven, not grind-driven</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Progression creates narrative moments, not stat increases. Level ups unlock story branches,
new ship classes open new narrative possibilities, and faction relationships shape your saga's plot.
<strong style={{ color: 'var(--fg)' }}>Your choices generate your mini-series.</strong>
</p>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>Exploration-first, not spreadsheet-first</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
The 3D viewport IS the game. Players spend their time flying between points of interest, discovering new sectors,
and engaging with the world. Station panels handle trading, fitting, and services but the joy is in
the journey, not the ledger. <strong style={{ color: 'var(--fg)' }}>Trade, quest, fight, or simply set sail and enjoy the ambiance.</strong>
the journey, not the ledger. <strong style={{ color: 'var(--fg)' }}>Trade, negotiate, fight, or simply set sail and enjoy the ambiance.</strong>
</p>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4">
<h4 style={{ color: 'var(--cyan)' }}>Authoritative backend</h4>
<h4 style={{ color: 'var(--cyan)' }}>Local-first architecture</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
SpacetimeDB owns authoritative game state. The browser renders state and sends player
intentions. The renderer should never become the source of truth.
The game client runs locally with local persistence for V1. Your story lives on your machine.
Future multiplayer servers will be optional the single-player experience is complete and standalone.
</p>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4">
<h4 style={{ color: 'var(--green)' }}>Every campaign is unique</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Procedurally generated galaxy maps ensure no two games are alike. Fog of war hides unexplored sectors.
Factions, resource distributions, and quest chains vary per seed. Replayability through discovery, not grind.
Factions, resource distributions, and narrative arcs vary per seed. Replayability through discovery and storytelling, not grind.
</p>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4">
<h4 style={{ color: 'var(--purple)' }}>FTL combat captain, not pilot</h4>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--orange)' }}>
<h4 style={{ color: 'var(--orange)' }}>Diplomacy and trade are viable paths</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
The player never directly flies or aims. Click a destination ship autopilots. Click a hostile ship auto-engages.
During combat, the player manages <strong style={{ color: 'var(--fg)' }}>reactor power allocation</strong> (FTL-style: weapons/shields/engines/aux)
and <strong style={{ color: 'var(--fg)' }}>subsystem targeting</strong>. You are the chief engineer deciding where
the reactor output goes not the pilot or gunner.
Inspired by Stellaris, commerce and conversation are equally valid ways to shape your galaxy. Supply factions to gain influence,
broker alliances between enemies, negotiate borders without firing a shot. <strong style={{ color: 'var(--fg)' }}>War is a choice, not the default.</strong>
</p>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--red)' }}>
<h4 style={{ color: 'var(--red)' }}>Conflict serves the story</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Combat exists but is not the focus. When conflict occurs, it's FTL-style tactical resource management — a dramatic beat in your narrative.
The player never directly flies or aims. Click a hostile → ship auto-engages. Manage reactor power allocation (weapons/shields/engines/aux).
You are the captain making strategic decisions, not the pilot dodging fire.
</p>
</div>
</div>
@@ -138,19 +158,19 @@ export function OverviewPage() {
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4 border-l-[3px] border-l-accent" style={{ padding: 'var(--sp-6) var(--sp-8)' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.85rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
<span style={{ color: 'var(--cyan)' }}>Spawn</span> <span style={{ color: 'var(--accent)' }}>Explore</span> {' '}
<span style={{ color: 'var(--green)' }}>Trade</span> {' '}
<span style={{ color: 'var(--purple)' }}>Quest</span> {' '}
<span style={{ color: 'var(--fg-bright)' }}>Fight</span> {' '}
<span style={{ color: 'var(--cyan)' }}>Upgrade Ship</span> {' '}
<span style={{ color: 'var(--accent)' }}>Sail Deeper</span>
<span style={{ color: 'var(--cyan)' }}>Spawn</span> → <span style={{ color: 'var(--accent)' }}>Discover</span> →{' '}
<span style={{ color: 'var(--green)' }}>Engage (Trade/Diplomacy/War)</span> →{' '}
<span style={{ color: 'var(--purple)' }}>AI Story Event</span> →{' '}
<span style={{ color: 'var(--fg-bright)' }}>Shape Galaxy</span> →{' '}
<span style={{ color: 'var(--cyan)' }}>Next Chapter</span>
</div>
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>The real loop is adventure.</strong> Spawn explore the unknown discover opportunities act on them grow your ship and reputation sail into deeper, riskier space.
The "mine → sell" cycle is the entry point. The endgame is about faction influence, rare discoveries, commanding powerful ships,
and shaping the galaxy through your choices <strong>whether by trade, combat, or diplomacy</strong>.
<strong>The real loop is your story.</strong> Spawn → explore the unknown → make choices that matter → watch the AI weave your actions into narrative threads →
shape factions and territories → sail into deeper chapters. The "mine → sell" cycle is the entry point. The endgame is about your unique saga,
faction influence shaped by your decisions, rare discoveries that become plot points, and commanding powerful ships as your narrative vessel.
<strong>Whether by trade, combat, or diplomacy — your choices generate your mini-series.</strong>
</div>
<h3 style={{ marginTop: 'var(--sp-6)' }}>Minimum Viable Screens</h3>

View File

@@ -4,16 +4,16 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
export function RoadmapPage() {
const eras = [
{
id: 'solo',
title: 'VOID::NAV — Single-Player Open-World RPG',
subtitle: 'Build the core adventure game locally. SpacetimeDB runs on the local machine from Phase 0 — there is no localStorage. One browser window, one captain, one procedurally generated galaxy to explore. Every feature is designed to be fun solo first.',
id: 'core',
title: 'VOID::NAV — Narrative-Driven Space Exploration Saga',
subtitle: 'Build a single-player experience where you author your own mini-series. Inspired by Windward\'s freedom, Stellaris\'s strategic depth, and AI-generated storytelling. Every campaign generates a unique tale. Local persistence from day one — your story lives on your machine.',
accent: 'var(--accent)',
phases: [
{
num: '0',
title: 'Local Skeleton & Star System',
goal: 'Vite app with local SpacetimeDB instance, rendered star system with station and asteroids. Player can connect and see the void.',
doneWhen: 'App boots, connects to local SpacetimeDB instance. Shows a star system with a station and 3 asteroids. Ship visible in 3D viewport. Game state persists in SpacetimeDB — no localStorage.',
goal: 'Game client with rendered star system, station, and asteroids. Player can spawn and see the void.',
doneWhen: 'Game boots. Shows a star system with a station and 3 asteroids. Ship visible in 3D viewport. Game state persists locally — closing and reopening restores state.',
status: 'current',
},
{
@@ -32,101 +32,37 @@ export function RoadmapPage() {
},
{
num: '3',
title: 'Combat — FTL Power Allocation',
goal: 'Auto-engage combat with reactor power management between weapons / shields / engines. Encounter NPC pirates at belts and anomalies.',
doneWhen: 'Target a hostile NPC at a belt or anomaly. Ship auto-engages. Player shifts reactor power between subsystems (FTL-style). Power allocation visibly changes combat outcome. Ship can be destroyed. NPC pirates drop loot and bounty.',
title: 'AI Story Director Foundation',
goal: 'Event tracking system. Player actions logged for narrative synthesis. Basic AI-generated story events trigger based on discoveries and choices.',
doneWhen: 'Player actions (discover POI, trade, fight, ally with faction) are logged. AI generates narrative text describing events. Story log accessible showing campaign history. Discoveries generate story beats.',
status: 'upcoming',
},
{
num: '4',
title: 'Ship Customization & Upgrades',
goal: 'Multiple ship classes (scout, hauler, warship). Module fitting with High/Med/Low slots. CPU/PG constraints. Buy new ships at stations.',
doneWhen: 'Dock at station. Open fitting screen. Equip weapons in high slots, shield booster in mid, cargo expander in low. Fitting affects combat and trade stats. Can buy a new ship class. Invalid fits rejected (insufficient CPU/PG).',
title: 'Diplomacy & Factions',
goal: 'NPC agents offer quests. Faction standing system. Stellaris-inspired diplomatic options. Trade routes and alliances shape both world and narrative.',
doneWhen: 'Talk to NPC agent at station. Accept a quest (deliver cargo, explore anomaly). Complete quest for ISK + standing. Standing unlocks better quests and prices. Supplying a station increases its prosperity. Faction relationships visible and affected by choices.',
status: 'upcoming',
},
{
num: '5',
title: 'Faction Quests & Dynamic World',
goal: 'NPC agents offer quests (kill, courier, mining, exploration). Faction standing system. Station prosperity responds to player supply. Dynamic world events spawn.',
doneWhen: 'Talk to NPC agent at station. Accept a quest (deliver cargo, kill pirates, explore anomaly). Complete quest for ISK + standing. Standing unlocks better quests and prices. Supplying a station increases its prosperity. World events spawn in explored systems.',
title: 'Ship Customization & Upgrades',
goal: 'Multiple ship classes (scout, hauler, warship). Module fitting with High/Med/Low slots. CPU/PG constraints. Buy new ships at stations.',
doneWhen: 'Dock at station. Open fitting screen. Equip weapons in high slots, shield booster in mid, cargo expander in low. Fitting affects stats. Can buy a new ship class. Invalid fits rejected (insufficient CPU/PG).',
status: 'upcoming',
},
{
num: '6',
title: 'Industry & Economy Depth',
goal: 'Refining ore into minerals. Manufacturing modules and items. Full NPC market with supply/demand. Price history and regional differences.',
doneWhen: 'Dock with ore. Refine at station. Minerals appear in hangar. Open manufacturing tab, select a blueprint, queue a job. Job completes. Product appears. Sell on NPC market. Prices react to trades. Different stations have different prices.',
title: 'Strategic Conflict (Secondary)',
goal: 'FTL-style combat as dramatic story beats, not gameplay loop. Auto-engage with power management. Consequences feed narrative.',
doneWhen: 'Target hostile NPC. Ship auto-engages. Manage reactor power (weapons/shields/engines). Combat outcome affects story (loot, bounty, reputation changes). Defeats and victories logged in story.',
status: 'upcoming',
},
{
num: '7',
title: 'Single-Player Polish & Procedural Galaxy',
goal: 'Procedurally generated multi-system galaxy. Fog of war. Full HUD. Notifications. Tutorial sequence. Ship AI companion (Zora Tier 0). Save/load.',
doneWhen: 'Full game loop is playable solo: explore → mine → trade → quest → fight → upgrade → explore deeper. Procedural galaxy generates unique systems each campaign. Fog of war reveals as you travel. HUD shows all relevant info. Tutorial guides new players through first 30 minutes. SpacetimeDB persists all state.',
status: 'future',
},
],
},
{
id: 'multi',
title: 'Optional — Co-op Servers',
subtitle: 'Promote local SpacetimeDB to a shared server for small co-op crews. Same adventure, shared with friends. Entirely optional — the core game is complete without this.',
accent: 'var(--cyan)',
phases: [
{
num: '8',
title: 'Shared Server Skeleton',
goal: 'Promote local SpacetimeDB to a shared server. Multiple clients connect and see the same state.',
doneWhen: 'Two browser windows connect to the same SpacetimeDB instance. Both see the same star system state. One client issues a move command, the other sees it. Connection status indicator works.',
status: 'future',
},
{
num: '9',
title: 'Co-op Presence & Movement',
goal: 'Players see each other in real time. Movement is server-authoritative with client-side interpolation. Small crew exploration.',
doneWhen: 'Two players in the same system. Each sees the other\'s ship. Click-to-move sends reducer, server validates, all clients interpolate movement. No desync under normal latency.',
status: 'future',
},
{
num: '10',
title: 'Shared Market & Trading',
goal: 'Player-to-player trading. Buy/sell orders between co-op crew members. Shared price discovery.',
doneWhen: 'Player A places a sell order. Player B sees it in the market table and buys. ISK and items transfer atomically. Order book shows depth. Market history is shared.',
status: 'future',
},
{
num: '11',
title: 'Chat & Communication',
goal: 'Local chat for co-op crew, delayed comms for immersion.',
doneWhen: 'Players in the same system see chat in real time. PMs arrive with light-speed delay. Crew can coordinate exploration.',
status: 'future',
},
{
num: '12',
title: 'Living Galaxy — World Agents + Ship AI Tier 1',
goal: 'Background agent scheduler (BitCraft model). NPC trade convoys, faction skirmishes, anomaly spawns, migration routes. Ship AI promoted to LLM-assisted dialogue.',
doneWhen: 'Server spawns world events without player input. Events appear in the galaxy story log. Faction borders shift over time. Anomalies appear and expire. A returning player sees the galaxy has changed. Tier 1 Zora: soul.md as real system prompt, LLM-generated dialogue, comms module enables natural language responses.',
status: 'future',
},
{
num: '13',
title: 'Co-op Combat',
goal: 'Crew members fight together. Multiple ships in an engagement. Coordinate against tough NPCs.',
doneWhen: 'Two players engage each other. Both manage power allocation. Server resolves combat ticks authoritatively. Both clients see damage applied. Loser\'s ship drops loot (or wreck). Kill log records the event.',
status: 'future',
},
{
num: '14',
title: 'Crews & Shared Progress',
goal: 'Form a persistent crew. Shared wallet, coordinated objectives, crew-level progression.',
doneWhen: 'Players form a corp. Corp can anchor a structure in a system. Structure provides bonuses (refining yield, market tax). Sovereignty map shows corp-held systems. Rival corps can contest.',
status: 'future',
},
{
num: '15',
title: 'Co-op Polish',
goal: 'Polish pass on co-op features. Error handling, reconnection, scaling tests for small crews.',
doneWhen: 'Fresh player can create account, complete the tutorial, explore, mine, trade, fight, and customize their ship — all without hitting a dead-end or a crash. Optionally, invite a friend to join the same server for co-op. Server handles a small crew of friends.',
goal: 'Procedurally generated multi-system galaxy. Fog of war. Full HUD. Notifications. Tutorial sequence. Ship AI companion (Zora Tier 0). Story-driven progression.',
doneWhen: 'Full game loop: discover → engage → AI story event → shape galaxynext chapter. Procedural galaxy generates unique systems each campaign. Fog of war reveals as you travel. HUD shows all relevant info. Tutorial guides first 30 minutes. State persists locally.',
status: 'future',
},
],
@@ -181,18 +117,16 @@ export function RoadmapPage() {
<div className="mx-auto max-w-content">
<h1 style={{ marginBottom: '8px' }}>Development Roadmap</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '720px' }}>
Two eras, sixteen phases. <strong style={{ color: 'var(--fg)' }}>The core game</strong> is a single-player open-world space RPG
with a local SpacetimeDB instance exploration, trading, FTL-style combat, ship customization, and faction quests.
<strong style={{ color: 'var(--fg)' }}>Optional co-op servers</strong> can be added later by promoting the local SpacetimeDB to a shared server.
Each phase has a verifiable done-when condition. Integration gates between phase groups ensure every system works together before advancing.
Eight phases to a complete single-player narrative experience. <strong style={{ color: 'var(--fg)' }}>Core game</strong> is a story-driven
space exploration saga inspired by Windward's freedom, Stellaris's strategic depth, and AI-generated storytelling.
Every phase has a verifiable done-when condition. Integration gates ensure all systems work together before advancing.
</p>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginTop: 'var(--sp-4)', maxWidth: '720px' }}>
<strong>Why single-player with local SpacetimeDB?</strong> The game is a solo adventure Windward Horizon in space. Networking is the biggest source of bugs and complexity.
By validating that exploration, trading, combat, fitting, and faction quests are fun locally using the <em>same</em> SpacetimeDB
persistence that would serve co-op we de-risk the entire project. There is no localStorage; SpacetimeDB is the
persistence layer from day 1. If co-op is added later, the question is only &quot;how do we share this server?&quot; not
&quot;is this game fun?&quot; or &quot;will the persistence migration work?&quot;
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-purple/20 bg-purple/8 text-purple" style={{ marginTop: 'var(--sp-4)', maxWidth: '720px' }}>
<strong>Why narrative-driven and single-player?</strong> The game is about <em>your story</em> Windward meets Stellaris with AI-generated
narrative in the style of The Templin Institute. Every choice feeds the AI Story Director, weaving your actions into an ongoing saga.
Local persistence means your story lives on your machine no cloud required, no subscriptions, no always-online. Just you and the void.
Future enhancements may add multiplayer, but the core experience is complete and standalone.
</div>
<div className="mb-5 flex items-center gap-3 border-b border-border pb-3 max-md:flex-col max-md:items-start max-md:gap-2 max-md:[&_h1]:text-[1.35rem] max-md:[&_h2]:text-[1.35rem]" style={{ marginTop: 'var(--sp-8)' }}>
@@ -209,53 +143,35 @@ export function RoadmapPage() {
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Navigate to asteroid mine fill cargo dock sell ore navigate to another station discover price difference trade.
The complete exploration and trade loop runs in a single session without errors. The galaxy feels alive and explorable.
SpacetimeDB persists the session closing the browser and reopening restores state.
Local persistence works closing and reopening restores state.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 0, 1, 2</div>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--red)' }}>
<h4 style={{ color: 'var(--red)' }}>Gate 2 Combat + Customization (after Phase 4)</h4>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--purple)' }}>
<h4 style={{ color: 'var(--purple)' }}>Gate 2 Story Foundation (after Phase 3)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Customize a ship at station undock encounter NPC pirate manage FTL power allocation destroy or be destroyed
collect loot buy upgrades try a different ship class. Combat and ship customization form a satisfying adventure loop.
Player actions generate story events. AI Story Director weaves discoveries, trades, and choices into narrative text.
Story log accessible showing campaign history. The game feels like it's telling YOUR story.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 04</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 03</div>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Gate 3 Full Adventure Loop (after Phase 6)</h4>
<h4 style={{ color: 'var(--green)' }}>Gate 3 — Full Adventure Loop (after Phase 5)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Explore mine ore refine manufacture a module fit it fight pirates accept faction quest deliver goods
earn standing unlock better missions sell excess across stations at different prices.
The complete adventure: exploration, economy, industry, and faction systems work as an integrated whole.
Explore → discover → AI story event → engage faction → complete quest → earn standing → unlock new narrative branches.
Diplomacy, trade, and story systems work as an integrated whole. Choices matter.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 06</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 05</div>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Gate 4 — Core Game Complete (after Phase 7)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Full solo game with all systems integrated: explore mine trade quest fight customize sail deeper.
Procedurally generated galaxy. Fog of war. HUD, notifications, Zora Tier 0, world events, tutorial sequence.
A new player can learn the game in one session and want to keep going. SpacetimeDB state survives restart.
Full solo game: discover → engage → AI story event → shape galaxy → next chapter.
Procedurally generated galaxy, fog of war, HUD, notifications, Zora Tier 0, world events, tutorial, conflict as story beats.
A new player can learn the game in one session and want to keep going. State persists locally.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 07 (core game)</div>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--purple)' }}>
<h4 style={{ color: 'var(--purple)' }}>Gate 5 Co-op Core (after Phase 10)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Two players on the same server. Both see each other. Both can trade on the shared market. ISK and items
transfer atomically. Movement is synced. Connection loss and reconnection work. The same solo adventure, shared with a friend.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 810</div>
</div>
<div className="mb-6 rounded-xl border border-border bg-surface p-6 max-md:rounded-lg max-md:p-4" style={{ borderLeft: '3px solid var(--fg-dim)' }}>
<h4 style={{ color: 'var(--fg-dim)' }}>Gate 6 Co-op Ready (after Phase 15)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Fresh player can: create account, complete tutorial, explore their first system, mine and trade, survive a PvE encounter,
customize their ship, accept and complete a faction quest and optionally invite a friend to share the adventure all without crashes or dead ends.
Server handles a small crew of friends.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 015 (all)</div>
</div>
</div>
<div style={{ marginTop: 'var(--sp-8)' }}>
@@ -281,7 +197,7 @@ export function RoadmapPage() {
color: era.accent,
border: `1px solid ${era.accent === 'var(--accent)' ? 'var(--accent-border)' : 'rgba(34,211,238,0.3)'}`,
}}>
ERA {ei + 1}
CORE GAME
</span>
<div>
<h2 style={{ margin: 0, fontSize: '1.1rem' }}>{era.title}</h2>
@@ -300,6 +216,12 @@ export function RoadmapPage() {
</div>
))}
</div>
<div className="mb-5 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan" style={{ marginTop: 'var(--sp-8)', maxWidth: '720px' }}>
<strong>Future Enhancements</strong> Post-V1 features may include episodic mini-series generation (AI looks back at campaign history
and generates "episodes" like a Templin Institute video), multiplayer co-op servers, and expanded narrative systems.
These are optional enhancements the core single-player narrative experience is complete and standalone.
</div>
</div>
);
}

View File

@@ -17,7 +17,7 @@ export function TechStackPage() {
frontend: {
choice: 'Vite + React + TypeScript',
reason: 'Fast client-side app setup, no server-rendering complexity, good for a real-time game UI.',
whyNot: "Next.js is not necessary for the MVP. The prototype is a client-side real-time game, not a content site. Next.js can be introduced later for marketing pages, account pages, SSR, or API routes outside SpacetimeDB.",
whyNot: "Next.js is not necessary for the MVP. The prototype is a client-side real-time game, not a content site. Next.js can be introduced later for marketing pages, account pages, SSR, or API routes.",
},
rendering: {
choice: 'React Three Fiber',
@@ -30,9 +30,9 @@ export function TechStackPage() {
whyNot: "Redux would add ceremony without benefit at prototype scale. Context API re-renders would hurt performance with frequent state updates.",
},
backend: {
choice: 'SpacetimeDB',
reason: 'Real-time backend/database with server-side reducers and live client subscriptions. Authoritative state, persistence, and optional co-op all in one.',
whyNot: "A custom Node.js + PostgreSQL backend would require building real-time sync, subscriptions, and authoritative game logic from scratch.",
choice: 'Local Persistence (Rust/Bevy)',
reason: 'Game client manages state locally with persistent storage. No cloud dependency, no always-online requirement. Your story lives on your machine.',
whyNot: "Server-based backends would add complexity, hosting costs, and always-online requirements. Local-first is simpler and respects player ownership.",
},
styling: {
choice: 'Tailwind CSS',
@@ -40,9 +40,9 @@ export function TechStackPage() {
whyNot: "CSS-in-JS adds runtime overhead. Vanilla CSS at this scale would slow panel iteration.",
},
auth: {
choice: 'SpacetimeDB Identity (MVP)',
reason: 'Keep early identity/session handling simple. Add external auth only after core loop works.',
whyNot: "Full OAuth/JWT would add complexity before we know the right identity model for the game.",
choice: 'Local Profile (MVP)',
reason: 'Simple local profile management. Player creates captain name and saves locally. Add external auth only if multiplayer is added later.',
whyNot: "Full OAuth/JWT would add complexity before we know the right identity model. Local is sufficient for single-player narrative experience.",
},
};
@@ -116,8 +116,8 @@ export function TechStackPage() {
</tr>
<tr>
<td style={{ color: 'var(--accent)' }}>Backend</td>
<td>SpacetimeDB</td>
<td style={{ color: 'var(--fg-dim)' }}>Real-time backend with reducers and subscriptions.</td>
<td>Local Persistence (Rust/Bevy)</td>
<td style={{ color: 'var(--fg-dim)' }}>Game client manages state locally with persistent storage.</td>
</tr>
<tr>
<td style={{ color: 'var(--fg-dim)' }}>Styling</td>
@@ -126,8 +126,8 @@ export function TechStackPage() {
</tr>
<tr>
<td style={{ color: 'var(--muted)' }}>Auth</td>
<td>SpacetimeDB identity</td>
<td style={{ color: 'var(--fg-dim)' }}>Simple identity first; add auth later.</td>
<td>Local Profile</td>
<td style={{ color: 'var(--fg-dim)' }}>Simple local profile management. Single-player experience.</td>
</tr>
</tbody>
</table>
@@ -146,14 +146,11 @@ export function TechStackPage() {
<div className="mb-5 overflow-x-auto rounded-lg border border-border bg-surface px-5 py-4 [&_code]:bg-transparent [&_code]:p-0 [&_code]:font-mono [&_code]:text-[0.82rem] [&_code]:leading-[1.6] [&_code]:text-fg-dim">
<code>
<span className="text-muted italic">{'//'} Starter project layout</span><br/>
<span className="text-purple">/client/src/app</span> <span className="text-muted italic">{' //'} App shell and providers</span><br/>
<span className="text-purple">/client/src/network</span> <span className="text-muted italic">{' //'} SpacetimeDB client, subscriptions</span><br/>
<span className="text-purple">/client/src/game</span> <span className="text-muted italic">{' //'} Renderer-independent types, view models</span><br/>
<span className="text-purple">/client/src/store</span> <span className="text-muted italic">{' //'} Zustand stores</span><br/>
<span className="text-purple">/client/src/renderers/r3f</span> <span className="text-muted italic">{'//'} R3F scene, meshes, camera</span><br/>
<span className="text-purple">/client/src/ui</span> <span className="text-muted italic">{' //'} HUD, inventory, market, chat</span><br/>
<span className="text-purple">/server-spacetime/src</span> <span className="text-muted italic">{' //'} SpacetimeDB module, reducers</span><br/>
<span className="text-muted italic">{'//'} Project layout</span><br/>
<span className="text-purple">/apps/game/src</span> <span className="text-muted italic">{' //'} Rust + Bevy game client</span><br/>
<span className="text-purple">/apps/docs/src</span> <span className="text-muted italic">{' //'} Design documentation</span><br/>
<span className="text-purple">/apps/site/src</span> <span className="text-muted italic">{' //'} Landing page website</span><br/>
<span className="text-purple">/packages/ui/src</span> <span className="text-muted italic">{' //'} Shared UI components</span><br/>
</code>
</div>
@@ -161,11 +158,11 @@ export function TechStackPage() {
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead><tr><th>Package</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td>vite, react, react-dom, typescript</td><td>Core frontend.</td></tr>
<tr><td>three, @react-three/fiber, @react-three/drei</td><td>3D scene and helper controls.</td></tr>
<tr><td>vite, react, react-dom, typescript</td><td>Core frontend (docs, site).</td></tr>
<tr><td>three, @react-three/fiber, @react-three/drei</td><td>3D scene and helper controls (docs demos).</td></tr>
<tr><td>zustand</td><td>Local UI/game view state.</td></tr>
<tr><td>tailwindcss</td><td>Panel and HUD styling.</td></tr>
<tr><td>spacetimedb TS client</td><td>Backend connection, reducers, subscriptions.</td></tr>
<tr><td>bevy, rust</td><td>Game client engine and language.</td></tr>
</tbody>
</table>
</div>

View File

@@ -1,922 +0,0 @@
// @ts-nocheck
import * as React from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
const LAST_UPDATED = '2026-06-02';
const BACKEND_TABLES_IMPLEMENTED = 9;
const BACKEND_TABLES_DESIGNED = 56;
const BACKEND_REDUCERS_IMPLEMENTED = 12;
const BACKEND_REDUCERS_DESIGNED = 40;
type Phase = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7';
type Layer = 'backend' | 'frontend' | 'scene' | 'prototype';
type Status = 'done' | 'partial' | 'missing' | 'blocked';
interface TodoItem {
id: string;
title: string;
phase: Phase;
layer: Layer;
status: Status;
desc: string;
detail?: string;
files?: string;
blockedBy?: string[];
}
const STATUS_COLORS: Record<Status, string> = {
done: 'var(--green)',
partial: 'var(--accent)',
missing: 'var(--red)',
blocked: 'var(--muted)',
};
const STATUS_LABELS: Record<Status, string> = {
done: 'DONE',
partial: 'PARTIAL',
missing: 'MISSING',
blocked: 'BLOCKED',
};
const LAYER_LABELS: Record<Layer, string> = {
backend: 'SpacetimeDB',
frontend: 'Game UI',
scene: '3D Scene',
prototype: 'Prototype Ready',
};
const items: TodoItem[] = [
// ── PHASE 0: LOCAL SKELETON ──
{
id: 'P0-B1',
title: 'Player identity & connection',
phase: '0',
layer: 'backend',
status: 'done',
desc: 'Player table, connectPlayer, renamePlayer, ping reducers. Auth token persistence.',
files: 'services/spacetimedb/src/index.ts:143-218',
},
{
id: 'P0-B2',
title: 'World seeding (single system)',
phase: '0',
layer: 'backend',
status: 'done',
desc: 'seedWorld creates Solace system, station, 2 POIs. Hardcoded constants.',
files: 'services/spacetimedb/src/index.ts:138-141, 458-500',
},
{
id: 'P0-B3',
title: 'Ship state & wallet',
phase: '0',
layer: 'backend',
status: 'done',
desc: 'Ship table (position, flightMode, dockedStationId, cargoCapacity). Wallet table with ISK.',
files: 'services/spacetimedb/src/index.ts:28-45, 93-100',
},
{
id: 'P0-B4',
title: 'Server event logging',
phase: '0',
layer: 'backend',
status: 'done',
desc: 'ServerEvent table. Every reducer writes a typed event.',
files: 'services/spacetimedb/src/index.ts:114-123, 580-588',
},
{
id: 'P0-F1',
title: 'SpacetimeDB client connection',
phase: '0',
layer: 'frontend',
status: 'done',
desc: 'Connection lifecycle, table subscriptions, auth token, re-render triggers.',
files: 'apps/game/src/spacetime/client.ts, useSpacetimeConnection.ts, useGameSession.ts',
},
{
id: 'P0-F2',
title: 'Game shell layout',
phase: '0',
layer: 'frontend',
status: 'partial',
desc: 'Three-column responsive layout with left/right sidebars and center 3D viewport. Missing: center panel area is empty (hidden on large screens). No loading/splash screen.',
detail: 'Middle column <section> is hidden lg:block with no content. No station mode panel swap. Inline "Pilot" info should be its own component.',
files: 'apps/game/src/GameShell.tsx:70',
},
{
id: 'P0-F3',
title: 'Connection panel',
phase: '0',
layer: 'frontend',
status: 'done',
desc: 'Connection status, identity, transport/reducer messages, pilot name input, rename/ping.',
files: 'apps/game/src/ui/ConnectionPanel.tsx',
},
{
id: 'P0-S1',
title: '3D space scene foundation',
phase: '0',
layer: 'scene',
status: 'partial',
desc: 'R3F Canvas, environment (stars, fog, lights), station mesh, asteroid mesh, ship mesh. All clickable.',
detail: 'Missing: camera controls (no orbit/zoom/pan — fixed position). No sun/star light source. No nebulae. Grid plane is debug-only.',
files: 'apps/game/src/scene/',
},
{
id: 'P0-S2',
title: 'Ship mesh with engine glow',
phase: '0',
layer: 'scene',
status: 'partial',
desc: 'Cone+box+sphere ship. Engine glow changes by flightMode. Bob/roll animation.',
detail: 'Missing: no damage state visualization, no shield bubble, no warp animation, no mining laser beam (only flat Line), no module hardpoints visible.',
files: 'apps/game/src/scene/ShipMesh.tsx',
},
// ── PHASE 1: NAVIGATION & EXPLORATION ──
{
id: 'P1-B1',
title: 'Approach operation (start + complete)',
phase: '1',
layer: 'backend',
status: 'done',
desc: 'Timed approach with 5s duration. Validates undocked, target selected, target in system. Position update on complete.',
files: 'services/spacetimedb/src/index.ts:248-299',
},
{
id: 'P1-B2',
title: 'Target selection',
phase: '1',
layer: 'backend',
status: 'done',
desc: 'selectTarget validates POI exists and is in current system.',
files: 'services/spacetimedb/src/index.ts:236-246',
},
{
id: 'P1-B3',
title: 'Warp travel between systems',
phase: '1',
layer: 'backend',
status: 'missing',
desc: 'No stargate table, no warp sequence, no jump mechanics. Ship is locked to single system (Solace).',
blockedBy: ['P5-B3'],
},
{
id: 'P1-B4',
title: 'Stargate network',
phase: '1',
layer: 'backend',
status: 'missing',
desc: 'No stargate table. No gate activation range, jump cooldown, gate cloak. Design describes full stargate mechanics.',
},
{
id: 'P1-F1',
title: 'Mini star map (SVG)',
phase: '1',
layer: 'frontend',
status: 'done',
desc: '2D SVG top-down system map showing POIs, ship position, operation lines, click-to-select.',
files: 'apps/game/src/ui/MiniStarMap.tsx',
},
{
id: 'P1-F2',
title: 'Target panel with POI list',
phase: '1',
layer: 'frontend',
status: 'partial',
desc: 'Shows selected target and clickable POI list.',
detail: 'Missing: no distance to target, no spatial grouping, no range info, no sortable overview-style list.',
files: 'apps/game/src/ui/TargetPanel.tsx',
},
{
id: 'P1-F3',
title: 'Command rail (contextual actions)',
phase: '1',
layer: 'frontend',
status: 'partial',
desc: 'Bottom-fixed action bar with contextual buttons and progress bars for operations.',
detail: 'Missing: no abort/cancel operation, no warp/jump commands, no combat commands, no station service buttons, keyboard shortcut hints not visible.',
files: 'apps/game/src/ui/CommandRail.tsx',
},
{
id: 'P1-F4',
title: 'Keyboard shortcuts',
phase: '1',
layer: 'frontend',
status: 'partial',
desc: 'U=undock, D=dock, A=approach, M=mine, 1-9=select target.',
detail: 'Missing: no shortcut for sell, refine, fit, combat, warp. No modifier keys. No shortcut discovery overlay.',
files: 'apps/game/src/ui/useKeyboardShortcuts.ts',
},
{
id: 'P1-S1',
title: 'Ship position interpolation during approach',
phase: '1',
layer: 'scene',
status: 'done',
desc: 'Lerp-based smooth movement from current position to target during approach operation.',
files: 'apps/game/src/scene/GameSpaceScene.tsx',
},
{
id: 'P1-S2',
title: 'Camera controls (orbit, zoom, pan)',
phase: '1',
layer: 'scene',
status: 'missing',
desc: 'Camera is fixed at [8,9,44] with gentle sine drift. No OrbitControls, no zoom, no user camera interaction.',
},
{
id: 'P1-S3',
title: 'Warp/travel visual effects',
phase: '1',
layer: 'scene',
status: 'missing',
desc: 'No warp tunnel effect, no gate jump animation, no system transition. Prototype has a full warp sequence demo.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/WarpTravelDemo.tsx',
},
// ── PHASE 2: MINING, INVENTORY & TRADE ──
{
id: 'P2-B1',
title: 'Mining operation (start + complete)',
phase: '2',
layer: 'backend',
status: 'done',
desc: 'Timed mining with 6s duration. Validates undocked, at asteroid belt, cargo not full. Adds 1000 Veldspar to cargo.',
files: 'services/spacetimedb/src/index.ts:326-409',
},
{
id: 'P2-B2',
title: 'Cargo/inventory system',
phase: '2',
layer: 'backend',
status: 'partial',
desc: 'CargoItem table with stacking. Capacity tracking.',
detail: 'Missing: only "ore" category used. No mineral, module, loot, commodity categories. No jettison. No item splitting.',
files: 'services/spacetimedb/src/index.ts:79-91',
},
{
id: 'P2-B3',
title: 'NPC ore market (sell only)',
phase: '2',
layer: 'backend',
status: 'partial',
desc: 'sellOreToNpcMarket validates docked, ore category, quantity. Credits wallet.',
detail: 'Missing: only 1 ore type (Veldspar) at fixed 12 ISK. No NPC buy orders for minerals/modules. No sell-side NPC. No dynamic pricing. No regional variation. Prices hardcoded in both backend AND frontend.',
files: 'services/spacetimedb/src/index.ts:411-456',
},
{
id: 'P2-B4',
title: 'Dynamic NPC pricing (supply/demand)',
phase: '2',
layer: 'backend',
status: 'missing',
desc: 'Design describes full NPC pricing algorithm with EMA, demand pressure, regional seeds, anti-arbitrage. Nothing implemented.',
},
{
id: 'P2-B5',
title: 'Regional price differences',
phase: '2',
layer: 'backend',
status: 'missing',
desc: 'Single system only. No way for prices to differ between stations.',
blockedBy: ['P1-B3', 'P5-B3'],
},
{
id: 'P2-F1',
title: 'Cargo panel',
phase: '2',
layer: 'frontend',
status: 'partial',
desc: 'Displays cargo items with name, quantity, category, unit price.',
detail: 'Missing: no jettison button, no item stacking/splitting, no item details, no ore-type filtering.',
files: 'apps/game/src/ui/CargoPanel.tsx',
},
{
id: 'P2-F2',
title: 'NPC market panel',
phase: '2',
layer: 'frontend',
status: 'partial',
desc: 'Sell ore UI with quantity input, max button, payout preview. Only visible when docked.',
detail: 'Missing: only Veldspar. Prices hardcoded client-side in NPC_PRICES constant. No buy orders, no order book, no price history, no charts.',
files: 'apps/game/src/ui/NpcMarketPanel.tsx:5-7',
},
{
id: 'P2-F3',
title: 'Wallet panel',
phase: '2',
layer: 'frontend',
status: 'partial',
desc: 'Shows ISK balance.',
detail: 'Missing: no transaction history, no income/expenditure breakdown.',
files: 'apps/game/src/ui/WalletPanel.tsx',
},
{
id: 'P2-F4',
title: 'Ship status panel',
phase: '2',
layer: 'frontend',
status: 'partial',
desc: 'Flight mode badge, hull/shield/speed bars, position, system, operation countdown.',
detail: 'HARDCODED: hullPct=100 always (line 44), shieldPct=Math.random() (line 45), cargoPct always 0 (line 46), speed labels are hardcoded strings (lines 32-42). No armor layer. No capacitor. No power allocation. No module activation indicators.',
files: 'apps/game/src/ui/ShipStatusPanel.tsx:44-46',
},
{
id: 'P2-S1',
title: 'Asteroid rendering (per-belt clusters)',
phase: '2',
layer: 'scene',
status: 'partial',
desc: '3 asteroids per belt with hardcoded offsets, tumble animation, clickable.',
detail: 'Missing: all identical geometry/color. No ore type differentiation. No depletion state. No mining laser beam (flat Line only).',
files: 'apps/game/src/scene/AsteroidMesh.tsx, GameSpaceScene.tsx:120-124',
},
{
id: 'P2-S2',
title: 'Mining laser beam visual',
phase: '2',
layer: 'scene',
status: 'missing',
desc: 'Only a flat amber Line drawn during mining. No beam/particle effect connecting ship to asteroid.',
},
// ── PHASE 3: COMBAT — FTL POWER ALLOCATION ──
{
id: 'P3-B1',
title: 'NPC pirate entity system',
phase: '3',
layer: 'backend',
status: 'missing',
desc: 'No npc_entities, npc_class_templates, or loot_tables. Design describes full NPC spawning, AI behavior, state machine (IDLE→AGGRO→COMBAT→FLEE→DEAD).',
},
{
id: 'P3-B2',
title: 'Combat encounter state',
phase: '3',
layer: 'backend',
status: 'missing',
desc: 'No combat session, no damage resolution, no shield/armor/hull tracking, no weapon cycling.',
},
{
id: 'P3-B3',
title: 'FTL power allocation (weapons/shields/engines/aux)',
phase: '3',
layer: 'backend',
status: 'missing',
desc: 'Design describes 4 subsystems with powered/unpowered behavior, reroute timing (1.5-3s), and failure modes. Nothing implemented.',
},
{
id: 'P3-B4',
title: 'Ship health model (shield/armor/hull)',
phase: '3',
layer: 'backend',
status: 'missing',
desc: 'Ship table has no health columns. Design describes 3 HP pools with different damage resistance.',
},
{
id: 'P3-B5',
title: 'Bounty & loot drop system',
phase: '3',
layer: 'backend',
status: 'missing',
desc: 'No bounty ISK awards, no loot tables, no wreck/loot containers. Design describes full bounty tiers and module drops.',
},
{
id: 'P3-B6',
title: 'Ship destruction & respawn',
phase: '3',
layer: 'backend',
status: 'missing',
desc: 'No death mechanic, no respawn, no insurance payout. Design describes rookie frigate respawn, loot drops, insurance.',
},
{
id: 'P3-F1',
title: 'Combat HUD (target lock, power allocation, damage)',
phase: '3',
layer: 'frontend',
status: 'missing',
desc: 'No combat UI at all. Design describes target lock reticle, power allocation bars (W/S/E/Aux), shield/armor/hull wheel, capacitor gauge, weapon timer, combat log.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/CombatDemo.tsx, game-slice/ui/SliceCombatStage.tsx',
},
{
id: 'P3-S1',
title: 'Combat 3D scene (projectiles, shields, explosions)',
phase: '3',
layer: 'scene',
status: 'missing',
desc: 'No NPC ships rendered, no projectiles, no shield effects, no explosions, no damage numbers.',
files: 'Prototype ref: apps/docs/src/prototypes/r3f/combat/CombatScene.tsx',
},
// ── PHASE 4: SHIP CUSTOMIZATION & UPGRADES ──
{
id: 'P4-B1',
title: 'Ship types catalog (multiple classes)',
phase: '4',
layer: 'backend',
status: 'missing',
desc: 'Only 1 hardcoded "Starter Frigate". Design describes 5 classes (Frigate through Battleship) with full stat tables.',
},
{
id: 'P4-B2',
title: 'Module catalog & fitting system',
phase: '4',
layer: 'backend',
status: 'missing',
desc: 'No modules_catalog, no ship_fittings table. Design describes High/Med/Low slot types, CPU/PG constraints, module categories.',
},
{
id: 'P4-B3',
title: 'Fit/unfit module reducers',
phase: '4',
layer: 'backend',
status: 'missing',
desc: 'No reducers for module management. Design validates CPU/PG budget, slot type matching.',
},
{
id: 'P4-B4',
title: 'Ship purchase/acquisition',
phase: '4',
layer: 'backend',
status: 'missing',
desc: 'No ship buying, no hangar storage, no ship switching. Design describes NPC market, hangar, and rookie frigate policy.',
},
{
id: 'P4-F1',
title: 'Fitting screen UI',
phase: '4',
layer: 'frontend',
status: 'missing',
desc: 'No fitting UI. Design describes slot layout with drag-and-drop, CPU/PG bars, module catalog.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/FittingDemo.tsx, game-slice/ui/SliceFittingService.tsx',
},
{
id: 'P4-F2',
title: 'Ship purchase screen',
phase: '4',
layer: 'frontend',
status: 'missing',
desc: 'No ship shop, no hangar view, no ship comparison UI.',
},
// ── PHASE 5: FACTION QUESTS & DYNAMIC WORLD ──
{
id: 'P5-B1',
title: 'Faction & standing system',
phase: '5',
layer: 'backend',
status: 'missing',
desc: 'No factions table, no player standing, no loyalty points. Design describes -10 to +10 standing with faction consequences.',
},
{
id: 'P5-B2',
title: 'NPC agents & mission templates',
phase: '5',
layer: 'backend',
status: 'missing',
desc: 'No npc_agents, mission_templates, active_missions tables. Design describes 6 mission types (kill, courier, mining, survey, escort, trade) with 4 reward tiers.',
},
{
id: 'P5-B3',
title: 'Procedural galaxy generation',
phase: '5',
layer: 'backend',
status: 'missing',
desc: 'Single hardcoded system (Solace). Design describes seeded RNG galaxy with MST stargate topology, regions, constellations.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/GalaxyDemo.tsx',
},
{
id: 'P5-B4',
title: 'Station prosperity (dynamic world impact)',
phase: '5',
layer: 'backend',
status: 'missing',
desc: 'No station prosperity, no resource demand model. Design describes stations growing when supplied, faction leaders taking provinces.',
},
{
id: 'P5-B5',
title: 'World events (anomalies, faction conflicts)',
phase: '5',
layer: 'backend',
status: 'missing',
desc: 'No event spawning, no participation tracking, no galaxy story log. Design describes 6 event categories with 3 notification tiers.',
},
{
id: 'P5-F1',
title: 'Agent/mission panel',
phase: '5',
layer: 'frontend',
status: 'missing',
desc: 'No NPC agent list, no available missions, no mission tracker, no standing display.',
},
{
id: 'P5-F2',
title: 'Galaxy map (multi-system)',
phase: '5',
layer: 'frontend',
status: 'missing',
desc: 'Only single-system SVG map exists. No multi-system navigation, no stargate routes, no faction overlay.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/StarMapDemo.tsx',
},
// ── PHASE 6: INDUSTRY & ECONOMY DEPTH ──
{
id: 'P6-B1',
title: 'Refining (ore → minerals)',
phase: '6',
layer: 'backend',
status: 'missing',
desc: 'No refineOre reducer, no mineral item types. Design describes 8 ore types refining to 8 minerals with skill-based yield (50-95%).',
files: 'Prototype ref: apps/docs/src/prototypes/game-slice/sliceEconomy.ts',
},
{
id: 'P6-B2',
title: 'Manufacturing (minerals → modules/ships)',
phase: '6',
layer: 'backend',
status: 'missing',
desc: 'No blueprints, no manufacturing_jobs, no production chain. Design describes 5-tier chain: Ore → Mineral → Component → Module → Ship.',
},
{
id: 'P6-B3',
title: 'Multiple ore types',
phase: '6',
layer: 'backend',
status: 'missing',
desc: 'Only Veldspar. Design describes 8 ore types (Veldspar, Scordite, Pyroxeres, Kernite, Omber, Jaspet, Hemorphite, Arkonor).',
},
{
id: 'P6-F1',
title: 'Refining UI',
phase: '6',
layer: 'frontend',
status: 'missing',
desc: 'No refining interface. Design describes batch processing with yield preview.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/RefiningDemo.tsx, game-slice/ui/SliceRefiningService.tsx',
},
{
id: 'P6-F2',
title: 'Manufacturing UI',
phase: '6',
layer: 'frontend',
status: 'missing',
desc: 'No manufacturing tab. Design describes blueprint selection, job queue, material requirements.',
},
{
id: 'P6-F3',
title: 'Market with order book & price history',
phase: '6',
layer: 'frontend',
status: 'missing',
desc: 'No order book, no charts, no bid/ask spread. Only flat NPC buy for ore.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/MarketDemo.tsx',
},
// ── PHASE 7: POLISH & PROCEDURAL GALAXY ──
{
id: 'P7-B1',
title: 'XP & skill progression',
phase: '7',
layer: 'backend',
status: 'missing',
desc: 'No skills_catalog, no XP awards. Design describes 5 skills (Mining, Industry, Trade, Gunnery, Navigation) with level 0-5.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/ProgressionDemo.tsx',
},
{
id: 'P7-B2',
title: 'Tutorial objective chain',
phase: '7',
layer: 'backend',
status: 'missing',
desc: 'No objective tracking. Prototype has 7-objective tutorial chain.',
files: 'Prototype ref: apps/docs/src/prototypes/game-slice/sliceObjectives.ts',
},
{
id: 'P7-B3',
title: 'Zora ship AI (Tier 0)',
phase: '7',
layer: 'backend',
status: 'missing',
desc: 'No Zora state, no soul depth, no personality axes, no module gating.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/ZoraDemo.tsx',
},
{
id: 'P7-B4',
title: 'CONCORD / law enforcement',
phase: '7',
layer: 'backend',
status: 'missing',
desc: 'No security status, no CONCORD response, no criminal flagging.',
},
{
id: 'P7-B5',
title: 'Insurance system',
phase: '7',
layer: 'backend',
status: 'missing',
desc: 'No insurance policies, no coverage tiers, no payout on death.',
},
{
id: 'P7-F1',
title: 'Tutorial / onboarding flow',
phase: '7',
layer: 'frontend',
status: 'missing',
desc: 'No guided mission sequence, no skip option, no Zora hints.',
},
{
id: 'P7-F2',
title: 'Full Flight Mode HUD (diegetic)',
phase: '7',
layer: 'frontend',
status: 'missing',
desc: 'No diegetic HUD overlays on 3D viewport. Design describes curved shield/armor bars, module rack, overview panel, targeting reticle, capacitor gauge.',
files: 'Prototype ref: apps/docs/src/prototypes/existing-demos/GameHudDemo.tsx',
},
{
id: 'P7-F3',
title: 'Station Mode panel UI',
phase: '7',
layer: 'frontend',
status: 'missing',
desc: 'No station service menu. No docked-mode panel swap. GameShell shows same panels regardless of dock/flight state.',
},
{
id: 'P7-F4',
title: 'Event feed with categories',
phase: '7',
layer: 'frontend',
status: 'partial',
desc: 'Basic event log exists (12 items, no pagination).',
detail: 'Missing: no color coding by event type, no filtering, no combat vs market vs system categorization, no click-to-zoom.',
files: 'apps/game/src/ui/EventFeed.tsx',
},
{
id: 'P7-F5',
title: 'Fog of war / map reveal',
phase: '7',
layer: 'frontend',
status: 'missing',
desc: 'No fog of war mechanic. All POIs visible immediately in current system.',
},
// ── CROSS-CUTTING / INFRASTRUCTURE ──
{
id: 'X-B1',
title: 'Scheduled agents (timed server logic)',
phase: '0',
layer: 'backend',
status: 'missing',
desc: 'All game logic is reducer-call driven. No timed agents for: NPC spawning, combat ticks, economy ticks, world events, balancing.',
},
{
id: 'X-F1',
title: 'Hardcoded system/station references',
phase: '1',
layer: 'frontend',
status: 'partial',
desc: 'Client subscribes to hardcoded "solace" system and "solace-prime" station. useGameSession hardcodes these lookups.',
detail: 'client.ts:77-78, useGameSession.ts:129-130. Must be replaced with dynamic system lookup once multi-system exists.',
files: 'apps/game/src/spacetime/client.ts:77-78',
},
{
id: 'X-F2',
title: 'Operation auto-completion (client polling)',
phase: '1',
layer: 'frontend',
status: 'partial',
desc: 'GameSpaceScene polls Date.now() at 120ms intervals to detect operation completion and fire completeApproach/completeMiningCycle.',
detail: 'Works but is client-driven. Design recommends server-authoritative completion via scheduled reducers.',
files: 'apps/game/src/scene/GameSpaceScene.tsx',
},
{
id: 'X-F3',
title: 'Ambient traffic / other entities',
phase: '2',
layer: 'frontend',
status: 'missing',
desc: 'No other ships visible in space. Prototype has 4 ambient entities (3 friendly + 1 hostile) with orbital movement.',
files: 'Prototype ref: apps/docs/src/prototypes/game-slice/ui/SliceFlightStage.tsx:78-92',
},
];
const PHASE_LABELS: Record<Phase, string> = {
'0': 'Phase 0 — Local Skeleton',
'1': 'Phase 1 — Navigation & Exploration',
'2': 'Phase 2 — Mining, Inventory & Trade',
'3': 'Phase 3 — Combat (FTL Power Allocation)',
'4': 'Phase 4 — Ship Customization & Upgrades',
'5': 'Phase 5 — Faction Quests & Dynamic World',
'6': 'Phase 6 — Industry & Economy Depth',
'7': 'Phase 7 — Polish & Procedural Galaxy',
};
export function TodoPage() {
const [filterPhase, setFilterPhase] = useState<string>('all');
const [filterLayer, setFilterLayer] = useState<string>('all');
const [filterStatus, setFilterStatus] = useState<string>('all');
const filtered = useMemo(() => {
return items.filter(item => {
if (filterPhase !== 'all' && item.phase !== filterPhase) return false;
if (filterLayer !== 'all' && item.layer !== filterLayer) return false;
if (filterStatus !== 'all' && item.status !== filterStatus) return false;
return true;
});
}, [filterPhase, filterLayer, filterStatus]);
const counts = useMemo(() => {
const byStatus = { done: 0, partial: 0, missing: 0, blocked: 0 };
const byPhase: Record<string, { done: number; partial: number; missing: number; blocked: number; total: number }> = {};
items.forEach(item => {
byStatus[item.status]++;
if (!byPhase[item.phase]) byPhase[item.phase] = { done: 0, partial: 0, missing: 0, blocked: 0, total: 0 };
byPhase[item.phase][item.status]++;
byPhase[item.phase].total++;
});
return { byStatus, byPhase };
}, []);
const grouped = useMemo(() => {
const groups: Record<string, TodoItem[]> = {};
filtered.forEach(item => {
const key = item.phase === '0' && item.id.startsWith('X-') ? 'cross' : item.phase;
if (!groups[key]) groups[key] = [];
groups[key].push(item);
});
return groups;
}, [filtered]);
const phaseOrder = ['0', '1', '2', '3', '4', '5', '6', '7', 'cross'];
const sortedGroups = phaseOrder.filter(p => grouped[p]).map(p => ({
key: p,
label: p === 'cross' ? 'Cross-Cutting / Infrastructure' : PHASE_LABELS[p as Phase],
items: grouped[p],
}));
return (
<div className="mx-auto max-w-content">
<h1 style={{ marginBottom: '8px' }}>Implementation Status</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '720px' }}>
Living TODO document tracking what's built vs. what's designed. Auto-generated from codebase review on {LAST_UPDATED}.
Each item maps to a specific phase, layer, and file reference.
</p>
<div className="mb-6 grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-4" style={{ marginTop: 'var(--sp-5)' }}>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums" style={{ color: 'var(--green)' }}>{counts.byStatus.done}</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Done</div>
</div>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums" style={{ color: 'var(--accent)' }}>{counts.byStatus.partial}</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Partial</div>
</div>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums" style={{ color: 'var(--red)' }}>{counts.byStatus.missing}</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Missing</div>
</div>
<div className="rounded-xl border border-border bg-surface p-5 max-md:rounded-lg max-md:p-4">
<div className="font-mono text-[1.6rem] font-bold tabular-nums" style={{ color: 'var(--muted)' }}>{counts.byStatus.blocked}</div>
<div className="mt-1 text-[0.75rem] uppercase tracking-[0.05em] text-muted">Blocked</div>
</div>
</div>
<div className="mb-6 rounded-lg px-5 py-4 text-[0.85rem] leading-[1.5] border border-cyan/20 bg-cyan/8 text-cyan">
<strong>Backend coverage:</strong> {BACKEND_TABLES_IMPLEMENTED} / {BACKEND_TABLES_DESIGNED} tables ({Math.round(BACKEND_TABLES_IMPLEMENTED / BACKEND_TABLES_DESIGNED * 100)}%).
{BACKEND_REDUCERS_IMPLEMENTED} / {BACKEND_REDUCERS_DESIGNED} reducers ({Math.round(BACKEND_REDUCERS_IMPLEMENTED / BACKEND_REDUCERS_DESIGNED * 100)}%).
All 14 interactive demos and the full game-slice prototype live in <code>apps/docs/src/prototypes/</code> using localStorage these serve as reference implementations for migration into the SpacetimeDB backend.
</div>
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-6)', flexWrap: 'wrap' }}>
<select
value={filterPhase}
onChange={e => setFilterPhase(e.target.value)}
className="rounded-lg border border-border bg-surface-raised px-3 py-1.5 font-mono text-[0.75rem] text-fg"
>
<option value="all">All Phases</option>
{Object.entries(PHASE_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
<option value="cross">Cross-Cutting</option>
</select>
<select
value={filterLayer}
onChange={e => setFilterLayer(e.target.value)}
className="rounded-lg border border-border bg-surface-raised px-3 py-1.5 font-mono text-[0.75rem] text-fg"
>
<option value="all">All Layers</option>
{Object.entries(LAYER_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
<select
value={filterStatus}
onChange={e => setFilterStatus(e.target.value)}
className="rounded-lg border border-border bg-surface-raised px-3 py-1.5 font-mono text-[0.75rem] text-fg"
>
<option value="all">All Status</option>
{Object.entries(STATUS_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
<span style={{ color: 'var(--muted)', fontSize: '0.75rem', lineHeight: '2' }}>
{filtered.length} / {items.length} items
</span>
</div>
{sortedGroups.map(group => {
const phaseCounts = group.key !== 'cross' ? counts.byPhase[group.key] : null;
const donePct = phaseCounts ? Math.round(((phaseCounts.done + phaseCounts.partial * 0.5) / phaseCounts.total) * 100) : null;
return (
<div key={group.key} style={{ marginBottom: 'var(--sp-8)' }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--sp-3)',
marginBottom: 'var(--sp-4)',
paddingBottom: 'var(--sp-2)',
borderBottom: '2px solid var(--border)',
}}>
<h2 style={{ margin: 0, fontSize: '1rem', flex: 1 }}>{group.label}</h2>
{phaseCounts && (
<div style={{ display: 'flex', gap: 'var(--sp-3)', alignItems: 'center' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--green)' }}>{phaseCounts.done} done</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--accent)' }}>{phaseCounts.partial} partial</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--red)' }}>{phaseCounts.missing + phaseCounts.blocked} missing</span>
<div style={{
width: '80px',
height: '6px',
borderRadius: '3px',
background: 'var(--surface-raised)',
overflow: 'hidden',
}}>
<div style={{
width: `${donePct}%`,
height: '100%',
borderRadius: '3px',
background: donePct > 70 ? 'var(--green)' : donePct > 30 ? 'var(--accent)' : 'var(--red)',
}} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--fg-dim)' }}>{donePct}%</span>
</div>
)}
</div>
<div className="w-full border-collapse" style={{ overflowX: 'auto' }}>
<table className="w-full border-collapse text-[0.85rem] max-md:block max-md:overflow-x-auto max-md:rounded-lg max-md:border max-md:border-border max-md:[&_thead]:table max-md:[&_tbody]:table max-md:[&_thead]:min-w-[680px] max-md:[&_tbody]:min-w-[680px] [&_th]:whitespace-nowrap [&_th]:border-b [&_th]:border-border [&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-mono [&_th]:text-[0.7rem] [&_th]:uppercase [&_th]:tracking-[0.06em] [&_th]:text-muted [&_td]:border-b [&_td]:border-border [&_td]:px-4 [&_td]:py-3 [&_td]:font-mono [&_td]:text-[0.8rem] [&_td]:text-fg [&_tr:hover_td]:bg-surface-raised">
<thead>
<tr>
<th>ID</th>
<th>Status</th>
<th>Layer</th>
<th>Title</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{group.items.map(item => (
<tr key={item.id}>
<td style={{ color: 'var(--muted)', fontSize: '0.7rem' }}>{item.id}</td>
<td>
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
padding: '2px 8px',
borderRadius: 'var(--radius-pill)',
fontSize: '0.65rem',
fontWeight: 600,
fontFamily: 'var(--font-mono)',
background: item.status === 'done' ? 'rgba(34,197,94,0.1)' : item.status === 'partial' ? 'rgba(240,160,48,0.1)' : item.status === 'blocked' ? 'var(--surface-raised)' : 'rgba(239,68,68,0.1)',
color: STATUS_COLORS[item.status],
border: `1px solid ${STATUS_COLORS[item.status]}33`,
}}>
{STATUS_LABELS[item.status]}
</span>
</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.7rem' }}>{LAYER_LABELS[item.layer]}</td>
<td style={{ fontWeight: 500, fontFamily: 'var(--font-body)', color: 'var(--fg)' }}>{item.title}</td>
<td>
<div style={{ color: 'var(--fg-dim)', fontSize: '0.8rem', fontFamily: 'var(--font-body)' }}>{item.desc}</div>
{item.detail && (
<div style={{ color: 'var(--accent)', fontSize: '0.75rem', marginTop: '4px', padding: '4px 8px', background: 'rgba(240,160,48,0.06)', borderRadius: '4px', borderLeft: '2px solid var(--accent)' }}>
{item.detail}
</div>
)}
{item.files && (
<div style={{ color: 'var(--muted)', fontSize: '0.65rem', marginTop: '4px', fontFamily: 'var(--font-mono)' }}>
{item.files}
</div>
)}
{item.blockedBy && (
<div style={{ color: 'var(--muted)', fontSize: '0.65rem', marginTop: '4px' }}>
Blocked by: {item.blockedBy.join(', ')}
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
})}
</div>
);
}

View File

@@ -7,4 +7,25 @@ export default defineConfig({
jsx: "automatic",
jsxImportSource: "react",
},
build: {
// Generate source maps for production debugging
sourcemap: true,
// Optimize chunk splitting for better caching
rollupOptions: {
output: {
manualChunks: {
// Bundle three.js and related libraries separately
"three-vendor": ["three", "@react-three/fiber", "@react-three/drei"],
// Bundle React separately (more stable across updates)
"react-vendor": ["react", "react-dom", "react-router-dom"],
},
},
},
// Chunk size warning limit (in kB)
chunkSizeWarningLimit: 1000,
},
// Optimize dependency pre-bundling
optimizeDeps: {
include: ["three", "@react-three/fiber", "@react-three/drei"],
},
});