- Restructure flat static prototype into pnpm workspace monorepo - apps/game: playable shell with R3F 3D scene, HUD, SpacetimeDB connection - apps/docs: design docs and prototypes - apps/site: landing page - packages/ui: shared Button and Panel primitives - services/spacetimedb: backend module (9 tables, 11 reducers) - Archive legacy static files to archive/legacy-static/ - Game loop: connect, undock, target, approach, dock, mine, sell - Add pnpm-workspace.yaml, tsconfig.base.json, spacetime.json
482 lines
35 KiB
JavaScript
482 lines
35 KiB
JavaScript
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> = {<br/>
|
||
ships: <span className="type">ShipViewModel</span>[];<br/>
|
||
stations: <span className="type">StationViewModel</span>[];<br/>
|
||
asteroids: <span className="type">AsteroidViewModel</span>[];<br/>
|
||
anomalies: <span className="type">AnomalyViewModel</span>[];<br/>
|
||
worldEvents: <span className="type">WorldEventViewModel</span>[];<br/>
|
||
selectedEntityId: <span className="type">string</span> | <span className="kw">null</span>;<br/>
|
||
};<br/>
|
||
<br/>
|
||
<span className="kw">type</span> <span className="type">GameRendererEvents</span> = {<br/>
|
||
<span className="fn">onSelectEntity</span>(entityId: <span className="type">string</span>): <span className="type">void</span>;<br/>
|
||
<span className="fn">onMoveCommand</span>(position: <span className="type">Vec3</span>): <span className="type">void</span>;<br/>
|
||
<span className="fn">onMineCommand</span>(asteroidId: <span className="type">string</span>): <span className="type">void</span>;<br/>
|
||
<span className="fn">onDockCommand</span>(stationId: <span className="type">string</span>): <span className="type">void</span>;<br/>
|
||
};
|
||
</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> 0–100%. 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 0–6 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: 1–2 weeks for translation + 2–3 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;
|