Initial commit

This commit is contained in:
2026-05-25 13:00:20 -04:00
commit e14e43da42
49 changed files with 26892 additions and 0 deletions

673
js/pages/agents.js Normal file
View File

@@ -0,0 +1,673 @@
window.GDD = window.GDD || {};
const { useState } = React;
/* ── Shared inline styles ── */
const mono = { fontFamily: 'var(--font-mono)' };
const dimText = { color: 'var(--fg-dim)', fontSize: '0.9rem' };
const dimSmall = { color: 'var(--fg-dim)', fontSize: '0.82rem' };
/* ── Small sub-components ── */
function AgentLifecycleDiagram() {
const phases = [
{ id: 'register', label: 'REGISTER', color: 'var(--purple)', desc: 'Scheduled table row inserted with initial interval, reducer target, and payload.' },
{ id: 'sleep', label: 'SLEEP', color: 'var(--muted)', desc: 'Row sits inert. No CPU, no allocation. SpacetimeDB\'s scheduler owns the timer.' },
{ id: 'wake', label: 'WAKE', color: 'var(--accent)', desc: 'Scheduler fires the bound reducer at the scheduled timestamp. Transaction begins.' },
{ id: 'execute', label: 'EXECUTE', color: 'var(--cyan)', desc: 'Reducer logic runs: read tables, mutate state, emit events, maybe re-schedule itself.' },
{ id: 'complete', label: 'COMPLETE', color: 'var(--green)', desc: 'Transaction commits or aborts. On commit, row updated (next_fire_at) or deleted. Subscribers notified.' },
];
return (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--sp-2)', overflowX: 'auto', padding: 'var(--sp-4) 0' }}>
{phases.map((p, i) => (
<React.Fragment key={p.id}>
<div style={{
flex: '1 1 0', minWidth: '140px',
background: 'var(--surface-raised)',
border: `1px solid var(--border)`,
borderRadius: 'var(--radius-lg)',
padding: 'var(--sp-4)',
borderTop: `3px solid ${p.color}`,
}}>
<div style={{ ...mono, fontSize: '0.7rem', color: p.color, letterSpacing: '0.06em', marginBottom: 'var(--sp-2)' }}>
{p.label}
</div>
<p style={{ ...dimSmall, margin: 0, lineHeight: 1.5 }}>
{p.desc}
</p>
</div>
{i < phases.length - 1 && (
<div style={{ display: 'flex', alignItems: 'center', color: 'var(--border-light)', fontSize: '1.2rem', paddingTop: '28px' }}>
</div>
)}
</React.Fragment>
))}
</div>
);
}
function ScheduleDiagram() {
return (
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', padding: 'var(--sp-5)', marginBottom: 'var(--sp-5)' }}>
{/* Timeline visualization */}
<div style={{ position: 'relative', height: '120px', marginBottom: 'var(--sp-4)' }}>
{/* Time axis */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '2px', background: 'var(--border-light)' }} />
<div style={{ position: 'absolute', bottom: '-18px', left: 0, ...mono, fontSize: '0.65rem', color: 'var(--muted)' }}>t=0</div>
<div style={{ position: 'absolute', bottom: '-18px', right: 0, ...mono, fontSize: '0.65rem', color: 'var(--muted)' }}>t+5min</div>
{/* Fixed-interval ticks */}
{[0.15, 0.35, 0.55, 0.75, 0.92].map((pct, i) => (
<React.Fragment key={`fixed-${i}`}>
<div style={{ position: 'absolute', bottom: 0, left: `${pct * 100}%`, width: '2px', height: '10px', background: 'var(--accent)' }} />
<div style={{
position: 'absolute', bottom: '14px', left: `${pct * 100}%`, transform: 'translateX(-50%)',
width: '8px', height: '8px', borderRadius: '50%', background: 'var(--accent)',
boxShadow: '0 0 8px rgba(240,160,48,0.4)',
}} />
</React.Fragment>
))}
{/* Conditional ticks */}
{[0.45, 0.78].map((pct, i) => (
<React.Fragment key={`cond-${i}`}>
<div style={{ position: 'absolute', bottom: 0, left: `${pct * 100}%`, width: '2px', height: '10px', background: 'var(--cyan)' }} />
<div style={{
position: 'absolute', top: '10px', left: `${pct * 100}%`, transform: 'translateX(-50%)',
width: '8px', height: '8px', borderRadius: '50%', background: 'var(--cyan)',
boxShadow: '0 0 8px rgba(34,211,238,0.4)',
}} />
</React.Fragment>
))}
{/* One-shot */}
<div style={{ position: 'absolute', bottom: 0, left: '60%', width: '2px', height: '10px', background: 'var(--purple)' }} />
<div style={{
position: 'absolute', top: '40px', left: '60%', transform: 'translateX(-50%)',
width: '10px', height: '10px', borderRadius: '50%', background: 'var(--purple)',
boxShadow: '0 0 8px rgba(167,139,250,0.4)',
}} />
{/* Labels */}
<div style={{ position: 'absolute', top: 0, left: 0, display: 'flex', gap: 'var(--sp-6)', ...mono, fontSize: '0.65rem' }}>
<span style={{ color: 'var(--accent)' }}> Fixed interval</span>
<span style={{ color: 'var(--cyan)' }}> Conditional</span>
<span style={{ color: 'var(--purple)' }}> One-shot</span>
</div>
</div>
</div>
);
}
/* ── Main page component ── */
function AgentsPage() {
const [activeSection, setActiveSection] = useState('lifecycle');
const [activeCatalogPkg, setActiveCatalogPkg] = useState('game');
const tabs = [
{ id: 'lifecycle', label: 'Lifecycle' },
{ id: 'scheduling', label: 'Scheduling' },
{ id: 'killswitch', label: 'Kill-Switch' },
{ id: 'catalog', label: 'Agent Catalog' },
];
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>Agent Lifecycle &amp; Scheduling</h1>
<p style={{ ...dimText, maxWidth: '720px' }}>
The server runs dozens of background agents that drive the living world from enemy health regeneration
and building decay to NPC trade migrations and day/night cycles. These agents are not OS-level processes
or threads; they are <strong style={{ color: 'var(--fg-bright)' }}>SpacetimeDB scheduled tables</strong> that
invoke reducer functions at configurable intervals, turning the database itself into a reliable task scheduler.
</p>
{/* Section tabs */}
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-6)', marginTop: 'var(--sp-4)' }}>
{tabs.map(t => (
<button key={t.id}
className={`btn btn-sm${activeSection === t.id ? ' btn-primary' : ''}`}
onClick={() => setActiveSection(t.id)}>
{t.label}
</button>
))}
</div>
{/* ────────────────────── LIFECYCLE ────────────────────── */}
{activeSection === 'lifecycle' && (
<>
<div className="section-header">
<span className="section-num">AGENT-1</span>
<h2 style={{ margin: 0 }}>Uniform Lifecycle</h2>
</div>
<p style={{ ...dimText, marginBottom: 'var(--sp-4)' }}>
Every agent regardless of what it simulates follows the same five-phase lifecycle.
This uniformity means monitoring, debugging, and load-testing share one toolchain.
</p>
<AgentLifecycleDiagram />
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">AGENT-1.1</span>
<h2 style={{ margin: 0 }}>Scheduled Table Schema</h2>
</div>
<p style={{ ...dimText, marginBottom: 'var(--sp-4)' }}>
Each agent is a row in a <code style={{ color: 'var(--accent)' }}>scheduled_agents</code> table.
SpacetimeDB's built-in scheduler reads <code>next_fire_at</code> and invokes the
bound reducer at exactly that timestamp — no external cron, no process supervisor.
</p>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Column</th><th>Type</th><th>Purpose</th></tr>
</thead>
<tbody>
{[
{ col: 'agent_id', type: 'Identity', purpose: 'Unique agent instance key. Auto-generated on insert.' },
{ col: 'agent_type', type: 'String', purpose: 'Catalog key: "enemy_regen", "decay_tick", "npc_trade", "day_cycle", etc.' },
{ col: 'target_entity_id', type: 'u64', purpose: 'Optional entity this agent operates on (e.g. enemy_id, building_id).' },
{ col: 'reducer_name', type: 'String', purpose: 'Fully-qualified reducer to invoke: "game.enemy_regen_tick".' },
{ col: 'interval_ms', type: 'u64', purpose: 'Fixed interval in milliseconds. Zero for one-shot agents.' },
{ col: 'next_fire_at', type: 'Duration', purpose: 'Timestamp of next invocation. Scheduler reads this; agent never touches it directly after registration.' },
{ col: 'payload', type: 'JSON', purpose: 'Opaque reducer arguments. Each agent type defines its own schema.' },
{ col: 'state', type: 'enum', purpose: 'active | paused | killed. Only "active" rows are scheduled.' },
{ col: 'generation', type: 'u32', purpose: 'Monotonic counter incremented on each fire. Prevents stale re-schedules.' },
{ col: 'created_at', type: 'Timestamp', purpose: 'Registration time. Used for uptime metrics and TTL enforcement.' },
{ col: 'owner_module', type: 'String', purpose: 'Package that owns this agent: "game" or "global_module".' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem' }}>{row.col}</code></td>
<td><span className="pill pill-cyan" style={{ fontSize: '0.65rem' }}>{row.type}</span></td>
<td style={{ ...dimSmall }}>{row.purpose}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">AGENT-1.2</span>
<h2 style={{ margin: 0 }}>Registration Reducer</h2>
</div>
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
<div style={{ padding: 'var(--sp-3) var(--sp-4)', background: 'var(--surface-raised)', borderBottom: '1px solid var(--border)' }}>
<span style={{ ...mono, fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
register_agent — Pseudocode
</span>
</div>
<div className="code-block" style={{ margin: 0, borderRadius: 0 }}>
<code>
<span className="cm">// Called by game logic to spawn a new agent</span><br/>
<span className="kw">reducer</span> <span className="fn">register_agent</span>(ctx, agent_type, target_id, interval_ms, payload) {'{'}<br/>
&nbsp;&nbsp;<span className="cm">// Validate agent_type exists in agent_catalog</span><br/>
&nbsp;&nbsp;<span className="kw">let</span> catalog_entry = ctx.db.agent_catalog().<span className="fn">find</span>(agent_type);<br/>
&nbsp;&nbsp;<span className="kw">require</span>!(catalog_entry.is_some(), <span className="str">"Unknown agent type"</span>);<br/>
<br/>
&nbsp;&nbsp;<span className="cm">// Enforce per-type concurrency limit</span><br/>
&nbsp;&nbsp;<span className="kw">let</span> active_count = ctx.db.scheduled_agents().<span className="fn">iter</span>()<br/>
&nbsp;&nbsp;&nbsp;&nbsp;.filter(|a| a.agent_type == agent_type && a.state == Active).count();<br/>
&nbsp;&nbsp;<span className="kw">require</span>!(active_count {'<'} catalog_entry.max_concurrent);<br/>
<br/>
&nbsp;&nbsp;ctx.db.scheduled_agents().<span className="fn">insert</span>(ScheduledAgent {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;agent_id: Identity::generate(),<br/>
&nbsp;&nbsp;&nbsp;&nbsp;agent_type,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;target_entity_id: target_id,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;reducer_name: catalog_entry.reducer_name,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;interval_ms,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;next_fire_at: ctx.timestamp + interval_ms,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;payload,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;state: AgentState::Active,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;generation: 0,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;created_at: ctx.timestamp,<br/>
&nbsp;&nbsp;&nbsp;&nbsp;owner_module: catalog_entry.module,<br/>
&nbsp;&nbsp;{'}'});<br/>
{'}'}
</code>
</div>
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">AGENT-1.3</span>
<h2 style={{ margin: 0 }}>Execution Contract</h2>
</div>
<div className="callout callout-warn" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Atomicity guarantee:</strong> each agent fire runs inside a single SpacetimeDB transaction.
If the reducer panics or returns an error, the entire transaction rolls back — no partial state mutation.
The agent row is not updated, so the scheduler retries at the same <code>next_fire_at</code> on the next tick.
</div>
<div className="grid-3">
{[
{ title: 'Read phase', color: 'var(--cyan)', items: [
'Load agent row + payload from scheduled_agents',
'Read target entity state (enemy HP, building condition, NPC inventory)',
'Query related tables (faction relations, player density, market state)',
]},
{ title: 'Mutate phase', color: 'var(--accent)', items: [
'Apply simulation logic (regen HP, decay condition, move NPC, rotate cycle)',
'Write updated entity state back to tables',
'Maybe emit world events, story log entries, or chat notifications',
]},
{ title: 'Re-schedule phase', color: 'var(--green)', items: [
'Increment generation counter (prevents double-fire)',
'Set next_fire_at = now + interval_ms (fixed) or compute next (conditional)',
'Or delete the row entirely (one-shot or TTL expired)',
]},
].map((phase, i) => (
<div key={i} className="card" style={{ borderTop: `3px solid ${phase.color}` }}>
<h4 style={{ color: phase.color, marginBottom: 'var(--sp-3)' }}>{phase.title}</h4>
<ul style={{ ...dimSmall, margin: 0, paddingLeft: 'var(--sp-5)', lineHeight: 1.7 }}>
{phase.items.map((item, j) => <li key={j}>{item}</li>)}
</ul>
</div>
))}
</div>
</>
)}
{/* ────────────────────── SCHEDULING ────────────────────── */}
{activeSection === 'scheduling' && (
<>
<div className="section-header">
<span className="section-num">AGENT-2</span>
<h2 style={{ margin: 0 }}>Three Scheduling Strategies</h2>
</div>
<p style={{ ...dimText, marginBottom: 'var(--sp-5)' }}>
All agents share the same table schema, but <em>when</em> they fire follows one of three
strategies. The choice is baked into the agent's catalog entry and cannot be changed at
runtime without re-registration.
</p>
<ScheduleDiagram />
{/* Strategy cards */}
{[
{
id: 'fixed',
title: 'Fixed Interval',
color: 'var(--accent)',
tag: 'Most common',
desc: 'Fire every N milliseconds, unconditionally. The workhorse strategy — used for health regen, resource ticks, decay timers, and day/night cycles.',
mechanics: [
'next_fire_at = last_fire_at + interval_ms — simple arithmetic, no conditional logic.',
'If the server is overloaded and fires late, the next fire is still interval_ms from the actual fire time (not from the scheduled time). Prevents cascade acceleration.',
'Configurable jitter: ±10% of interval_ms to spread simultaneous agent fires across a window. Prevents thundering-herd CPU spikes when 200 enemies share the same regen tick.',
],
examples: 'enemy_regen (2s), building_decay (60s), resource_respawn (300s), day_night_cycle (600s)',
},
{
id: 'conditional',
title: 'Conditional',
color: 'var(--cyan)',
tag: 'Event-gated',
desc: 'Fire only when a precondition table meets criteria. The scheduler still checks on a fixed cadence, but the reducer body is a no-op until the condition is true.',
mechanics: [
'The reducer reads a condition table first (e.g. "has at least one player in system?"). If false, it re-schedules without doing work.',
'Avoids wasting cycles on empty systems: NPC trade migrations only run in systems with active markets and at least one player docked.',
'Condition tables are indexed. The read cost of a no-op conditional fire is ~microseconds.',
],
examples: 'npc_trade_route (fires only if market activity above threshold), faction_tension_eval (fires only if two factions share a border system with standing < -5)',
},
{
id: 'oneshot',
title: 'One-Shot',
color: 'var(--purple)',
tag: 'Delayed execution',
desc: 'Fire exactly once after a delay, then self-delete. Used for delayed consequences, timed explosions, cooldown expirations, and cascading event chains.',
mechanics: [
'interval_ms = 0 in the row. The scheduler fires at next_fire_at and the reducer deletes the row.',
'No re-schedule step — the agent simply ceases to exist after execution.',
'Can be re-registered by the reducer itself or by another agent, creating event chains: "pirate_raid_warning fires → 30s later pirate_raid_arrival fires → raid logic runs".',
],
examples: 'delayed_damage (3s after mine explosion), event_cascade (fires next event in a story chain), cooldown_expire (marks ability ready again)',
},
].map((strategy) => (
<div key={strategy.id} className="card" style={{ borderTop: `3px solid ${strategy.color}`, marginBottom: 'var(--sp-5)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-4)' }}>
<h3 style={{ margin: 0, color: strategy.color }}>{strategy.title}</h3>
<span className="pill pill-amber" style={{ fontSize: '0.65rem' }}>{strategy.tag}</span>
</div>
<p style={{ ...dimText, marginBottom: 'var(--sp-4)' }}>{strategy.desc}</p>
<h4 style={{ color: 'var(--fg-bright)', fontSize: '0.85rem', marginBottom: 'var(--sp-2)' }}>Mechanics</h4>
<ul style={{ ...dimSmall, margin: 0, paddingLeft: 'var(--sp-5)', lineHeight: 1.7, marginBottom: 'var(--sp-4)' }}>
{strategy.mechanics.map((m, i) => <li key={i}>{m}</li>)}
</ul>
<div style={{ ...mono, fontSize: '0.75rem', color: 'var(--muted)', borderTop: '1px solid var(--border)', paddingTop: 'var(--sp-3)' }}>
<span style={{ color: 'var(--fg-dim)' }}>Examples:</span> {strategy.examples}
</div>
</div>
))}
<div className="section-header" style={{ marginTop: 'var(--sp-6)' }}>
<span className="section-num">AGENT-2.1</span>
<h2 style={{ margin: 0 }}>Jitter &amp; Thundering Herd Prevention</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
When 500 enemy entities all share a 2-second regen interval, they were likely registered at the same time meaning
all 500 fire simultaneously. Jitter spreads each agent's <code>next_fire_at</code> by a random ±X% of its interval,
turning a single CPU spike into a smooth load curve.
</div>
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
<div style={{ padding: 'var(--sp-3) var(--sp-4)', background: 'var(--surface-raised)', borderBottom: '1px solid var(--border)' }}>
<span style={{ ...mono, fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Jitter Application — Pseudocode
</span>
</div>
<div className="code-block" style={{ margin: 0, borderRadius: 0 }}>
<code>
<span className="kw">fn</span> <span className="fn">apply_jitter</span>(base_interval_ms: <span className="type">u64</span>, jitter_pct: <span className="type">f32</span>) -> <span className="type">Duration</span> {'{'}<br/>
&nbsp;&nbsp;<span className="kw">let</span> jitter_range = (base_interval_ms <span className="kw">as</span> <span className="type">f64</span>) * (jitter_pct <span className="kw">as</span> <span className="type">f64</span>);<br/>
&nbsp;&nbsp;<span className="kw">let</span> offset = thread_rng().<span className="fn">gen_range</span>(-jitter_range..jitter_range);<br/>
&nbsp;&nbsp;Duration::from_millis((base_interval_ms <span className="kw">as</span> <span className="type">f64</span> + offset) <span className="kw">as</span> <span className="type">u64</span>)<br/>
{'}'}<br/>
<br/>
<span className="cm">// Applied during registration and each re-schedule:</span><br/>
agent.next_fire_at = ctx.timestamp + <span className="fn">apply_jitter</span>(agent.interval_ms, <span className="str">0.10</span>);
</code>
</div>
</div>
</>
)}
{/* ────────────────────── KILL-SWITCH ────────────────────── */}
{activeSection === 'killswitch' && (
<>
<div className="section-header">
<span className="section-num">AGENT-3</span>
<h2 style={{ margin: 0 }}>Global Kill-Switch</h2>
</div>
<p style={{ ...dimText, marginBottom: 'var(--sp-5)' }}>
A single admin-level toggle can pause or terminate every agent of a given type — or all agents
across the entire server. This is the primary operational lever for hotfixes, load shedding,
and emergency maintenance.
</p>
<div className="card" style={{ borderTop: '3px solid var(--red)', marginBottom: 'var(--sp-5)' }}>
<div style={{ display: 'flex', gap: 'var(--sp-5)', alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<h4 style={{ color: 'var(--red)', marginBottom: 'var(--sp-2)' }}>Emergency Shutdown</h4>
<p style={{ ...dimSmall, margin: 0 }}>
A single reducer <code style={{ color: 'var(--red)' }}>kill_all_agents</code> iterates every row
in <code>scheduled_agents</code> and sets <code>state = Killed</code>. The scheduler skips killed
rows entirely. No reducers fire. No CPU. Agents can be selectively revived by type or module.
</p>
</div>
<div style={{ ...mono, fontSize: '0.75rem', color: 'var(--red)', background: 'var(--red-bg)', padding: 'var(--sp-3) var(--sp-4)', borderRadius: 'var(--radius-md)', whiteSpace: 'nowrap', border: '1px solid rgba(239,68,68,0.25)' }}>
⬛ KILL ALL
</div>
</div>
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-6)' }}>
<span className="section-num">AGENT-3.1</span>
<h2 style={{ margin: 0 }}>Kill-Switch Mechanics</h2>
</div>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Operation</th><th>Scope</th><th>Behavior</th></tr>
</thead>
<tbody>
{[
{ op: 'kill_all_agents()', scope: 'Server-wide', behavior: 'Sets state=Killed on every row. Scheduler skips all. Agents retain their payload and next_fire_at for potential revival.' },
{ op: 'kill_agents_by_type(type)', scope: 'Type-wide', behavior: 'Kills only agents matching agent_type. Used when one simulation is misbehaving (e.g. enemy_regen running amok) without touching NPC trades.' },
{ op: 'kill_agents_by_module(module)', scope: 'Package-wide', behavior: 'Kills all agents owned by a module ("game" or "global_module"). Allows safe module hot-reload.' },
{ op: 'pause_agent(agent_id)', scope: 'Single agent', behavior: 'Sets state=Paused. Scheduler skips but row persists. Used to suspend a specific NPC\'s trade route.' },
{ op: 'revive_agent(agent_id)', scope: 'Single agent', behavior: 'Sets state=Active, recalculates next_fire_at from now. Agent resumes as if freshly registered.' },
{ op: 'revive_all_by_type(type)', scope: 'Type-wide', behavior: 'Revives all killed/paused agents of a type. Re-applies jitter to prevent synchronized firing.' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem', color: 'var(--accent)' }}>{row.op}</code></td>
<td><span className="pill pill-purple">{row.scope}</span></td>
<td style={{ ...dimSmall }}>{row.behavior}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">AGENT-3.2</span>
<h2 style={{ margin: 0 }}>Safety Properties</h2>
</div>
<div className="grid-2">
{[
{
title: 'No orphan state',
color: 'var(--green)',
desc: 'When an agent is killed mid-execution (transaction aborted), the entity it was modifying rolls back. No enemy is half-healed, no building is half-decayed. The atomicity guarantee ensures killed agents leave zero partial state.',
},
{
title: 'Revival preserves payload',
color: 'var(--cyan)',
desc: 'Killed agents retain their payload, target_entity_id, and interval_ms. Revival simply flips state back to Active and recalculates next_fire_at. The agent continues as if it had paused — no re-registration needed.',
},
{
title: 'Cascade protection',
color: 'var(--accent)',
desc: 'If agent A fires and registers agent B (event chain), killing agent A does NOT kill agent B. Each agent row is independent. Kill operations are explicitly scoped — cascade kills require a custom reducer.',
},
{
title: 'Generation guard',
color: 'var(--purple)',
desc: 'Each fire increments a generation counter. If a kill arrives between the scheduler reading the row and the reducer executing, the generation mismatch aborts the transaction. Prevents zombie fires.',
},
].map((item, i) => (
<div key={i} className="card" style={{ borderLeft: `3px solid ${item.color}` }}>
<h4 style={{ color: item.color, marginBottom: 'var(--sp-2)' }}>{item.title}</h4>
<p style={{ ...dimSmall, margin: 0, lineHeight: 1.6 }}>{item.desc}</p>
</div>
))}
</div>
</>
)}
{/* ────────────────────── CATALOG ────────────────────── */}
{activeSection === 'catalog' && (
<>
<div className="section-header">
<span className="section-num">AGENT-4</span>
<h2 style={{ margin: 0 }}>Agent Catalog</h2>
</div>
<p style={{ ...dimText, marginBottom: 'var(--sp-4)' }}>
Complete inventory of every background agent in the server, organized by owning package.
Each entry defines the reducer to invoke, scheduling strategy, interval, concurrency cap,
and what it simulates.
</p>
{/* Package tabs */}
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-5)' }}>
{[
{ id: 'game', label: 'game', icon: '◉' },
{ id: 'global_module', label: 'global_module', icon: '◈' },
].map(pkg => (
<button key={pkg.id}
className={`btn btn-sm${activeCatalogPkg === pkg.id ? ' btn-primary' : ''}`}
onClick={() => setActiveCatalogPkg(pkg.id)}>
{pkg.icon} {pkg.label}
</button>
))}
</div>
{activeCatalogPkg === 'game' && (
<>
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>Combat &amp; Entity Agents</h4>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
</thead>
<tbody>
{[
{ type: 'enemy_regen', strat: 'fixed', interval: '2s', max: '∞', desc: 'Regenerates enemy shield/armor HP per tick. Payload contains regen_rate, max_hp. Skips if enemy dead.' },
{ type: 'enemy_respawn', strat: 'one-shot', interval: '30120s', max: '∞', desc: 'Delays enemy respawn after death. Fires once, deletes self, inserts new enemy entity row.' },
{ type: 'aggro_scan', strat: 'fixed', interval: '1s', max: '∞', desc: 'Scans nearby players within aggro range. Transitions enemy from idle → combat state.' },
{ type: 'pirate_spawn', strat: 'conditional', interval: '300s', max: '∞', desc: 'Spawns NPC pirates at belts, gates, and anomalies. Condition: player proximity OR world tick in low/null-sec. Respects per-system density cap (10 max).' },
{ type: 'pirate_combat_tick', strat: 'fixed', interval: '1s', max: '1 per engaged NPC', desc: 'Executes NPC behavior template per tick: orbit/approach/kite/flee. Applies damage to target, regenerates shields/armor. Only runs for NPCs in combat state.' },
{ type: 'pirate_loot_drop', strat: 'one-shot', interval: '0s', max: '∞', desc: 'Generates loot and bounty from destroyed NPC. Rolls loot table, awards ISK bounty to top damage contributor, creates wreck with items.' },
{ type: 'loot_decay', strat: 'fixed', interval: '120s', max: '500', desc: 'Counts down loot container lifetime. On expiry, removes container row and frees the slot.' },
{ type: 'pvp_session_timer', strat: 'one-shot', interval: 'varies', max: '∞', desc: 'Tracks PvP engagement timers (combat flags, weapons timers). Fires on expiry to clear flags.' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
<td style={{ ...dimSmall }}>{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>World &amp; Environment Agents</h4>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
</thead>
<tbody>
{[
{ type: 'day_night_cycle', strat: 'fixed', interval: '600s', max: '1', desc: 'Advances the global day/night phase. Updates lighting table, triggers fauna behavior shifts, affects stealth detection range.' },
{ type: 'weather_tick', strat: 'fixed', interval: '300s', max: '1 per system', desc: 'Evolves system weather state (clear → storm → nebula haze). Affects sensor range, weapon accuracy, and visual overlay.' },
{ type: 'asteroid_respawn', strat: 'conditional', interval: '300s', max: '1 per belt', desc: 'Checks if belt is below max asteroids. Condition: player count in system > 0 AND asteroid count < belt_capacity.' },
{ type: 'anomaly_lifecycle', strat: 'one-shot', interval: 'varies', max: '1 per anomaly', desc: 'Spawned by world_tick. Self-deletes on expiry, removing the anomaly row and dropping any unresolved loot.' },
{ type: 'fauna_migration', strat: 'fixed', interval: '1800s', max: '1 per species', desc: 'Advances fauna along their migration route. Updates current_system_id, writes story log if entering a populated system.' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
<td style={{ ...dimSmall }}>{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>Structure &amp; Decay Agents</h4>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
</thead>
<tbody>
{[
{ type: 'building_decay', strat: 'fixed', interval: '60s', max: '∞', desc: 'Reduces building condition per tick (0.12% depending on material). At 0%, marks for demolition.' },
{ type: 'fuel_consumption', strat: 'fixed', interval: '300s', max: '1 per structure', desc: 'Deducts fuel from powered structures. If fuel hits 0, disables structure services.' },
{ type: 'production_cycle', strat: 'fixed', interval: 'varies', max: '1 per structure', desc: 'Advances manufacturing jobs. On completion, moves output to structure inventory.' },
{ type: 'reinforcement_timer', strat: 'one-shot', interval: '2472h', max: '1 per structure', desc: 'Delays structure destruction in PvP. Allows owners time to defend. Fires → structure becomes vulnerable.' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
<td style={{ ...dimSmall }}>{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{activeCatalogPkg === 'global_module' && (
<>
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>Economy &amp; Trade Agents</h4>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
</thead>
<tbody>
{[
{ type: 'npc_trade_route', strat: 'conditional', interval: '600s', max: '50', desc: 'Evaluates NPC trade route viability. Condition: market activity > threshold AND at least one player docked. Moves goods between stations.' },
{ type: 'market_price_adjust', strat: 'fixed', interval: '300s', max: '1 per item_type', desc: 'Ticks NPC demand pressure algorithm: updates flow_ema, recomputes demand_pressure, applies idle decay. See Economy → NPC Pricing tab for full algorithm spec.' },
{ type: 'bounty_pool_refresh', strat: 'fixed', interval: '3600s', max: '1', desc: 'Refreshes the global bounty pool based on faction military budgets. Allocates bounty value to active threats.' },
{ type: 'insurance_payout', strat: 'one-shot', interval: '30120s', max: '∞', desc: 'Delays ship insurance payout. Fires once, credits ISK to player, deletes self. Prevents instant-PvP-profit loops.' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
<td style={{ ...dimSmall }}>{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>NPC &amp; Faction Agents</h4>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
</thead>
<tbody>
{[
{ type: 'faction_tension_eval', strat: 'conditional', interval: '300s', max: '1 per pair', desc: 'Evaluates if two factions should escalate. Condition: shared border AND standing < -5. Spawns skirmish events if threshold met.' },
{ type: 'npc_patrol_route', strat: 'fixed', interval: '60s', max: '100', desc: 'Advances NPC patrol along waypoints. Updates position, checks for player encounters, may trigger aggro.' },
{ type: 'npc_mission_refresh', strat: 'fixed', interval: '1800s', max: '1 per station', desc: 'Regenerates NPC mission offerings. Removes expired missions, adds new ones from template pool weighted by local faction state.' },
{ type: 'diplomacy_shift', strat: 'fixed', interval: '7200s', max: '1 per pair', desc: 'Slowly drifts faction standing toward baseline. Prevents permanent extreme standings from transient events.' },
{ type: 'concord_response', strat: 'one-shot', interval: '315s', max: '∞', desc: 'Spawns CONCORD fleet at criminal location. Delay based on system security level. Applies overwhelming damage. One-shot: fires, destroys criminal ship, deletes self.' },
{ type: 'security_status_tick', strat: 'fixed', interval: '3600s', max: '1 per player', desc: 'Passive security status recovery. +0.01 per tick for players with no hostile acts in the last hour. Clean record slowly heals.' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
<td style={{ ...dimSmall }}>{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>Galaxy Story Agents</h4>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Agent Type</th><th>Strategy</th><th>Interval</th><th>Max Concurrent</th><th>Description</th></tr>
</thead>
<tbody>
{[
{ type: 'world_tick', strat: 'fixed', interval: '300s', max: '1', desc: 'Master galaxy simulation tick. Evaluates faction matrix, anomaly slots, fauna routes, player density. Spawns conditional events.' },
{ type: 'story_chapter_advance', strat: 'one-shot', interval: 'varies', max: '1 per event', desc: 'Advances multi-chapter story events. Each chapter is a one-shot agent that spawns the next on completion.' },
{ type: 'galaxy_census', strat: 'fixed', interval: '3600s', max: '1', desc: 'Snapshots player counts per system, faction territory control, active event density. Writes to metrics table for admin dashboards.' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem' }}>{row.type}</code></td>
<td><span className={`pill ${row.strat === 'fixed' ? 'pill-amber' : row.strat === 'one-shot' ? 'pill-purple' : 'pill-cyan'}`}>{row.strat}</span></td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.interval}</td>
<td style={{ ...mono, fontSize: '0.8rem' }}>{row.max}</td>
<td style={{ ...dimSmall }}>{row.desc}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</>
)}
</div>
);
}
window.GDD.AgentsPage = AgentsPage;

481
js/pages/architecture.js Normal file
View File

@@ -0,0 +1,481 @@
window.GDD = window.GDD || {};
function ArchitecturePage() {
return (
<div className="content-inner">
<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 Era 1 where SpacetimeDB runs locally.
</p>
{/* Architecture diagram */}
<div className="card" style={{ padding: 0, overflow: 'hidden', marginBottom: 'var(--sp-6)' }}>
<div style={{ padding: 'var(--sp-3) var(--sp-4)', background: 'var(--surface-raised)', borderBottom: '1px solid var(--border)' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Layer Architecture
</span>
</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: '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.' },
].map((layer, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'stretch', gap: 'var(--sp-3)', marginBottom: i < 5 ? '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 }}>
{layer.name}
</span>
<span style={{ fontSize: '0.82rem', color: 'var(--fg-dim)' }}>{layer.desc}</span>
</div>
</div>
))}
</div>
</div>
<div className="section-header">
<span className="section-num">ARCH-1</span>
<h2 style={{ margin: 0 }}>Client Architecture</h2>
</div>
<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>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>Camera controls, entity picking, hover states, click-to-move command creation.</li>
<li>Interpolated movement between authoritative state updates.</li>
<li>Expose renderer events upward never push game logic downward into the renderer.</li>
</ul>
<div className="section-header">
<span className="section-num">ARCH-2</span>
<h2 style={{ margin: 0 }}>State Management Model</h2>
</div>
<table className="data-table">
<thead>
<tr><th>State Type</th><th>Lives Where</th><th>Examples</th></tr>
</thead>
<tbody>
<tr>
<td><span className="pill pill-red">Authoritative</span></td>
<td>SpacetimeDB tables/subscriptions</td>
<td>Ship position, inventory, market orders, asteroid resources.</td>
</tr>
<tr>
<td><span className="pill pill-cyan">Derived view</span></td>
<td>Client game store/view models</td>
<td>Selected ship details, rendered position, distance to target.</td>
</tr>
<tr>
<td><span className="pill pill-green">Local UI</span></td>
<td>Zustand/React state</td>
<td>Open panels, sort order, camera zoom, modal visibility.</td>
</tr>
<tr>
<td><span className="pill pill-purple">Transient visual</span></td>
<td>Renderer internals</td>
<td>Hover glow, particle lifetime, temporary path line.</td>
</tr>
</tbody>
</table>
<div className="section-header">
<span className="section-num">ARCH-3</span>
<h2 style={{ margin: 0 }}>Renderer Replaceability</h2>
</div>
<div className="callout callout-warn" 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="code-block">
<code>
<span className="cm">// Renderer interface — implementation-agnostic</span><br/>
<span className="kw">type</span> <span className="type">GameRendererInput</span> = &#123;<br/>
&nbsp;&nbsp;ships: <span className="type">ShipViewModel</span>[];<br/>
&nbsp;&nbsp;stations: <span className="type">StationViewModel</span>[];<br/>
&nbsp;&nbsp;asteroids: <span className="type">AsteroidViewModel</span>[];<br/>
&nbsp;&nbsp;anomalies: <span className="type">AnomalyViewModel</span>[];<br/>
&nbsp;&nbsp;worldEvents: <span className="type">WorldEventViewModel</span>[];<br/>
&nbsp;&nbsp;selectedEntityId: <span className="type">string</span> | <span className="kw">null</span>;<br/>
&#125;;<br/>
<br/>
<span className="kw">type</span> <span className="type">GameRendererEvents</span> = &#123;<br/>
&nbsp;&nbsp;<span className="fn">onSelectEntity</span>(entityId: <span className="type">string</span>): <span className="type">void</span>;<br/>
&nbsp;&nbsp;<span className="fn">onMoveCommand</span>(position: <span className="type">Vec3</span>): <span className="type">void</span>;<br/>
&nbsp;&nbsp;<span className="fn">onMineCommand</span>(asteroidId: <span className="type">string</span>): <span className="type">void</span>;<br/>
&nbsp;&nbsp;<span className="fn">onDockCommand</span>(stationId: <span className="type">string</span>): <span className="type">void</span>;<br/>
&#125;;
</code>
</div>
<div className="grid-2" style={{ marginTop: 'var(--sp-5)' }}>
<div className="card card-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="card card-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="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">ARCH-4</span>
<h2 style={{ margin: 0 }}>Error Handling & Reconnection</h2>
</div>
<div className="callout callout-info" 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="data-table">
<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)', 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-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" 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="card" 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)' }}>{'Era 1 (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)' }}>{'Era 2 (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 \u2014 disconnecting to escape PvP is not allowed.'}</span>
</p>
</div>
</div>
</div>
<div className="callout callout-warn" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Anti-exploit: combat disconnect.</strong> 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="section-header">
<span className="section-num">ARCH-5</span>
<h2 style={{ margin: 0 }}>Session Persistence & Save/Load</h2>
</div>
<div className="callout callout-info" 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.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>What Gets Persisted</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Category</th><th>Tables</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.' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--fg)' }}>{row.cat}</td>
<td style={{ fontSize: '0.8rem', color: 'var(--cyan)', fontFamily: 'var(--font-mono)' }}>{row.tables}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.guarantee}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" 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="card" 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>
</ul>
</div>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Era 1 vs Era 2 persistence:</strong> Both eras use SpacetimeDB exclusively there is no localStorage.
In Era 1, SpacetimeDB runs locally on the player's machine. "Persistence" means the local database file survives browser restart.
In Era 2, SpacetimeDB runs on a server. Persistence is permanent and shared. The only difference is where the database
process runs; the persistence model is identical.
</div>
{/* ═══ SOUND & AUDIO ═══ */}
<div className="section-header">
<span className="section-num">ARCH-6</span>
<h2 style={{ margin: 0 }}>Sound & Audio Design</h2>
</div>
<div className="callout callout-info" 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.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Audio Categories</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Category</th><th>Purpose</th><th>Examples</th><th>Priority</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.' },
].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-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" 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="card" 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="callout callout-info" 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="section-header">
<span className="section-num">ARCH-7</span>
<h2 style={{ margin: 0 }}>Localization & Internationalization</h2>
</div>
<div className="callout callout-info" 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.
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>What Ships in English Only (MVP)</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>
</ul>
</div>
<div className="card" 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>
</div>
</div>
<div className="callout callout-warn" 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="callout callout-info" 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="section-header">
<span className="section-num">ARCH-8</span>
<h2 style={{ margin: 0 }}>Accessibility</h2>
</div>
<div className="callout callout-info" 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).
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Accessibility Requirements</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Area</th><th>Requirement</th><th>Implementation</th><th>Phase</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 in Era 2, which is inherently competitive.", phase: '7' },
].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="mono">{row.phase}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="callout callout-warn" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>PvP accessibility note:</strong> PvP combat in Era 2 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>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-6)' }}>
<strong>Testing:</strong> Accessibility validation is part of Gate 4 (Era 1 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>
</div>
);
}
window.GDD.ArchitecturePage = ArchitecturePage;

676
js/pages/backend.js Normal file
View File

@@ -0,0 +1,676 @@
window.GDD = window.GDD || {};
function BackendPage() {
const [activeSection, setActiveSection] = React.useState('tables');
const [galaxySubSection, setGalaxySubSection] = React.useState('galaxy-overview');
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>SpacetimeDB Backend 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.
</p>
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-6)' }}>
{[
{ id: 'tables', label: 'Tables' },
{ id: 'reducers', label: 'Reducers' },
{ id: 'movement', label: 'Movement Model' },
{ id: 'galaxy', label: 'Galaxy Simulation' },
{ id: 'er', label: 'ER Diagram' },
].map(t => (
<button key={t.id} className={`btn btn-sm${activeSection === t.id ? ' btn-primary' : ''}`}
onClick={() => setActiveSection(t.id)}>{t.label}</button>
))}
</div>
{activeSection === 'tables' && (
<>
<h3>Data Tables</h3>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Table</th><th>Purpose</th><th>Key Fields</th></tr>
</thead>
<tbody>
{[
{ name: 'players', purpose: 'Player account/session profile', fields: 'player_id, identity, display_name, current_system_id, created_at' },
{ name: 'ships', purpose: 'Active ship state', fields: 'ship_id, owner_player_id, system_id, x/y/z, destination, speed, status' },
{ name: 'regions', purpose: 'Galaxy regions (core, frontier, deep null)', fields: 'region_id, name, description, faction_id, security_profile, x/y/z (galaxy coords)' },
{ name: 'constellations', purpose: 'Star clusters within regions', fields: 'constellation_id, region_id, name, gate_connections (json), x/y/z (region coords)' },
{ name: 'factions', purpose: 'NPC factions with territory, goals, and military/economic strength', fields: 'faction_id, name, ideology, territory_region_ids (json), military_strength (f64), economy_strength (f64), diplomatic_stance (enum)' },
{ name: 'systems', purpose: 'Star systems within the single galaxy', fields: 'system_id, name, region_id, constellation_id, security_level, x/y/z (galaxy coords), star_type, description' },
{ name: 'planets', purpose: 'Planets orbiting stars', fields: 'planet_id, system_id, name, planet_type, orbit_radius, orbit_period, resources' },
{ name: 'moons', purpose: 'Moons orbiting planets', fields: 'moon_id, planet_id, name, moon_type, orbit_radius, resources' },
{ name: 'orbiting_objects', purpose: 'Stations, belts, anomalies in orbital slots', fields: 'object_id, parent_body_id, object_type, orbit_radius, name, state' },
{ name: 'stations', purpose: 'Docking and trading locations', fields: 'station_id, system_id, name, x/y/z, services' },
{ name: 'asteroids', purpose: 'Mineable resource nodes', fields: 'asteroid_id, system_id, resource_type, quantity, x/y/z' },
{ name: 'inventory_items', purpose: 'Player/station item storage', fields: 'item_id, owner_player_id, location, item_type, quantity' },
{ name: 'market_orders', purpose: 'Buy/sell orders', fields: 'order_id, station_id, seller_id, item_type, price, quantity, status' },
{ name: 'chat_messages', purpose: 'Local/system chat stream', fields: 'message_id, channel_id, sender_id, body, created_at' },
{ name: 'active_actions', purpose: 'Long-running actions', fields: 'action_id, player_id, action_type, target_id, started_at, completes_at' },
{ name: 'faction_relations', purpose: 'Dynamic NPC faction relationship matrix', fields: 'faction_a_id, faction_b_id, standing (-10 to +10), trend (rising/falling/stable), last_event_id' },
{ name: 'world_events', purpose: 'Active world simulation events', fields: 'event_id, event_type, system_id, severity, started_at, expires_at, state, participants' },
{ name: 'galaxy_story_log', purpose: 'Persistent server story timeline', fields: 'log_id, event_id, chapter, headline, body, timestamp' },
{ name: 'bookmarks', purpose: 'Player-saved locations', fields: 'bookmark_id, player_id, system_id, x/y/z, name, created_at' },
{ name: 'waypoints', purpose: 'Multi-stop navigation routes', fields: 'route_id, player_id, stops (ordered system list), name, shared' },
{ name: 'bounties', purpose: 'Bounty pool per target', fields: 'target_player_id, total_pool, tier, last_hostile_act' },
{ name: 'bounty_contributions', purpose: 'Individual bounty payments', fields: 'contribution_id, target_id, contributor_id, amount, timestamp' },
{ name: 'kill_feed', purpose: 'Ship destruction events', fields: 'kill_id, victim_id, killer_id, ship_type, system_id, bounty_collected, timestamp' },
{ name: 'player_skills', purpose: 'XP and levels per skill', fields: 'player_id, skill_name, xp, level, last_action_at' },
{ name: 'fleet_beacons', purpose: 'Temporary fleet rally points (post-MVP)', fields: 'beacon_id, fleet_id, creator_id, system_id, x/y/z, expires_at' },
{ name: 'ship_ai_soul', purpose: 'Soul document and personality state per ship', fields: 'ship_id, soul_md (text), growth_vectors (json), personality_state (json), soul_depth (u32), created_at, last_updated_at' },
{ name: 'ship_ai_modules', purpose: 'Installed AI modules and fitting state', fields: 'module_id, ship_id, module_type (enum), tier, slot (med/low), cpu_cost, grid_cost, active, fitted_at' },
{ name: 'ship_ai_tools', purpose: 'Tool registry derived from fitted modules', fields: 'tool_id, ship_id, tool_name, source_module_id, parameters_schema (json), return_schema (json)' },
{ name: 'ship_ai_memory', purpose: 'Event log and conversation history', fields: 'memory_id, ship_id, category, content, related_event_id, timestamp, importance_score' },
{ name: 'ship_ai_directives', purpose: 'Player-set goals for autonomous mode', fields: 'directive_id, ship_id, description, priority, deadline, status, created_at' },
{ name: 'ship_ai_agent_runtime', purpose: 'Per-ship agent loop state and tick schedule', fields: 'ship_id, implementation_tier (0/1/2), tick_interval_ms, next_tick_at, token_budget, status' },
{ name: 'ship_ai_soul_events', purpose: 'Audit log of soul-shaping events', fields: 'event_id, ship_id, event_type, soul_md_delta, applied_at' },
{ name: 'ship_types', purpose: 'Ship class definitions (stats, slot layout)', fields: 'type_id, name, class, hull, armor, shield, high_slots, med_slots, low_slots, cpu, power_grid, cargo, speed, mass, base_hull_value' },
{ name: 'modules_catalog', purpose: 'Module definitions (stats, slot type, costs)', fields: 'module_id, name, slot_type (high/med/low), cpu_cost, grid_cost, category, tier, effects_json' },
{ name: 'ship_fittings', purpose: 'Which modules are fitted to which ship slots', fields: 'fitting_id, ship_id, slot_index, module_id, online (bool)' },
{ name: 'npc_entities', purpose: 'Active NPC pirates and hostiles', fields: 'npc_id, system_id, class_id, behavior_template, x/y/z, hull/armor/shield, target_id, state, spawn_location, spawn_time' },
{ name: 'npc_class_templates', purpose: 'NPC type definitions and stats', fields: 'class_id, name, tier, hull_base, armor_base, shield_base, speed, damage, behavior, loot_table_id, bounty' },
{ name: 'loot_tables', purpose: 'Drop tables for NPC kills and wrecks', fields: 'table_id, entries (item_type, min_qty, max_qty, drop_chance), security_band' },
{ name: 'blueprints', purpose: 'Manufacturing recipes', fields: 'bp_id, product_type, product_qty, materials_json, time_seconds, skill_requirements' },
{ name: 'manufacturing_jobs', purpose: 'Active manufacturing queues', fields: 'job_id, player_id, station_id, bp_id, started_at, completes_at, status, output_location' },
{ name: 'skills_catalog', purpose: 'Skill definitions and XP curves', fields: 'skill_id, name, category, xp_curve (array), unlocks_json, max_level' },
{ name: 'chat_channels', purpose: 'Channel definitions and properties', fields: 'channel_id, name, type (local/trade/private/corp), scope, owner_id, created_at' },
{ name: 'insurance_policies', purpose: 'Active ship insurance contracts', fields: 'policy_id, player_id, ship_id, tier, premium_paid, payout_value, purchased_at, expires_at, active' },
{ name: 'ship_type_base_values', purpose: 'Base hull values for insurance', fields: 'ship_type_id, base_hull_value, insurance_premium_mult' },
{ name: 'station_commodity_demand', purpose: 'Per-station per-commodity demand state for NPC pricing', fields: 'station_id, commodity_id, flow_ema (f64), demand_pressure (f64, [0.81.4]), volume_sold_to_npc, volume_bought_from_npc, npc_stock_remaining, last_tick' },
{ name: 'commodity_price_params', purpose: 'Base prices and adjustment parameters per commodity', fields: 'commodity_id, base_price (f64), buy_spread (f64, [0.650.85]), sell_spread (f64, [1.101.35]), ema_alpha (f64), pressure_beta (f64), decay_gamma (f64)' },
{ name: 'regional_price_seeds', purpose: 'Static regional modifiers set at galaxy generation', fields: 'region_id, commodity_id, modifier (f64, [0.61.5])' },
{ name: 'npc_agents', purpose: 'NPC agents at stations offering missions', fields: 'agent_id, name, faction_id, station_id, specialty (kill/courier/mining/survey/trade/escort), quality (u32), mission_levels_offered, dialogue_seed' },
{ name: 'mission_templates', purpose: 'Mission type definitions and objectives', fields: 'template_id, type (enum), level (14), title, description_template, objectives_json, reward_base, time_limit_seconds, security_band_min, skill_requirements_json, faction_id' },
{ name: 'active_missions', purpose: 'Currently active player missions', fields: 'mission_id, player_id, agent_id, template_id, objectives_state_json, status (active/completed/failed/expired), accepted_at, expires_at, completed_at' },
{ name: 'player_standing', purpose: 'Player standing with agents and factions', fields: 'player_id, entity_id, entity_type (agent/faction), standing (f64, 10 to +10), last_mission_at' },
{ name: 'player_loyalty_points', purpose: 'Faction loyalty point balances', fields: 'player_id, faction_id, lp_balance (u64), lifetime_earned (u64)' },
{ name: 'mission_offers', purpose: 'Current mission offerings at stations', fields: 'offer_id, agent_id, station_id, template_id, reward_modifier, expires_at, generated_at' },
{ name: 'balance_metrics', purpose: 'Balancing Agent metric tracking', fields: 'metric_name, current_value (f64), healthy_min, healthy_max, last_updated, trend (rising/falling/stable)' },
{ name: 'balance_levers', purpose: 'Balancing Agent control levers', fields: 'lever_name, current_multiplier (f64), target_multiplier (f64), clamp_min, clamp_max, last_adjusted_at' },
{ name: 'balance_audit', purpose: 'Balancing Agent intervention log', fields: 'audit_id, tick_time, metrics_snapshot_json, adjustments_json, reason' },
].map((row, i) => (
<tr key={i}>
<td><code>{row.name}</code></td>
<td style={{ color: 'var(--fg-dim)' }}>{row.purpose}</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>{row.fields}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{activeSection === 'reducers' && (
<>
<h3>Reducers (Server Commands)</h3>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Reducer</th><th>Client Trigger</th><th>Server Responsibility</th></tr>
</thead>
<tbody>
{[
{ name: 'connect_player(display_name)', trigger: 'Player opens app', resp: 'Create/update player row, spawn initial ship if needed.' },
{ name: 'set_destination(ship_id, x, y, z)', trigger: 'Click in space', resp: 'Validate ownership/status, update destination/vector.' },
{ name: 'dock(station_id)', trigger: 'Click dock', resp: 'Check distance, set ship docked, update location/state.' },
{ name: 'start_mining(asteroid_id)', trigger: 'Click mine', resp: 'Check range, asteroid quantity, ship status, create active action.' },
{ name: 'complete_mining(action_id)', trigger: 'Timer/event', resp: 'Transfer ore into inventory, reduce asteroid quantity.' },
{ name: 'sell_item(item_type, qty, station)', trigger: 'Sell from UI', resp: 'Validate inventory and station, exchange ore for ISK.' },
{ name: 'place_market_order(...)', trigger: 'Market UI', resp: 'Reserve inventory, create sell order.' },
{ name: 'send_chat(channel_id, body)', trigger: 'Chat box', resp: 'Validate/rate-limit, append chat message row.' },
{ name: 'world_tick(ctx)', trigger: 'Server timer (5 min)', resp: 'Evaluate galaxy state, player density, faction matrix. Conditionally spawn PvE events (faction conflicts, anomalies, migrations, raids). Propagate active events.' },
{ name: 'spawn_world_event(event_type, system_id, params)', trigger: 'World tick evaluation', resp: 'Create world_event row, generate story log entry, notify nearby players via sensors, set expiration timer.' },
{ name: 'resolve_world_event(event_id, outcome)', trigger: 'Event timer or player action', resp: 'Update galaxy state based on outcome, write story log chapter, adjust faction relations, trigger cascading events.' },
].map((row, i) => (
<tr key={i}>
<td><code style={{ fontSize: '0.75rem' }}>{row.name}</code></td>
<td style={{ color: 'var(--fg-dim)' }}>{row.trigger}</td>
<td style={{ color: 'var(--fg-dim)' }}>{row.resp}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{activeSection === 'movement' && (
<>
<h3>Movement Model</h3>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
Avoid sending per-frame movement. Store destination and speed. Clients interpolate visually;
the backend periodically updates authoritative positions.
</div>
<div className="card card-accent">
<h4>Movement Flow</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
1. Client calls <code>set_destination(ship_id, x, y, z)</code><br/>
2. Server validates ownership + ship status<br/>
3. Server updates <code>ships.destination</code> + calculates velocity vector<br/>
4. Server broadcasts updated ship state to subscribers<br/>
5. Client interpolates visual position between last known + destination<br/>
6. Server periodically updates authoritative <code>x/y/z</code> position
</div>
</div>
<div className="grid-2" style={{ marginTop: 'var(--sp-5)' }}>
<div className="card">
<h4 style={{ color: 'var(--green)' }}>Client-side interpolation</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Smooth visual movement between authoritative position updates. Uses dead reckoning
with periodic server correction. Handles latency spikes gracefully.
</p>
</div>
<div className="card">
<h4 style={{ color: 'var(--cyan)' }}>Server authority</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Backend is the source of truth for all positions. Clients never modify their own
position directly they submit intentions and wait for confirmation.
</p>
</div>
</div>
</>
)}
{activeSection === 'galaxy' && (
<>
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-5)' }}>
{[
{ id: 'galaxy-overview', label: 'Overview' },
{ id: 'galaxy-gen', label: 'Galaxy Generation' },
{ id: 'galaxy-events', label: 'World Events' },
].map(g => (
<button key={g.id} className={`btn btn-sm${galaxySubSection === g.id ? ' btn-primary' : ''}`}
onClick={() => setGalaxySubSection(g.id)}>{g.label}</button>
))}
</div>
{galaxySubSection === 'galaxy-overview' && (<>
<h3>Galaxy Simulation Layer</h3>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Single galaxy, simulated world.</strong> The server maintains one persistent galaxy with a connected graph of star systems,
each containing planets, moons, asteroid belts, and stations. A world simulation layer runs on top, spawning dynamic PvE events
that create a unique story per server. This is not instanced content every player in the galaxy shares the same world state.
</div>
<div className="card card-accent" style={{ padding: 'var(--sp-6) var(--sp-8)', marginBottom: 'var(--sp-5)' }}>
<h4 style={{ marginBottom: 'var(--sp-4)' }}>Galaxy Topology</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2.2 }}>
<span style={{ color: 'var(--accent)' }}>Galaxy</span> contains <span style={{ color: 'var(--cyan)' }}>Regions</span> (48 regions, each with distinct character)<br/>
<span style={{ color: 'var(--cyan)' }}>Region</span> contains <span style={{ color: 'var(--green)' }}>Constellations</span> (36 per region, connected clusters)<br/>
<span style={{ color: 'var(--green)' }}>Constellation</span> contains <span style={{ color: 'var(--purple)' }}>Systems</span> (28 per constellation, gate-connected)<br/>
<span style={{ color: 'var(--purple)' }}>System</span> contains <span style={{ color: 'var(--fg)' }}>Star + Planets + Moons + Belts + Stations + Anomalies</span><br/>
<span style={{ color: 'var(--fg)' }}>Planet</span> has <span style={{ color: 'var(--accent)' }}>orbiting_objects</span> (stations, moon mining outposts, customs offices)
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>World Simulation Tables</h3>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)', fontSize: '0.82rem' }}>
<strong>Overlap note:</strong> <code>faction_relations</code>, <code>world_events</code>, and <code>galaxy_story_log</code> also appear in the Tables tab with abbreviated field descriptions. The definitions below are the expanded versions with full field detail. Both tabs describe the same underlying tables.
</div>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Table</th><th>Purpose</th><th>Key Fields</th></tr>
</thead>
<tbody>
{[
{ name: 'regions', purpose: 'Galaxy regions (core, frontier, deep null)', fields: 'region_id, name, description, faction_id, security_profile' },
{ name: 'constellations', purpose: 'Star clusters within regions', fields: 'constellation_id, region_id, name, gate_connections' },
{ name: 'factions', purpose: 'NPC factions with territory and goals', fields: 'faction_id, name, ideology, territory_region_ids, military_strength, economy_strength' },
{ name: 'faction_relations', purpose: 'Dynamic relationship matrix', fields: 'faction_a_id, faction_b_id, standing (-10 to +10), trend (rising/falling/stable), last_event_id' },
{ name: 'world_events', purpose: 'Active PvE events in the galaxy', fields: 'event_id, event_type, system_id, severity (15), started_at, expires_at, state, participants_json, params_json' },
{ name: 'world_event_templates', purpose: 'Event blueprints with spawn conditions', fields: 'template_id, type, name, min_severity, max_severity, spawn_weight, required_faction_state, cooldown_hours' },
{ name: 'galaxy_story_log', purpose: 'Persistent server timeline — the “history” of this galaxy', fields: 'log_id, event_id, chapter_index, headline, body, affected_systems, timestamp' },
{ name: 'space_fauna', purpose: 'Migrating space creatures', fields: 'fauna_id, species, current_system_id, migration_route (json), next_waypoint, arrival_at, cycle_phase' },
{ name: 'anomalies', purpose: 'Temporary spatial phenomena', fields: 'anomaly_id, type (wormhole/nebula/storm/void), system_id, x/y/z, severity, expires_at, loot_table' },
].map((row, i) => (
<tr key={i}>
<td><code>{row.name}</code></td>
<td style={{ color: 'var(--fg-dim)' }}>{row.purpose}</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>{row.fields}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 style={{ marginTop: 'var(--sp-6)', marginBottom: 'var(--sp-4)' }}>Event Spawn Logic</h3>
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
<div style={{ padding: 'var(--sp-3) var(--sp-4)', background: 'var(--surface-raised)', borderBottom: '1px solid var(--border)' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
World Tick Reducer Pseudocode
</span>
</div>
<div className="code-block" style={{ margin: 0, borderRadius: 0 }}>
<code>
<span className="cm">// Runs every 5 minutes on the server</span><br/>
<span className="kw">reducer</span> <span className="fn">world_tick</span>(ctx) {'{'}<br/>
&nbsp;&nbsp;<span className="cm">// 1. Evaluate faction tension matrix</span><br/>
&nbsp;&nbsp;<span className="kw">for</span> each faction_pair {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;<span className="kw">if</span> standing {'<'} -5 && random {'<'} tension_weight {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span className="fn">spawn_event</span>(<span className="str">"faction_skirmish"</span>, contested_system);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span className="fn">log_story</span>({'"'}Hostilities erupt between {'{'}A{'}'} and {'{'}B{'}'} in {'{'}system{'}'}{'"'});<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{'}'}<br/>
&nbsp;&nbsp;{'}'}<br/>
<br/>
&nbsp;&nbsp;<span className="cm">// 2. Check anomaly spawn slots</span><br/>
&nbsp;&nbsp;<span className="kw">if</span> active_anomalies {'<'} max_anomalies {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;pick random system weighted by distance_from_hub;<br/>
&nbsp;&nbsp;&nbsp;&nbsp;<span className="fn">spawn_anomaly</span>(system, random_type);<br/>
&nbsp;&nbsp;{'}'}<br/>
<br/>
&nbsp;&nbsp;<span className="cm">// 3. Advance fauna migration routes</span><br/>
&nbsp;&nbsp;<span className="kw">for</span> each fauna {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;<span className="kw">if</span> now {'>='} next_waypoint.arrival_at {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;advance fauna to next system in route;<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span className="fn">log_story</span>({'"'}{'{'}species{'}'} migration enters {'{'}system{'}'}{'"'});<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{'}'}<br/>
&nbsp;&nbsp;{'}'}<br/>
<br/>
&nbsp;&nbsp;<span className="cm">// 4. Cascade: check if any active events should trigger follow-ons</span><br/>
&nbsp;&nbsp;<span className="kw">for</span> each active_event near expiry {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;evaluate_outcome(participants, event_state);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;<span className="fn">resolve_event</span>(event_id, outcome);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;<span className="cm">// outcome may shift faction relations → future events</span><br/>
&nbsp;&nbsp;{'}'}<br/>
{'}'}
</code>
</div>
</div>
</>)}
{galaxySubSection === 'galaxy-gen' && (<>
<div className="section-header">
<span className="section-num">GALAXY-GEN</span>
<h2 style={{ margin: 0 }}>Galaxy Generation Seeded Parameters & Algorithm</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Every galaxy begins with a seed.</strong> The galaxy generation algorithm is deterministic: the same seed always produces
the same galaxy. This means server operators can share a seed for a known-good galaxy layout, or generate a unique one.
Generation runs once at server bootstrap and writes immutable topology tables (regions, constellations, systems, stargates,
planets, moons, stations, asteroid belts). Faction territories and NPC agent placement are also seeded at this stage.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Galaxy Parameters</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Parameter</th><th>MVP Value</th><th>Full Launch</th><th>Rationale</th></tr>
</thead>
<tbody>
{[
{ param: 'Regions', mvp: '4', full: '68', reason: 'Core, Frontier, Null, Deep Null for MVP. Add 24 faction-specific regions at launch.' },
{ param: 'Constellations per region', mvp: '34', full: '48', reason: 'Minimum 3 for gate connectivity. More in Core region, fewer in Deep Null.' },
{ param: 'Systems per constellation', mvp: '25', full: '38', reason: 'Density varies by region type. Core constellations are denser (trade hubs).' },
{ param: 'Total systems (MVP)', mvp: '~50', full: '~300500', reason: '4 regions × 3.5 const × 3.5 sys ≈ 49 systems. Enough for economic loops without barren stretches.' },
{ param: 'Stargates per system', mvp: '14', full: '16', reason: 'Minimum 1 (no dead ends). Hub systems get 4+. Frontier gets 23.' },
{ param: 'Stations per system', mvp: '13', full: '16', reason: 'High-sec: 23 stations. Low-sec: 12. Null: 01 NPC stations. Stations are where the economy lives.' },
{ param: 'Planets per system', mvp: '15', full: '28', reason: 'Aesthetic + resource variety. Orbital mechanics run on a slow tick (no gameplay impact in MVP).' },
{ param: 'Asteroid belts per system', mvp: '13', full: '15', reason: 'Belts are where mining happens. More belts in lower-sec = better ore = risk/reward.' },
{ param: 'Factions', mvp: '4', full: '46', reason: 'One per region in MVP. Each has territory, ideology, and agent networks.' },
{ param: 'NPC agents per station', mvp: '12', full: '14', reason: 'Mission-givers. Specialty and quality randomized from seed.' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--accent)' }}>{row.param}</td>
<td className="mono" style={{ color: 'var(--cyan)' }}>{row.mvp}</td>
<td className="mono" style={{ color: 'var(--fg-dim)' }}>{row.full}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.85rem' }}>{row.reason}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Galaxy Shape & Layout</h3>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Spiral galaxy with 4 arms.</strong> The galaxy is rendered as a top-down 2D map (with a Z-depth dimension for 3D system
coordinates within each system). Regions are assigned to spiral arm segments:
Core (center), Frontier (inner arms), Null (outer arms), Deep Null (tips and gaps between arms).
Systems within a region are placed using a Poisson disk distribution to ensure minimum spacing while maintaining natural clustering.
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>Region Assignment Rules</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Core (sec +0.8 +1.0):</strong> Center of galaxy map. Dense, high station count, trade hub. Starter systems live here. 1 region.</li>
<li><strong style={{ color: 'var(--fg)' }}>Frontier (sec +0.1 +0.7):</strong> Inner spiral arms. Moderate density. Faction border zones. Mission territory. 12 regions.</li>
<li><strong style={{ color: 'var(--fg)' }}>Null (sec 0.0 0.4):</strong> Outer spiral arms. Sparse stations. Rich belts. PvP-free. 1 region.</li>
<li><strong style={{ color: 'var(--fg)' }}>Deep Null (sec 0.5 1.0):</strong> Tips and gaps. Very sparse. Wormhole connections only. Elite content. 1 region.</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>System Placement Algorithm</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
1. Place constellation centroids via Poisson disk (min 40px apart on map)<br/>
2. For each centroid, place 25 systems in a cluster (Gaussian offset from centroid, σ = 15px)<br/>
3. Assign security level band based on region assignment<br/>
4. Add jitter to security within band (±0.1 random) for natural variation<br/>
5. Place star type (O/B/A/F/G/K/M) weighted by frequency G/K most common<br/>
6. System name generated from faction language + sequential number
</div>
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Stargate Topology</h3>
<div className="callout callout-warn" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>No disconnected components. Every system must be reachable from every other system.</strong>
The stargate graph is the transportation backbone. If a system has only one gate, it\'s a dead-end —
risky because you can\'t flee without going back through the same gate. The algorithm ensures minimum 2 gates
per system (MVP) and creates "choke point" systems with 4+ gates that become natural trade hubs and conflict zones.
</div>
<div className="card card-accent" style={{ marginBottom: 'var(--sp-5)' }}>
<h4 style={{ marginBottom: 'var(--sp-4)' }}>Stargate Connectivity Algorithm</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2.2 }}>
<span style={{ color: 'var(--green)' }}>Phase 1 — Minimum Spanning Tree:</span> Compute MST over all systems using Euclidean distance. This guarantees full connectivity with minimum total gate length.<br/>
<span style={{ color: 'var(--cyan)' }}>Phase 2 — Intra-constellation edges:</span> For each constellation, add 12 extra gates between systems within the constellation. This creates local redundancy and multiple routes within a cluster.<br/>
<span style={{ color: 'var(--accent)' }}>Phase 3 — Inter-region choke points:</span> Identify 24 systems on region boundaries. Add gates between them to create known choke points. These become strategic PvP locations.<br/>
<span style={{ color: 'var(--purple)' }}>Phase 4 — Shortcut edges:</span> Add 1015% extra gates weighted toward connecting high-sec systems to create trade route variety. Never add shortcuts into/out of Deep Null (wormhole-only access preserved).<br/>
<span style={{ color: 'var(--red)' }}>Validation:</span> After generation, verify: (a) graph is fully connected (BFS from any node reaches all), (b) no system has &lt;2 gates, (c) Deep Null systems have no direct high-sec gates, (d) average path length &lt;15 jumps for MVP galaxy.
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Starter System Template</h3>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Starter System Layout</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
<strong style={{ color: 'var(--fg)' }}>Security:</strong> +1.0 (maximum safety)<br/>
<strong style={{ color: 'var(--fg)' }}>Stations:</strong> 3 — Home Station, Trade Hub, Factory<br/>
<strong style={{ color: 'var(--fg)' }}>Belts:</strong> 3 — Veldspar/Scordite (easy ore)<br/>
<strong style={{ color: 'var(--fg)' }}>NPC agents:</strong> 2 — Tutorial agent + Level 1 kill agent<br/>
<strong style={{ color: 'var(--fg)' }}>Gates:</strong> 3 — connects to 3 adjacent high-sec systems<br/>
<strong style={{ color: 'var(--fg)' }}>NPC pirates:</strong> None (CONCORD-protected + no belt spawns in 1.0)<br/>
<strong style={{ color: 'var(--fg)' }}>Services:</strong> Refinery, Factory, Market, Fitting, Insurance, Medical
</div>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>New Player Spawn Rules</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>New players always spawn in a starter system (sec 1.0)</li>
<li>Each faction has exactly 1 starter system in their Core region</li>
<li>Player receives a <strong style={{ color: 'var(--fg)' }}>Rookie Frigate</strong> (free, uninsurable, untradeable)</li>
<li>Player receives the tutorial mission sequence from the tutorial agent</li>
<li>Starter system is guaranteed to have Veldspar and Scordite at NPC buy prices that make the first-30-minute walkthrough viable</li>
<li>Multiple new players can share the same starter system (no instancing)</li>
</ul>
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Station & Belt Placement Rules</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Entity</th><th>Placement Rule</th><th>Density by Sec</th><th>Notes</th></tr>
</thead>
<tbody>
{[
{ entity: 'Station', rule: 'Orbiting a planet or moon. Placed at galaxy gen, never moved.', density: 'High-sec: 23/station. Low: 12. Null: 01 NPC. Player stations (post-MVP) can be anchored.', notes: 'Stations define where economy happens. Every station has a Market. Refinery and Factory depend on station size.' },
{ entity: 'Asteroid Belt', rule: 'Circular orbit around star. 35 asteroids per belt.', density: 'High-sec: 12. Low: 23. Null: 24. Deep Null: 35.', notes: 'Belt ore quality scales with sec level. High-sec: Veldspar/Scordite only. Null: adds Arkonor/Megacyte.' },
{ entity: 'Moon', rule: 'Orbiting planets. 03 moons per planet.', density: '13 moons per planet (uniform distribution).', notes: 'Moons have moon minerals (post-MVP). MVP: moons exist for visual flavor and as station anchors.' },
{ entity: 'Stargate', rule: 'At system edge, paired with gate in target system. Placed at fixed (x,y) = system edge toward destination.', density: 'Matches graph topology. 24 per system typically.', notes: 'Gates are always paired. Jumping gate AB always works. No fuel cost for jumping (MVP).' },
{ entity: 'NPC Agent', rule: 'Located at a station. 12 per station in MVP.', density: 'High-sec stations: 2. Low-sec: 1. Null: 1 or 0.', notes: 'Agent specialty drawn from faction pool. Quality randomized from seed.' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--accent)' }}>{row.entity}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.85rem' }}>{row.rule}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.85rem' }}>{row.density}</td>
<td style={{ color: 'var(--muted)', fontSize: '0.82rem' }}>{row.notes}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Faction Territory Seeding</h3>
<div className="card card-accent" style={{ marginBottom: 'var(--sp-5)' }}>
<h4 style={{ marginBottom: 'var(--sp-4)' }}>Faction Assignment at Galaxy Gen</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2.2 }}>
<span style={{ color: 'var(--green)' }}>1. Assign regions:</span> Each faction claims 1 region as home territory. The Core region is shared (contested).<br/>
<span style={{ color: 'var(--cyan)' }}>2. Place capitals:</span> Each faction gets a capital station in their home region — largest station, most agents, best services.<br/>
<span style={{ color: 'var(--accent)' }}>3. Seed diplomatic stance:</span> Initial faction relations set to baseline matrix (allies: +5, neutral: 0, rivals: 3). This drifts via world simulation.<br/>
<span style={{ color: 'var(--purple)' }}>4. Distribute agents:</span> NPC agents placed at stations within faction territory. Specialty weighted by faction ideology (militarist → kill agents, trader → trade/escort agents).<br/>
<span style={{ color: 'var(--fg)' }}>5. Set regional price seeds:</span> <code>regional_price_seeds</code> table populated at gen time. Each region gets commodity modifiers (0.61.5) that create baseline price differences for traders to discover.<br/>
<span style={{ color: 'var(--red)' }}>6. Faction military/economy:</span> Initial <code>military_strength</code> and <code>economy_strength</code> set from faction template. These are the starting values the world simulation modifies.
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Generation Pseudocode</h3>
<div className="card" style={{ padding: 0, overflow: 'hidden', marginBottom: 'var(--sp-5)' }}>
<div style={{ padding: 'var(--sp-3) var(--sp-4)', background: 'var(--surface-raised)', borderBottom: '1px solid var(--border)' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Galaxy Generation — Pseudocode
</span>
</div>
<div className="code-block" style={{ margin: 0, borderRadius: 0 }}>
<code>
<span className="kw">fn</span> <span className="fn">generate_galaxy</span>(seed: u64) {'{'}<br/>
&nbsp;&nbsp;<span className="kw">let</span> rng = SeededRng::new(seed);<br/>
<br/>
&nbsp;&nbsp;<span className="cm">// 1. Create regions</span><br/>
&nbsp;&nbsp;<span className="kw">let</span> regions = ['{'Core, Frontier_A, Null, Deep_Null'}'];<br/>
&nbsp;&nbsp;<span className="kw">for</span> region <span className="kw">in</span> regions {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;insert region row (id, name, faction_id, security_profile);<br/>
&nbsp;&nbsp;{'}'}<br/>
<br/>
&nbsp;&nbsp;<span className="cm">// 2. Place constellation centroids (Poisson disk)</span><br/>
&nbsp;&nbsp;<span className="kw">let</span> centroids = poisson_disk(min_dist=40, rng);<br/>
&nbsp;&nbsp;<span className="kw">for</span> centroid <span className="kw">in</span> centroids {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;<span className="kw">let</span> region = assign_region(centroid.position);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;insert constellation row (region_id, centroid.x, centroid.y);<br/>
&nbsp;&nbsp;{'}'}<br/>
<br/>
&nbsp;&nbsp;<span className="cm">// 3. Place systems within constellations (Gaussian cluster)</span><br/>
&nbsp;&nbsp;<span className="kw">for</span> constellation <span className="kw">in</span> constellations {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;<span className="kw">let</span> count = rng.range(2..5); <span className="cm">// MVP: 25 systems per constellation</span><br/>
&nbsp;&nbsp;&nbsp;&nbsp;<span className="kw">for</span> i <span className="kw">in</span> 0..count {'{'}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span className="kw">let</span> offset = gaussian(σ=15, rng);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span className="kw">let</span> sec = assign_security(constellation.region) + rng.range(-0.1..+0.1);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;insert system row (name, constellation_id, sec, star_type, x, y);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;place_planets(system, rng);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;place_stations(system, sec, rng);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;place_belts(system, sec, rng);<br/>
&nbsp;&nbsp;&nbsp;&nbsp;{'}'}<br/>
&nbsp;&nbsp;{'}'}<br/>
<br/>
&nbsp;&nbsp;<span className="cm">// 4. Stargate topology</span><br/>
&nbsp;&nbsp;<span className="kw">let</span> mst = minimum_spanning_tree(all_systems, euclidean_distance);<br/>
&nbsp;&nbsp;<span className="kw">for</span> edge <span className="kw">in</span> mst {'{'} insert_gate(edge.a, edge.b); {'}'}<br/>
&nbsp;&nbsp;add_intra_constellation_edges(rng); <span className="cm">// +12 per constellation</span><br/>
&nbsp;&nbsp;add_region_chokepoints(rng); <span className="cm">// 24 cross-region gates</span><br/>
&nbsp;&nbsp;add_shortcut_edges(percentage=0.12, rng); <span className="cm">// 12% extra high-sec shortcuts</span><br/>
&nbsp;&nbsp;validate_connectivity(); <span className="cm">// BFS from node 0 reaches all?</span><br/>
<br/>
&nbsp;&nbsp;<span className="cm">// 5. Faction seeding</span><br/>
&nbsp;&nbsp;seed_faction_territories(factions, regions);<br/>
&nbsp;&nbsp;seed_capital_stations(factions);<br/>
&nbsp;&nbsp;seed_diplomatic_matrix(factions);<br/>
&nbsp;&nbsp;seed_npc_agents(stations, factions, rng);<br/>
&nbsp;&nbsp;seed_regional_prices(regions, commodities, rng);<br/>
{'}'}
</code>
</div>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Determinism guarantee:</strong> Given the same seed, <code>generate_galaxy</code> always produces identical topology.
This enables: (1) shared "known-good" galaxy seeds for competitive servers, (2) reproducible bug reports with exact galaxy layout,
(3) automated testing against fixed galaxy configurations. The seed is stored in a single <code>galaxy_meta</code> table row:
<code>{'{'} seed: u64, generated_at: timestamp, system_count: u32, total_gates: u32 {'}'}</code>.
</div>
<div className="callout callout-warn">
<strong>MVP scope note:</strong> For Phase 0, the galaxy can be hand-authored (a 510 system "mini galaxy") as long as
it follows these rules. The procedural generator ships when the galaxy needs to scale beyond ~50 systems (Phase 7+).
Hand-authored galaxies must still pass the same connectivity validation.
</div>
</>)}
{galaxySubSection === 'galaxy-events' && (<>
<h3>World Simulation Tables</h3>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)', fontSize: '0.82rem' }}>
<strong>Overlap note:</strong> <code>faction_relations</code>, <code>world_events</code>, and <code>galaxy_story_log</code> also appear in the Tables tab with abbreviated field descriptions. The definitions below are the expanded versions with full field detail.
</div>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Table</th><th>Purpose</th><th>Key Fields</th></tr>
</thead>
<tbody>
{[
{ name: 'faction_relations', purpose: 'Dynamic relationship matrix', fields: 'faction_a_id, faction_b_id, standing (-10 to +10), trend, last_event_id' },
{ name: 'world_events', purpose: 'Active PvE events in the galaxy', fields: 'event_id, event_type, system_id, severity (15), started_at, expires_at, state, participants_json' },
{ name: 'world_event_templates', purpose: 'Event blueprints with spawn conditions', fields: 'template_id, type, name, spawn_weight, required_faction_state, cooldown_hours' },
{ name: 'galaxy_story_log', purpose: 'Persistent server timeline', fields: 'log_id, event_id, chapter_index, headline, body, affected_systems, timestamp' },
{ name: 'space_fauna', purpose: 'Migrating space creatures', fields: 'fauna_id, species, current_system_id, migration_route (json), cycle_phase' },
{ name: 'anomalies', purpose: 'Temporary spatial phenomena', fields: 'anomaly_id, type, system_id, x/y/z, severity, expires_at, loot_table' },
].map((row, i) => (
<tr key={i}>
<td><code>{row.name}</code></td>
<td style={{ color: 'var(--fg-dim)' }}>{row.purpose}</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>{row.fields}</td>
</tr>
))}
</tbody>
</table>
</div>
</>)}
</>
)}
{activeSection === 'er' && (
<>
<div className="section-header">
<span className="section-num">BACKEND-ER</span>
<h2 style={{ margin: 0 }}>Entity-Relationship Diagram</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>50+ tables organized into 5 clusters.</strong> This diagram shows the core entity relationships
and how data flows between clusters. Each cluster is color-coded. Foreign key relationships shown as arrows.
Subscription patterns indicate which tables clients subscribe to for reactive updates.
</div>
{[
{
name: 'Player & Identity',
color: 'var(--cyan)',
desc: 'Core player data, ships, inventory, skills, and session state.',
tables: [
{ name: 'players', pk: 'player_id', fk: '', note: 'Root entity. One row per identity.' },
{ name: 'ships', pk: 'ship_id', fk: '\u2192 players.player_id', note: 'Multiple ships per player. owner_player_id.' },
{ name: 'ship_fittings', pk: 'fitting_id', fk: '\u2192 ships.ship_id, \u2192 modules_catalog.module_id', note: 'Many-to-many: ships \u2194 modules.' },
{ name: 'inventory_items', pk: 'item_id', fk: '\u2192 players.player_id', note: 'Location field: ship cargo or station hangar.' },
{ name: 'player_skills', pk: 'player_id + skill_name', fk: '\u2192 players.player_id', note: 'XP and level per skill per player.' },
{ name: 'player_standing', pk: 'player_id + entity_id', fk: '\u2192 players.player_id', note: 'Standing with agents and factions.' },
{ name: 'player_loyalty_points', pk: 'player_id + faction_id', fk: '\u2192 players.player_id', note: 'LP balance per faction.' },
],
},
{
name: 'Economy & Industry',
color: 'var(--green)',
desc: 'Market, manufacturing, blueprints, and NPC pricing.',
tables: [
{ name: 'market_orders', pk: 'order_id', fk: '\u2192 stations.station_id, \u2192 players.player_id', note: 'Buy/sell orders. Core market table.' },
{ name: 'blueprints', pk: 'bp_id', fk: '', note: 'Manufacturing recipes. Materials JSON.' },
{ name: 'manufacturing_jobs', pk: 'job_id', fk: '\u2192 players.player_id, \u2192 stations.station_id, \u2192 blueprints.bp_id', note: 'Active production queues.' },
{ name: 'station_commodity_demand', pk: 'station_id + commodity_id', fk: '\u2192 stations.station_id', note: 'Per-station demand state for NPC pricing.' },
{ name: 'commodity_price_params', pk: 'commodity_id', fk: '', note: 'Base prices and EMA parameters.' },
{ name: 'regional_price_seeds', pk: 'region_id + commodity_id', fk: '', note: 'Static modifiers from galaxy gen.' },
{ name: 'insurance_policies', pk: 'policy_id', fk: '\u2192 players.player_id, \u2192 ships.ship_id', note: 'Active insurance contracts.' },
],
},
{
name: 'World & Galaxy',
color: 'var(--accent)',
desc: 'Galaxy topology, world events, factions, anomalies, and fauna.',
tables: [
{ name: 'systems', pk: 'system_id', fk: '\u2192 regions.region_id', note: 'Star systems. Security level immutable.' },
{ name: 'stations', pk: 'station_id', fk: '\u2192 systems.system_id', note: 'Docking locations with services.' },
{ name: 'asteroids', pk: 'asteroid_id', fk: '\u2192 systems.system_id', note: 'Mineable resource nodes.' },
{ name: 'factions', pk: 'faction_id', fk: '', note: 'NPC factions with territory.' },
{ name: 'faction_relations', pk: 'faction_a_id + faction_b_id', fk: '\u2192 factions.faction_id (\u00d72)', note: 'Dynamic relationship matrix.' },
{ name: 'world_events', pk: 'event_id', fk: '\u2192 systems.system_id', note: 'Active PvE events. Severity, state, params.' },
{ name: 'galaxy_story_log', pk: 'log_id', fk: '\u2192 world_events.event_id', note: 'Persistent server timeline.' },
{ name: 'anomalies', pk: 'anomaly_id', fk: '\u2192 systems.system_id', note: 'Temporary spatial phenomena.' },
{ name: 'space_fauna', pk: 'fauna_id', fk: '\u2192 systems.system_id', note: 'Migrating creatures. Route JSON.' },
],
},
{
name: 'Social & PvP',
color: 'var(--red)',
desc: 'Chat, bounty, kill feed, waypoints, and missions.',
tables: [
{ name: 'chat_channels', pk: 'channel_id', fk: '', note: 'Channel definitions: local, trade, private.' },
{ name: 'chat_messages', pk: 'message_id', fk: '\u2192 chat_channels.channel_id, \u2192 players.player_id', note: 'Message stream with delay.' },
{ name: 'bounties', pk: 'target_player_id', fk: '\u2192 players.player_id', note: 'Bounty pool per target.' },
{ name: 'bounty_contributions', pk: 'contribution_id', fk: '\u2192 bounties.target_player_id, \u2192 players.player_id', note: 'Individual payments into pools.' },
{ name: 'kill_feed', pk: 'kill_id', fk: '\u2192 players.player_id (victim + killer)', note: 'Ship destruction events.' },
{ name: 'npc_agents', pk: 'agent_id', fk: '\u2192 factions.faction_id, \u2192 stations.station_id', note: 'NPC agents at stations.' },
{ name: 'active_missions', pk: 'mission_id', fk: '\u2192 players.player_id, \u2192 npc_agents.agent_id', note: 'Player mission state.' },
{ name: 'waypoints', pk: 'route_id', fk: '\u2192 players.player_id', note: 'Multi-stop routes.' },
],
},
{
name: 'Ship AI (Zora)',
color: 'var(--purple)',
desc: 'Soul state, modules, tools, memory, directives, and runtime.',
tables: [
{ name: 'ship_ai_soul', pk: 'ship_id', fk: '\u2192 ships.ship_id', note: 'One-to-one with ships. Soul document + personality.' },
{ name: 'ship_ai_modules', pk: 'module_id', fk: '\u2192 ships.ship_id', note: 'Installed AI modules. Medium/low slots.' },
{ name: 'ship_ai_tools', pk: 'tool_id', fk: '\u2192 ship_ai_modules.module_id', note: 'Derived from modules. Tool registry.' },
{ name: 'ship_ai_memory', pk: 'memory_id', fk: '\u2192 ships.ship_id', note: 'Event log. Category + importance scoring.' },
{ name: 'ship_ai_directives', pk: 'directive_id', fk: '\u2192 ships.ship_id', note: 'Player-set goals for autonomous mode.' },
{ name: 'ship_ai_agent_runtime', pk: 'ship_id', fk: '\u2192 ships.ship_id', note: 'Per-ship agent loop state. One-to-one.' },
],
},
].map((cluster, ci) => (
<div key={ci} style={{ marginBottom: 'var(--sp-6)' }}>
<h3 style={{ color: cluster.color, marginBottom: 'var(--sp-4)' }}>
{cluster.name}
<span style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', fontWeight: 400, marginLeft: 'var(--sp-3)' }}>{cluster.desc}</span>
</h3>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Table</th><th>PK</th><th>FK Relationships</th><th>Notes</th></tr>
</thead>
<tbody>
{cluster.tables.map((t, ti) => (
<tr key={ti}>
<td><code style={{ color: cluster.color }}>{t.name}</code></td>
<td className="mono" style={{ fontSize: '0.75rem' }}>{t.pk}</td>
<td style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>{t.fk || '\u2014'}</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>{t.note}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
<div className="callout callout-info" style={{ marginTop: 'var(--sp-4)' }}>
<strong>Cross-cluster flows:</strong> The most important cross-cluster relationships are:
(1) <code style={{ color: 'var(--cyan)' }}>players</code> \u2192 <code style={{ color: 'var(--green)' }}>market_orders</code> \u2192 <code style={{ color: 'var(--accent)' }}>stations</code> (the economic loop),
(2) <code style={{ color: 'var(--cyan)' }}>ships</code> \u2192 <code style={{ color: 'var(--accent)' }}>systems</code> \u2192 <code style={{ color: 'var(--red)' }}>chat_messages</code> (the spatial-social loop),
(3) <code style={{ color: 'var(--purple)' }}>ship_ai_soul</code> \u2192 <code style={{ color: 'var(--green)' }}>market_orders</code> (Zora reads market data for intelligence).
Every reducer call touches at least two clusters.
</div>
</>
)}
</div>
);
}
window.GDD.BackendPage = BackendPage;

186
js/pages/demo-gallery.js Normal file
View File

@@ -0,0 +1,186 @@
window.GDD = window.GDD || {};
function DemoGalleryPage() {
const demos = [
{
id: 'starmap',
name: 'Star Map (Era 2 Galaxy Map)',
color: 'var(--accent)',
icon: '🗺',
validates: 'Multi-system galaxy view with system connections, faction territories, click-to-navigate interaction, warp routes. Validates the Era 2 Galaxy Map (region/constellation/system hierarchy). Each system has celestial body rendering.',
limitations: 'This is the Era 2 Galaxy Map, NOT the Era 1 System Map. No single-system detail view showing belts, stations, and orbital bodies at navigation scale. Era 1 needs a separate System Map demo. Static galaxy topology — systems don\'t shift. No faction territory animation. World events shown as static icons, not animated effects. No waypoint creation or route planning integration.',
docPage: 'Overview → HUD & View Mode Architecture → Map Mode',
},
{
id: 'movement',
name: 'Movement',
color: 'var(--green)',
icon: '→',
validates: 'Click-to-move autopilot, ship interpolation between waypoints, ETA calculation, warp-to for distant objects. Validates the Movement Model design.',
limitations: 'Client-side only — no server authority. Interpolation assumes constant speed; no acceleration/deceleration curves yet.',
docPage: 'Backend → Movement Model',
},
{
id: 'market',
name: 'Market',
color: 'var(--cyan)',
icon: '📈',
validates: 'Order book depth, bid/ask spread, price history charts, buy/sell order placement, margin accounts, commodity ticker. Validates the full market surface design.',
limitations: 'Fixed seed data — prices don\'t propagate between systems (no info diffusion). NPC orders only, no player-to-player trades. ₢ symbol used for ISK. Futures contracts are simulated, not backed by SpacetimeDB.',
docPage: 'Overview → Market Panel ★',
},
{
id: 'combat',
name: 'Combat',
color: 'var(--red)',
icon: '⚔',
validates: 'FTL-style reactor power allocation (weapons/shields/engines/aux), target selection, auto-engage, module auto-cycling, capacitor drain, shield/armor/hull layers.',
limitations: 'Solo PvE only — no PvP. Single opponent at a time. No subsystem targeting. No weapon type differentiation (all deal generic damage). Power allocation is instant, not gradual.',
docPage: 'Gameplay → Combat Model',
},
{
id: 'fitting',
name: 'Fitting',
color: 'var(--purple)',
icon: '🔧',
validates: 'CPU/Power Grid slot constraints, High/Med/Low slot assignment, module fitting and unfiting, invalid fit rejection, fitting stat preview.',
limitations: 'Single ship only (Frigate). No AI module slot type yet. No rig slots. No ship bonuses per skill level. Fitting only changes displayed stats, doesn\'t affect combat demo.',
docPage: 'Ships & Fitting → Fitting / Slots',
},
{
id: 'refining',
name: 'Refining',
color: 'var(--accent)',
icon: '⚗',
validates: 'Ore → mineral refining, batch sizes, efficiency curve by Industry skill level, yield calculation, station reprocessing interface.',
limitations: 'No manufacturing chain beyond refining. No blueprint research. No production queues. Efficiency slider is manual, not skill-gated.',
docPage: 'Economy → Refining',
},
{
id: 'progression',
name: 'Progression',
color: 'var(--green)',
icon: '📊',
validates: 'XP-based skill progression across categories, level-up notifications, skill tree visualization, session XP tracking.',
limitations: 'Flat XP curve — not the exponential curve described in the Social page. No skill prerequisites. No skill-based module restrictions. No time-based skill queue.',
docPage: 'Progression & Social → XP & Skills',
},
{
id: 'bounty',
name: 'Bounty',
color: 'var(--red)',
icon: '💰',
validates: 'Bounty placement, bounty tiers by pool size, kill feed display, bounty collection on ship destruction, anti-abuse rules (no self-claiming).',
limitations: 'Bounty pool is static — no decay over time. Kill feed is pre-seeded, not generated from real combat. No galaxy-wide visibility tiers — all bounties visible everywhere.',
docPage: 'Progression & Social → Bounty System',
},
{
id: 'gamehud',
name: 'Game HUD (Flight Mode)',
color: 'var(--cyan)',
icon: '🖥',
validates: 'Flight Mode diegetic HUD: 3D viewport with overlay panels (shield/armor/hull arcs, module rack, overview sidebar, target lock, capacitor gauge, speed/ETA, chat stub). Validates the undocked in-space experience with diegetic overlays on the 3D scene.',
limitations: 'Only shows Flight Mode (undocked). Station Mode (panel-based) not shown here — see Market/Fitting/Refining demos for panel UI. No panel undocking or free-floating mode. No multi-monitor support. No saved layout profiles. Chat is stub-only (no real messages).',
docPage: 'Overview → HUD & View Mode Architecture → Flight Mode',
},
{
id: 'chat',
name: 'Chat & Comms',
color: 'var(--cyan)',
icon: '💬',
validates: 'Range-based chat propagation, light-speed delay mechanics, channel switching (Local/Trade/Private), message delivery visualization, pilot proximity display. Validates the core social surface design.',
limitations: 'Simulated delays only — no real network latency. NPC responses are scripted. Fleet channel is disabled (post-MVP). No corporation or alliance channels. Delay formula is simplified.',
docPage: 'Progression & Social → Chat & Comms',
},
{
id: 'zora',
name: 'Zora Tier 0',
color: 'var(--purple)',
icon: '🤖',
validates: 'Deterministic template selection by personality state × module availability × soul depth. Soul depth progression from blank (raw status codes) to deep (full personality). Module gating logic. Personality axes influence. Validates the Tier 0 implementation spec.',
limitations: 'Tier 0 only — no LLM generation. Template database is a sample, not production-scale. Personality axes affect template selection but not text generation. No autonomous mode or inter-agent communication.',
docPage: 'Ship AI — Zora → Implementation Tiers',
},
{
id: 'galaxy',
name: 'Galaxy Generation',
color: 'var(--accent)',
icon: '🌌',
validates: 'Deterministic seeded galaxy generation with concrete parameters: 4 regions (Core/Frontier/Null/Deep Null), ~50 systems, Poisson disk constellation placement, MST + extra edges stargate topology, station/belt placement by security level, faction territory seeding, connectivity validation. Validates the Galaxy Generation spec in Backend → Galaxy Simulation → Galaxy Generation tab.',
limitations: '2D top-down view only — no Z-depth or 3D system rendering. Galaxy shape is simplified (diamond layout, not true spiral). No NPC agent dialogue or mission seeding details. No wormhole connections for Deep Null. No world event overlay. Connectivity validation is BFS only (no minimum-gate-per-system check).',
docPage: 'Backend → Galaxy Simulation → Galaxy Generation',
},
];
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>Interactive Demo Gallery</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '680px' }}>
Twelve interactive demos that validate specific game systems. Each demo is a standalone prototype
focused on one aspect of the design. They use fake data and simulated APIs not connected to
SpacetimeDB or a real server. Use these to evaluate feel and UX, not performance or scale.
<strong>Note:</strong> The Star Map demo covers the <em>Era 2 Galaxy Map</em>. A separate Era 1 System Map demo (single-system
navigation view) is still needed before Phase 1.
</p>
<div className="stat-grid" style={{ marginTop: 'var(--sp-5)' }}>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--accent)' }}>12</div>
<div className="stat-label">Demos</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--cyan)' }}>Fake Data</div>
<div className="stat-label">Data Source</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--green)' }}>No Server</div>
<div className="stat-label">Backend</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--purple)' }}>UX Only</div>
<div className="stat-label">Purpose</div>
</div>
</div>
<div className="callout callout-info" style={{ marginTop: 'var(--sp-5)', marginBottom: 'var(--sp-6)' }}>
<strong>How to use:</strong> Open demos from the sidebar under "Interactive Demos". Each demo has its own
fullscreen mode. The limitations listed below are known gaps they describe what the demo does <em>not</em>
validate, not what the final game will lack.
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sp-4)' }}>
{demos.map((demo, i) => (
<div key={i} className="card" style={{ borderLeft: `3px solid ${demo.color}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-4)' }}>
<span style={{ fontSize: '1.4rem' }}>{demo.icon}</span>
<h3 style={{ color: demo.color, margin: 0 }}>{demo.name} Demo</h3>
<span className="pill" style={{
background: 'var(--surface-raised)',
color: demo.color,
border: `1px solid ${demo.color}40`,
fontSize: '0.65rem',
}}>
{demo.id}
</span>
</div>
<div className="grid-2" style={{ gap: 'var(--sp-4)' }}>
<div>
<div style={{ fontSize: '0.7rem', color: 'var(--green)', fontWeight: 600, fontFamily: 'var(--font-mono)', letterSpacing: '0.04em', marginBottom: 'var(--sp-2)' }}> VALIDATES</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>{demo.validates}</p>
</div>
<div>
<div style={{ fontSize: '0.7rem', color: 'var(--red)', fontWeight: 600, fontFamily: 'var(--font-mono)', letterSpacing: '0.04em', marginBottom: 'var(--sp-2)' }}> LIMITATIONS</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>{demo.limitations}</p>
</div>
</div>
<div style={{ marginTop: 'var(--sp-3)', paddingTop: 'var(--sp-3)', borderTop: '1px solid var(--border)', fontSize: '0.8rem', color: 'var(--muted)' }}>
<strong style={{ color: 'var(--fg-dim)' }}>Doc reference:</strong> {demo.docPage}
</div>
</div>
))}
</div>
</div>
);
}
window.GDD.DemoGalleryPage = DemoGalleryPage;

1050
js/pages/economy.js Normal file

File diff suppressed because it is too large Load Diff

1430
js/pages/gameplay.js Normal file

File diff suppressed because it is too large Load Diff

315
js/pages/overview.js Normal file
View File

@@ -0,0 +1,315 @@
window.GDD = window.GDD || {};
function OverviewPage() {
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>EVE-Inspired Multiplayer Prototype</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '1.1rem', maxWidth: '680px', marginBottom: 'var(--sp-6)' }}>
A browser-based <strong style={{ color: 'var(--fg-bright)' }}>spreadsheet simulator</strong> in the EVE Online tradition,
set inside a <strong style={{ color: 'var(--fg-bright)' }}>single persistent galaxy</strong> that the server simulates as a living world.
The game is played through UI panels, market tables, inventory grids, and chat channels not by flying a ship.
Movement and combat are deliberately rudimentary; the depth lives in the economy, information diffusion,
strategic decisions, and a galaxy that evolves around you through <strong style={{ color: 'var(--fg-bright)' }}>dynamic PvE events and emergent world story</strong>.
</p>
<div className="stat-grid">
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--accent)' }}>UI-first</div>
<div className="stat-label">Design Pillar</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--cyan)' }}>SpacetimeDB</div>
<div className="stat-label">Backend</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--green)' }}>~3 hrs</div>
<div className="stat-label">Session Target</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--purple)' }}>Player-led</div>
<div className="stat-label">Economy Model</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--red)' }}>Living Galaxy</div>
<div className="stat-label">World Model</div>
</div>
</div>
<div className="section-header">
<span className="section-num">OV-01</span>
<h2 style={{ margin: 0 }}>Product Vision</h2>
</div>
<div className="callout callout-info">
<strong>Primary recommendation:</strong> Build the MVP as a UI-heavy browser game a <em>spreadsheet simulator</em>.
The 3D scene is a strategic map layer for context, not the game itself. Players spend 90% of their time
in tables, charts, and panels: market depth, order books, cargo manifests, fitting spreadsheets,
route planners, and chat. The 3D viewport exists to give spatial awareness, not twitch gameplay.
</div>
<h3>Core Pillars</h3>
<table className="data-table">
<thead>
<tr>
<th>Pillar</th>
<th>Prototype Interpretation</th>
<th>MVP Scope</th>
</tr>
</thead>
<tbody>
<tr>
<td><span className="pill pill-amber">Economy &amp; markets</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Player-led economy with NPC support. Mining refining manufacturing trade. Geographic price differences, contract markets, order books, and <strong style={{ color: 'var(--accent)' }}>information asymmetry between systems</strong> create emergent trade routes and speculative opportunities. <strong style={{ color: 'var(--accent)' }}>This IS the game.</strong></td>
<td><span className="pill pill-green">Era 1</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>NPC economy, single-player mining/refining/manufacturing</span><br/><span className="pill pill-cyan" style={{ fontSize: '0.65rem' }}>Era 2</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Player-to-player market, info diffusion</span></td>
</tr>
<tr>
<td><span className="pill pill-cyan">Social & multiplayer</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Local chat, delayed PMs, bounty system, emergent player-driven justice. Communication is range-based. <strong style={{ color: 'var(--accent)' }}>Priority pillar.</strong></td>
<td><span className="pill pill-cyan" style={{ fontSize: '0.65rem' }}>Era 2</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>All social features require multiplayer</span></td>
</tr>
<tr>
<td><span className="pill pill-green">Command-based (not action)</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Players issue high-level intentions click a point of interest and the ship autopilots there. Click a hostile and the ship auto-engages. During combat the player manages <strong style={{ color: 'var(--fg)' }}>reactor power allocation</strong> (FTL-style) between systems. No manual flight, no aiming, no skill shots. The skill is in <strong style={{ color: 'var(--fg)' }}>what</strong> you power, not <strong style={{ color: 'var(--fg)' }}>how fast</strong> you click.</td>
<td><span className="pill pill-green">Era 1</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Core combat & movement loop</span></td>
</tr>
<tr>
<td><span className="pill pill-purple">Ship fitting</span></td>
<td style={{ color: 'var(--fg-dim)' }}>CPU/Power Grid slot system. High/Med/Low slots with meaningful fitting tradeoffs. Multiple ships, AI crew (post-MVP).</td>
<td><span className="pill pill-green">Era 1</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Basic fitting, single ship class</span></td>
</tr>
<tr>
<td><span className="pill pill-amber">Emergent lore</span></td>
<td style={{ color: 'var(--fg-dim)' }}>No server has the same lore as another. The galaxy is a <strong style={{ color: 'var(--fg)' }}>single persistent world</strong> with systems, planets, anomalies, and orbiting objects. A world simulation layer spawns PvE events dynamically faction wars, space anomalies, migrations, raids that create a <strong style={{ color: 'var(--fg)' }}>living story unique to every server</strong>. Lore evolves through both player actions and server-driven world events.</td>
<td><span className="pill pill-cyan" style={{ fontSize: '0.65rem' }}>Era 2</span> <span style={{ fontSize: '0.75rem', color: 'var(--fg-dim)' }}>Living galaxy requires world agents</span></td>
</tr>
</tbody>
</table>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">OV-02</span>
<h2 style={{ margin: 0 }}>Design Principles</h2>
</div>
<div className="grid-2">
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>Spreadsheet simulator, not flight sim</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
90% of gameplay happens in tables, charts, and panels: market order books, cargo manifests, fitting spreadsheets,
route planners, ship AI logs, and chat channels. The 3D viewport gives spatial awareness, not twitch gameplay.
Movement is click-to-autopilot. Combat is click-to-engage with FTL-style power management. <strong style={{ color: 'var(--fg)' }}>The depth is in the economy and information.</strong>
</p>
</div>
<div className="card">
<h4 style={{ color: 'var(--cyan)' }}>Authoritative backend</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.
</p>
</div>
<div className="card">
<h4 style={{ color: 'var(--green)' }}>Information is the real currency</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Market data propagates at the speed of player travel, not the speed of light. Knowing a price discrepancy exists
before other traders that's the skill. The ship AI (Zora) is a living market intelligence tool. See the
<em> Economy → 📡 Info Diffusion</em> tab for the full model.
</p>
</div>
<div className="card">
<h4 style={{ color: 'var(--purple)' }}>Movement & combat are not action</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
The player never pilots the ship directly. 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>. That's it. Ship destruction
is an economic event (ISK sink, insurance payout, loot drop), not a competitive action moment. ISK (symbol ) is the canonical in-game currency.
</p>
</div>
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">OV-03</span>
<h2 style={{ margin: 0 }}>Core MVP Loop</h2>
</div>
<div className="card card-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)' }}>Connect</span> <span style={{ color: 'var(--accent)' }}>Spawn Ship</span> {' '}
<span style={{ color: 'var(--green)' }}>Navigate</span> {' '}
<span style={{ color: 'var(--purple)' }}>Mine</span> {' '}
<span style={{ color: 'var(--fg-bright)' }}>Inventory</span> {' '}
<span style={{ color: 'var(--cyan)' }}>Station</span> {' '}
<span style={{ color: 'var(--accent)' }}>Sell Ore</span> {' '}
<span style={{ color: 'var(--green)' }}>Chat</span>
</div>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>The real loop is economic.</strong> Connect gather information identify opportunity act on it profit.
The "mine → sell" cycle is the entry point. The endgame is inter-regional arbitrage, supply chain management,
manufacturing empires, and market manipulation all driven by <strong>information diffusion between systems</strong>.
</div>
<h3 style={{ marginTop: 'var(--sp-6)' }}>Minimum Viable Screens</h3>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-4)' }}>
<strong>Era 1 screens</strong> ship in the single-player proof of concept (Roadmap phases 07). These are the minimum screens needed to validate the core loop is fun.
</div>
<table className="data-table">
<thead>
<tr><th>Screen / Panel</th><th>Minimum Functionality</th></tr>
</thead>
<tbody>
<tr><td>Login / Connect</td><td>Display current identity and connection status.</td></tr>
<tr><td>3D Star-System Map</td><td>Strategic overview ships, asteroids, station, click-to-set-waypoint. <strong>Not a flight sim.</strong></td></tr>
<tr><td>Ship Status Panel</td><td>Name, owner, status, cargo, current action, location.</td></tr>
<tr><td>Inventory Panel</td><td>Item type + quantity grid. Sell button when docked. Cargo capacity bar.</td></tr>
<tr><td>Station Panel</td><td>Dock/undock state, sell ore, view market, refit ship.</td></tr>
<tr><td>Market Panel</td><td>Order book, price per unit, place sell order from inventory. NPC-only economy in Era 1.</td></tr>
<tr><td>Combat HUD</td><td>Target selection, module activation, reactor power allocation bars (FTL-style).</td></tr>
<tr><td>Debug Panel</td><td>Reducer call log, error display, connection metrics, entity count.</td></tr>
</tbody>
</table>
<div className="callout callout-info" style={{ marginTop: 'var(--sp-6)', marginBottom: 'var(--sp-4)' }}>
<strong>Era 2 screens</strong> require SpacetimeDB multiplayer infrastructure (Roadmap phases 815).
</div>
<table className="data-table">
<thead>
<tr><th>Screen / Panel</th><th>Minimum Functionality</th></tr>
</thead>
<tbody>
<tr><td style={{ fontWeight: 600, color: 'var(--accent)' }}>Market Panel </td><td><strong>Primary game surface.</strong> Order book with depth, price history charts, contract specifications, bid/ask spread, long/short positions, margin account, place orders (market/limit/stop). See the Interactive Demos Market demo.</td></tr>
<tr><td style={{ fontWeight: 600, color: 'var(--cyan)' }}>Commodity Ticker</td><td>Scrolling price ticker across all contracts. Real-time price updates. Category filters. Sparkline charts.</td></tr>
<tr><td>Chat Panel</td><td>Send and receive local/system messages. Range-based propagation.</td></tr>
<tr><td>Bounty Board</td><td>Active bounties by tier, place bounty on player, kill feed.</td></tr>
<tr><td>Galaxy Map</td><td>Region/constellation/system hierarchy, faction territory overlay, active world events.</td></tr>
<tr><td>World Event Panel</td><td>Active events in current region, countdown timers, story log access.</td></tr>
</tbody>
</table>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">OV-04</span>
<h2 style={{ margin: 0 }}>HUD & View Mode Architecture</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Decision: Hybrid diegetic + panel approach.</strong>
The game uses <strong>two distinct view modes</strong> depending on the player's state. This resolves the ambiguity
between the gamehud demo (which renders diegetic overlays on a 3D viewport) and the spec's description of traditional panels.
Both are correct — they apply to different contexts.
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>🚀 Flight Mode (Undocked)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
When the player's ship is in space (undocked), the primary view is a <strong style={{ color: 'var(--fg)' }}>3D viewport
with diegetic HUD overlays</strong>. This is what the Game HUD demo validates. The 3D scene shows the ship's
surroundings (asteroids, stations, other ships, celestials) while the HUD overlays provide:
</p>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Shield/armor/hull bars</strong> curved around the viewport center, not flat rectangles</li>
<li><strong style={{ color: 'var(--fg)' }}>Module activation buttons</strong> arranged in a bottom rack, grouped by slot type</li>
<li><strong style={{ color: 'var(--fg)' }}>Overview panel</strong> collapsible sidebar listing all on-grid entities with type, distance, and hostile/friendly status</li>
<li><strong style={{ color: 'var(--fg)' }}>Target lock indicator</strong> centered targeting reticle with lock timer</li>
<li><strong style={{ color: 'var(--fg)' }}>Capacitor gauge</strong> circular arc display</li>
<li><strong style={{ color: 'var(--fg)' }}>Speed/distance HUD</strong> current speed, target distance, ETA</li>
<li><strong style={{ color: 'var(--fg)' }}>Chat stub</strong> minimized chat bubble, expands to full chat on click</li>
</ul>
<div style={{ marginTop: 'var(--sp-3)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>
Validated by: Game HUD demo · Covers: movement, combat, mining interactions
</div>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>🏢 Station Mode (Docked)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
When the player is docked at a station, the 3D viewport is replaced by a <strong style={{ color: 'var(--fg)' }}>traditional
panel-based UI</strong> the "spreadsheet simulator" surface. This is the Overview spec's "tables, charts, and panels"
description. Station mode panels include:
</p>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Market Panel</strong> order book, price history charts, contract specifications</li>
<li><strong style={{ color: 'var(--fg)' }}>Inventory Panel</strong> item grid with quantity, type, value estimation</li>
<li><strong style={{ color: 'var(--fg)' }}>Fitting Screen</strong> ship slot layout with drag-and-drop module fitting</li>
<li><strong style={{ color: 'var(--fg)' }}>Refining Interface</strong> batch processing with yield preview</li>
<li><strong style={{ color: 'var(--fg)' }}>Manufacturing Tab</strong> blueprint selection, job queue, material requirements</li>
<li><strong style={{ color: 'var(--fg)' }}>Insurance Panel</strong> coverage tiers, premium calculator, active policies</li>
<li><strong style={{ color: 'var(--fg)' }}>Agent/Mission Panel</strong> NPC agent list, available missions, standings</li>
</ul>
<div style={{ marginTop: 'var(--sp-3)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>
Validated by: Market, Fitting, Refining demos · Covers: all station-based gameplay
</div>
</div>
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>🗺 Map Mode (Both)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
Accessible from either view mode, the map replaces the main viewport with a full-screen 3D strategic map.
<strong style={{ color: 'var(--fg)' }}>Era 1</strong> shows the current star system (single system with celestials, belts, stations).
<strong style={{ color: 'var(--fg)' }}>Era 2</strong> adds the Galaxy Map (multi-system with region/constellation hierarchy, faction overlay, world events).
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>
Era 1: System Map needed · Era 2: Validated by Star Map demo
</div>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--purple)' }}>
<h4 style={{ color: 'var(--purple)' }}>Transition Rules</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Undock:</strong> Flight Mode fades in with 3D viewport. HUD elements animate in sequence (shields modules overview).</li>
<li><strong style={{ color: 'var(--fg)' }}>Dock:</strong> 3D viewport zooms toward station. Fades to Station Mode panel layout.</li>
<li><strong style={{ color: 'var(--fg)' }}>Open Map:</strong> Current viewport shrinks into a corner (minimap). Full map overlay fades in.</li>
<li><strong style={{ color: 'var(--fg)' }}>Close Map:</strong> Map fades out. Previous viewport mode restores.</li>
<li><strong style={{ color: 'var(--fg)' }}>Combat ambush:</strong> If attacked while docked, player auto-undocks into Flight Mode (no safe space abuse).</li>
</ul>
</div>
</div>
<div className="callout callout-warn">
<strong>Why not all-diegetic or all-panel?</strong>
A fully diegetic HUD (Dead Space style) works for immersion but is terrible for spreadsheet gameplay you can't read
an order book through a holographic visor. A fully panel UI (traditional MMO) loses the spatial awareness that makes
space feel like space. The hybrid approach keeps the best of both: diegetic immersion during the action, panel efficiency
during the economy game. The key insight is that <strong>the game alternates between two distinct cognitive modes</strong> —
reactive (in-space, monitoring health/modules/overview) and analytical (docked, reading tables/planning routes).
Each view mode is optimized for its cognitive mode.
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">OV-05</span>
<h2 style={{ margin: 0 }}>Onboarding & Tutorial</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>The first 30 minutes teach the game through doing, not reading.</strong> New players learn by playing a guided
mission sequence that introduces each system naturally. There is no separate "tutorial mode" \u2014 the tutorial IS the game.
See Economy \u2192 First 30 Minutes tab for the full moment-by-moment walkthrough.
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Guided Mission Sequence</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Mission 1:</strong> "Welcome, Pilot" \u2014 undock, warp to belt, mine 100 ore, dock, sell. Teaches: navigation, mining, market.</li>
<li><strong style={{ color: 'var(--fg)' }}>Mission 2:</strong> "Armed and Ready" \u2014 accept kill mission, engage NPC frigate, manage power allocation, collect bounty. Teaches: combat, insurance.</li>
<li><strong style={{ color: 'var(--fg)' }}>Mission 3:</strong> "Supply Chain" \u2014 refine ore, manufacture a module, fit it to ship. Teaches: industry, fitting.</li>
<li><strong style={{ color: 'var(--fg)' }}>Mission 4:</strong> "Price Watcher" \u2014 fly to second station, compare prices, sell at the better one. Teaches: price discovery, trade routes.</li>
<li><strong style={{ color: 'var(--fg)' }}>Mission 5:</strong> "Your AI Companion" \u2014 Zora introduces herself, explains her modules, offers first market tip. Teaches: Zora, AI modules.</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Tutorial Principles</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Teach by doing:</strong> Every tutorial element is an actual game action, not a text popup.</li>
<li><strong style={{ color: 'var(--fg)' }}>Earn while learning:</strong> Tutorial missions pay real ISK and XP. No wasted time.</li>
<li><strong style={{ color: 'var(--fg)' }}>Skip allowed:</strong> Veterans can skip the tutorial sequence. No forced hand-holding.</li>
<li><strong style={{ color: 'var(--fg)' }}>Zora as guide:</strong> Zora delivers tutorial hints in-character. "I notice you haven't opened the fitting screen yet. Want me to walk you through it?"</li>
<li><strong style={{ color: 'var(--fg)' }}>No dead ends:</strong> If a player gets stuck, Zora proactively offers help after 60s of inactivity.</li>
</ul>
</div>
</div>
</div>
);
}
window.GDD.OverviewPage = OverviewPage;

142
js/pages/risks.js Normal file
View File

@@ -0,0 +1,142 @@
window.GDD = window.GDD || {};
function RisksPage() {
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>Risks and Open Questions</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '680px' }}>
Known risks and their mitigations. Each risk is assessed for impact and likelihood.
</p>
<div style={{ marginTop: 'var(--sp-6)' }}>
{[
{
risk: 'SpacetimeDB learning curve',
severity: 'medium',
mitigation: 'Start with one table, one reducer, one subscription before adding game complexity. Build the skeleton phase slowly.',
impact: 'Could slow Phase 01 significantly if the SDK has undocumented behavior.',
},
{
risk: 'Renderer coupling',
severity: 'high',
mitigation: 'Create view models and renderer boundary before adding many meshes/effects. Keep renderer-specific code in /renderers/r3f only.',
impact: 'Without a clean boundary, migrating to Unity/Bevy later requires rewriting most of the client.',
},
{
risk: 'Too much UI scope',
severity: 'medium',
mitigation: 'Build only inventory, chat, station, and market-lite for MVP. Everything else is Phase 7+.',
impact: 'Scope creep in panels/screens is the #1 way the prototype never ships.',
},
{
risk: 'Movement update frequency',
severity: 'medium',
mitigation: 'Use destination-based movement, not frame-based syncing. Server updates positions periodically, not per-frame.',
impact: 'Per-frame state writes would overwhelm SpacetimeDB and make multiplayer unreliable.',
},
{
risk: 'Economy complexity explosion',
severity: 'low',
mitigation: 'Begin with fixed station pricing. Add market orders only after core loop works.',
impact: 'Market manipulation, arbitrage, and order matching are deep rabbit holes.',
},
{
risk: '3D asset rabbit hole',
severity: 'medium',
mitigation: 'Use primitives/icons/placeholders until gameplay works. Visual fidelity is Phase 7+.',
impact: 'Spending time on ship models and particle effects before the loop works is pure waste.',
},
{
risk: 'Authentication complexity',
severity: 'low',
mitigation: 'Use SpacetimeDB identity for the MVP. Add proper account/auth only when persistence is proven.',
impact: 'OAuth/session management is a distraction until the game actually works multiplayer.',
},
{
risk: 'World simulation tuning',
severity: 'medium',
mitigation: 'Start with a simple world tick that spawns one event type (anomalies only). Add faction conflicts and fauna migrations iteratively. Make all event parameters tunable via a config table so adjustments don\'t require redeployment.',
impact: 'If event spawn rates are wrong, the galaxy either feels dead or chaotic. Faction AI that escalates too fast could lock players out of systems permanently. Fauna migrations that overlap trade hubs could crash local economies.',
},
].map((r, i) => (
<div key={i} className="card" style={{ marginBottom: 'var(--sp-4)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-3)' }}>
<span className={`pill ${r.severity === 'high' ? 'pill-red' : r.severity === 'medium' ? 'pill-amber' : 'pill-green'}`}>
{r.severity.toUpperCase()}
</span>
<h4 style={{ margin: 0 }}>{r.risk}</h4>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
<strong style={{ color: 'var(--cyan)' }}>Mitigation:</strong> {r.mitigation}
</p>
<div style={{ background: 'var(--surface-raised)', borderRadius: 'var(--radius-md)', padding: 'var(--sp-2) var(--sp-3)', fontSize: '0.82rem', color: 'var(--fg-dim)' }}>
<strong style={{ color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '0.7rem' }}>IMPACT: </strong>
{r.impact}
</div>
</div>
))}
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">RISK-?</span>
<h2 style={{ margin: 0 }}>Open Questions</h2>
</div>
<div className="grid-2">
<div className="card">
<h4 style={{ color: 'var(--accent)' }}>Scale targets</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Build for 25 concurrent testers first. What's the target for the beta? 50? 500?
SpacetimeDB scaling characteristics are unproven at our scale.
</p>
</div>
<div className="card">
<h4 style={{ color: 'var(--cyan)' }}>Persistence strategy</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Keep player, inventory, market, and world tables persistent from day one. But: do we
need world resets? How do we handle schema migrations?
</p>
</div>
<div className="card">
<h4 style={{ color: 'var(--green)' }}>Combat depth</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
The MVP is explicitly not a twitch-combat game. When do we add targeting, weapon cycles,
damage types? What's the minimum viable combat?
</p>
</div>
<div className="card">
<h4 style={{ color: 'var(--purple)' }}>Testing approach</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
Open multiple browser windows with separate identities to validate shared state. Do we
need automated integration tests? Playwright against the game client?
</p>
</div>
<div className="card">
<h4 style={{ color: 'var(--accent)' }}>Galaxy event balance</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0 }}>
How many world events should be active simultaneously? Too few and the galaxy feels static;
too many and players get event fatigue. What's the right spawn rate per region? How does
event density scale with player count?
</p>
</div>
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">RISK-REF</span>
<h2 style={{ margin: 0 }}>References</h2>
</div>
<table className="data-table">
<thead><tr><th>Source</th><th>Relevant Point</th><th>URL</th></tr></thead>
<tbody>
<tr><td>SpacetimeDB</td><td style={{ color: 'var(--fg-dim)' }}>Real-time backend for apps and games</td><td><a href="https://spacetimedb.com/" target="_blank">spacetimedb.com</a></td></tr>
<tr><td>SpacetimeDB GitHub</td><td style={{ color: 'var(--fg-dim)' }}>Clients connect to database, logic runs in DB</td><td><a href="https://github.com/clockworklabs/spacetimedb" target="_blank">GitHub</a></td></tr>
<tr><td>React Three Fiber</td><td style={{ color: 'var(--fg-dim)' }}>React renderer for Three.js</td><td><a href="https://r3f.docs.pmnd.rs/" target="_blank">docs.pmnd.rs</a></td></tr>
<tr><td>Vite</td><td style={{ color: 'var(--fg-dim)' }}>Frontend build tooling</td><td><a href="https://vite.dev/" target="_blank">vite.dev</a></td></tr>
</tbody>
</table>
</div>
);
}
window.GDD.RisksPage = RisksPage;

303
js/pages/roadmap.js Normal file
View File

@@ -0,0 +1,303 @@
window.GDD = window.GDD || {};
function RoadmapPage() {
const eras = [
{
id: 'solo',
title: 'Era 1 — Single-Player Proof of Concept',
subtitle: 'Validate core loops locally. SpacetimeDB runs on the local machine from Phase 0 — there is no localStorage. One browser window, one player, one simulated galaxy.',
accent: 'var(--accent)',
phases: [
{
num: '0',
title: 'Local Skeleton',
goal: 'Vite app with local SpacetimeDB instance, game state manager, tick loop, and a single rendered star system.',
doneWhen: 'App boots, connects to local SpacetimeDB instance. Shows a star system with a station and 3 asteroids. Game state updates on a local tick (60fps render, 1Hz sim tick). All persistence through SpacetimeDB — no localStorage.',
status: 'current',
},
{
num: '1',
title: 'Movement & Commands',
goal: 'Click-to-move autopilot with local path resolution. Ship accelerates, cruises, decelerates.',
doneWhen: 'Click an asteroid or station. Ship plots a course and moves there with smooth interpolation. ETA display updates. Warp-to for distant objects works.',
status: 'upcoming',
},
{
num: '2',
title: 'Mining & Inventory',
goal: 'Asteroid mining cycle, ore extraction, cargo hold, and jettison.',
doneWhen: 'Approach asteroid, start mining. Mining cycle shows progress. Ore appears in cargo. Cargo full warning. Can jettison into a can.',
status: 'upcoming',
},
{
num: '3',
title: 'Combat — FTL Power Allocation',
goal: 'Auto-engage combat with reactor power management between weapons / shields / engines.',
doneWhen: 'Target a hostile NPC. Ship auto-engages. Player shifts reactor power between 3 subsystems (FTL-style). Power allocation visibly changes combat outcome. Ship can be destroyed.',
status: 'upcoming',
},
{
num: '4',
title: 'Ship Fitting',
goal: 'CPU / Power Grid slot system. High / Med / Low racks with modules that change ship behavior.',
doneWhen: 'Dock at station. Open fitting screen. Equip weapons in high slots, shield booster in mid, cargo expander in low. Fitting affects combat and mining stats. Invalid fits rejected (insufficient CPU/PG). AI module slot type added to fitting schema.',
status: 'upcoming',
},
{
num: '5',
title: 'Refining & Manufacturing',
goal: 'Refine ore into minerals at a station. Use minerals to manufacture modules and ammo.',
doneWhen: 'Dock with ore. Refine at station facility (with yield efficiency). Minerals stored locally. Open manufacturing tab, select a blueprint, queue a job. Job completes after sim time. Product appears in hangar.',
status: 'upcoming',
},
{
num: '6',
title: 'NPC Economy Sim',
goal: 'Simulated NPC market with supply/demand. Prices react to player trades. Regional price differences.',
doneWhen: 'Sell ore at a station. Price adjusts (supply increases, price drops). Fly to another system, price is different. Buy low / sell high works. Market history table shows price movement.',
status: 'upcoming',
},
{
num: '7',
title: 'Single-Player Polish',
goal: 'Complete HUD, notifications, empty states, tutorial hints, and save/load.',
doneWhen: 'Full game loop is playable solo: mine → refine → manufacture → fit → fight → trade. HUD shows all relevant info. SpacetimeDB persists all state (ships, inventory, market, skills) — no localStorage. No dead-end states. Tier 0 Zora: status readouts, basic shield warnings, bare-bones soul state vector in SpacetimeDB. Lightweight exploration events spawn in visited systems.',
status: 'future',
},
],
},
{
id: 'multi',
title: 'Era 2 — Multiplayer Environment',
subtitle: 'Promote local SpacetimeDB to a shared server. Add multiplayer networking, social systems, and the full living galaxy simulation. Multiple players, one persistent world.',
accent: 'var(--cyan)',
phases: [
{
num: '8',
title: 'SpacetimeDB Skeleton',
goal: 'Replace local game state with SpacetimeDB tables and reducers. Client subscribes to 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: 'Presence & Movement Sync',
goal: 'Players see each other in real time. Movement is server-authoritative with client-side interpolation.',
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 Economy',
goal: 'Player-to-player market. Buy/sell orders, contracts, and real 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: 'Social — Chat & Bounty',
goal: 'Local chat (system-range), delayed PMs, and player-posted bounty system.',
doneWhen: 'Players in the same system see local chat in real time. PMs arrive with configurable delay (light-speed). Any player can post a bounty on another. Bounty board is visible galaxy-wide.',
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: 'Multiplayer Combat',
goal: 'PvP combat with FTL power allocation. Multiple ships in an engagement. Target calling, range bands.',
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: 'Corporations & Territory',
goal: 'Player corps, structure anchoring, system sovereignty claims.',
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: 'Full MVP — Launch Candidate',
goal: 'Polish pass on all systems. Error handling, reconnection, scaling tests, and onboarding flow.',
doneWhen: 'Fresh player can create account, complete a guided tutorial, mine their first ore, fit their first ship, survive a PvE encounter, make their first trade, and join a corp — all without hitting a dead-end or a crash. Server handles 50 concurrent players.',
status: 'future',
},
],
},
];
function PhaseItem({ phase, isLast }) {
const statusStyle = phase.status === 'current'
? { background: 'var(--accent-bg)', color: 'var(--accent)', border: '1px solid var(--accent-border)' }
: phase.status === 'upcoming'
? { background: 'var(--cyan-bg)', color: 'var(--cyan)', border: '1px solid rgba(34,211,238,0.25)' }
: { background: 'var(--surface-raised)', color: 'var(--muted)', border: '1px solid var(--border)' };
return (
<div className="phase-item">
<div className="phase-marker">
<div
className={`phase-dot${phase.status === 'future' ? ' future' : ''}`}
style={phase.status === 'current' ? {
background: 'var(--accent)',
boxShadow: '0 0 12px rgba(240,160,48,0.4)',
animation: 'pulse 2s infinite',
} : {}}
/>
{!isLast && <div className="phase-line" />}
</div>
<div className="phase-content">
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-2)' }}>
<span style={{
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
padding: '2px 8px',
borderRadius: 'var(--radius-pill)',
...statusStyle,
}}>
PHASE {phase.num}
</span>
<h4 style={{ margin: 0 }}>{phase.title}</h4>
{phase.status === 'current' && <span className="pill pill-amber">IN PROGRESS</span>}
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>{phase.goal}</p>
<div style={{ background: 'var(--surface-raised)', borderRadius: 'var(--radius-md)', padding: 'var(--sp-2) var(--sp-3)', fontSize: '0.8rem' }}>
<span style={{ color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '0.7rem' }}>DONE WHEN: </span>
<span style={{ color: 'var(--fg-dim)' }}>{phase.doneWhen}</span>
</div>
</div>
</div>
);
}
return (
<div className="content-inner">
<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)' }}>Era 1</strong> proves the game is fun as a single-player simulation with a local
SpacetimeDB instance the same persistence architecture as multiplayer, just one player. <strong style={{ color: 'var(--fg)' }}>Era 2</strong> promotes
that local SpacetimeDB to a shared server and adds social systems, the living galaxy, and multiplayer combat.
Each phase has a verifiable done-when condition. Integration gates between phase groups ensure every system works together before advancing.
</p>
<div className="callout callout-info" style={{ marginTop: 'var(--sp-4)', maxWidth: '720px' }}>
<strong>Why single-player first with local SpacetimeDB?</strong> Networking is the biggest source of bugs and complexity.
By validating that mining, combat, fitting, and the economy are fun locally using the <em>same</em> SpacetimeDB
persistence that will serve multiplayer we de-risk the entire project. There is no localStorage; SpacetimeDB is the
persistence layer from day 1. When Era 2 begins, 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>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">IG</span>
<h2 style={{ margin: 0 }}>Integration Gates</h2>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '720px', marginBottom: 'var(--sp-4)' }}>
Between phase groups, an integration gate ensures all systems work together before new ones are added.
A gate is a focused playtest that exercises every previously-built feature end-to-end.
</p>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>Gate 1 Core Loop (after Phase 2)</h4>
<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. The complete economic loop runs in a single session
without errors. SpacetimeDB persists the session closing the browser 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="card" style={{ borderLeft: '3px solid var(--red)' }}>
<h4 style={{ color: 'var(--red)' }}>Gate 2 Combat + Fitting (after Phase 4)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Fit a ship at station undock encounter NPC pirate manage power allocation destroy or be destroyed
insurance payout (if destroyed) refit at station. Combat and fitting form a closed loop with economic consequences.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 04</div>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Gate 3 Full Economy (after Phase 6)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Mine ore refine manufacture a module fit it use it in combat sell excess minerals across systems at different prices.
The complete production chain and NPC market work as an integrated system. Price differences between stations are discoverable.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 06</div>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Gate 4 Era 1 Complete (after Phase 7)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Full solo game loop with all systems integrated: mine refine manufacture fit fight trade repeat.
HUD, notifications, Zora Tier 0, exploration events, and missions all work without dead ends. A new player
can learn the game in one session. SpacetimeDB state survives restart.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 07 (all Era 1)</div>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--purple)' }}>
<h4 style={{ color: 'var(--purple)' }}>Gate 5 Multiplayer Core (after Phase 10)</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>
Two players in the same galaxy. Both see each other. Both can trade on the shared market. ISK and items
transfer atomically. Movement is synced. Connection loss and reconnection work. No desync under normal latency.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>Phases covered: 810</div>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--fg-dim)' }}>
<h4 style={{ color: 'var(--fg-dim)' }}>Gate 6 Launch 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, mine ore, fit ship, survive PvE, make a trade, join a corp,
participate in a world event all without crashes or dead ends. Server handles 50 concurrent. Full game loop validated.
</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)' }}>
{eras.map((era, ei) => (
<div key={era.id} style={{ marginBottom: ei < eras.length - 1 ? 'var(--sp-8)' : 0 }}>
{/* Era header */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--sp-4)',
marginBottom: 'var(--sp-5)',
paddingBottom: 'var(--sp-3)',
borderBottom: `2px solid ${era.accent}`,
}}>
<span style={{
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
fontWeight: 700,
letterSpacing: '0.08em',
padding: '4px 12px',
borderRadius: 'var(--radius-pill)',
background: era.accent === 'var(--accent)' ? 'var(--accent-bg)' : 'var(--cyan-bg)',
color: era.accent,
border: `1px solid ${era.accent === 'var(--accent)' ? 'var(--accent-border)' : 'rgba(34,211,238,0.3)'}`,
}}>
ERA {ei + 1}
</span>
<div>
<h2 style={{ margin: 0, fontSize: '1.1rem' }}>{era.title}</h2>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.82rem', margin: '4px 0 0 0' }}>{era.subtitle}</p>
</div>
</div>
{/* Phase list */}
{era.phases.map((phase, pi) => (
<PhaseItem
key={phase.num}
phase={phase}
isLast={pi === era.phases.length - 1}
/>
))}
</div>
))}
</div>
</div>
);
}
window.GDD.RoadmapPage = RoadmapPage;

1700
js/pages/ship-ai.js Normal file

File diff suppressed because it is too large Load Diff

555
js/pages/ships.js Normal file
View File

@@ -0,0 +1,555 @@
window.GDD = window.GDD || {};
function ShipsPage() {
const [activeSection, setActiveSection] = React.useState('classes');
const shipClasses = [
{
name: 'Frigate',
hull: 400, armor: 350, shield: 300,
highSlots: 3, medSlots: 3, lowSlots: 2,
cpu: 120, powerGrid: 40, cargo: 150,
speed: 280, mass: 1200,
role: 'Fast scout and tackle. Low slot count but quick to align and warp. Good for new players.',
examples: ['Merlin', 'Rifter', 'Incursus', 'Punisher'],
},
{
name: 'Destroyer',
hull: 650, armor: 550, shield: 500,
highSlots: 7, medSlots: 3, lowSlots: 3,
cpu: 180, powerGrid: 65, cargo: 300,
speed: 210, mass: 1800,
role: 'Anti-frigate platform. Many turret hardpoints but slow and vulnerable to larger ships.',
examples: ['Cormorant', 'Thrasher', 'Catalyst', 'Coercer'],
},
{
name: 'Cruiser',
hull: 1200, armor: 1000, shield: 900,
highSlots: 5, medSlots: 4, lowSlots: 4,
cpu: 280, powerGrid: 110, cargo: 600,
speed: 175, mass: 3500,
role: 'Versatile workhorse. Can mine, fight, or explore. Good cargo hold and balanced slot layout.',
examples: ['Osprey', 'Rupture', 'Vexor', 'Maller'],
},
{
name: 'Battlecruiser',
hull: 2800, armor: 2400, shield: 2000,
highSlots: 7, medSlots: 5, lowSlots: 5,
cpu: 380, powerGrid: 180, cargo: 1000,
speed: 130, mass: 7000,
role: 'Command ship. Can fit warfare links and project damage. Excellent fleet support.',
examples: ['Drake', 'Hurricane', 'Myrmidon', 'Harbinger'],
},
{
name: 'Battleship',
hull: 6000, armor: 5000, shield: 4500,
highSlots: 8, medSlots: 5, lowSlots: 6,
cpu: 520, powerGrid: 280, cargo: 1800,
speed: 90, mass: 15000,
role: 'Heavy assault platform. Maximum firepower and tank but very slow. Fleet anchor.',
examples: ['Rokh', 'Tempest', 'Dominix', 'Apocalypse'],
},
];
const slotTypes = [
{
name: 'High Slots',
color: 'var(--red)',
icon: '◆',
description: 'Weapons, mining lasers, cloaks, salvagers. The things that do stuff to other things.',
modules: ['150mm Railgun', '200mm Autocannon', 'Heavy Missile Launcher', 'Mining Laser II', 'Salvager I', 'Cloaking Device'],
fitting: 'Turrets and launchers require both CPU and Power Grid. Heavy weapons need more of both.',
},
{
name: 'Medium Slots',
color: 'var(--cyan)',
icon: '◇',
description: 'Shields, propulsion, electronic warfare, tackle. The things that keep you alive or stop them.',
modules: ['Shield Booster', '1MN Afterburner', 'Warp Scrambler', 'Stasis Webifier', 'ECM Jammer', 'Shield Extender'],
fitting: 'Shield and propulsion modules are CPU-heavy. EWAR fits are tight on CPU, light on grid.',
},
{
name: 'Low Slots',
color: 'var(--green)',
icon: '○',
description: 'Armor, damage mods, cargo expanders, power diagnostics. Passive upgrades and tank.',
modules: ['Armor Plate', 'Magnetic Field Stabilizer', 'Cargo Expander', 'Power Diagnostic System', 'Capacitor Power Relay', 'Armor Repairer'],
fitting: 'Armor and damage modules are Power Grid-heavy. Passive modules use less CPU.',
},
];
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>Ships & Fitting System</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '680px' }}>
Ships are the player's primary asset. Each ship has a slot layout with CPU and Power Grid limits
that constrain what modules can be fitted. Players own multiple ships and can assign AI crew to
pilot them on autonomous tasks.
</p>
{/* Tab navigation */}
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-6)' }}>
{[
{ id: 'classes', label: 'Ship Classes' },
{ id: 'fitting', label: 'Fitting / Slots' },
{ id: 'acquisition', label: '🚀 Acquisition' },
{ id: 'crew', label: 'AI Crew' },
].map(t => (
<button key={t.id} className={`btn btn-sm${activeSection === t.id ? ' btn-primary' : ''}`}
onClick={() => setActiveSection(t.id)}>{t.label}</button>
))}
</div>
{/* SHIP CLASSES */}
{activeSection === 'classes' && (
<>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>MVP scope:</strong> One ship class per faction to start (Frigate). Expand to Destroyer
and Cruiser in Phase 5+. Full roster is the launch target.
</div>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr>
<th>Class</th>
<th>Hull</th>
<th>Armor</th>
<th>Shield</th>
<th>High</th>
<th>Med</th>
<th>Low</th>
<th>CPU</th>
<th>Grid</th>
<th>Speed</th>
<th>Cargo</th>
</tr>
</thead>
<tbody>
{shipClasses.map((ship, i) => (
<tr key={i}>
<td style={{ color: 'var(--accent)', fontWeight: 600 }}>{ship.name}</td>
<td className="mono">{ship.hull}</td>
<td className="mono">{ship.armor}</td>
<td className="mono">{ship.shield}</td>
<td style={{ color: 'var(--red)' }}>{ship.highSlots}</td>
<td style={{ color: 'var(--cyan)' }}>{ship.medSlots}</td>
<td style={{ color: 'var(--green)' }}>{ship.lowSlots}</td>
<td className="mono">{ship.cpu}</td>
<td className="mono">{ship.powerGrid}</td>
<td className="mono">{ship.speed}</td>
<td className="mono">{ship.cargo}</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ marginTop: 'var(--sp-6)' }}>
<h3>Class Details</h3>
<div className="grid-2">
{shipClasses.slice(0, 4).map((ship, i) => (
<div key={i} className="card">
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-2)' }}>{ship.name}</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>{ship.role}</p>
<div style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>
<strong>Variants:</strong> {ship.examples.join(', ')}
</div>
</div>
))}
</div>
</div>
</>
)}
{/* FITTING / SLOTS */}
{activeSection === 'fitting' && (
<>
<div className="section-header">
<span className="section-num">SHIP-FIT</span>
<h2 style={{ margin: 0 }}>Slot Types & Fitting Constraints</h2>
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
{slotTypes.map((slot, i) => (
<div key={i} className="card" style={{ borderLeft: `3px solid ${slot.color}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-3)' }}>
<span style={{ color: slot.color, fontSize: '1.2rem' }}>{slot.icon}</span>
<h4 style={{ color: slot.color, margin: 0 }}>{slot.name}</h4>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>{slot.description}</p>
<div style={{ fontSize: '0.8rem', color: 'var(--fg-dim)', marginBottom: 'var(--sp-2)' }}>
<strong style={{ color: 'var(--muted)' }}>Typical modules:</strong>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--sp-1)' }}>
{slot.modules.map((m, j) => (
<span key={j} className="pill" style={{ background: 'var(--surface-raised)', color: 'var(--fg-dim)', border: '1px solid var(--border)', fontSize: '0.7rem' }}>
{m}
</span>
))}
</div>
</div>
))}
</div>
<div className="section-header">
<span className="section-num">SHIP-CPU</span>
<h2 style={{ margin: 0 }}>CPU & Power Grid</h2>
</div>
<div className="card card-accent">
<h4>Fitting Mechanics</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
1. Each module has a CPU cost and a Power Grid cost.<br/>
2. Total fitted module costs must not exceed ship's CPU and Power Grid.<br/>
3. Some modules have ship bonuses (e.g. "+5% mining laser yield per Cruiser level").<br/>
4. Rig slots add permanent modifications cannot be removed, only destroyed.<br/>
5. Fitting is only possible when docked at a station with fitting service.
</div>
</div>
<div className="grid-2" style={{ marginTop: 'var(--sp-5)' }}>
<div className="card">
<h4 style={{ color: 'var(--accent)' }}>Example: Mining Cruiser Fit</h4>
<table className="data-table">
<thead><tr><th>Slot</th><th>Module</th><th>CPU</th><th>Grid</th></tr></thead>
<tbody>
<tr><td style={{ color: 'var(--red)' }}>High 1</td><td>Mining Laser II</td><td className="mono">30</td><td className="mono">10</td></tr>
<tr><td style={{ color: 'var(--red)' }}>High 2</td><td>Mining Laser II</td><td className="mono">30</td><td className="mono">10</td></tr>
<tr><td style={{ color: 'var(--cyan)' }}>Med 1</td><td>1MN Afterburner</td><td className="mono">25</td><td className="mono">15</td></tr>
<tr><td style={{ color: 'var(--cyan)' }}>Med 2</td><td>Shield Booster I</td><td className="mono">30</td><td className="mono">10</td></tr>
<tr><td style={{ color: 'var(--cyan)' }}>Med 3</td><td>Market Analyzer (AI)</td><td className="mono">15</td><td className="mono">0</td></tr>
<tr><td style={{ color: 'var(--green)' }}>Low 1</td><td>Mining Upgrade I</td><td className="mono">20</td><td className="mono">5</td></tr>
<tr><td style={{ color: 'var(--green)' }}>Low 2</td><td>Cargo Expander I</td><td className="mono">15</td><td className="mono">0</td></tr>
<tr><td style={{ color: 'var(--green)' }}>Low 3</td><td>Nav Processor (AI)</td><td className="mono">10</td><td className="mono">0</td></tr>
<tr style={{ borderTop: '2px solid var(--border-light)' }}>
<td colSpan="2" style={{ fontWeight: 600 }}>Total</td>
<td className="mono" style={{ fontWeight: 600, color: 'var(--cyan)' }}>175/280</td>
<td className="mono" style={{ fontWeight: 600, color: 'var(--green)' }}>50/110</td>
</tr>
</tbody>
</table>
<div style={{ marginTop: 'var(--sp-2)', fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--purple)' }}>
AI modules: Market Analyzer (med) tracks local prices and flags arbitrage opportunities. Nav Processor (low) optimizes warp routes. See Ship AI \u2192 Module Gates tab.
</div>
</div>
<div className="card">
<h4 style={{ color: 'var(--red)' }}>Example: Combat Frigate Fit</h4>
<table className="data-table">
<thead><tr><th>Slot</th><th>Module</th><th>CPU</th><th>Grid</th></tr></thead>
<tbody>
<tr><td style={{ color: 'var(--red)' }}>High 1</td><td>150mm Railgun</td><td className="mono">35</td><td className="mono">12</td></tr>
<tr><td style={{ color: 'var(--red)' }}>High 2</td><td>200mm Autocannon</td><td className="mono">30</td><td className="mono">14</td></tr>
<tr><td style={{ color: 'var(--cyan)' }}>Med 1</td><td>Warp Scrambler I</td><td className="mono">25</td><td className="mono">1</td></tr>
<tr><td style={{ color: 'var(--cyan)' }}>Med 2</td><td>1MN Afterburner</td><td className="mono">25</td><td className="mono">15</td></tr>
<tr><td style={{ color: 'var(--green)' }}>Low 1</td><td>Armor Plate I</td><td className="mono">10</td><td className="mono">20</td></tr>
<tr><td style={{ color: 'var(--green)' }}>Low 2</td><td>Magnetic Field Stab.</td><td className="mono">15</td><td className="mono">5</td></tr>
<tr style={{ borderTop: '2px solid var(--border-light)' }}>
<td colSpan="2" style={{ fontWeight: 600 }}>Total</td>
<td className="mono" style={{ fontWeight: 600, color: 'var(--cyan)' }}>140/120 </td>
<td className="mono" style={{ fontWeight: 600, color: 'var(--green)' }}>67/40 </td>
</tr>
</tbody>
</table>
<div className="callout callout-danger" style={{ marginTop: 'var(--sp-3)', fontSize: '0.8rem' }}>
<strong>Overfit!</strong> CPU 140/120 and Grid 67/40 exceeds ship capacity. Drop a turret or fit lower-tier modules.
</div>
</div>
</div>
</>
)}
{/* SHIP ACQUISITION */}
{activeSection === 'acquisition' && (<>
<div className="section-header">
<span className="section-num">SHIP-ACQ</span>
<h2 style={{ margin: 0 }}>Ship Acquisition & Hangar</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Ships are the player's primary asset, and acquiring them is a core economic milestone.</strong>
The system balances accessibility (new players always have a ship) with economic consequence
(better ships cost real ISK and represent player investment). Players can own multiple ships
stored in station hangars, but only fly one at a time. Switching ships requires docking.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Ship Ownership Model</h3>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Single Active Ship</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
A player has exactly <strong style={{ color: 'var(--fg)' }}>one active ship</strong> at a time — the ship they're currently piloting.
This is the ship that appears in the star system, has a position, and can perform actions (mine, fight, warp).
The active ship cannot be traded, contracted, or stored while it is active.
</p>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Hangar Storage</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
Ships not currently being piloted are stored in a <strong style={{ color: 'var(--fg)' }}>station hangar</strong>.
Each station has a hangar per player. A player can store unlimited ships at any station they have docking access to.
Hangar ships are safe they cannot be destroyed or stolen while stored.
</p>
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>How Players Get Ships</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Method</th><th>Cost</th><th>Available At</th><th>Phase</th></tr>
</thead>
<tbody>
{[
{ method: 'Rookie Frigate (free)', cost: '0 ISK — granted on first spawn and on death respawn', available: 'Automatic on new player creation. Automatic on respawn after ship destruction.', phase: 'Phase 0', color: 'var(--green)' },
{ method: 'NPC Market (sell orders)', cost: 'Hull base value × 1.01.5 (station markup)', available: 'Any station with Market service. NPC sell orders seeded at galaxy gen.', phase: 'Phase 6', color: 'var(--cyan)' },
{ method: 'Player Market (sell orders)', cost: 'Player-set price — typically below NPC price for T1, above for T2', available: 'Any station with Market service. Player-placed sell orders.', phase: 'Phase 10', color: 'var(--accent)' },
{ method: 'Manufacturing', cost: 'Minerals (from refining ore) + blueprint + factory fee + time', available: 'Stations with Factory service. Requires Industry skill ≥ ship class tier.', phase: 'Phase 5', color: 'var(--purple)' },
{ method: 'Loyalty Point Store', cost: 'ISK + LP — faction ships at below-market rates', available: 'Faction stations only. Requires high standing + LP balance.', phase: 'Phase 12', color: 'var(--red)' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: row.color }}>{row.method}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.85rem' }}>{row.cost}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.85rem' }}>{row.available}</td>
<td className="mono">{row.phase}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>The Rookie Frigate</h3>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Free Ship Policy</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>On first spawn:</strong> New player receives a Rookie Frigate at their faction's starter station.</li>
<li><strong style={{ color: 'var(--fg)' }}>On death (ship destroyed):</strong> Player respawns at their home station with a <em>new</em> Rookie Frigate. Always free.</li>
<li><strong style={{ color: 'var(--fg)' }}>Cannot be sold or traded:</strong> The Rookie Frigate has no market value.</li>
<li><strong style={{ color: 'var(--fg)' }}>Cannot be insured:</strong> Insurance doesn't apply you always get a new one free.</li>
<li><strong style={{ color: 'var(--fg)' }}>Basic stats:</strong> 2 high slots, 2 mid slots, 1 low slot. 200 hull, 150 armor, 100 shield. 100 cargo. Very limited enough for basic mining and combat, but weak.</li>
<li><strong style={{ color: 'var(--fg)' }}>NPC market exclusion:</strong> Rookie Frigates are never sold on the NPC market they exist only as free grants.</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--red)' }}>
<h4 style={{ color: 'var(--red)' }}>Why Free Respawn?</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
A player without a ship has no way to earn ISK they can't mine, fight, or trade. A permanent
"ship-less" state is a dead-end that causes player churn. The free Rookie Frigate ensures that
<strong style={{ color: 'var(--fg)' }}> every player always has a path back into the game</strong>, no matter how
many times they're destroyed. The cost of death is the <em>upgraded</em> ship and modules you lost
not the ability to play at all.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>
Design rule: "Never softlock the player out of the game loop." The Rookie Frigate is the safety net.
</div>
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>NPC Market Ship Pricing</h3>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>NPC sell orders provide baseline ship availability.</strong> At galaxy generation, the server seeds NPC sell orders
for every ship type at stations with Factory services. These orders have <em>infinite</em> quantity (they never run out)
but are priced at a premium over manufacturing cost. This ensures:
(1) players can always buy a basic ship hull, (2) player manufacturers can undercut NPC prices profitably,
(3) the economy has a known ISK sink ceiling per ship class.
</div>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Ship Class</th><th>Manufacturing Cost</th><th>NPC Sell Price</th><th>Insurance (Standard)</th><th>Effective Replacement</th></tr>
</thead>
<tbody>
{[
{ cls: 'Frigate', mfg: '~8,000 ISK', npc: '~15,000 ISK', insurance: '~10,500 ISK (70%)', effective: '~4,500 ISK + modules' },
{ cls: 'Destroyer', mfg: '~20,000 ISK', npc: '~35,000 ISK', insurance: '~24,500 ISK (70%)', effective: '~10,500 ISK + modules' },
{ cls: 'Cruiser', mfg: '~60,000 ISK', npc: '~100,000 ISK', insurance: '~70,000 ISK (70%)', effective: '~30,000 ISK + modules' },
{ cls: 'Battlecruiser', mfg: '~180,000 ISK', npc: '~300,000 ISK', insurance: '~210,000 ISK (70%)', effective: '~90,000 ISK + modules' },
{ cls: 'Battleship', mfg: '~500,000 ISK', npc: '~800,000 ISK', insurance: '~560,000 ISK (70%)', effective: '~240,000 ISK + modules' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: 'var(--accent)' }}>{row.cls}</td>
<td className="mono">{row.mfg}</td>
<td className="mono" style={{ color: 'var(--red)' }}>{row.npc}</td>
<td className="mono" style={{ color: 'var(--green)' }}>{row.insurance}</td>
<td className="mono" style={{ color: 'var(--fg-dim)' }}>{row.effective}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Ship Switching Flow</h3>
<div className="card card-accent" style={{ marginBottom: 'var(--sp-5)' }}>
<h4 style={{ marginBottom: 'var(--sp-4)' }}>Docked Ship Switch</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2.2 }}>
<span style={{ color: 'var(--cyan)' }}>1. Dock at station</span> — Player must be docked. Cannot switch ships while in space.<br/>
<span style={{ color: 'var(--accent)' }}>2. Open Hangar panel</span> — Station Mode → Hangar tab. Shows all ships stored at this station.<br/>
<span style={{ color: 'var(--green)' }}>3. Select a ship</span> — Shows ship name, class, fitting summary, cargo, insurance status.<br/>
<span style={{ color: 'var(--purple)' }}>4. Activate</span> — Click "Make Active". Current active ship moves to hangar. Selected ship becomes active.<br/>
<span style={{ color: 'var(--fg)' }}>5. Cargo transfer</span> — Prompt to transfer cargo between ships (limited by destination capacity). Remaining stays in station inventory.<br/>
<span style={{ color: 'var(--muted)' }}>6. Undock</span> Player undocks in the new active ship. Old ship stays in hangar.
</div>
</div>
<div className="callout callout-warn" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Cannot switch while in space.</strong> If your ship is destroyed, you respawn at your home station
in a Rookie Frigate you cannot switch to a hangar ship remotely. You must fly to the station where the ship is stored and dock.
This is intentional: it creates geographic identity ("my ships are in Amarr") and makes home station choice meaningful.
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-6)' }}>
<span className="section-num">SHIP-ACQ-DB</span>
<h2 style={{ margin: 0 }}>Backend Impact</h2>
</div>
<div className="card card-accent">
<h4 style={{ marginBottom: 'var(--sp-4)' }}>Schema & Reducer Changes</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
<span style={{ color: 'var(--accent)' }}>ships</span> add column: <code>storage_location</code> (enum: active / hangar:station_id). When active, ship has system_id/x/y/z. When in hangar, stored at station.<br/>
<span style={{ color: 'var(--cyan)' }}>ships</span> add column: <code>is_rookie</code> (bool, default false). Rookie frigates are untradeable, uninsurable, free-replace.<br/>
<span style={{ color: 'var(--green)' }}>npc_sell_orders</span> seeded at galaxy gen for every <code>ship_types</code> entry at Factory stations. Infinite quantity. Price = <code>base_hull_value × 1.5</code>.<br/>
<span style={{ color: 'var(--purple)' }}>New reducer:</span> <code>switch_ship(player_id, target_ship_id)</code> validate docked, validate target in same station hangar, swap active hangar.<br/>
<span style={{ color: 'var(--red)' }}>Updated reducer:</span> <code>connect_player(display_name)</code> on first connection, spawn Rookie Frigate at faction starter station. On subsequent, load existing active ship.<br/>
<span style={{ color: 'var(--fg)' }}>Updated reducer:</span> <code>respawn_player(player_id)</code> on ship destruction, create new Rookie Frigate at home station, set as active.
</div>
</div>
</>)}
{/* AI CREW */}
{activeSection === 'crew' && (
<>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Post-MVP feature.</strong> AI crew is designed here so the ship and backend architecture
supports it from the start, but implementation is planned after the core gameplay loop ships.
</div>
<div className="section-header">
<span className="section-num">SHIP-AI</span>
<h2 style={{ margin: 0 }}>AI Crew System</h2>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem', maxWidth: '680px', marginBottom: 'var(--sp-5)' }}>
Players can own multiple ships and assign AI crew members to pilot them. AI ships execute
autonomous tasks mining runs, patrol routes, trade delivery while the player controls
their primary ship directly. AI crew gain experience over time and improve at their assigned role.
</p>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>Crew Roles</h4>
<table className="data-table">
<thead><tr><th>Role</th><th>Behavior</th><th>XP Growth</th></tr></thead>
<tbody>
<tr>
<td><span className="pill pill-amber">Miner</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Warps to belt, mines until cargo full, returns to station, sells ore.</td>
<td style={{ color: 'var(--fg-dim)' }}>Faster cycle times, better ore selection.</td>
</tr>
<tr>
<td><span className="pill pill-cyan">Patrol</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Circulates waypoints, engages hostiles, reports contacts.</td>
<td style={{ color: 'var(--fg-dim)' }}>Better target priority, longer patrol endurance.</td>
</tr>
<tr>
<td><span className="pill pill-green">Hauler</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Moves cargo between stations along trade routes.</td>
<td style={{ color: 'var(--fg-dim)' }}>Faster warp align, larger cargo optimization.</td>
</tr>
<tr>
<td><span className="pill pill-red">Guard</span></td>
<td style={{ color: 'var(--fg-dim)' }}>Escorts player ship, engages threats within range.</td>
<td style={{ color: 'var(--fg-dim)' }}>Better reaction time, coordination with fleet.</td>
</tr>
</tbody>
</table>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Crew Progression</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
<strong style={{ color: 'var(--fg)' }}>Rank levels:</strong><br/>
<span style={{ color: 'var(--muted)' }}>Cadet</span> <span style={{ color: 'var(--green)' }}>Ensign</span> {' '}
<span style={{ color: 'var(--cyan)' }}>Lieutenant</span> {' '}
<span style={{ color: 'var(--purple)' }}>Commander</span> {' '}
<span style={{ color: 'var(--accent)' }}>Captain</span><br/><br/>
<strong style={{ color: 'var(--fg)' }}>XP sources:</strong><br/>
Task completion (base XP)<br/>
Task success rate bonus<br/>
Survival bonus (ship not destroyed)<br/>
Player commendation (manual XP grant)
</div>
</div>
</div>
<div className="section-header">
<span className="section-num">SHIP-OPS</span>
<h2 style={{ margin: 0 }}>Task Assignment Flow</h2>
</div>
<div className="card card-accent">
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
1. Player docks at station and opens <strong style={{ color: 'var(--fg)' }}>Crew Management</strong> panel<br/>
2. Selects an idle ship + available crew member<br/>
3. Assigns a <strong style={{ color: 'var(--accent)' }}>task template</strong> (mine system X, patrol route Y, haul to station Z)<br/>
4. Crew departs autonomously ship disappears from player's direct control<br/>
5. Player receives periodic <strong style={{ color: 'var(--cyan)' }}>status reports</strong> in a dedicated channel<br/>
6. Task completes (or ship is destroyed) → crew returns to station or sends distress signal<br/>
7. Resources earned are deposited in player's station inventory
</div>
</div>
<div className="callout callout-warn" style={{ marginTop: 'var(--sp-5)' }}>
<strong>Risk:</strong> AI crew earning passive income could trivialize the economy. Mitigation: AI
operations have costs (fuel, maintenance, crew wages) and diminishing returns at scale. A player
with 10 AI miners shouldn't earn 10× a single player — there should be coordination overhead.
</div>
<div className="callout callout-info" style={{ marginTop: 'var(--sp-4)' }}>
<strong>AI Crew vs. Zora (Ship AI):</strong> These are two different systems. <strong>AI Crew</strong> (this tab) are autonomous
pilots that fly <em>other</em> ships on your behalf — mining runs, patrol routes, hauling. <strong>Zora</strong> (see Ship AI page)
is a companion AI installed on <em>your current ship</em> that provides market intelligence, tactical advice,
and dialogue. Think of AI Crew as your employees and Zora as your ship's computer. They do not share
systems, modules, or XP. A future expansion may let Zora be assigned to an AI Crew ship, but that is post-MVP scope.
</div>
</>
)}
{/* Death & Loss */}
<div className="callout callout-info" style={{ marginTop: 'var(--sp-6)' }}>
<strong>Slot scope note:</strong> High, Medium, and Low slots cover combat, mining, propulsion, and tank modules. Ship AI modules (Communications Processor, Market Analyzer, etc.) are a separate system see the <em>Ship AI Module Gates</em> tab for the full AI module catalog and how they install alongside standard fittings.
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">SHIP-DEATH</span>
<h2 style={{ margin: 0 }}>Ship Destruction</h2>
</div>
<div className="grid-2">
<div className="card" style={{ borderLeft: '3px solid var(--red)' }}>
<h4 style={{ color: 'var(--red)' }}>What you lose</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>The ship hull itself (destroyed)</li>
<li>All fitted modules (50% chance each to drop as loot)</li>
<li>Cargo destroyed or dropped (50/50 split)</li>
<li>AI crew aboard injured, require medical bay recovery time</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>What you keep</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Your other ships in hangars</li>
<li>Station inventory and assets</li>
<li>Player XP and progression</li>
<li>ISK in wallet ()</li>
</ul>
</div>
</div>
<div className="callout callout-danger" style={{ marginTop: 'var(--sp-5)' }}>
<strong>Respawn:</strong> Player respawns at their home station (or nearest friendly station) in a
rookie frigate. Insurance policies can partially reimburse ship loss.
</div>
</div>
);
}
window.GDD.ShipsPage = ShipsPage;

590
js/pages/social.js Normal file
View File

@@ -0,0 +1,590 @@
window.GDD = window.GDD || {};
function SocialPage() {
const [activeSection, setActiveSection] = React.useState('progression');
const skillCategories = [
{
name: 'Combat',
color: 'var(--red)',
skills: ['Gunnery', 'Missiles', 'Shield Operation', 'Armor Tanking', 'Electronic Warfare'],
xpSource: 'Dealing damage, destroying NPCs, completing combat missions',
},
{
name: 'Industry',
color: 'var(--accent)',
skills: ['Mining', 'Refining', 'Manufacturing', 'Blueprint Research', 'Resource Processing'],
xpSource: 'Mining cycles, refining batches, manufactured items, research jobs',
},
{
name: 'Navigation',
color: 'var(--cyan)',
skills: ['Warp Drive Operation', 'Afterburner', 'Evasive Maneuvering', 'Capital Navigation'],
xpSource: 'Distance warped, systems visited, successful evasion from combat',
},
{
name: 'Trade',
color: 'var(--green)',
skills: ['Market Analysis', 'Broker Relations', 'Hauling', 'Contracting', 'Regional Trading'],
xpSource: 'Completed trades, market orders filled, cargo hauled between systems',
},
{
name: 'Leadership',
color: 'var(--purple)',
skills: ['Fleet Command', 'Wing Command', 'AI Coordination', 'Crew Management'],
xpSource: 'Fleet actions, AI crew task completions, group mission success (post-MVP)',
},
];
const chatChannels = [
{ name: 'Local', range: 'Current system', delay: 'Instant', description: 'Everyone in the same star system sees messages immediately. Core social channel.', color: 'var(--fg-bright)' },
{ name: 'Private Message', range: 'Based on distance', delay: 'Light-speed delayed', description: 'Direct messages to another player. Messages arrive after a delay proportional to light-years between systems. Across the galaxy = minutes of delay.', color: 'var(--cyan)' },
{ name: 'Trade', range: 'Station / Region', delay: 'Instant at station, delayed region-wide', description: 'Buy/sell offers, price checks, trade negotiations. Station-local is instant; regional relay has a 30s delay.', color: 'var(--green)' },
];
const bountyTiers = [
{ tier: 'Petty', threshold: '500 ISK', reward: '10% of bounty', visibility: 'Current system only', color: 'var(--muted)' },
{ tier: 'Standard', threshold: '5,000 ISK', reward: '15% of bounty', visibility: 'Current region (constellation cluster)', color: 'var(--cyan)' },
{ tier: 'Dangerous', threshold: '50,000 ISK', reward: '20% of bounty + kill bonus', visibility: 'All regions (galaxy-wide board)', color: 'var(--accent)' },
{ tier: 'Most Wanted', threshold: '500,000 ISK', reward: '25% + unique cosmetic reward', visibility: 'Galaxy-wide + leaderboard', color: 'var(--red)' },
];
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>Progression & Social</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '680px' }}>
XP-based progression across five skill categories. Chat ranges from instant local to
light-speed-delayed private messages. Bounty system creates emergent player-driven justice
and piracy consequences. Designed for ~3-hour play sessions with meaningful progression per session.
</p>
{/* Tab navigation */}
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-6)' }}>
{[
{ id: 'progression', label: 'XP & Skills' },
{ id: 'chat', label: 'Chat & Comms' },
{ id: 'bounty', label: 'Bounty System' },
{ id: 'waypoints', label: 'Waypoints' },
{ id: 'corps', label: 'Corporations' },
].map(t => (
<button key={t.id} className={`btn btn-sm${activeSection === t.id ? ' btn-primary' : ''}`}
onClick={() => setActiveSection(t.id)}>{t.label}</button>
))}
</div>
{/* PROGRESSION */}
{activeSection === 'progression' && (
<>
<div className="stat-grid">
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--accent)' }}>Action-based</div>
<div className="stat-label">XP Model</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--cyan)' }}>5 Categories</div>
<div className="stat-label">Skill Trees</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--green)' }}>~3 hours</div>
<div className="stat-label">Session Target</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ color: 'var(--purple)' }}>V Levels</div>
<div className="stat-label">Per Skill</div>
</div>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Design intent:</strong> XP comes from doing things, not waiting. A 3-hour mining session
should unlock at least one meaningful skill upgrade. Combat and trade have comparable XP rates
so no playstyle feels punished.
</div>
<div className="section-header">
<span className="section-num">SOC-SKL</span>
<h2 style={{ margin: 0 }}>Skill Categories</h2>
</div>
{skillCategories.map((cat, i) => (
<div key={i} className="card" style={{ borderLeft: `3px solid ${cat.color}`, marginBottom: 'var(--sp-4)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-3)' }}>
<h4 style={{ color: cat.color, margin: 0 }}>{cat.name}</h4>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--sp-1)', marginBottom: 'var(--sp-3)' }}>
{cat.skills.map((s, j) => (
<span key={j} className="pill" style={{ background: 'var(--surface-raised)', color: cat.color, border: `1px solid ${cat.color}40`, fontSize: '0.7rem' }}>
{s}
</span>
))}
</div>
<div style={{ fontSize: '0.82rem', color: 'var(--fg-dim)' }}>
<strong style={{ color: 'var(--muted)' }}>XP source:</strong> {cat.xpSource}
</div>
</div>
))}
<div className="section-header" style={{ marginTop: 'var(--sp-6)' }}>
<span className="section-num">SOC-LVL</span>
<h2 style={{ margin: 0 }}>Level Progression</h2>
</div>
<div className="card card-accent">
<h4 style={{ marginBottom: 'var(--sp-4)' }}>XP Curve (per skill)</h4>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr>
<th>Level</th>
<th>Cumulative XP</th>
<th>Approx. Active Play</th>
<th>Typical Unlock</th>
</tr>
</thead>
<tbody>
<tr><td style={{ color: 'var(--muted)' }}>I</td><td className="mono">100</td><td className="mono">~15 min</td><td style={{ color: 'var(--fg-dim)' }}>Basic modules, rookie ships</td></tr>
<tr><td style={{ color: 'var(--green)' }}>II</td><td className="mono">500</td><td className="mono">~1 hour</td><td style={{ color: 'var(--fg-dim)' }}>Standard modules, refining</td></tr>
<tr><td style={{ color: 'var(--cyan)' }}>III</td><td className="mono">2,000</td><td className="mono">~3 hours</td><td style={{ color: 'var(--fg-dim)' }}>T2 modules, cruisers, manufacturing</td></tr>
<tr><td style={{ color: 'var(--purple)' }}>IV</td><td className="mono">8,000</td><td className="mono">~8 hours</td><td style={{ color: 'var(--fg-dim)' }}>Advanced fittings, battlecruisers</td></tr>
<tr><td style={{ color: 'var(--accent)' }}>V</td><td className="mono">32,000</td><td className="mono">~20 hours</td><td style={{ color: 'var(--fg-dim)' }}>Battleships, T2 ships, mastery</td></tr>
</tbody>
</table>
</div>
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">SOC-RESPEC</span>
<h2 style={{ margin: 0 }}>Respec (Skill Reallocation)</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Mistakes should be fixable, but not free.</strong> Players can reallocate (respec) their skill points,
returning XP from one skill and applying it to another. This prevents permanent lock-in from early uninformed
decisions while making frequent respeccing uneconomical.
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>Respec Mechanics</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Cost:</strong> 20% XP penalty on reallocated points. Respeccing 1,000 XP returns 800 XP to redistribute.</li>
<li><strong style={{ color: 'var(--fg)' }}>Cooldown:</strong> 7 real-world days between respeccs. Cannot be bypassed.</li>
<li><strong style={{ color: 'var(--fg)' }}>Scope:</strong> Full respec (all skills) or single-skill respec. Full respec costs 30% instead of 20%.</li>
<li><strong style={{ color: 'var(--fg)' }}>Location:</strong> Must be docked at a station with a "Neural Remapping" facility (not all stations have this).</li>
<li><strong style={{ color: 'var(--fg)' }}>Preview:</strong> Before confirming, the respec UI shows the exact XP loss and resulting skill levels. No surprises.</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Design Rationale</h4>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-3) 0' }}>
The 20% cost ensures that respeccing is a meaningful decision, not a free toggle. A player who respeccs
from Mining to Gunnery loses 200 of their 1,000 mining XP they can try combat, but they've paid for the
privilege. The cooldown prevents rapid role-switching and preserves the value of specialization.
Players who plan their skill progression carefully are rewarded with more total XP than those who respec frequently.
</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem', color: 'var(--muted)' }}>
Example: Mining III (2,000 XP) \u2192 respec \u2192 receive 1,600 XP \u2192 invest in Gunnery (now Gunnery II at 500 XP + 1,100 banked)
</div>
</div>
</div>
</>
)}
{/* CHAT & COMMS */}
{activeSection === 'chat' && (
<>
<div className="section-header">
<span className="section-num">SOC-COM</span>
<h2 style={{ margin: 0 }}>Communication System</h2>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem', maxWidth: '680px', marginBottom: 'var(--sp-5)' }}>
Communication range is physical — messages travel at light speed. Local chat in the same system
is instant. Private messages to someone across the galaxy are delayed by minutes. This creates
meaningful tactical information asymmetry and makes relay networks valuable.
</p>
{chatChannels.map((ch, i) => (
<div key={i} className="card" style={{ borderLeft: `3px solid ${ch.color}`, marginBottom: 'var(--sp-4)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-3)' }}>
<h4 style={{ color: ch.color, margin: 0 }}>{ch.name}</h4>
<span className="pill" style={{ background: 'var(--surface-raised)', color: 'var(--fg-dim)', border: '1px solid var(--border)', fontSize: '0.7rem' }}>
{ch.range}
</span>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: '0 0 var(--sp-2) 0' }}>{ch.description}</p>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.75rem', color: 'var(--muted)' }}>
Delay: {ch.delay}
</div>
</div>
))}
<div className="section-header" style={{ marginTop: 'var(--sp-6)' }}>
<span className="section-num">SOC-PHYS</span>
<h2 style={{ margin: 0 }}>Light-Speed Delay Mechanics</h2>
</div>
<div className="card card-accent">
<h4 style={{ marginBottom: 'var(--sp-4)' }}>Message Travel Time</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
<strong style={{ color: 'var(--fg)' }}>Same system:</strong> 0s (quantum relay)<br/>
<strong style={{ color: 'var(--fg)' }}>Adjacent system (1 gate):</strong> ~2s<br/>
<strong style={{ color: 'var(--fg)' }}>5 jumps:</strong> ~10s<br/>
<strong style={{ color: 'var(--fg)' }}>20 jumps:</strong> ~45s<br/>
<strong style={{ color: 'var(--fg)' }}>Across galaxy:</strong> ~23 minutes<br/>
<br/>
<span style={{ color: 'var(--muted)' }}>Formula: baseDelay × sqrt(gateDistance) × 2s</span>
</div>
</div>
<div className="grid-2" style={{ marginTop: 'var(--sp-5)' }}>
<div className="card">
<h4 style={{ color: 'var(--accent)' }}>Strategic implications</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Fleet commanders near the front have better intel</li>
<li>Market data is delayed — arbitrage exists between regions</li>
<li>Distress calls from deep null-sec arrive minutes late</li>
<li>Players can set up communication relay networks</li>
</ul>
</div>
<div className="card">
<h4 style={{ color: 'var(--cyan)' }}>Post-MVP channels</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong>Corporation</strong> — instant for corp members (post-MVP)</li>
<li><strong>Fleet</strong> — instant for fleet members (post-MVP)</li>
<li><strong>Relay Beacons</strong> — player-placed structures that boost range</li>
<li><strong>Alliance</strong> — meta-corp channels (far future)</li>
</ul>
</div>
</div>
</>
)}
{/* BOUNTY SYSTEM */}
{activeSection === 'bounty' && (
<>
<div className="section-header">
<span className="section-num">SOC-BNT</span>
<h2 style={{ margin: 0 }}>Bounty System</h2>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem', maxWidth: '680px', marginBottom: 'var(--sp-5)' }}>
Any player can place a bounty on another player. Bounties create consequences for piracy
and emergent "space justice" dynamics. Higher bounties attract more bounty hunters and
increase visibility across the galaxy.
</p>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead>
<tr><th>Tier</th><th>Bounty Threshold</th><th>Hunter Reward</th><th>Visibility (Sector-Specific)</th></tr>
</thead>
<tbody>
{bountyTiers.map((tier, i) => (
<tr key={i}>
<td><span className={`pill ${i === 0 ? 'pill-green' : i === 1 ? 'pill-cyan' : i === 2 ? 'pill-amber' : 'pill-red'}`}>
{tier.tier}
</span></td>
<td className="mono">{tier.threshold}</td>
<td style={{ color: 'var(--fg-dim)' }}>{tier.reward}</td>
<td style={{ color: tier.color }}>{tier.visibility}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="grid-2" style={{ marginTop: 'var(--sp-5)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--red)' }}>
<h4 style={{ color: 'var(--red)' }}>How bounties work</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
1. Player A places bounty on Player B (ISK deducted immediately)<br/>
2. Bounty pool accumulates — multiple players can contribute<br/>
3. When Player B's ship is destroyed, bounty pays out<br/>
4. Payout = percentage of bounty pool based on tier<br/>
5. Killer gets the reward; remaining pool stays active<br/>
6. If Player B stays clean for 30 days, bounty decays 10%/week
</div>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Anti-abuse rules</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>You cannot claim your own bounty (alt check)</li>
<li>Bounty payout never exceeds ship loss value</li>
<li>Minimum bounty placement: 500 ISK (prevents spam)</li>
<li>Bounty target must have negative security status or have committed a hostile act in the last 24h (<em>see Gameplay Security Levels</em>)</li>
<li>Kill feed shows bounty collected, not total pool</li>
</ul>
</div>
</div>
<div className="callout callout-info" style={{ marginTop: 'var(--sp-5)' }}>
<strong>Sector-specific bounties:</strong> Bounty visibility is sector-based, not galaxy-wide by default. A petty bounty
in Jita is invisible in Amarr. This means a pirate can be hunted in one region while operating freely in another.
Only Dangerous and Most Wanted bounties propagate galaxy-wide. This creates regional justice ecosystems and
makes bounty hunting a localized profession rather than a galaxy-wide pursuit.
</div>
<div className="callout callout-warn" style={{ marginTop: 'var(--sp-4)' }}>
<strong>Kill feed:</strong> A galaxy-wide feed shows ship destruction events with pilot names,
ship types, system, and bounty collected. This creates reputation dynamics and emergent
storylines "CMDR Worf has been destroyed in O-WAMW, 45,000 ISK bounty collected."
</div>
</>
)}
{/* WAYPOINTS */}
{activeSection === 'waypoints' && (
<>
<div className="section-header">
<span className="section-num">SOC-NAV</span>
<h2 style={{ margin: 0 }}>Waypoints & Bookmarks</h2>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem', maxWidth: '680px', marginBottom: 'var(--sp-5)' }}>
Players can bookmark arbitrary locations in space and create waypoint routes for navigation.
Bookmarks are personal; waypoints can be shared. Fleet beacons are visible to all fleet members
and create tactical rally points.
</p>
<div className="grid-2">
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>Bookmarks</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Save any point in space as a named bookmark</li>
<li>Personal only you see them</li>
<li>Can warp to any bookmark (if in range)</li>
<li>Used for: safe spots, mining positions, ambush points, salvage sites</li>
<li>Storage limit: 100 bookmarks per player (MVP)</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Waypoints & Routes</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Create multi-stop routes through star systems</li>
<li>Autopilot follows route automatically (slower than manual)</li>
<li>Route avoids low-sec if preference set</li>
<li>Share routes with other players (trade routes, patrol paths)</li>
<li>Optimize for: shortest, safest, or most profitable</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--purple)' }}>
<h4 style={{ color: 'var(--purple)' }}>Fleet Beacons</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Visible to all fleet members on star map</li>
<li>Create rally points and tactical positions</li>
<li>Any fleet member can create a beacon</li>
<li>Beacons expire after 1 hour or when creator leaves fleet</li>
<li>Post-MVP feature designed into data model now</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Map Annotations</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Mark systems with notes ("pirate camp", "good mining", "friendly station")</li>
<li>Notes visible on star map hover</li>
<li>Statistics overlay: jumps/hour, ship kills, pod kills</li>
<li>Color-code systems by danger level or personal preference</li>
<li>Export/import annotations for sharing</li>
</ul>
</div>
</div>
<div className="section-header" style={{ marginTop: 'var(--sp-6)' }}>
<span className="section-num">SOC-DB</span>
<h2 style={{ margin: 0 }}>Backend Tables (New)</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)', fontSize: '0.82rem' }}>
<strong>Canonical location:</strong> These tables are also listed in the <em>Backend Tables</em> tab, which serves as the master schema reference. The definitions here include Social-specific context.
</div>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead><tr><th>Table</th><th>Purpose</th><th>Key Fields</th></tr></thead>
<tbody>
<tr>
<td><code>bookmarks</code></td>
<td style={{ color: 'var(--fg-dim)' }}>Player-saved locations</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>bookmark_id, player_id, system_id, x/y/z, name, created_at</td>
</tr>
<tr>
<td><code>waypoints</code></td>
<td style={{ color: 'var(--fg-dim)' }}>Multi-stop routes</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>route_id, player_id, stops (ordered system list), name, shared</td>
</tr>
<tr>
<td><code>bounties</code></td>
<td style={{ color: 'var(--fg-dim)' }}>Bounty pool per target</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>target_player_id, total_pool, tier, last_hostile_act</td>
</tr>
<tr>
<td><code>bounty_contributions</code></td>
<td style={{ color: 'var(--fg-dim)' }}>Individual bounty payments</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>contribution_id, target_id, contributor_id, amount, timestamp</td>
</tr>
<tr>
<td><code>kill_feed</code></td>
<td style={{ color: 'var(--fg-dim)' }}>Ship destruction events</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>kill_id, victim_id, killer_id, ship_type, system_id, bounty_collected, timestamp</td>
</tr>
<tr>
<td><code>player_skills</code></td>
<td style={{ color: 'var(--fg-dim)' }}>XP and levels per skill</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>player_id, skill_name, xp, level, last_action_at</td>
</tr>
<tr>
<td><code>fleet_beacons</code></td>
<td style={{ color: 'var(--fg-dim)' }}>Temporary fleet rally points</td>
<td style={{ fontSize: '0.75rem', color: 'var(--muted)' }}>beacon_id, fleet_id, creator_id, system_id, x/y/z, expires_at</td>
</tr>
</tbody>
</table>
</div>
</>
)}
{/* CORPORATIONS & TERRITORY */}
{activeSection === 'corps' && (<>
<div className="section-header">
<span className="section-num">SOC-CORP</span>
<h2 style={{ margin: 0 }}>Corporations & Territory</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Era 2 feature designed now, implemented in Phase 14.</strong>
Corporations ("corps") are player organizations: persistent groups with shared wallets, hangars, roles,
and territory claims. They are the endgame social structure the reason players stay for years. This spec
establishes the design direction so the backend schema can accommodate it without breaking changes.
Implementation details may evolve, but the core model is fixed.
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Corporation Lifecycle</h3>
<div className="card card-accent" style={{ marginBottom: 'var(--sp-6)' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2.2 }}>
<span style={{ color: 'var(--cyan)' }}>1. Found</span> Any player pays 1,000,000 founding fee. Chooses name + ticker (35 chars). Becomes CEO. Minimum 1 member.
<br/>
<span style={{ color: 'var(--green)' }}>2. Recruit</span> CEO (or Director) sends invite. Player accepts. Member appears on corp roster. Corp chat channel auto-created.
<br/>
<span style={{ color: 'var(--accent)' }}>3. Operate</span> Members contribute to corp wallet (ISK tax on bounties/missions, optional). Shared hangar at corp HQ station. Roles control access.
<br/>
<span style={{ color: 'var(--purple)' }}>4. Claim</span> Corp anchors a structure in a null-sec system. Structure asserts sovereignty. System shows on sov map as corp territory.
<br/>
<span style={{ color: 'var(--red)' }}>5. Defend</span> Rival corps can contest sovereignty. Structure has a vulnerability window (CEO-configured, 4h/day). During vulnerability, structure can be attacked.
<br/>
<span style={{ color: 'var(--fg)' }}>6. Dissolve</span> If CEO disbands or corp drops below 3 members for 14 days, corp dissolves. Assets liquidated to CEO wallet. Territory unclaimed.
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Corporation Roles</h3>
<div style={{ overflowX: 'auto', marginBottom: 'var(--sp-6)' }}>
<table className="data-table">
<thead>
<tr><th>Role</th><th>Permissions</th><th>Assignment</th></tr>
</thead>
<tbody>
{[
{ role: 'CEO', perms: 'All permissions. Can disband corp. Can transfer CEO role. Cannot be kicked.', assignment: 'Founder (auto). Transferable.' },
{ role: 'Director', perms: 'Invite/kick members, manage roles, corp wallet full access, anchor/destroy structures, set tax rate, configure vulnerability window.', assignment: 'Appointed by CEO or other Directors.' },
{ role: 'Accountant', perms: 'View corp wallet history. Pay bills. Set market orders from corp funds. Cannot withdraw ISK.', assignment: 'Appointed by Director+.' },
{ role: 'Pilot', perms: 'Access corp hangar (configured per station). Use corp fittings. Join corp fleet.', assignment: 'Default role for new members.' },
{ role: 'Recruit', perms: 'Corp chat access. View roster. No hangar access. No wallet access.', assignment: 'Probationary role (optional, 7-day default). Auto-promotes to Pilot.' },
].map((row, i) => (
<tr key={i}>
<td style={{ fontWeight: 600, color: ['var(--accent)', 'var(--cyan)', 'var(--green)', 'var(--purple)', 'var(--fg-dim)'][i] }}>{row.role}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.perms}</td>
<td style={{ color: 'var(--fg-dim)', fontSize: '0.82rem' }}>{row.assignment}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Corporation Wallet & Tax</h3>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--green)' }}>
<h4 style={{ color: 'var(--green)' }}>Corp Wallet</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li>Shared ISK balance separate from all member wallets</li>
<li>Funded by: tax on member income, voluntary donations, structure taxes, corp market orders</li>
<li>Spent on: structure fuel, alliance dues, SRP (ship replacement program), member bonuses</li>
<li>Full transaction log visible to Accountant+ roles</li>
<li>Withdrawal requires Director+ approval (or CEO)</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)' }}>Tax System</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Bounty tax:</strong> CEO sets % (0100%). Deducted from NPC bounties and mission rewards. Default 10%.</li>
<li><strong style={{ color: 'var(--fg)' }}>Market tax:</strong> Optional tax on market orders placed in corp-controlled stations. 05%. Set per station.</li>
<li><strong style={{ color: 'var(--fg)' }}>Refining tax:</strong> Optional tax on refining at corp-controlled facilities. 05%.</li>
<li>Tax revenue goes to corp wallet automatically</li>
<li>Members see their personal tax contribution in wallet history</li>
</ul>
</div>
</div>
<h3 style={{ marginBottom: 'var(--sp-4)' }}>Territory & Sovereignty</h3>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Null-sec only.</strong> Territory claims are only possible in null-sec (security 0.0). High-sec and low-sec systems
cannot be claimed. This preserves the "wild west" nature of null-sec as the player-driven frontier while keeping
NPC-controlled space as the shared commons.
</div>
<div className="grid-2" style={{ marginBottom: 'var(--sp-6)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--red)' }}>
<h4 style={{ color: 'var(--red)' }}>Corporation Structures</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Starbase (Medium):</strong> Claims sovereignty in 1 system. Provides: refining bonus (+10%), market access, hangar, clone bay. Cost: 50M + fuel.</li>
<li><strong style={{ color: 'var(--fg)' }}>Citadel (Large):</strong> Claims constellation-wide influence. Provides: all Starbase bonuses + manufacturing slots + insurance desk. Cost: 500M + fuel.</li>
<li><strong style={{ color: 'var(--fg)' }}>Keepstar (XL):</strong> Regional capital. Provides: all Citadel bonuses + capital ship construction + super-capital docking. Cost: 5B + fuel. One per region.</li>
<li>Structures have shields, armor, and hull they can be destroyed during vulnerability windows</li>
<li>Structure destruction = loss of territory claim. System becomes unclaimed.</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)' }}>Sovereignty Mechanics</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg)' }}>Anchoring:</strong> Corp anchors structure in unclaimed null-sec system. 24-hour onlining timer. During onlining, structure is invulnerable.</li>
<li><strong style={{ color: 'var(--fg)' }}>Vulnerability window:</strong> CEO picks a 4-hour daily window (must overlap with corp prime time). Structure can only be attacked during this window.</li>
<li><strong style={{ color: 'var(--fg)' }}>Reinforcement:</strong> When structure shields reach 0%, it enters reinforcement (18h timer). Armor becomes targetable after timer expires.</li>
<li><strong style={{ color: 'var(--fg)' }}>Destruction:</strong> When structure hull reaches 0%, it explodes. Corp loses sovereignty. All items in corp hangar drop as loot (50%) or are destroyed (50%).</li>
<li><strong style={{ color: 'var(--fg)' }}>Strategic index:</strong> The longer a corp holds a system, the higher the strategic index (05). Higher index = longer reinforcement timer, stronger structure shields. Rewards long-term ownership.</li>
</ul>
</div>
</div>
<div className="callout callout-warn" style={{ marginBottom: 'var(--sp-5)' }}>
<strong>Design intent:</strong> Territory warfare is the endgame that keeps players engaged for years. A corp's home system
is its identity losing it is a major event, conquering one is a major achievement. The vulnerability window ensures
battles happen when both sides can participate (no 3 AM structure kills). The strategic index rewards corps that invest
in their territory rather than nomadic expansion. The goal is <em>meaningful conflict</em>, not meaningless destruction.
</div>
<div className="section-header">
<span className="section-num">SOC-CORP-DB</span>
<h2 style={{ margin: 0 }}>Backend Impact</h2>
</div>
<div className="card card-accent" style={{ marginBottom: 'var(--sp-5)' }}>
<h4 style={{ marginBottom: 'var(--sp-4)' }}>New Tables (Phase 14)</h4>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.82rem', color: 'var(--fg-dim)', lineHeight: 2 }}>
<span style={{ color: 'var(--accent)' }}>corporations</span> <code>corp_id, name, ticker, ceo_player_id, founded_at, wallet_balance, tax_rate_bounty, tax_rate_market, tax_rate_refining, member_count, hq_station_id, vulnerability_window_start, vulnerability_window_hours, dissolved_at</code><br/>
<span style={{ color: 'var(--cyan)' }}>corp_members</span> <code>corp_id, player_id, role (enum), joined_at, tax_contribution_lifetime</code><br/>
<span style={{ color: 'var(--green)' }}>corp_wallet_journal</span> <code>entry_id, corp_id, entry_type (tax/donation/withdrawal/expense), amount, player_id, timestamp, description</code><br/>
<span style={{ color: 'var(--red)' }}>corp_structures</span> <code>structure_id, corp_id, system_id, structure_type (starbase/citadel/keepstar), shield/armor/hull, state (onlining/online/reinforced/vulnerable/destroyed), reinforced_at, vulnerability_start, strategic_index</code><br/>
<span style={{ color: 'var(--purple)' }}>corp_hangar</span> <code>hangar_id, corp_id, station_id, item_type, quantity, access_role_min</code><br/>
<span style={{ color: 'var(--fg)' }}>corp_fittings</span> <code>fitting_id, corp_id, name, ship_type, modules_json, created_by, access_role_min</code><br/>
<br/>
<span style={{ color: 'var(--cyan)' }}>Reducers:</span> <code>found_corp(name, ticker)</code>, <code>invite_member(player_id)</code>, <code>kick_member(player_id)</code>, <code>set_role(player_id, role)</code>, <code>anchor_structure(system_id, type)</code>, <code>attack_structure(structure_id)</code>, <code>reinforce_structure(structure_id)</code>, <code>destroy_structure(structure_id)</code>, <code>set_tax_rate(type, rate)</code>, <code>deposit_corp_wallet(amount)</code>, <code>withdraw_corp_wallet(amount, reason)</code>
</div>
</div>
<div className="callout callout-info">
<strong>Corp hangar integration:</strong> When a player is docked at a station with a corp hangar, they see a
"Corp Hangar" tab in the Inventory panel. Access is gated by role. Items in corp hangar can be used for corp
manufacturing jobs, corp market orders, or distributed to members. The hangar respects the same item/inventory
system as personal hangars it's just owned by the corp entity rather than a player entity.
</div>
</>)}
</div>
);
}
window.GDD.SocialPage = SocialPage;

174
js/pages/techstack.js Normal file
View File

@@ -0,0 +1,174 @@
window.GDD = window.GDD || {};
function TechStackPage() {
const [activeTab, setActiveTab] = React.useState('frontend');
const tabs = [
{ id: 'frontend', label: 'Frontend' },
{ id: 'rendering', label: '3D Rendering' },
{ id: 'state', label: 'State' },
{ id: 'backend', label: 'Backend' },
{ id: 'styling', label: 'Styling' },
{ id: 'auth', label: 'Auth' },
];
const decisions = {
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.",
},
rendering: {
choice: 'React Three Fiber',
reason: 'Declarative React renderer for Three.js; good for a browser prototype and React integration.',
whyNot: "A full engine like Unity or Bevy would slow iteration on panels, tables, forms, chat, and market UX. The gameplay is UI-heavy and economy/social-system-heavy.",
},
state: {
choice: 'Zustand',
reason: 'Simple local state for panels, selection, active tabs, camera preferences, and modal state.',
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, multiplayer all in one.',
whyNot: "A custom Node.js + PostgreSQL backend would require building real-time sync, subscriptions, and authoritative game logic from scratch.",
},
styling: {
choice: 'Tailwind CSS',
reason: 'Fast UI iteration for panels, tables, HUDs, and dense game interfaces.',
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.",
},
};
const d = decisions[activeTab];
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>Technical Direction</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.95rem', maxWidth: '680px' }}>
Each layer is chosen for iteration speed during the prototype phase. Architecture is designed
so any layer can be replaced as the game evolves.
</p>
{/* Decision tabs */}
<div style={{ display: 'flex', gap: 'var(--sp-2)', marginBottom: 'var(--sp-6)', flexWrap: 'wrap' }}>
{tabs.map(t => (
<button
key={t.id}
className={`btn btn-sm${activeTab === t.id ? ' btn-primary' : ''}`}
onClick={() => setActiveTab(t.id)}
>
{t.label}
</button>
))}
</div>
{/* Active decision */}
<div className="card" style={{ marginBottom: 'var(--sp-6)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sp-3)', marginBottom: 'var(--sp-4)' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{activeTab}
</span>
<span style={{ width: '1px', height: '16px', background: 'var(--border)' }} />
<span style={{ fontFamily: 'var(--font-display)', fontSize: '1.3rem', fontWeight: 600, color: 'var(--fg-bright)' }}>
{d.choice}
</span>
</div>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem', marginBottom: 'var(--sp-4)' }}>
<strong style={{ color: 'var(--fg)' }}>Why:</strong> {d.reason}
</p>
<div style={{ background: 'var(--surface-raised)', borderRadius: 'var(--radius-md)', padding: 'var(--sp-4)', fontSize: '0.85rem', color: 'var(--fg-dim)' }}>
<strong style={{ color: 'var(--muted)' }}>Why not the alternatives:</strong> {d.whyNot}
</div>
</div>
{/* Full decision table */}
<div className="section-header">
<span className="section-num">TECH-ALL</span>
<h2 style={{ margin: 0 }}>Decision Matrix</h2>
</div>
<table className="data-table">
<thead>
<tr><th>Layer</th><th>Choice</th><th>Reason</th></tr>
</thead>
<tbody>
<tr>
<td style={{ color: 'var(--cyan)' }}>Frontend</td>
<td>Vite + React + TypeScript</td>
<td style={{ color: 'var(--fg-dim)' }}>Fast client-side app, no SSR complexity.</td>
</tr>
<tr>
<td style={{ color: 'var(--purple)' }}>3D Rendering</td>
<td>React Three Fiber</td>
<td style={{ color: 'var(--fg-dim)' }}>Declarative React renderer for Three.js.</td>
</tr>
<tr>
<td style={{ color: 'var(--green)' }}>UI State</td>
<td>Zustand</td>
<td style={{ color: 'var(--fg-dim)' }}>Simple local state for panels and UI.</td>
</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>
</tr>
<tr>
<td style={{ color: 'var(--fg-dim)' }}>Styling</td>
<td>Tailwind CSS</td>
<td style={{ color: 'var(--fg-dim)' }}>Fast iteration for panels, tables, HUDs.</td>
</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>
</tr>
</tbody>
</table>
{/* File structure */}
<div className="section-header" style={{ marginTop: 'var(--sp-8)' }}>
<span className="section-num">TECH-FS</span>
<h2 style={{ margin: 0 }}>Starter File Structure</h2>
</div>
<div className="callout callout-info" style={{ marginBottom: 'var(--sp-5)', fontSize: '0.82rem' }}>
<strong>Note:</strong> The file structure below is the <em>production target</em> for a Vite + React + TypeScript project.
The current prototype uses a simpler layout: <code>js/pages/</code>, <code>js/components/</code>, <code>js/demos/</code>, <code>js/lib/</code>, and <code>css/</code>
a flat structure loaded via Babel standalone without a build step. The prototype structure will migrate to the layout below when moving to Vite.
</div>
<div className="code-block">
<code>
<span className="cm">{'//'} Starter project layout</span><br/>
<span className="kw">/client/src/app</span> <span className="cm">{' //'} App shell and providers</span><br/>
<span className="kw">/client/src/network</span> <span className="cm">{' //'} SpacetimeDB client, subscriptions</span><br/>
<span className="kw">/client/src/game</span> <span className="cm">{' //'} Renderer-independent types, view models</span><br/>
<span className="kw">/client/src/store</span> <span className="cm">{' //'} Zustand stores</span><br/>
<span className="kw">/client/src/renderers/r3f</span> <span className="cm">{'//'} R3F scene, meshes, camera</span><br/>
<span className="kw">/client/src/ui</span> <span className="cm">{' //'} HUD, inventory, market, chat</span><br/>
<span className="kw">/server-spacetime/src</span> <span className="cm">{' //'} SpacetimeDB module, reducers</span><br/>
</code>
</div>
<h3>Packages</h3>
<table className="data-table">
<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>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>
</tbody>
</table>
</div>
);
}
window.GDD.TechStackPage = TechStackPage;