diff --git a/.gitignore b/.gitignore index 98de87d..1e5ffcb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,18 @@ node_modules/ dist/ +build/ +coverage/ +.turbo/ +.nx/ +.vite/ +.parcel-cache/ +.cache/ +.DS_Store +.env +.env.local +.env.*.local +spacetime.local.json *.tsbuildinfo vite.config.js vite.config.d.ts +.playwright-mcp/ diff --git a/README.md b/README.md index b112408..3d68adb 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,84 @@ # VOID::NAV -Vite + React + TypeScript documentation and prototype hub for the Space-Game project. +VOID::NAV is now a pnpm workspace monorepo for the game website, living design docs, browser game shell, and SpacetimeDB backend module. + +## Layout + +```text +apps/docs Existing docs, interactive demos, and vertical slice +apps/site Public landing page / game website +apps/game Playable game shell connected to SpacetimeDB bindings +packages/ui Small shared UI primitives and CSS tokens +services/spacetimedb TypeScript SpacetimeDB module +archive/legacy-static Legacy static prototype files kept for reference +``` + +## Prerequisites + +- Node.js 24+ +- pnpm 9+ +- SpacetimeDB CLI for backend build, generation, and local DB runs + +The SpacetimeDB CLI is not vendored. Frontend builds use the committed placeholder bindings in `apps/game/src/module_bindings`; run `pnpm generate:bindings` after installing the CLI to replace them with generated bindings. ## Development ```bash pnpm install -pnpm dev +pnpm dev:docs +pnpm dev:site +pnpm dev:game ``` -## Build +Default ports: -```bash -pnpm build -``` +- docs: `http://localhost:5173/docs` +- site: `http://localhost:5174` +- game: `http://localhost:5175` -## Routes - -- `/` - landing page -- `/docs` - documentation overview -- `/docs/demos/*` - converted interactive demos -- `/docs/prototypes/game-hud` - HUD style reference for future demos -- `/app` - future game application shell - -## Browser Routing - -This app uses browser-history routes. The Vite dev server works without extra setup. -Static production hosting must rewrite all routes to `index.html`. - -Example Netlify rule: +The public site links to docs and game via: ```text -/* /index.html 200 +VITE_DOCS_URL=http://localhost:5173/docs +VITE_GAME_URL=http://localhost:5175 ``` -For Vercel or another static host, configure the equivalent SPA fallback to `index.html`. +The game shell reads: + +```text +VITE_SPACETIME_URI=http://localhost:3000 +VITE_SPACETIME_DATABASE=void-nav-dev +``` + +## Builds + +```bash +pnpm --filter @void-nav/docs build +pnpm --filter @void-nav/site build +pnpm --filter @void-nav/game build +pnpm build +pnpm check +``` + +## SpacetimeDB + +Root `spacetime.json` points at `services/spacetimedb` and generates TypeScript bindings into `apps/game/src/module_bindings`. + +With the CLI installed: + +```bash +spacetime build --module-path services/spacetimedb +pnpm generate:bindings +pnpm dev:db +``` + +`pnpm dev:db` targets the SpacetimeDB CLI server nickname `local` so development publishes to `http://127.0.0.1:3000` instead of any configured cloud default. + +Initial reducers: + +- `connectPlayer(displayName: string)` +- `renamePlayer(displayName: string)` +- `seedWorld()` +- `ping()` + +The game shell calls `connectPlayer` after connection, subscribes to starter shell rows, and displays connection, player, ship, system, station, and reducer status. When SpacetimeDB is unavailable or bindings are still placeholders, it shows a clear disconnected/error state. diff --git a/index.html b/apps/docs/index.html similarity index 100% rename from index.html rename to apps/docs/index.html diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 0000000..0c78ab2 --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,29 @@ +{ + "name": "@void-nav/docs", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "tsc --noEmit && vite build", + "check": "tsc --noEmit", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^8.17.10", + "@tailwindcss/vite": "^4.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.1", + "tailwindcss": "^4.3.0", + "three": "^0.160.0", + "vite": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@types/three": "^0.160.0", + "typescript": "^5.8.3" + } +} diff --git a/mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx b/apps/docs/public/assets/mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx similarity index 100% rename from mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx rename to apps/docs/public/assets/mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx diff --git a/mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png b/apps/docs/public/assets/mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png similarity index 100% rename from mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png rename to apps/docs/public/assets/mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png diff --git a/mph38gxn-drawing-2026-05-22T15-42-07-328Z.png b/apps/docs/public/assets/mph38gxn-drawing-2026-05-22T15-42-07-328Z.png similarity index 100% rename from mph38gxn-drawing-2026-05-22T15-42-07-328Z.png rename to apps/docs/public/assets/mph38gxn-drawing-2026-05-22T15-42-07-328Z.png diff --git a/mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png b/apps/docs/public/assets/mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png similarity index 100% rename from mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png rename to apps/docs/public/assets/mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png diff --git a/gap-analysis.md b/apps/docs/public/docs/gap-analysis.md similarity index 100% rename from gap-analysis.md rename to apps/docs/public/docs/gap-analysis.md diff --git a/apps/docs/public/docs/vertical-slice-evaluation.md b/apps/docs/public/docs/vertical-slice-evaluation.md new file mode 100644 index 0000000..47a5c08 --- /dev/null +++ b/apps/docs/public/docs/vertical-slice-evaluation.md @@ -0,0 +1,415 @@ +# Vertical Slice Evaluation + +**Date:** 2026-05-31 +**Scope:** Compare the current playable game implementation against the MVP Loop Slice demo and the Era 1 roadmap goals. + +--- + +## Executive Assessment + +The project has a strong vertical-slice prototype in the docs app, but the standalone game app is still a Phase 0 technical shell. The MVP Loop Slice demonstrates the intended solo loop and connects many previously standalone demos through a shared browser session. The actual game app connects to SpacetimeDB, creates/subscribes to starter rows, and exposes connection/reducer status, but it does not yet implement the playable slice loop. + +In practical terms: + +- **Design/demo confidence is high.** The docs app proves the target loop and system UX at prototype fidelity. +- **Runtime implementation readiness is early.** The game app has connection plumbing and starter persistence only. +- **The main gap is migration from prototype state to authoritative game state.** The loop exists in localStorage and React state, not in SpacetimeDB tables/reducers. +- **Phase 0 is partially complete.** SpacetimeDB connectivity, starter player/ship/system/station rows, and reducer invocation exist. The rendered star system, tick loop, 3D scene, and gameplay commands are not yet present in the game app. + +--- + +## Baselines Compared + +### Intended Era 1 Vertical Slice + +The roadmap defines Era 1 as a single-player proof of concept using local SpacetimeDB from day one. The minimum full loop is: + +1. Boot the game and connect to local SpacetimeDB. +2. Render a single star system with station and asteroids. +3. Undock. +4. Navigate to an asteroid belt. +5. Mine ore into cargo. +6. Return and dock. +7. Refine ore. +8. Fit or confirm a module. +9. Sell goods on a market. +10. Optionally run a combat trial. +11. Persist state through SpacetimeDB, not localStorage. + +### Docs-App MVP Loop Slice + +The docs app already implements a connected prototype of that loop: + +- Session/objective flow: `apps/docs/src/prototypes/game-slice/sliceObjectives.ts` +- Local session state and event reducer: `apps/docs/src/prototypes/game-slice/gameSliceState.ts` +- Action gating and contextual commands: `apps/docs/src/prototypes/game-slice/sliceController.ts` +- Runtime controller: `apps/docs/src/prototypes/game-slice/useSliceController.ts` +- Loop shell/UI composition: `apps/docs/src/prototypes/game-slice/SeamlessGameLoopSlice.tsx` + +It validates the user-facing flow, command availability, objective tracking, event log, cargo, station services, fitting handoff, market selling, XP awards, Zora observation events, and optional combat branch. + +Its explicit limitation is architectural: it uses local browser persistence and fake/deterministic data. It does not validate backend authority, persistence model, multiplayer path, generated bindings, or SpacetimeDB reducer design. + +### Standalone Game App + +The standalone game app currently validates the connection path: + +- SpacetimeDB connection lifecycle: `apps/game/src/spacetime/useSpacetimeConnection.ts` +- Subscription/read facade: `apps/game/src/spacetime/usePlayerSession.ts` +- Flight console shell: `apps/game/src/GameShell.tsx` +- Starter backend module: `services/spacetimedb/src/index.ts` + +The backend currently provides: + +- `player` +- `ship` +- `system` +- `station` +- `server_event` +- `connectPlayer` +- `renamePlayer` +- `seedWorld` +- `ping` + +This is useful and necessary, but it is not yet the playable vertical slice. + +--- + +## Alignment Matrix + +| Slice expectation | Demo status | Game app status | Gap | +|---|---:|---:|---| +| App boots | Yes | Yes | Mostly aligned. | +| Local SpacetimeDB connection | No | Partial | Game connects, but the slice does not use it. | +| Starter player identity | No | Yes | Present in game shell only. | +| Starter ship row | No | Yes | Present, but no position/mode/cargo/fitting state. | +| Single star system | Yes, fake/system data | Partial | Backend has one system and station; no asteroid rows or scene rendering. | +| Station and asteroids visible | Yes | No | Game shell displays data cards only. | +| 60fps render / 1Hz sim tick | Prototype timers only | No | No authoritative sim tick or render scene in game app. | +| Undock/dock | Yes | No | Needs reducers, row state, and UI commands. | +| Local movement / approach target | Yes | No | Needs target/position model and movement reducer/tick. | +| Warp/travel handoff | Yes in demos | No | Needs backend travel operation model. | +| Mining cycle | Yes | No | Needs asteroid/cargo/mining operation tables and reducers. | +| Cargo and inventory | Yes | No | Needs authoritative inventory schema. | +| Refining | Yes | No | Needs station service reducer and inventory transaction. | +| Fitting | Yes | No | Needs module catalog, ship fitting rows, CPU/PG validation. | +| Market selling | Yes | No | Needs market/order/transaction state, even if NPC-only first. | +| Combat trial | Yes | No | Needs NPC entity/combat reducer loop. | +| Objective tracker | Yes | No | Needs game app UI or server-backed tutorial/objective state. | +| Event log | Yes | Partial | Game has server events, but not gameplay events. | +| Skills/XP | Yes | No | Needs skill rows and XP reducer. | +| Zora Tier 0 hooks | Yes | No | Needs backend state and game UI hooks. | +| Persistence | localStorage | Partial SpacetimeDB | Game persists starter rows only; slice state still localStorage. | + +--- + +## What Lines Up Well + +### 1. The vertical slice is coherent + +The MVP Loop Slice is not just a gallery of isolated screens. It has a real objective chain: + +`undock -> navigate_to_belt -> mine_ore -> dock_at_station -> refine_ore -> fit_module -> sell_goods -> combat_trial` + +That is the correct shape for Gate 4 and gives implementation a clear target. + +### 2. Command gating is already well modeled + +The slice controller has concrete command availability rules: dock only at stations, mine only at belts, services only while docked, market only with sellable cargo, refining only with enough ore, and busy-state blocking while operations run. These are exactly the rules that should become server-side validation in reducers. + +### 3. The game app has the right backend direction + +The game app already uses SpacetimeDB bindings, reducer calls, subscriptions, identity, and server event rows. This lines up with the roadmap decision to avoid localStorage as the final persistence layer. + +### 4. The demo gallery honestly documents limitations + +The docs already distinguish UX validation from production implementation. That makes the remaining work clearer: the problem is not unknown design, it is backend-backed integration. + +--- + +## Key Gaps + +### 1. The demo and the game app use different state models + +The MVP Loop Slice stores a rich `GameSliceSession` in localStorage. The game app stores only starter identity, ship, system, station, and event rows in SpacetimeDB. + +This creates the largest implementation gap. Nearly every field in the slice session needs either a backend table, a derived client view, or an explicit decision that it remains client-only. + +Important missing authoritative state includes: + +- current ship mode: station, flight, travel, mining, docking, combat +- ship position and current point of interest +- docked station state +- active operation with start time and duration +- selected target +- cargo and cargo capacity +- fitted modules +- wallet +- skills and XP +- objectives/tutorial progress +- Zora module/state hooks + +### 2. Phase 0 rendering is not implemented in the game app + +The roadmap says Phase 0 is done when the app shows a star system with a station and three asteroids, updates local state on a tick, and persists through SpacetimeDB. The current game app is a useful diagnostic console, but it does not render the playable space scene or asteroid targets. + +### 3. Backend schema is intentionally minimal + +The backend module currently supports connection and starter world creation. It does not yet contain the tables required for the slice loop: + +- points of interest or asteroid belts +- inventory/cargo +- wallet transactions +- mining operations +- refining jobs +- module catalog +- ship fittings +- market orders or NPC buy prices +- skills +- objectives/tutorial state +- NPCs/combat state + +### 4. Reducers do not yet represent gameplay commands + +The current reducers are connection/admin-level: + +- `connectPlayer` +- `renamePlayer` +- `seedWorld` +- `ping` + +The slice needs command reducers such as: + +- `undock` +- `dock` +- `selectTarget` +- `startApproach` +- `completeMovement` or server tick movement resolution +- `startMining` +- `stopMining` +- `completeMiningCycle` +- `refineOre` +- `fitModule` +- `unfitModule` +- `sellItemToNpcMarket` +- `startCombatTrial` +- `resolveCombatTick` + +### 5. The demo validates flow, not balance or backend correctness + +The slice has deterministic prices, deterministic refining, simplified XP, and simplified combat. It proves the player journey, but not the production economy, exploit resistance, transaction safety, concurrent updates, or server authority. + +### 6. Naming and world setup are not fully unified + +The game shell seeds `solace` / `solace-prime` / "Solace Prime Orbital". The slice defaults to `sol` / `sol-station-0` / "Jita IV - Moon 4". This is acceptable for prototypes, but it will create migration friction if not unified before gameplay reducers are added. + +### 7. Existing standalone demos are only partially integrated + +The docs note that several demos can consume the slice session, but core effects still remain demo-local. For example, fitting can update slice modules and combat can emit victory, but the final game still needs one authoritative source of truth where fitting affects mining/combat stats and combat affects wallet/cargo/ship state. + +--- + +## Phase Readiness + +### Phase 0 - Local Skeleton + +**Status:** Partially implemented. + +Done: + +- pnpm workspace exists. +- standalone game app exists. +- SpacetimeDB module exists. +- generated/placeholder bindings are committed. +- client attempts connection and handles unavailable backend clearly. +- starter player, ship, system, station, and events exist. + +Remaining: + +- render a star system in the game app. +- add station plus three asteroid targets to backend state. +- add local simulation tick concept. +- persist position/mode/state through SpacetimeDB. +- replace diagnostic-only console with first playable flight shell. + +### Phase 1 - Movement & Commands + +**Status:** Prototyped, not implemented in game app. + +The movement demo and slice validate UX, target selection, approach, ETA/progress, and arrival. The game app needs backend-backed target state, command reducers, and client interpolation. + +### Phase 2 - Mining & Inventory + +**Status:** Prototyped, not implemented in game app. + +The slice validates mining cycles and cargo filling. The game app needs inventory rows, asteroid/belt rows, mining operation state, cargo capacity, and transaction-safe ore insertion. + +### Phase 3 - Combat + +**Status:** Prototyped, not implemented in game app. + +The demo validates power allocation and combat feel. The game app needs NPC entities, combat session state, damage resolution, ship health, loot, bounty payout, and failure/death handling. + +### Phase 4 - Fitting + +**Status:** Prototyped, not implemented in game app. + +The fitting demo validates module constraints and UI feel. The game app needs module catalog rows, fitted module rows, CPU/PG validation in reducers, and stat derivation that affects mining/combat. + +### Phase 5 - Refining & Manufacturing + +**Status:** Refining prototyped; manufacturing not in slice runtime. + +The slice supports ore-to-mineral refining. Manufacturing remains a later implementation gap for the full Era 1 economy loop. + +### Phase 6 - NPC Economy Sim + +**Status:** Market surface prototyped; backend economy not implemented. + +The market demo validates presentation and selling flow. The game app needs NPC price state, transaction reducers, market history, and later regional differences/diffusion. + +### Phase 7 - Single-Player Polish + +**Status:** Design-ready, implementation not started in game app. + +Tutorial/objectives, events, Zora Tier 0, accessibility, persistence, and polish are documented/prototyped in parts, but not integrated into the standalone game. + +--- + +## Recommended Implementation Path + +### Step 1 - Promote the slice state into a backend contract + +Create a mapping document or code comments that classify each `GameSliceSession` field as: + +- persisted backend state +- derived client state +- temporary UI state +- prototype-only state to delete + +This prevents blindly copying localStorage structure into SpacetimeDB while still preserving the proven flow. + +### Step 2 - Finish Phase 0 in the game app + +Add the smallest playable world: + +- one system +- one station +- three asteroid/belt POIs +- one starter ship with position, mode, and docked state +- a 3D or 2.5D in-space view +- a simple read-only HUD showing authoritative rows +- a 1Hz server/sim update path or explicit reducer-driven operation completion + +Acceptance check: open the game app, connect to local SpacetimeDB, and see the starter system represented visually rather than only as data cards. + +### Step 3 - Port the command rail one command at a time + +Use the slice action gating as the implementation source for server validation. Add reducers in this order: + +1. `undock` +2. `selectTarget` +3. `startApproach` +4. `dock` +5. `startMining` +6. `stopMining` +7. `refineOre` +8. `fitModule` +9. `sellItemToNpcMarket` +10. `startCombatTrial` + +Each reducer should produce a `server_event` row so the current game shell remains useful while the UI evolves. + +### Step 4 - Add the minimum schema for Gate 1 + +For the first integrated gate, implement only what is needed for: + +`navigate to asteroid -> mine -> fill cargo -> dock -> sell ore` + +Minimum tables/fields: + +- ship location/mode/position/docked state +- points of interest +- cargo/inventory +- wallet +- active operation +- market NPC buy price or station commodity price +- event log + +Avoid implementing manufacturing, full market depth, skills, Zora, or combat until Gate 1 works end to end. + +### Step 5 - Replace localStorage handoff with SpacetimeDB-backed session state + +The docs slice can continue to exist as a UX reference, but the standalone game should become the source of truth for vertical-slice progress. Once equivalent reducers exist, either: + +- point the docs slice at the same backend for live validation, or +- freeze it as an archived prototype and move active work into `apps/game`. + +### Step 6 - Add focused verification + +Add tests at the reducer/logic level before broad UI tests: + +- connecting creates one player and one starter ship +- repeated connect is idempotent +- undock changes docked state and mode +- movement cannot start while docked +- docking only works at a station POI +- mining only works at a belt and respects cargo capacity +- refining consumes ore and creates minerals +- selling removes inventory and increases wallet +- invalid fitting is rejected + +--- + +## Open Decisions + +1. **Which app is the product surface?** + Recommendation: `apps/game` should become the product surface; `apps/docs` remains design/prototype reference. + +2. **How much of the slice UI should be ported?** + Recommendation: port the interaction model and command gating, not the localStorage architecture. + +3. **Should Phase 0 include Three.js immediately?** + Recommendation: yes, if the target is a space game with Flight Mode as a first-class surface. The docs already have reusable R3F scene pieces that can guide the implementation. + +4. **What is the canonical starter world?** + Recommendation: choose one naming/id set before adding gameplay reducers. Prefer production-neutral IDs over inherited EVE names. + +5. **Server tick vs. reducer-scheduled completion?** + Recommendation: for early phases, reducer-driven operations with authoritative timestamps are enough. Add a scheduler/tick once NPCs, economy, and combat need autonomous progression. + +--- + +## Priority Work List + +### Immediate + +1. Decide canonical starter system/station/POI IDs. +2. Add backend POI/asteroid state. +3. Add ship mode, position, docked state, wallet, and cargo state. +4. Build the game app's first visual system scene. +5. Add `undock`, `dock`, and `startApproach` reducers. + +### Next + +1. Add mining operation and cargo reducers. +2. Add station sell/refine reducers. +3. Port the command rail/objective tracker into `apps/game`. +4. Add reducer tests for Gate 1. +5. Run a manual Gate 1 playtest. + +### Later + +1. Fitting with CPU/PG validation. +2. Combat trial with NPC state and loot. +3. Skills/XP progression. +4. Zora Tier 0 backend state. +5. Manufacturing and deeper NPC economy. + +--- + +## Bottom Line + +The game lines up well with the vertical slice at the design and prototype level, but not yet at the playable implementation level. The docs app has already answered "what should the first loop feel like?" The standalone game app is now ready to answer "can that loop run against authoritative SpacetimeDB state?" + +The next milestone should be a narrow Gate 1 implementation in `apps/game`: connect, undock, approach asteroid, mine ore, dock, and sell. That is the shortest path from demo confidence to a real playable vertical slice. diff --git a/src/App.tsx b/apps/docs/src/App.tsx similarity index 92% rename from src/App.tsx rename to apps/docs/src/App.tsx index 3e3c509..05b20ab 100644 --- a/src/App.tsx +++ b/apps/docs/src/App.tsx @@ -1,9 +1,6 @@ import { Navigate, Route, Routes } from "react-router-dom"; import { DocsLayout } from "./layouts/DocsLayout"; -import { SiteLayout } from "./layouts/SiteLayout"; import { NotFound } from "./components/NotFound"; -import { LandingPage } from "./pages/LandingPage"; -import { ApplicationPage } from "./pages/ApplicationPage"; import { OverviewPage } from "./pages/docs/OverviewPage"; import { ArchitecturePage } from "./pages/docs/ArchitecturePage"; import { TechStackPage } from "./pages/docs/TechStackPage"; @@ -18,6 +15,7 @@ import { RoadmapPage } from "./pages/docs/RoadmapPage"; import { RisksPage } from "./pages/docs/RisksPage"; import { DemoGalleryPage } from "./pages/docs/DemoGalleryPage"; import { GapAnalysisPage } from "./pages/docs/GapAnalysisPage"; +import { VerticalSliceEvaluationPage } from "./pages/docs/VerticalSliceEvaluationPage"; import { DesignDocPage } from "./pages/docs/DesignDocPage"; import { StarMapDemo } from "./prototypes/existing-demos/StarMapDemo"; import { ShipMovementDemo } from "./prototypes/existing-demos/ShipMovementDemo"; @@ -38,10 +36,7 @@ import { GameHudPrototype } from "./prototypes/standalone-huds/GameHudPrototype" export function App() { return ( - }> - } /> - } /> - + } /> }> } /> @@ -59,6 +54,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> diff --git a/src/components/NotFound.tsx b/apps/docs/src/components/NotFound.tsx similarity index 100% rename from src/components/NotFound.tsx rename to apps/docs/src/components/NotFound.tsx diff --git a/src/components/Sidebar.tsx b/apps/docs/src/components/Sidebar.tsx similarity index 100% rename from src/components/Sidebar.tsx rename to apps/docs/src/components/Sidebar.tsx diff --git a/src/components/TopBar.tsx b/apps/docs/src/components/TopBar.tsx similarity index 100% rename from src/components/TopBar.tsx rename to apps/docs/src/components/TopBar.tsx diff --git a/src/data/fakeBackend.ts b/apps/docs/src/data/fakeBackend.ts similarity index 100% rename from src/data/fakeBackend.ts rename to apps/docs/src/data/fakeBackend.ts diff --git a/src/data/nav.ts b/apps/docs/src/data/nav.ts similarity index 96% rename from src/data/nav.ts rename to apps/docs/src/data/nav.ts index 9b5b4d2..59cf5bc 100644 --- a/src/data/nav.ts +++ b/apps/docs/src/data/nav.ts @@ -26,6 +26,7 @@ export const navSections: NavSection[] = [ { path: "/docs/roadmap", icon: "⊞", label: "Roadmap" }, { path: "/docs/risks", icon: "◬", label: "Risks & Questions" }, { path: "/docs/gap-analysis", icon: "□", label: "Gap Analysis" }, + { path: "/docs/vertical-slice-evaluation", icon: "▤", label: "Slice Evaluation" }, { path: "/docs/design-doc", icon: "▣", label: "Design Doc" }, ], }, diff --git a/src/layouts/DocsLayout.tsx b/apps/docs/src/layouts/DocsLayout.tsx similarity index 100% rename from src/layouts/DocsLayout.tsx rename to apps/docs/src/layouts/DocsLayout.tsx diff --git a/src/lib/threeHelpers.ts b/apps/docs/src/lib/threeHelpers.ts similarity index 100% rename from src/lib/threeHelpers.ts rename to apps/docs/src/lib/threeHelpers.ts diff --git a/src/main.tsx b/apps/docs/src/main.tsx similarity index 100% rename from src/main.tsx rename to apps/docs/src/main.tsx diff --git a/src/pages/docs/AgentsPage.tsx b/apps/docs/src/pages/docs/AgentsPage.tsx similarity index 100% rename from src/pages/docs/AgentsPage.tsx rename to apps/docs/src/pages/docs/AgentsPage.tsx diff --git a/src/pages/docs/ArchitecturePage.tsx b/apps/docs/src/pages/docs/ArchitecturePage.tsx similarity index 100% rename from src/pages/docs/ArchitecturePage.tsx rename to apps/docs/src/pages/docs/ArchitecturePage.tsx diff --git a/src/pages/docs/BackendPage.tsx b/apps/docs/src/pages/docs/BackendPage.tsx similarity index 100% rename from src/pages/docs/BackendPage.tsx rename to apps/docs/src/pages/docs/BackendPage.tsx diff --git a/src/pages/docs/DemoGalleryPage.tsx b/apps/docs/src/pages/docs/DemoGalleryPage.tsx similarity index 100% rename from src/pages/docs/DemoGalleryPage.tsx rename to apps/docs/src/pages/docs/DemoGalleryPage.tsx diff --git a/src/pages/docs/DesignDocPage.tsx b/apps/docs/src/pages/docs/DesignDocPage.tsx similarity index 100% rename from src/pages/docs/DesignDocPage.tsx rename to apps/docs/src/pages/docs/DesignDocPage.tsx diff --git a/src/pages/docs/EconomyPage.tsx b/apps/docs/src/pages/docs/EconomyPage.tsx similarity index 100% rename from src/pages/docs/EconomyPage.tsx rename to apps/docs/src/pages/docs/EconomyPage.tsx diff --git a/src/pages/docs/GameplayPage.tsx b/apps/docs/src/pages/docs/GameplayPage.tsx similarity index 100% rename from src/pages/docs/GameplayPage.tsx rename to apps/docs/src/pages/docs/GameplayPage.tsx diff --git a/src/pages/docs/GapAnalysisPage.tsx b/apps/docs/src/pages/docs/GapAnalysisPage.tsx similarity index 100% rename from src/pages/docs/GapAnalysisPage.tsx rename to apps/docs/src/pages/docs/GapAnalysisPage.tsx diff --git a/src/pages/docs/OverviewPage.tsx b/apps/docs/src/pages/docs/OverviewPage.tsx similarity index 100% rename from src/pages/docs/OverviewPage.tsx rename to apps/docs/src/pages/docs/OverviewPage.tsx diff --git a/src/pages/docs/RisksPage.tsx b/apps/docs/src/pages/docs/RisksPage.tsx similarity index 100% rename from src/pages/docs/RisksPage.tsx rename to apps/docs/src/pages/docs/RisksPage.tsx diff --git a/src/pages/docs/RoadmapPage.tsx b/apps/docs/src/pages/docs/RoadmapPage.tsx similarity index 100% rename from src/pages/docs/RoadmapPage.tsx rename to apps/docs/src/pages/docs/RoadmapPage.tsx diff --git a/src/pages/docs/ShipAIPage.tsx b/apps/docs/src/pages/docs/ShipAIPage.tsx similarity index 100% rename from src/pages/docs/ShipAIPage.tsx rename to apps/docs/src/pages/docs/ShipAIPage.tsx diff --git a/src/pages/docs/ShipsPage.tsx b/apps/docs/src/pages/docs/ShipsPage.tsx similarity index 100% rename from src/pages/docs/ShipsPage.tsx rename to apps/docs/src/pages/docs/ShipsPage.tsx diff --git a/src/pages/docs/SocialPage.tsx b/apps/docs/src/pages/docs/SocialPage.tsx similarity index 100% rename from src/pages/docs/SocialPage.tsx rename to apps/docs/src/pages/docs/SocialPage.tsx diff --git a/src/pages/docs/TechStackPage.tsx b/apps/docs/src/pages/docs/TechStackPage.tsx similarity index 100% rename from src/pages/docs/TechStackPage.tsx rename to apps/docs/src/pages/docs/TechStackPage.tsx diff --git a/apps/docs/src/pages/docs/VerticalSliceEvaluationPage.tsx b/apps/docs/src/pages/docs/VerticalSliceEvaluationPage.tsx new file mode 100644 index 0000000..08c197b --- /dev/null +++ b/apps/docs/src/pages/docs/VerticalSliceEvaluationPage.tsx @@ -0,0 +1,23 @@ +// @ts-nocheck +export function VerticalSliceEvaluationPage() { + return ( +
+

Vertical Slice Evaluation

+

+ Current assessment of how the standalone game app lines up against the + MVP Loop Slice demo and Era 1 roadmap, including implementation gaps + and the recommended next work. +

+
+

Source Document

+

+ Open the markdown evaluation for the full matrix, gap breakdown, + phase readiness notes, and priority work list. +

+ + Open vertical-slice-evaluation.md + +
+
+ ); +} diff --git a/src/prototypes/existing-demos/BountyDemo.tsx b/apps/docs/src/prototypes/existing-demos/BountyDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/BountyDemo.tsx rename to apps/docs/src/prototypes/existing-demos/BountyDemo.tsx diff --git a/src/prototypes/existing-demos/ChatDemo.tsx b/apps/docs/src/prototypes/existing-demos/ChatDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/ChatDemo.tsx rename to apps/docs/src/prototypes/existing-demos/ChatDemo.tsx diff --git a/src/prototypes/existing-demos/CombatDemo.tsx b/apps/docs/src/prototypes/existing-demos/CombatDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/CombatDemo.tsx rename to apps/docs/src/prototypes/existing-demos/CombatDemo.tsx diff --git a/src/prototypes/existing-demos/FittingDemo.tsx b/apps/docs/src/prototypes/existing-demos/FittingDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/FittingDemo.tsx rename to apps/docs/src/prototypes/existing-demos/FittingDemo.tsx diff --git a/src/prototypes/existing-demos/GalaxyDemo.tsx b/apps/docs/src/prototypes/existing-demos/GalaxyDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/GalaxyDemo.tsx rename to apps/docs/src/prototypes/existing-demos/GalaxyDemo.tsx diff --git a/src/prototypes/existing-demos/GameHudDemo.tsx b/apps/docs/src/prototypes/existing-demos/GameHudDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/GameHudDemo.tsx rename to apps/docs/src/prototypes/existing-demos/GameHudDemo.tsx diff --git a/src/prototypes/existing-demos/GameLoopSliceDemo.tsx b/apps/docs/src/prototypes/existing-demos/GameLoopSliceDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/GameLoopSliceDemo.tsx rename to apps/docs/src/prototypes/existing-demos/GameLoopSliceDemo.tsx diff --git a/src/prototypes/existing-demos/MarketDemo.tsx b/apps/docs/src/prototypes/existing-demos/MarketDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/MarketDemo.tsx rename to apps/docs/src/prototypes/existing-demos/MarketDemo.tsx diff --git a/src/prototypes/existing-demos/ProgressionDemo.tsx b/apps/docs/src/prototypes/existing-demos/ProgressionDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/ProgressionDemo.tsx rename to apps/docs/src/prototypes/existing-demos/ProgressionDemo.tsx diff --git a/src/prototypes/existing-demos/RefiningDemo.tsx b/apps/docs/src/prototypes/existing-demos/RefiningDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/RefiningDemo.tsx rename to apps/docs/src/prototypes/existing-demos/RefiningDemo.tsx diff --git a/src/prototypes/existing-demos/ShipMovementDemo.tsx b/apps/docs/src/prototypes/existing-demos/ShipMovementDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/ShipMovementDemo.tsx rename to apps/docs/src/prototypes/existing-demos/ShipMovementDemo.tsx diff --git a/src/prototypes/existing-demos/StarMapDemo.tsx b/apps/docs/src/prototypes/existing-demos/StarMapDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/StarMapDemo.tsx rename to apps/docs/src/prototypes/existing-demos/StarMapDemo.tsx diff --git a/src/prototypes/existing-demos/WarpTravelDemo.tsx b/apps/docs/src/prototypes/existing-demos/WarpTravelDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/WarpTravelDemo.tsx rename to apps/docs/src/prototypes/existing-demos/WarpTravelDemo.tsx diff --git a/src/prototypes/existing-demos/ZoraDemo.tsx b/apps/docs/src/prototypes/existing-demos/ZoraDemo.tsx similarity index 100% rename from src/prototypes/existing-demos/ZoraDemo.tsx rename to apps/docs/src/prototypes/existing-demos/ZoraDemo.tsx diff --git a/src/prototypes/game-slice/SeamlessGameLoopSlice.tsx b/apps/docs/src/prototypes/game-slice/SeamlessGameLoopSlice.tsx similarity index 100% rename from src/prototypes/game-slice/SeamlessGameLoopSlice.tsx rename to apps/docs/src/prototypes/game-slice/SeamlessGameLoopSlice.tsx diff --git a/src/prototypes/game-slice/gameSliceState.ts b/apps/docs/src/prototypes/game-slice/gameSliceState.ts similarity index 100% rename from src/prototypes/game-slice/gameSliceState.ts rename to apps/docs/src/prototypes/game-slice/gameSliceState.ts diff --git a/src/prototypes/game-slice/sliceController.ts b/apps/docs/src/prototypes/game-slice/sliceController.ts similarity index 100% rename from src/prototypes/game-slice/sliceController.ts rename to apps/docs/src/prototypes/game-slice/sliceController.ts diff --git a/src/prototypes/game-slice/sliceEconomy.ts b/apps/docs/src/prototypes/game-slice/sliceEconomy.ts similarity index 100% rename from src/prototypes/game-slice/sliceEconomy.ts rename to apps/docs/src/prototypes/game-slice/sliceEconomy.ts diff --git a/src/prototypes/game-slice/sliceNavigationBridge.ts b/apps/docs/src/prototypes/game-slice/sliceNavigationBridge.ts similarity index 100% rename from src/prototypes/game-slice/sliceNavigationBridge.ts rename to apps/docs/src/prototypes/game-slice/sliceNavigationBridge.ts diff --git a/src/prototypes/game-slice/sliceObjectives.ts b/apps/docs/src/prototypes/game-slice/sliceObjectives.ts similarity index 100% rename from src/prototypes/game-slice/sliceObjectives.ts rename to apps/docs/src/prototypes/game-slice/sliceObjectives.ts diff --git a/src/prototypes/game-slice/sliceWorld.ts b/apps/docs/src/prototypes/game-slice/sliceWorld.ts similarity index 100% rename from src/prototypes/game-slice/sliceWorld.ts rename to apps/docs/src/prototypes/game-slice/sliceWorld.ts diff --git a/src/prototypes/game-slice/types.ts b/apps/docs/src/prototypes/game-slice/types.ts similarity index 100% rename from src/prototypes/game-slice/types.ts rename to apps/docs/src/prototypes/game-slice/types.ts diff --git a/src/prototypes/game-slice/ui/SliceActionRail.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceActionRail.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceActionRail.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceActionRail.tsx diff --git a/src/prototypes/game-slice/ui/SliceCargoPanel.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceCargoPanel.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceCargoPanel.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceCargoPanel.tsx diff --git a/src/prototypes/game-slice/ui/SliceCombatStage.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceCombatStage.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceCombatStage.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceCombatStage.tsx diff --git a/src/prototypes/game-slice/ui/SliceDemoLinks.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceDemoLinks.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceDemoLinks.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceDemoLinks.tsx diff --git a/src/prototypes/game-slice/ui/SliceEventLog.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceEventLog.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceEventLog.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceEventLog.tsx diff --git a/src/prototypes/game-slice/ui/SliceFittingService.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceFittingService.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceFittingService.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceFittingService.tsx diff --git a/src/prototypes/game-slice/ui/SliceFlightStage.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceFlightStage.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceFlightStage.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceFlightStage.tsx diff --git a/src/prototypes/game-slice/ui/SliceMarketService.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceMarketService.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceMarketService.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceMarketService.tsx diff --git a/src/prototypes/game-slice/ui/SliceMiningStage.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceMiningStage.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceMiningStage.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceMiningStage.tsx diff --git a/src/prototypes/game-slice/ui/SliceModuleRack.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceModuleRack.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceModuleRack.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceModuleRack.tsx diff --git a/src/prototypes/game-slice/ui/SliceObjectiveTracker.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceObjectiveTracker.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceObjectiveTracker.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceObjectiveTracker.tsx diff --git a/src/prototypes/game-slice/ui/SliceProgressBar.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceProgressBar.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceProgressBar.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceProgressBar.tsx diff --git a/src/prototypes/game-slice/ui/SliceRefiningService.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceRefiningService.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceRefiningService.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceRefiningService.tsx diff --git a/src/prototypes/game-slice/ui/SliceServicesStage.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceServicesStage.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceServicesStage.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceServicesStage.tsx diff --git a/src/prototypes/game-slice/ui/SliceShell.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceShell.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceShell.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceShell.tsx diff --git a/src/prototypes/game-slice/ui/SliceShipStatus.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceShipStatus.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceShipStatus.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceShipStatus.tsx diff --git a/src/prototypes/game-slice/ui/SliceStage.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceStage.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceStage.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceStage.tsx diff --git a/src/prototypes/game-slice/ui/SliceStationPanel.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceStationPanel.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceStationPanel.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceStationPanel.tsx diff --git a/src/prototypes/game-slice/ui/SliceStationStage.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceStationStage.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceStationStage.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceStationStage.tsx diff --git a/src/prototypes/game-slice/ui/SliceTopBar.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceTopBar.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceTopBar.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceTopBar.tsx diff --git a/src/prototypes/game-slice/ui/SliceTravelStage.tsx b/apps/docs/src/prototypes/game-slice/ui/SliceTravelStage.tsx similarity index 100% rename from src/prototypes/game-slice/ui/SliceTravelStage.tsx rename to apps/docs/src/prototypes/game-slice/ui/SliceTravelStage.tsx diff --git a/src/prototypes/game-slice/ui/sliceStyles.ts b/apps/docs/src/prototypes/game-slice/ui/sliceStyles.ts similarity index 100% rename from src/prototypes/game-slice/ui/sliceStyles.ts rename to apps/docs/src/prototypes/game-slice/ui/sliceStyles.ts diff --git a/src/prototypes/game-slice/useGameSliceSession.ts b/apps/docs/src/prototypes/game-slice/useGameSliceSession.ts similarity index 100% rename from src/prototypes/game-slice/useGameSliceSession.ts rename to apps/docs/src/prototypes/game-slice/useGameSliceSession.ts diff --git a/src/prototypes/game-slice/useSliceController.ts b/apps/docs/src/prototypes/game-slice/useSliceController.ts similarity index 100% rename from src/prototypes/game-slice/useSliceController.ts rename to apps/docs/src/prototypes/game-slice/useSliceController.ts diff --git a/src/prototypes/r3f/combat/CombatScene.tsx b/apps/docs/src/prototypes/r3f/combat/CombatScene.tsx similarity index 100% rename from src/prototypes/r3f/combat/CombatScene.tsx rename to apps/docs/src/prototypes/r3f/combat/CombatScene.tsx diff --git a/src/prototypes/r3f/combat/combatMath.ts b/apps/docs/src/prototypes/r3f/combat/combatMath.ts similarity index 100% rename from src/prototypes/r3f/combat/combatMath.ts rename to apps/docs/src/prototypes/r3f/combat/combatMath.ts diff --git a/src/prototypes/r3f/combat/combatState.ts b/apps/docs/src/prototypes/r3f/combat/combatState.ts similarity index 100% rename from src/prototypes/r3f/combat/combatState.ts rename to apps/docs/src/prototypes/r3f/combat/combatState.ts diff --git a/src/prototypes/r3f/galaxy/GalaxyScene.tsx b/apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx similarity index 100% rename from src/prototypes/r3f/galaxy/GalaxyScene.tsx rename to apps/docs/src/prototypes/r3f/galaxy/GalaxyScene.tsx diff --git a/src/prototypes/r3f/hud/HudSpaceScene.tsx b/apps/docs/src/prototypes/r3f/hud/HudSpaceScene.tsx similarity index 100% rename from src/prototypes/r3f/hud/HudSpaceScene.tsx rename to apps/docs/src/prototypes/r3f/hud/HudSpaceScene.tsx diff --git a/src/prototypes/r3f/movement/MovementScene.tsx b/apps/docs/src/prototypes/r3f/movement/MovementScene.tsx similarity index 100% rename from src/prototypes/r3f/movement/MovementScene.tsx rename to apps/docs/src/prototypes/r3f/movement/MovementScene.tsx diff --git a/src/prototypes/r3f/movement/movementState.ts b/apps/docs/src/prototypes/r3f/movement/movementState.ts similarity index 100% rename from src/prototypes/r3f/movement/movementState.ts rename to apps/docs/src/prototypes/r3f/movement/movementState.ts diff --git a/src/prototypes/r3f/navigation/travelSession.ts b/apps/docs/src/prototypes/r3f/navigation/travelSession.ts similarity index 100% rename from src/prototypes/r3f/navigation/travelSession.ts rename to apps/docs/src/prototypes/r3f/navigation/travelSession.ts diff --git a/src/prototypes/r3f/shared/AsteroidMesh.tsx b/apps/docs/src/prototypes/r3f/shared/AsteroidMesh.tsx similarity index 100% rename from src/prototypes/r3f/shared/AsteroidMesh.tsx rename to apps/docs/src/prototypes/r3f/shared/AsteroidMesh.tsx diff --git a/src/prototypes/r3f/shared/CameraRig.tsx b/apps/docs/src/prototypes/r3f/shared/CameraRig.tsx similarity index 100% rename from src/prototypes/r3f/shared/CameraRig.tsx rename to apps/docs/src/prototypes/r3f/shared/CameraRig.tsx diff --git a/src/prototypes/r3f/shared/PoiApproachShip.tsx b/apps/docs/src/prototypes/r3f/shared/PoiApproachShip.tsx similarity index 100% rename from src/prototypes/r3f/shared/PoiApproachShip.tsx rename to apps/docs/src/prototypes/r3f/shared/PoiApproachShip.tsx diff --git a/src/prototypes/r3f/shared/Projectiles.tsx b/apps/docs/src/prototypes/r3f/shared/Projectiles.tsx similarity index 100% rename from src/prototypes/r3f/shared/Projectiles.tsx rename to apps/docs/src/prototypes/r3f/shared/Projectiles.tsx diff --git a/src/prototypes/r3f/shared/RouteLine.tsx b/apps/docs/src/prototypes/r3f/shared/RouteLine.tsx similarity index 100% rename from src/prototypes/r3f/shared/RouteLine.tsx rename to apps/docs/src/prototypes/r3f/shared/RouteLine.tsx diff --git a/src/prototypes/r3f/shared/ShipMesh.tsx b/apps/docs/src/prototypes/r3f/shared/ShipMesh.tsx similarity index 100% rename from src/prototypes/r3f/shared/ShipMesh.tsx rename to apps/docs/src/prototypes/r3f/shared/ShipMesh.tsx diff --git a/src/prototypes/r3f/shared/SpaceCanvas.tsx b/apps/docs/src/prototypes/r3f/shared/SpaceCanvas.tsx similarity index 100% rename from src/prototypes/r3f/shared/SpaceCanvas.tsx rename to apps/docs/src/prototypes/r3f/shared/SpaceCanvas.tsx diff --git a/src/prototypes/r3f/shared/SpaceEnvironment.tsx b/apps/docs/src/prototypes/r3f/shared/SpaceEnvironment.tsx similarity index 100% rename from src/prototypes/r3f/shared/SpaceEnvironment.tsx rename to apps/docs/src/prototypes/r3f/shared/SpaceEnvironment.tsx diff --git a/src/prototypes/r3f/shared/StarSystemContents.tsx b/apps/docs/src/prototypes/r3f/shared/StarSystemContents.tsx similarity index 100% rename from src/prototypes/r3f/shared/StarSystemContents.tsx rename to apps/docs/src/prototypes/r3f/shared/StarSystemContents.tsx diff --git a/src/prototypes/r3f/shared/StarSystemNode.tsx b/apps/docs/src/prototypes/r3f/shared/StarSystemNode.tsx similarity index 100% rename from src/prototypes/r3f/shared/StarSystemNode.tsx rename to apps/docs/src/prototypes/r3f/shared/StarSystemNode.tsx diff --git a/src/prototypes/r3f/shared/StationMesh.tsx b/apps/docs/src/prototypes/r3f/shared/StationMesh.tsx similarity index 100% rename from src/prototypes/r3f/shared/StationMesh.tsx rename to apps/docs/src/prototypes/r3f/shared/StationMesh.tsx diff --git a/src/prototypes/r3f/shared/galaxyData.ts b/apps/docs/src/prototypes/r3f/shared/galaxyData.ts similarity index 100% rename from src/prototypes/r3f/shared/galaxyData.ts rename to apps/docs/src/prototypes/r3f/shared/galaxyData.ts diff --git a/src/prototypes/r3f/shared/galaxyMap.ts b/apps/docs/src/prototypes/r3f/shared/galaxyMap.ts similarity index 100% rename from src/prototypes/r3f/shared/galaxyMap.ts rename to apps/docs/src/prototypes/r3f/shared/galaxyMap.ts diff --git a/src/prototypes/r3f/shared/poiOrbit.ts b/apps/docs/src/prototypes/r3f/shared/poiOrbit.ts similarity index 100% rename from src/prototypes/r3f/shared/poiOrbit.ts rename to apps/docs/src/prototypes/r3f/shared/poiOrbit.ts diff --git a/src/prototypes/r3f/shared/routing.ts b/apps/docs/src/prototypes/r3f/shared/routing.ts similarity index 100% rename from src/prototypes/r3f/shared/routing.ts rename to apps/docs/src/prototypes/r3f/shared/routing.ts diff --git a/src/prototypes/r3f/shared/types.ts b/apps/docs/src/prototypes/r3f/shared/types.ts similarity index 100% rename from src/prototypes/r3f/shared/types.ts rename to apps/docs/src/prototypes/r3f/shared/types.ts diff --git a/src/prototypes/r3f/shared/useAnimationClock.ts b/apps/docs/src/prototypes/r3f/shared/useAnimationClock.ts similarity index 100% rename from src/prototypes/r3f/shared/useAnimationClock.ts rename to apps/docs/src/prototypes/r3f/shared/useAnimationClock.ts diff --git a/src/prototypes/r3f/starmap/StarMapScene.tsx b/apps/docs/src/prototypes/r3f/starmap/StarMapScene.tsx similarity index 100% rename from src/prototypes/r3f/starmap/StarMapScene.tsx rename to apps/docs/src/prototypes/r3f/starmap/StarMapScene.tsx diff --git a/src/prototypes/r3f/starmap/starMapState.ts b/apps/docs/src/prototypes/r3f/starmap/starMapState.ts similarity index 100% rename from src/prototypes/r3f/starmap/starMapState.ts rename to apps/docs/src/prototypes/r3f/starmap/starMapState.ts diff --git a/src/prototypes/r3f/warp/WarpBubbleScene.tsx b/apps/docs/src/prototypes/r3f/warp/WarpBubbleScene.tsx similarity index 100% rename from src/prototypes/r3f/warp/WarpBubbleScene.tsx rename to apps/docs/src/prototypes/r3f/warp/WarpBubbleScene.tsx diff --git a/src/prototypes/standalone-huds/GameHudPrototype.tsx b/apps/docs/src/prototypes/standalone-huds/GameHudPrototype.tsx similarity index 100% rename from src/prototypes/standalone-huds/GameHudPrototype.tsx rename to apps/docs/src/prototypes/standalone-huds/GameHudPrototype.tsx diff --git a/src/prototypes/standalone-huds/PrototypeFrame.tsx b/apps/docs/src/prototypes/standalone-huds/PrototypeFrame.tsx similarity index 100% rename from src/prototypes/standalone-huds/PrototypeFrame.tsx rename to apps/docs/src/prototypes/standalone-huds/PrototypeFrame.tsx diff --git a/src/styles/tailwind.css b/apps/docs/src/styles/tailwind.css similarity index 100% rename from src/styles/tailwind.css rename to apps/docs/src/styles/tailwind.css diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json new file mode 100644 index 0000000..d8df7cf --- /dev/null +++ b/apps/docs/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/tsconfig.node.json b/apps/docs/tsconfig.node.json similarity index 100% rename from tsconfig.node.json rename to apps/docs/tsconfig.node.json diff --git a/vite.config.ts b/apps/docs/vite.config.ts similarity index 100% rename from vite.config.ts rename to apps/docs/vite.config.ts diff --git a/apps/game/index.html b/apps/game/index.html new file mode 100644 index 0000000..7ce33fe --- /dev/null +++ b/apps/game/index.html @@ -0,0 +1,12 @@ + + + + + + VOID::NAV Game + + +
+ + + diff --git a/apps/game/package.json b/apps/game/package.json new file mode 100644 index 0000000..ab497cc --- /dev/null +++ b/apps/game/package.json @@ -0,0 +1,30 @@ +{ + "name": "@void-nav/game", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "tsc --noEmit && vite build", + "check": "tsc --noEmit", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^8.17.10", + "@tailwindcss/vite": "^4.3.0", + "@void-nav/ui": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "spacetimedb": "^2.3.0", + "tailwindcss": "^4.3.0", + "three": "^0.160.0", + "vite": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@types/three": "^0.160.0", + "typescript": "^5.8.3" + } +} diff --git a/apps/game/src/GameShell.tsx b/apps/game/src/GameShell.tsx new file mode 100644 index 0000000..31f19bb --- /dev/null +++ b/apps/game/src/GameShell.tsx @@ -0,0 +1,111 @@ +import { useMemo, useState } from "react"; +import { Panel } from "@void-nav/ui"; +import { GameSpaceScene } from "./scene/GameSpaceScene"; +import { CommandRail } from "./ui/CommandRail"; +import { CargoPanel } from "./ui/CargoPanel"; +import { ConnectionPanel } from "./ui/ConnectionPanel"; +import { EventFeed } from "./ui/EventFeed"; +import { TargetPanel } from "./ui/TargetPanel"; +import { WalletPanel } from "./ui/WalletPanel"; +import { useGameSession } from "./spacetime/useGameSession"; +import { useSpacetimeConnection } from "./spacetime/useSpacetimeConnection"; + +const defaultPilotName = "New Pilot"; + +export function GameShell() { + const [displayName, setDisplayName] = useState(defaultPilotName); + const [manualReducerStatus, setManualReducerStatus] = useState<{ message: string; isError: boolean }>(); + const connectionState = useSpacetimeConnection(displayName); + const reducerStatus = manualReducerStatus ?? connectionState.reducerStatus; + const session = useGameSession( + connectionState.connection, + connectionState.revision, + (message, isError = false) => setManualReducerStatus({ message, isError }), + connectionState.identity, + ); + + const connectionLabel = useMemo(() => { + if (connectionState.status === "error") return "Disconnected"; + if (connectionState.status === "connected") return "Connected"; + return connectionState.status.charAt(0).toUpperCase() + connectionState.status.slice(1); + }, [connectionState.status]); + + const selectedPoi = session.pois.find((poi) => poi.poiId === session.ship?.selectedPoiId); + const pilotName = session.player?.displayName ?? displayName; + + return ( +
+
+ +
+ +
+ + +
+ + +
+ +
+ session.actions.sellOreToNpcMarket(item.itemId, item.quantity)} + /> +
+
+ ); +} + +function InfoRow({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/apps/game/src/main.tsx b/apps/game/src/main.tsx new file mode 100644 index 0000000..36cd823 --- /dev/null +++ b/apps/game/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { GameShell } from "./GameShell"; +import "./styles/tailwind.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/game/src/module_bindings/cargo_item_table.ts b/apps/game/src/module_bindings/cargo_item_table.ts new file mode 100644 index 0000000..d86d4b3 --- /dev/null +++ b/apps/game/src/module_bindings/cargo_item_table.ts @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + cargoItemId: __t.u64().primaryKey().name("cargo_item_id"), + ownerIdentity: __t.identity().name("owner_identity"), + shipId: __t.u64().name("ship_id"), + itemId: __t.string().name("item_id"), + itemName: __t.string().name("item_name"), + category: __t.string(), + quantity: __t.u64(), + unitPrice: __t.u64().name("unit_price"), +}); diff --git a/apps/game/src/module_bindings/complete_approach_reducer.ts b/apps/game/src/module_bindings/complete_approach_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/apps/game/src/module_bindings/complete_approach_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/apps/game/src/module_bindings/complete_mining_cycle_reducer.ts b/apps/game/src/module_bindings/complete_mining_cycle_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/apps/game/src/module_bindings/complete_mining_cycle_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/apps/game/src/module_bindings/connect_player_reducer.ts b/apps/game/src/module_bindings/connect_player_reducer.ts new file mode 100644 index 0000000..547493e --- /dev/null +++ b/apps/game/src/module_bindings/connect_player_reducer.ts @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + displayName: __t.string(), +}; diff --git a/apps/game/src/module_bindings/dock_reducer.ts b/apps/game/src/module_bindings/dock_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/apps/game/src/module_bindings/dock_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/apps/game/src/module_bindings/index.ts b/apps/game/src/module_bindings/index.ts new file mode 100644 index 0000000..d977f8f --- /dev/null +++ b/apps/game/src/module_bindings/index.ts @@ -0,0 +1,258 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +// This was generated using spacetimedb cli version 2.3.0 (commit aa73d1c35b4b346b98eeba10a3d756b4ae72162f). + +/* eslint-disable */ +/* tslint:disable */ +import { + DbConnectionBuilder as __DbConnectionBuilder, + DbConnectionImpl as __DbConnectionImpl, + SubscriptionBuilderImpl as __SubscriptionBuilderImpl, + TypeBuilder as __TypeBuilder, + Uuid as __Uuid, + convertToAccessorMap as __convertToAccessorMap, + makeQueryBuilder as __makeQueryBuilder, + procedureSchema as __procedureSchema, + procedures as __procedures, + reducerSchema as __reducerSchema, + reducers as __reducers, + schema as __schema, + t as __t, + table as __table, + type AlgebraicTypeType as __AlgebraicTypeType, + type DbConnectionConfig as __DbConnectionConfig, + type ErrorContextInterface as __ErrorContextInterface, + type Event as __Event, + type EventContextInterface as __EventContextInterface, + type Infer as __Infer, + type QueryBuilder as __QueryBuilder, + type ReducerEventContextInterface as __ReducerEventContextInterface, + type RemoteModule as __RemoteModule, + type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, + type SubscriptionHandleImpl as __SubscriptionHandleImpl, +} from "spacetimedb"; + +// Import all reducer arg schemas +import CompleteApproachReducer from "./complete_approach_reducer"; +import CompleteMiningCycleReducer from "./complete_mining_cycle_reducer"; +import ConnectPlayerReducer from "./connect_player_reducer"; +import DockReducer from "./dock_reducer"; +import PingReducer from "./ping_reducer"; +import RenamePlayerReducer from "./rename_player_reducer"; +import SeedWorldReducer from "./seed_world_reducer"; +import SelectTargetReducer from "./select_target_reducer"; +import SellOreToNpcMarketReducer from "./sell_ore_to_npc_market_reducer"; +import StartApproachReducer from "./start_approach_reducer"; +import StartMiningReducer from "./start_mining_reducer"; +import UndockReducer from "./undock_reducer"; + +// Import all procedure arg schemas + +// Import all table schema definitions +import CargoItemRow from "./cargo_item_table"; +import PlayerRow from "./player_table"; +import PointOfInterestRow from "./point_of_interest_table"; +import ServerEventRow from "./server_event_table"; +import ShipRow from "./ship_table"; +import ShipOperationRow from "./ship_operation_table"; +import StationRow from "./station_table"; +import SystemRow from "./system_table"; +import WalletRow from "./wallet_table"; + +/** Type-only namespace exports for generated type groups. */ + +/** The schema information for all tables in this module. This is defined the same was as the tables would have been defined in the server. */ +const tablesSchema = __schema({ + cargo_item: __table({ + name: 'cargo_item', + indexes: [ + { accessor: 'cargo_item_id', name: 'cargo_item_cargo_item_id_idx_btree', algorithm: 'btree', columns: [ + 'cargoItemId', + ] }, + { accessor: 'owner_identity', name: 'cargo_item_owner_identity_idx_btree', algorithm: 'btree', columns: [ + 'ownerIdentity', + ] }, + { accessor: 'ship_id', name: 'cargo_item_ship_id_idx_btree', algorithm: 'btree', columns: [ + 'shipId', + ] }, + ], + constraints: [ + { name: 'cargo_item_cargo_item_id_key', constraint: 'unique', columns: ['cargoItemId'] }, + ], + }, CargoItemRow), + player: __table({ + name: 'player', + indexes: [ + { accessor: 'identity', name: 'player_identity_idx_btree', algorithm: 'btree', columns: [ + 'identity', + ] }, + ], + constraints: [ + { name: 'player_identity_key', constraint: 'unique', columns: ['identity'] }, + ], + }, PlayerRow), + point_of_interest: __table({ + name: 'point_of_interest', + indexes: [ + { accessor: 'poi_id', name: 'point_of_interest_poi_id_idx_btree', algorithm: 'btree', columns: [ + 'poiId', + ] }, + { accessor: 'system_id', name: 'point_of_interest_system_id_idx_btree', algorithm: 'btree', columns: [ + 'systemId', + ] }, + ], + constraints: [ + { name: 'point_of_interest_poi_id_key', constraint: 'unique', columns: ['poiId'] }, + ], + }, PointOfInterestRow), + server_event: __table({ + name: 'server_event', + indexes: [ + { accessor: 'actor_identity', name: 'server_event_actor_identity_idx_btree', algorithm: 'btree', columns: [ + 'actorIdentity', + ] }, + { accessor: 'event_id', name: 'server_event_event_id_idx_btree', algorithm: 'btree', columns: [ + 'eventId', + ] }, + ], + constraints: [ + { name: 'server_event_event_id_key', constraint: 'unique', columns: ['eventId'] }, + ], + }, ServerEventRow), + ship: __table({ + name: 'ship', + indexes: [ + { accessor: 'owner_identity', name: 'ship_owner_identity_idx_btree', algorithm: 'btree', columns: [ + 'ownerIdentity', + ] }, + { accessor: 'ship_id', name: 'ship_ship_id_idx_btree', algorithm: 'btree', columns: [ + 'shipId', + ] }, + ], + constraints: [ + { name: 'ship_ship_id_key', constraint: 'unique', columns: ['shipId'] }, + ], + }, ShipRow), + ship_operation: __table({ + name: 'ship_operation', + indexes: [ + { accessor: 'ship_id', name: 'ship_operation_ship_id_idx_btree', algorithm: 'btree', columns: [ + 'shipId', + ] }, + ], + constraints: [ + { name: 'ship_operation_ship_id_key', constraint: 'unique', columns: ['shipId'] }, + ], + }, ShipOperationRow), + station: __table({ + name: 'station', + indexes: [ + { accessor: 'station_id', name: 'station_station_id_idx_btree', algorithm: 'btree', columns: [ + 'stationId', + ] }, + { accessor: 'system_id', name: 'station_system_id_idx_btree', algorithm: 'btree', columns: [ + 'systemId', + ] }, + ], + constraints: [ + { name: 'station_station_id_key', constraint: 'unique', columns: ['stationId'] }, + ], + }, StationRow), + system: __table({ + name: 'system', + indexes: [ + { accessor: 'system_id', name: 'system_system_id_idx_btree', algorithm: 'btree', columns: [ + 'systemId', + ] }, + ], + constraints: [ + { name: 'system_system_id_key', constraint: 'unique', columns: ['systemId'] }, + ], + }, SystemRow), + wallet: __table({ + name: 'wallet', + indexes: [ + { accessor: 'owner_identity', name: 'wallet_owner_identity_idx_btree', algorithm: 'btree', columns: [ + 'ownerIdentity', + ] }, + ], + constraints: [ + { name: 'wallet_owner_identity_key', constraint: 'unique', columns: ['ownerIdentity'] }, + ], + }, WalletRow), +}); + +/** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ +const reducersSchema = __reducers( + __reducerSchema("complete_approach", CompleteApproachReducer), + __reducerSchema("complete_mining_cycle", CompleteMiningCycleReducer), + __reducerSchema("connect_player", ConnectPlayerReducer), + __reducerSchema("dock", DockReducer), + __reducerSchema("ping", PingReducer), + __reducerSchema("rename_player", RenamePlayerReducer), + __reducerSchema("seed_world", SeedWorldReducer), + __reducerSchema("select_target", SelectTargetReducer), + __reducerSchema("sell_ore_to_npc_market", SellOreToNpcMarketReducer), + __reducerSchema("start_approach", StartApproachReducer), + __reducerSchema("start_mining", StartMiningReducer), + __reducerSchema("undock", UndockReducer), +); + +/** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ +const proceduresSchema = __procedures( +); + +/** The remote SpacetimeDB module schema, both runtime and type information. */ +const REMOTE_MODULE = { + versionInfo: { + cliVersion: "2.3.0" as const, + }, + tables: tablesSchema.schemaType.tables, + reducers: reducersSchema.reducersType.reducers, + ...proceduresSchema, +} satisfies __RemoteModule< + typeof tablesSchema.schemaType, + typeof reducersSchema.reducersType, + typeof proceduresSchema +>; + +/** The tables available in this remote SpacetimeDB module. Each table reference doubles as a query builder. */ +export const tables: __QueryBuilder = __makeQueryBuilder(tablesSchema.schemaType); + +/** The reducers available in this remote SpacetimeDB module. */ +export const reducers = __convertToAccessorMap(reducersSchema.reducersType.reducers); + +/** The procedures available in this remote SpacetimeDB module. */ +export const procedures = __convertToAccessorMap(proceduresSchema.procedures); + +/** The context type returned in callbacks for all possible events. */ +export type EventContext = __EventContextInterface; +/** The context type returned in callbacks for reducer events. */ +export type ReducerEventContext = __ReducerEventContextInterface; +/** The context type returned in callbacks for subscription events. */ +export type SubscriptionEventContext = __SubscriptionEventContextInterface; +/** The context type returned in callbacks for error events. */ +export type ErrorContext = __ErrorContextInterface; +/** The subscription handle type to manage active subscriptions created from a {@link SubscriptionBuilder}. */ +export type SubscriptionHandle = __SubscriptionHandleImpl; + +/** Builder class to configure a new subscription to the remote SpacetimeDB instance. */ +export class SubscriptionBuilder extends __SubscriptionBuilderImpl {} + +/** Builder class to configure a new database connection to the remote SpacetimeDB instance. */ +export class DbConnectionBuilder extends __DbConnectionBuilder {} + +/** The typed database connection to manage connections to the remote SpacetimeDB instance. This class has type information specific to the generated module. */ +export class DbConnection extends __DbConnectionImpl { + /** Creates a new {@link DbConnectionBuilder} to configure and connect to the remote SpacetimeDB instance. */ + static builder = (): DbConnectionBuilder => { + return new DbConnectionBuilder(REMOTE_MODULE, (config: __DbConnectionConfig) => new DbConnection(config)); + }; + + /** Creates a new {@link SubscriptionBuilder} to configure a subscription to the remote SpacetimeDB instance. */ + override subscriptionBuilder = (): SubscriptionBuilder => { + return new SubscriptionBuilder(this); + }; +} + diff --git a/apps/game/src/module_bindings/ping_reducer.ts b/apps/game/src/module_bindings/ping_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/apps/game/src/module_bindings/ping_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/apps/game/src/module_bindings/player_table.ts b/apps/game/src/module_bindings/player_table.ts new file mode 100644 index 0000000..fbf45fe --- /dev/null +++ b/apps/game/src/module_bindings/player_table.ts @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + identity: __t.identity().primaryKey(), + displayName: __t.string().name("display_name"), + createdAt: __t.timestamp().name("created_at"), + updatedAt: __t.timestamp().name("updated_at"), + lastConnectedAt: __t.timestamp().name("last_connected_at"), + isConnected: __t.bool().name("is_connected"), +}); diff --git a/apps/game/src/module_bindings/point_of_interest_table.ts b/apps/game/src/module_bindings/point_of_interest_table.ts new file mode 100644 index 0000000..b8f97a6 --- /dev/null +++ b/apps/game/src/module_bindings/point_of_interest_table.ts @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + poiId: __t.string().primaryKey().name("poi_id"), + systemId: __t.string().name("system_id"), + name: __t.string(), + poiType: __t.string().name("poi_type"), + x: __t.f32(), + y: __t.f32(), + z: __t.f32(), + stationId: __t.string().name("station_id"), +}); diff --git a/apps/game/src/module_bindings/rename_player_reducer.ts b/apps/game/src/module_bindings/rename_player_reducer.ts new file mode 100644 index 0000000..547493e --- /dev/null +++ b/apps/game/src/module_bindings/rename_player_reducer.ts @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + displayName: __t.string(), +}; diff --git a/apps/game/src/module_bindings/seed_world_reducer.ts b/apps/game/src/module_bindings/seed_world_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/apps/game/src/module_bindings/seed_world_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/apps/game/src/module_bindings/select_target_reducer.ts b/apps/game/src/module_bindings/select_target_reducer.ts new file mode 100644 index 0000000..7baac0d --- /dev/null +++ b/apps/game/src/module_bindings/select_target_reducer.ts @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + poiId: __t.string(), +}; diff --git a/apps/game/src/module_bindings/sell_ore_to_npc_market_reducer.ts b/apps/game/src/module_bindings/sell_ore_to_npc_market_reducer.ts new file mode 100644 index 0000000..ead95ca --- /dev/null +++ b/apps/game/src/module_bindings/sell_ore_to_npc_market_reducer.ts @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default { + itemId: __t.string(), + quantity: __t.u64(), +}; diff --git a/apps/game/src/module_bindings/server_event_table.ts b/apps/game/src/module_bindings/server_event_table.ts new file mode 100644 index 0000000..83d1952 --- /dev/null +++ b/apps/game/src/module_bindings/server_event_table.ts @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + eventId: __t.u64().primaryKey().name("event_id"), + at: __t.timestamp(), + actorIdentity: __t.identity().name("actor_identity"), + eventType: __t.string().name("event_type"), + message: __t.string(), +}); diff --git a/apps/game/src/module_bindings/ship_operation_table.ts b/apps/game/src/module_bindings/ship_operation_table.ts new file mode 100644 index 0000000..9d99249 --- /dev/null +++ b/apps/game/src/module_bindings/ship_operation_table.ts @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + shipId: __t.u64().primaryKey().name("ship_id"), + operationType: __t.string().name("operation_type"), + targetPoiId: __t.string().name("target_poi_id"), + startedAt: __t.timestamp().name("started_at"), + durationMs: __t.u64().name("duration_ms"), + completesAtMs: __t.u64().name("completes_at_ms"), +}); diff --git a/apps/game/src/module_bindings/ship_table.ts b/apps/game/src/module_bindings/ship_table.ts new file mode 100644 index 0000000..b084418 --- /dev/null +++ b/apps/game/src/module_bindings/ship_table.ts @@ -0,0 +1,27 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + shipId: __t.u64().primaryKey().name("ship_id"), + ownerIdentity: __t.identity().name("owner_identity"), + shipName: __t.string().name("ship_name"), + hullType: __t.string().name("hull_type"), + currentSystemId: __t.string().name("current_system_id"), + dockedStationId: __t.string().name("docked_station_id"), + currentPoiId: __t.string().name("current_poi_id"), + selectedPoiId: __t.string().name("selected_poi_id"), + flightMode: __t.string().name("flight_mode"), + x: __t.f32(), + y: __t.f32(), + z: __t.f32(), + cargoCapacity: __t.u64().name("cargo_capacity"), +}); diff --git a/apps/game/src/module_bindings/start_approach_reducer.ts b/apps/game/src/module_bindings/start_approach_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/apps/game/src/module_bindings/start_approach_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/apps/game/src/module_bindings/start_mining_reducer.ts b/apps/game/src/module_bindings/start_mining_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/apps/game/src/module_bindings/start_mining_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/apps/game/src/module_bindings/station_table.ts b/apps/game/src/module_bindings/station_table.ts new file mode 100644 index 0000000..494e728 --- /dev/null +++ b/apps/game/src/module_bindings/station_table.ts @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + stationId: __t.string().primaryKey().name("station_id"), + systemId: __t.string().name("system_id"), + name: __t.string(), +}); diff --git a/apps/game/src/module_bindings/system_table.ts b/apps/game/src/module_bindings/system_table.ts new file mode 100644 index 0000000..df55936 --- /dev/null +++ b/apps/game/src/module_bindings/system_table.ts @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + systemId: __t.string().primaryKey().name("system_id"), + name: __t.string(), + securityLevel: __t.f32().name("security_level"), +}); diff --git a/apps/game/src/module_bindings/types.ts b/apps/game/src/module_bindings/types.ts new file mode 100644 index 0000000..3aebeac --- /dev/null +++ b/apps/game/src/module_bindings/types.ts @@ -0,0 +1,103 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export const CargoItem = __t.object("CargoItem", { + cargoItemId: __t.u64(), + ownerIdentity: __t.identity(), + shipId: __t.u64(), + itemId: __t.string(), + itemName: __t.string(), + category: __t.string(), + quantity: __t.u64(), + unitPrice: __t.u64(), +}); +export type CargoItem = __Infer; + +export const Player = __t.object("Player", { + identity: __t.identity(), + displayName: __t.string(), + createdAt: __t.timestamp(), + updatedAt: __t.timestamp(), + lastConnectedAt: __t.timestamp(), + isConnected: __t.bool(), +}); +export type Player = __Infer; + +export const PointOfInterest = __t.object("PointOfInterest", { + poiId: __t.string(), + systemId: __t.string(), + name: __t.string(), + poiType: __t.string(), + x: __t.f32(), + y: __t.f32(), + z: __t.f32(), + stationId: __t.string(), +}); +export type PointOfInterest = __Infer; + +export const ServerEvent = __t.object("ServerEvent", { + eventId: __t.u64(), + at: __t.timestamp(), + actorIdentity: __t.identity(), + eventType: __t.string(), + message: __t.string(), +}); +export type ServerEvent = __Infer; + +export const Ship = __t.object("Ship", { + shipId: __t.u64(), + ownerIdentity: __t.identity(), + shipName: __t.string(), + hullType: __t.string(), + currentSystemId: __t.string(), + dockedStationId: __t.string(), + currentPoiId: __t.string(), + selectedPoiId: __t.string(), + flightMode: __t.string(), + x: __t.f32(), + y: __t.f32(), + z: __t.f32(), + cargoCapacity: __t.u64(), +}); +export type Ship = __Infer; + +export const ShipOperation = __t.object("ShipOperation", { + shipId: __t.u64(), + operationType: __t.string(), + targetPoiId: __t.string(), + startedAt: __t.timestamp(), + durationMs: __t.u64(), + completesAtMs: __t.u64(), +}); +export type ShipOperation = __Infer; + +export const Station = __t.object("Station", { + stationId: __t.string(), + systemId: __t.string(), + name: __t.string(), +}); +export type Station = __Infer; + +export const System = __t.object("System", { + systemId: __t.string(), + name: __t.string(), + securityLevel: __t.f32(), +}); +export type System = __Infer; + +export const Wallet = __t.object("Wallet", { + ownerIdentity: __t.identity(), + isk: __t.u64(), + updatedAt: __t.timestamp(), +}); +export type Wallet = __Infer; + diff --git a/apps/game/src/module_bindings/types/procedures.ts b/apps/game/src/module_bindings/types/procedures.ts new file mode 100644 index 0000000..d5ac825 --- /dev/null +++ b/apps/game/src/module_bindings/types/procedures.ts @@ -0,0 +1,10 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { type Infer as __Infer } from "spacetimedb"; + +// Import all procedure arg schemas + + diff --git a/apps/game/src/module_bindings/types/reducers.ts b/apps/game/src/module_bindings/types/reducers.ts new file mode 100644 index 0000000..4969aa6 --- /dev/null +++ b/apps/game/src/module_bindings/types/reducers.ts @@ -0,0 +1,34 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { type Infer as __Infer } from "spacetimedb"; + +// Import all reducer arg schemas +import CompleteApproachReducer from "../complete_approach_reducer"; +import CompleteMiningCycleReducer from "../complete_mining_cycle_reducer"; +import ConnectPlayerReducer from "../connect_player_reducer"; +import DockReducer from "../dock_reducer"; +import PingReducer from "../ping_reducer"; +import RenamePlayerReducer from "../rename_player_reducer"; +import SeedWorldReducer from "../seed_world_reducer"; +import SelectTargetReducer from "../select_target_reducer"; +import SellOreToNpcMarketReducer from "../sell_ore_to_npc_market_reducer"; +import StartApproachReducer from "../start_approach_reducer"; +import StartMiningReducer from "../start_mining_reducer"; +import UndockReducer from "../undock_reducer"; + +export type CompleteApproachParams = __Infer; +export type CompleteMiningCycleParams = __Infer; +export type ConnectPlayerParams = __Infer; +export type DockParams = __Infer; +export type PingParams = __Infer; +export type RenamePlayerParams = __Infer; +export type SeedWorldParams = __Infer; +export type SelectTargetParams = __Infer; +export type SellOreToNpcMarketParams = __Infer; +export type StartApproachParams = __Infer; +export type StartMiningParams = __Infer; +export type UndockParams = __Infer; + diff --git a/apps/game/src/module_bindings/undock_reducer.ts b/apps/game/src/module_bindings/undock_reducer.ts new file mode 100644 index 0000000..e18fbc0 --- /dev/null +++ b/apps/game/src/module_bindings/undock_reducer.ts @@ -0,0 +1,13 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default {}; diff --git a/apps/game/src/module_bindings/wallet_table.ts b/apps/game/src/module_bindings/wallet_table.ts new file mode 100644 index 0000000..d48d14b --- /dev/null +++ b/apps/game/src/module_bindings/wallet_table.ts @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from "spacetimedb"; + +export default __t.row({ + ownerIdentity: __t.identity().primaryKey().name("owner_identity"), + isk: __t.u64(), + updatedAt: __t.timestamp().name("updated_at"), +}); diff --git a/apps/game/src/scene/AsteroidMesh.tsx b/apps/game/src/scene/AsteroidMesh.tsx new file mode 100644 index 0000000..271dba8 --- /dev/null +++ b/apps/game/src/scene/AsteroidMesh.tsx @@ -0,0 +1,45 @@ +import { useFrame, type ThreeEvent } from "@react-three/fiber"; +import { useRef } from "react"; +import type { Group } from "three"; + +type Vector3Tuple = [number, number, number]; + +export function AsteroidMesh({ + position, + scale = 1, + selected, + onSelect, +}: { + position: Vector3Tuple; + scale?: number; + selected: boolean; + onSelect: () => void; +}) { + const groupRef = useRef(null); + + useFrame(({ clock }) => { + if (!groupRef.current) return; + groupRef.current.rotation.x = clock.elapsedTime * 0.08 * scale; + groupRef.current.rotation.y = clock.elapsedTime * 0.05; + }); + + function handleClick(event: ThreeEvent) { + event.stopPropagation(); + onSelect(); + } + + return ( + + + + + + {selected ? ( + + + + + ) : null} + + ); +} diff --git a/apps/game/src/scene/GameSpaceScene.tsx b/apps/game/src/scene/GameSpaceScene.tsx new file mode 100644 index 0000000..c005c3b --- /dev/null +++ b/apps/game/src/scene/GameSpaceScene.tsx @@ -0,0 +1,175 @@ +import { Line } from "@react-three/drei"; +import { useFrame } from "@react-three/fiber"; +import { useEffect, useMemo, useRef, useState } from "react"; +import type { Group } from "three"; +import type { PointOfInterest, Ship, ShipOperation } from "../module_bindings/types"; +import { AsteroidMesh } from "./AsteroidMesh"; +import { ShipMesh } from "./ShipMesh"; +import { SpaceCanvas } from "./SpaceCanvas"; +import { SpaceEnvironment } from "./SpaceEnvironment"; +import { StationMesh } from "./StationMesh"; + +type Vector3Tuple = [number, number, number]; + +type GameSpaceSceneProps = { + ship?: Ship; + pois: PointOfInterest[]; + selectedPoiId?: string; + operation?: ShipOperation; + onSelectPoi: (poiId: string) => void; + onCompleteApproach: () => void; + onCompleteMiningCycle: () => void; +}; + +export function GameSpaceScene(props: GameSpaceSceneProps) { + return ( + + + + ); +} + +function SceneContents({ + ship, + pois, + selectedPoiId, + operation, + onSelectPoi, + onCompleteApproach, + onCompleteMiningCycle, +}: GameSpaceSceneProps) { + const driftRef = useRef(null); + const completedOperationRef = useRef(); + const [nowMs, setNowMs] = useState(() => Date.now()); + const poiById = useMemo(() => new Map(pois.map((poi) => [poi.poiId, poi])), [pois]); + const targetPoi = operation ? poiById.get(operation.targetPoiId) : undefined; + const selectedPoi = selectedPoiId ? poiById.get(selectedPoiId) : undefined; + + useEffect(() => { + const timer = window.setInterval(() => setNowMs(Date.now()), 120); + return () => window.clearInterval(timer); + }, []); + + useEffect(() => { + const operationKey = operation ? `${operation.shipId}-${operation.operationType}-${operation.completesAtMs}` : undefined; + if (completedOperationRef.current !== operationKey) { + completedOperationRef.current = undefined; + } + + if (!operation || !operationKey || nowMs < Number(operation.completesAtMs) || completedOperationRef.current === operationKey) { + return; + } + + completedOperationRef.current = operationKey; + if (operation.operationType === "approach") onCompleteApproach(); + if (operation.operationType === "mining") onCompleteMiningCycle(); + }, [nowMs, onCompleteApproach, onCompleteMiningCycle, operation]); + + useFrame(({ clock, camera }) => { + if (driftRef.current) { + driftRef.current.rotation.y = Math.sin(clock.elapsedTime * 0.07) * 0.035; + driftRef.current.rotation.x = Math.sin(clock.elapsedTime * 0.05) * 0.018; + } + camera.position.x = 8 + Math.sin(clock.elapsedTime * 0.18) * 1.4; + camera.position.y = 9 + Math.cos(clock.elapsedTime * 0.14) * 0.7; + camera.lookAt(15, -1, -8); + }); + + const shipPosition = ship ? getShipPosition(ship, operation, targetPoi, nowMs) : ([0, 0, 0] as Vector3Tuple); + const stationPois = pois.filter((poi) => poi.poiType === "station"); + const beltPois = pois.filter((poi) => poi.poiType === "asteroid_belt"); + + return ( + <> + + + + {stationPois.map((poi) => ( + onSelectPoi(poi.poiId)} + /> + ))} + {beltPois.map((poi) => ( + onSelectPoi(poi.poiId)} /> + ))} + {ship ? : null} + {operation?.operationType === "approach" && targetPoi ? ( + + ) : null} + {operation?.operationType === "mining" && selectedPoi ? ( + + ) : null} + + + ); +} + +function AsteroidCluster({ + poi, + selected, + onSelect, +}: { + poi: PointOfInterest; + selected: boolean; + onSelect: () => void; +}) { + const base = poiPosition(poi); + const offsets: Array<[number, number, number, number]> = [ + [0, 0, 0, 1.25], + [-2.7, 0.8, 1.4, 0.78], + [2.4, -0.6, -1.6, 0.95], + ]; + + return ( + + {offsets.map(([x, y, z, scale], index) => ( + + ))} + {selected ? ( + + + + + ) : null} + + ); +} + +function GridPlane() { + return ( + + ); +} + +function getShipPosition(ship: Ship, operation: ShipOperation | undefined, targetPoi: PointOfInterest | undefined, nowMs: number) { + const start = [ship.x, ship.y, ship.z] as Vector3Tuple; + if (operation?.operationType !== "approach" || !targetPoi) return start; + + const startedAt = Number(operation.startedAt.toMillis()); + const duration = Number(operation.durationMs); + const progress = Math.max(0, Math.min(1, (nowMs - startedAt) / duration)); + const target = poiPosition(targetPoi); + + return [ + lerp(start[0], target[0], progress), + lerp(start[1], target[1], progress), + lerp(start[2], target[2], progress), + ] as Vector3Tuple; +} + +function poiPosition(poi: PointOfInterest): Vector3Tuple { + return [poi.x, poi.y, poi.z]; +} + +function lerp(start: number, end: number, progress: number) { + return start + (end - start) * progress; +} diff --git a/apps/game/src/scene/ShipMesh.tsx b/apps/game/src/scene/ShipMesh.tsx new file mode 100644 index 0000000..3839e1f --- /dev/null +++ b/apps/game/src/scene/ShipMesh.tsx @@ -0,0 +1,34 @@ +import { useFrame } from "@react-three/fiber"; +import { useRef } from "react"; +import type { Group } from "three"; + +type Vector3Tuple = [number, number, number]; + +export function ShipMesh({ position, flightMode }: { position: Vector3Tuple; flightMode?: string }) { + const groupRef = useRef(null); + + useFrame(({ clock }) => { + if (!groupRef.current) return; + groupRef.current.rotation.z = Math.sin(clock.elapsedTime * 2) * 0.045; + groupRef.current.position.y = position[1] + Math.sin(clock.elapsedTime * 1.4) * 0.08; + }); + + const engineActive = flightMode === "approaching" || flightMode === "flight"; + + return ( + + + + + + + + + + + + + + + ); +} diff --git a/apps/game/src/scene/SpaceCanvas.tsx b/apps/game/src/scene/SpaceCanvas.tsx new file mode 100644 index 0000000..01c2904 --- /dev/null +++ b/apps/game/src/scene/SpaceCanvas.tsx @@ -0,0 +1,10 @@ +import { Canvas } from "@react-three/fiber"; +import type { ReactNode } from "react"; + +export function SpaceCanvas({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/game/src/scene/SpaceEnvironment.tsx b/apps/game/src/scene/SpaceEnvironment.tsx new file mode 100644 index 0000000..3255290 --- /dev/null +++ b/apps/game/src/scene/SpaceEnvironment.tsx @@ -0,0 +1,27 @@ +import { Stars } from "@react-three/drei"; +import { useFrame } from "@react-three/fiber"; +import { useRef } from "react"; +import type { Group } from "three"; + +export function SpaceEnvironment() { + const dustRef = useRef(null); + + useFrame(({ clock }) => { + if (!dustRef.current) return; + dustRef.current.rotation.y = clock.elapsedTime * 0.015; + dustRef.current.rotation.x = Math.sin(clock.elapsedTime * 0.08) * 0.015; + }); + + return ( + <> + + + + + + + + + + ); +} diff --git a/apps/game/src/scene/StationMesh.tsx b/apps/game/src/scene/StationMesh.tsx new file mode 100644 index 0000000..1125066 --- /dev/null +++ b/apps/game/src/scene/StationMesh.tsx @@ -0,0 +1,45 @@ +import type { ThreeEvent } from "@react-three/fiber"; + +type Vector3Tuple = [number, number, number]; + +export function StationMesh({ + position, + selected, + onSelect, +}: { + position: Vector3Tuple; + selected: boolean; + onSelect: () => void; +}) { + function handleClick(event: ThreeEvent) { + event.stopPropagation(); + onSelect(); + } + + return ( + + + + + + + + + + + + + + {selected ? : null} + + ); +} + +function SelectionRing({ radius, color }: { radius: number; color: string }) { + return ( + + + + + ); +} diff --git a/apps/game/src/spacetime/client.ts b/apps/game/src/spacetime/client.ts new file mode 100644 index 0000000..d456c60 --- /dev/null +++ b/apps/game/src/spacetime/client.ts @@ -0,0 +1,135 @@ +import type { Identity } from "spacetimedb"; +import { DbConnection, type DbConnection as GameDbConnection } from "../module_bindings"; + +export type IdentityLike = Pick; + +export type ConnectionStatus = "idle" | "connecting" | "connected" | "disconnected" | "error"; + +export type ConnectionConfig = { + uri: string; + database: string; + displayName: string; + onStatus: (status: ConnectionStatus, message?: string) => void; + onIdentity: (identity?: IdentityLike) => void; + onDataChanged: () => void; + onReducerStatus: (message: string, isError?: boolean) => void; +}; + +export function createSpacetimeConnection(config: ConnectionConfig): GameDbConnection | null { + try { + config.onStatus("connecting"); + + const authTokenKey = getAuthTokenKey(config); + const connection = DbConnection.builder() + .withUri(config.uri) + .withDatabaseName(config.database) + .withToken(readStoredAuthToken(authTokenKey)) + .onConnect((conn, identity, token) => { + storeAuthToken(authTokenKey, token); + config.onIdentity(identity); + config.onStatus("connected"); + subscribeToShellRows(conn, identity); + invokeReducer(config, "seedWorld", () => conn.reducers.seedWorld({})); + invokeReducer(config, "connectPlayer", () => conn.reducers.connectPlayer({ displayName: config.displayName })); + config.onDataChanged(); + }) + .onConnectError((_ctx, error) => { + config.onStatus("error", error.message); + config.onReducerStatus(`Connection failed: ${error.message}`, true); + }) + .onDisconnect((_ctx, error) => { + config.onStatus("disconnected", error?.message); + config.onIdentity(undefined); + }) + .build(); + + registerTableRefresh(connection, config.onDataChanged); + return connection; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + config.onStatus("error", message); + config.onReducerStatus(message, true); + return null; + } +} + +export function invokeReducer(config: Pick, name: string, call: () => Promise) { + try { + call() + .then(() => config.onReducerStatus(`${name} reducer completed`)) + .catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + config.onReducerStatus(`${name} reducer failed: ${message}`, true); + }); + config.onReducerStatus(`${name} reducer sent`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + config.onReducerStatus(`${name} reducer failed: ${message}`, true); + } +} + +function subscribeToShellRows(conn: GameDbConnection, _identity: IdentityLike) { + // Keep the first shell subscriptions broad; the session hook selects caller/starter rows. + conn.subscriptionBuilder().subscribe([ + "SELECT * FROM player", + "SELECT * FROM ship", + "SELECT * FROM system WHERE system_id = 'solace'", + "SELECT * FROM station WHERE station_id = 'solace-prime'", + "SELECT * FROM point_of_interest WHERE system_id = 'solace'", + "SELECT * FROM cargo_item", + "SELECT * FROM wallet", + "SELECT * FROM ship_operation", + "SELECT * FROM server_event", + ]); +} + +function registerTableRefresh(conn: GameDbConnection, onDataChanged: () => void) { + const tables = [ + conn.db.player, + conn.db.ship, + conn.db.system, + conn.db.station, + conn.db.point_of_interest, + conn.db.cargo_item, + conn.db.wallet, + conn.db.ship_operation, + conn.db.server_event, + ] as unknown as Array<{ + onInsert?: (callback: (...args: unknown[]) => void) => void; + onUpdate?: (callback: (...args: unknown[]) => void) => void; + onDelete?: (callback: (...args: unknown[]) => void) => void; + }>; + + for (const table of tables) { + table.onInsert?.(onDataChanged); + table.onUpdate?.(onDataChanged); + table.onDelete?.(onDataChanged); + } +} + +export function formatIdentity(identity?: IdentityLike | string) { + if (!identity) return "unassigned"; + if (typeof identity === "string") return identity; + return identity.toHexString?.() ?? identity.toString?.() ?? "identity"; +} + +function getAuthTokenKey(config: Pick) { + return `void-nav:spacetime-token:${config.uri}:${config.database}`; +} + +function readStoredAuthToken(key: string) { + try { + return window.localStorage.getItem(key) ?? undefined; + } catch { + return undefined; + } +} + +function storeAuthToken(key: string, token?: string) { + if (!token) return; + try { + window.localStorage.setItem(key, token); + } catch { + // Losing the auth token only affects reconnect identity continuity. + } +} diff --git a/apps/game/src/spacetime/useGameSession.ts b/apps/game/src/spacetime/useGameSession.ts new file mode 100644 index 0000000..e105e72 --- /dev/null +++ b/apps/game/src/spacetime/useGameSession.ts @@ -0,0 +1,156 @@ +import { useMemo } from "react"; +import type { DbConnection } from "../module_bindings"; +import type { + CargoItem, + Player, + PointOfInterest, + ServerEvent, + Ship, + ShipOperation, + Station, + System, + Wallet, +} from "../module_bindings/types"; +import { formatIdentity, invokeReducer, type IdentityLike } from "./client"; + +type ReducerReporter = (message: string, isError?: boolean) => void; + +export type GameSession = { + player: Player | undefined; + ship: Ship | undefined; + system: System | undefined; + station: Station | undefined; + pois: PointOfInterest[]; + cargo: CargoItem[]; + wallet: Wallet | undefined; + operation: ShipOperation | undefined; + events: ServerEvent[]; + actions: { + renamePlayer(displayName: string): void; + ping(): void; + undock(): void; + selectTarget(poiId: string): void; + startApproach(): void; + completeApproach(): void; + dock(): void; + startMining(): void; + completeMiningCycle(): void; + sellOreToNpcMarket(itemId: string, quantity: bigint): void; + }; +}; + +export function useGameSession( + connection: DbConnection | null, + revision: number, + onReducerStatus: ReducerReporter, + identity?: IdentityLike, +): GameSession { + const rows = useMemo(() => readRows(connection, identity), [connection, revision, identity]); + + return { + ...rows, + actions: { + renamePlayer(displayName: string) { + invokeIfConnected(connection, onReducerStatus, "renamePlayer", () => + connection!.reducers.renamePlayer({ displayName }), + ); + }, + ping() { + invokeIfConnected(connection, onReducerStatus, "ping", () => connection!.reducers.ping({})); + }, + undock() { + invokeIfConnected(connection, onReducerStatus, "undock", () => connection!.reducers.undock({})); + }, + selectTarget(poiId: string) { + invokeIfConnected(connection, onReducerStatus, "selectTarget", () => connection!.reducers.selectTarget({ poiId })); + }, + startApproach() { + invokeIfConnected(connection, onReducerStatus, "startApproach", () => connection!.reducers.startApproach({})); + }, + completeApproach() { + invokeIfConnected(connection, onReducerStatus, "completeApproach", () => connection!.reducers.completeApproach({})); + }, + dock() { + invokeIfConnected(connection, onReducerStatus, "dock", () => connection!.reducers.dock({})); + }, + startMining() { + invokeIfConnected(connection, onReducerStatus, "startMining", () => connection!.reducers.startMining({})); + }, + completeMiningCycle() { + invokeIfConnected(connection, onReducerStatus, "completeMiningCycle", () => + connection!.reducers.completeMiningCycle({}), + ); + }, + sellOreToNpcMarket(itemId: string, quantity: bigint) { + invokeIfConnected(connection, onReducerStatus, "sellOreToNpcMarket", () => + connection!.reducers.sellOreToNpcMarket({ itemId, quantity }), + ); + }, + }, + }; +} + +function invokeIfConnected( + connection: DbConnection | null, + onReducerStatus: ReducerReporter, + name: string, + call: () => Promise, +) { + if (!connection) { + onReducerStatus(`${name} unavailable until connected`, true); + return; + } + + invokeReducer({ onReducerStatus }, name, call); +} + +function readRows(connection: DbConnection | null, identity?: IdentityLike) { + const players = readTable(connection?.db.player); + const ships = readTable(connection?.db.ship); + const systems = readTable(connection?.db.system); + const stations = readTable(connection?.db.station); + const pois = readTable(connection?.db.point_of_interest).sort((a, b) => a.name.localeCompare(b.name)); + const cargoRows = readTable(connection?.db.cargo_item); + const wallets = readTable(connection?.db.wallet); + const operations = readTable(connection?.db.ship_operation); + const events = readTable(connection?.db.server_event).sort((a, b) => + compareBigintsDesc(a.eventId, b.eventId), + ); + + const player = players.find((row) => identitiesEqual(row.identity, identity)) ?? players[0]; + const ship = ships.find((row) => identitiesEqual(row.ownerIdentity, player?.identity ?? identity)) ?? ships[0]; + const cargo = ship ? cargoRows.filter((row) => row.shipId === ship.shipId) : []; + const wallet = wallets.find((row) => identitiesEqual(row.ownerIdentity, player?.identity ?? identity)) ?? wallets[0]; + const operation = ship ? operations.find((row) => row.shipId === ship.shipId) : undefined; + + return { + player, + ship, + system: systems.find((row) => row.systemId === "solace") ?? systems[0], + station: stations.find((row) => row.stationId === "solace-prime") ?? stations[0], + pois, + cargo, + wallet, + operation, + events: events.slice(0, 12), + }; +} + +function readTable(table?: { iter?: () => Iterable }): Row[] { + if (!table?.iter) return []; + try { + return Array.from(table.iter()); + } catch { + return []; + } +} + +function compareBigintsDesc(left: bigint, right: bigint) { + if (left === right) return 0; + return left > right ? -1 : 1; +} + +function identitiesEqual(left?: IdentityLike, right?: IdentityLike) { + if (!left || !right) return false; + return formatIdentity(left) === formatIdentity(right); +} diff --git a/apps/game/src/spacetime/usePlayerSession.ts b/apps/game/src/spacetime/usePlayerSession.ts new file mode 100644 index 0000000..44b2229 --- /dev/null +++ b/apps/game/src/spacetime/usePlayerSession.ts @@ -0,0 +1,58 @@ +import { useMemo } from "react"; +import type { DbConnection } from "../module_bindings"; +import type { Player, ServerEvent, Ship, Station, System } from "../module_bindings/types"; +import { invokeReducer } from "./client"; + +type ReducerReporter = (message: string, isError?: boolean) => void; + +export function usePlayerSession(connection: DbConnection | null, revision: number, onReducerStatus: ReducerReporter) { + const rows = useMemo(() => readRows(connection), [connection, revision]); + + return { + ...rows, + renamePlayer(displayName: string) { + if (!connection) { + onReducerStatus("renamePlayer unavailable until connected", true); + return; + } + invokeReducer({ onReducerStatus }, "renamePlayer", () => connection.reducers.renamePlayer({ displayName })); + }, + ping() { + if (!connection) { + onReducerStatus("ping unavailable until connected", true); + return; + } + invokeReducer({ onReducerStatus }, "ping", () => connection.reducers.ping({})); + }, + }; +} + +function readRows(connection: DbConnection | null) { + const players = readTable(connection?.db.player); + const ships = readTable(connection?.db.ship); + const systems = readTable(connection?.db.system); + const stations = readTable(connection?.db.station); + const events = readTable(connection?.db.server_event).sort((a, b) => compareBigintsDesc(a.eventId, b.eventId)); + + return { + player: players[0], + ship: ships[0], + system: systems.find((row) => row.systemId === "solace") ?? systems[0], + station: stations.find((row) => row.stationId === "solace-prime") ?? stations[0], + events: events.slice(0, 6), + }; +} + +function readTable(table?: { iter?: () => Iterable }): Row[] { + if (!table?.iter) return []; + try { + return Array.from(table.iter()); + } catch { + return []; + } +} + +function compareBigintsDesc(left: bigint, right: bigint) { + if (left === right) return 0; + return left > right ? -1 : 1; +} diff --git a/apps/game/src/spacetime/useSpacetimeConnection.ts b/apps/game/src/spacetime/useSpacetimeConnection.ts new file mode 100644 index 0000000..d3a5be6 --- /dev/null +++ b/apps/game/src/spacetime/useSpacetimeConnection.ts @@ -0,0 +1,60 @@ +import { useEffect, useMemo, useState } from "react"; +import type { DbConnection } from "../module_bindings"; +import { createSpacetimeConnection, type ConnectionStatus, type IdentityLike } from "./client"; + +const defaultUri = import.meta.env.VITE_SPACETIME_URI ?? import.meta.env.VITE_SPACETIMEDB_HOST ?? "http://localhost:3000"; +const defaultDatabase = + import.meta.env.VITE_SPACETIME_DATABASE ?? import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? "void-nav-dev"; + +export function useSpacetimeConnection(displayName: string) { + const [status, setStatus] = useState("idle"); + const [message, setMessage] = useState(); + const [identity, setIdentity] = useState(); + const [connection, setConnection] = useState(null); + const [revision, setRevision] = useState(0); + const [reducerStatus, setReducerStatus] = useState<{ message: string; isError: boolean }>(); + + const config = useMemo( + () => ({ + uri: defaultUri, + database: defaultDatabase, + }), + [], + ); + + useEffect(() => { + const conn = createSpacetimeConnection({ + ...config, + displayName, + onStatus: (nextStatus, nextMessage) => { + setStatus(nextStatus); + setMessage(nextMessage); + }, + onIdentity: setIdentity, + onDataChanged: () => setRevision((value) => value + 1), + onReducerStatus: (nextMessage, isError = false) => setReducerStatus({ message: nextMessage, isError }), + }); + + setConnection(conn); + + const refresh = window.setInterval(() => { + if (conn) setRevision((value) => value + 1); + }, 1500); + + return () => { + window.clearInterval(refresh); + conn?.disconnect?.(); + }; + }, [config, displayName]); + + return { + connection, + status, + message, + identity, + revision, + reducerStatus, + uri: config.uri, + database: config.database, + }; +} diff --git a/apps/game/src/styles/tailwind.css b/apps/game/src/styles/tailwind.css new file mode 100644 index 0000000..150f69b --- /dev/null +++ b/apps/game/src/styles/tailwind.css @@ -0,0 +1,8 @@ +@import "tailwindcss"; +@import "@void-nav/ui/styles.css"; + +@source "../../../packages/ui/src"; + +#root { + min-height: 100vh; +} diff --git a/apps/game/src/ui/CargoPanel.tsx b/apps/game/src/ui/CargoPanel.tsx new file mode 100644 index 0000000..f337e37 --- /dev/null +++ b/apps/game/src/ui/CargoPanel.tsx @@ -0,0 +1,35 @@ +import { Panel } from "@void-nav/ui"; +import type { CargoItem, Ship } from "../module_bindings/types"; + +export function CargoPanel({ cargo, ship }: { cargo: CargoItem[]; ship?: Ship }) { + const used = cargo.reduce((total, item) => total + item.quantity, 0n); + const capacity = ship?.cargoCapacity ?? 0n; + + return ( + +
+

Cargo

+ + {used.toString()} / {capacity.toString()} + +
+
+ {cargo.length > 0 ? ( + cargo.map((item) => ( +
+
+ {item.itemName} + {item.quantity.toString()} +
+
+ {item.category} / {item.unitPrice.toString()} ISK +
+
+ )) + ) : ( +

Cargo hold empty.

+ )} +
+
+ ); +} diff --git a/apps/game/src/ui/CommandRail.tsx b/apps/game/src/ui/CommandRail.tsx new file mode 100644 index 0000000..6396834 --- /dev/null +++ b/apps/game/src/ui/CommandRail.tsx @@ -0,0 +1,77 @@ +import { Button } from "@void-nav/ui"; +import type { CargoItem, PointOfInterest, Ship, ShipOperation } from "../module_bindings/types"; + +export function CommandRail({ + ship, + pois, + cargo, + operation, + onUndock, + onStartApproach, + onDock, + onStartMining, + onSellOre, +}: { + ship?: Ship; + pois: PointOfInterest[]; + cargo: CargoItem[]; + operation?: ShipOperation; + onUndock: () => void; + onStartApproach: () => void; + onDock: () => void; + onStartMining: () => void; + onSellOre: (item: CargoItem) => void; +}) { + const selected = pois.find((poi) => poi.poiId === ship?.selectedPoiId); + const current = pois.find((poi) => poi.poiId === ship?.currentPoiId); + const ore = cargo.find((item) => item.category === "ore" && item.quantity > 0n); + + let content = Connect to SpacetimeDB to initialize the starter ship.; + + if (ship) { + if (operation?.operationType === "approach") { + content = ; + } else if (operation?.operationType === "mining") { + content = ; + } else if (ship.dockedStationId.length > 0) { + content = ( + <> + + {ore ? : null} + + ); + } else if (selected && selected.poiId !== ship.currentPoiId) { + content = ( + + ); + } else if (current?.stationId) { + content = ( + + ); + } else if (current?.poiType === "asteroid_belt") { + content = ( + + ); + } else { + content = Select a station or asteroid belt target.; + } + } + + return ( +
+ {content} +
+ ); +} + +function cargoUsed(cargo: CargoItem[]) { + return cargo.reduce((total, item) => total + item.quantity, 0n); +} diff --git a/apps/game/src/ui/ConnectionPanel.tsx b/apps/game/src/ui/ConnectionPanel.tsx new file mode 100644 index 0000000..e022145 --- /dev/null +++ b/apps/game/src/ui/ConnectionPanel.tsx @@ -0,0 +1,78 @@ +import { Button, Panel } from "@void-nav/ui"; +import { formatIdentity, type ConnectionStatus, type IdentityLike } from "../spacetime/client"; + +export function ConnectionPanel({ + status, + label, + identity, + uri, + database, + message, + reducerMessage, + reducerIsError, + displayName, + onDisplayNameChange, + onRename, + onPing, +}: { + status: ConnectionStatus; + label: string; + identity?: IdentityLike; + uri: string; + database: string; + message?: string; + reducerMessage?: string; + reducerIsError?: boolean; + displayName: string; + onDisplayNameChange: (value: string) => void; + onRename: () => void; + onPing: () => void; +}) { + return ( + +
+

Connection

+ + {label} + +
+
+ + + + +
+ +
+ + +
+
+ ); +} + +function InfoRow({ label, value, error = false }: { label: string; value: string; error?: boolean }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function statusClass(status: ConnectionStatus) { + if (status === "connected") return "border-green text-green"; + if (status === "error") return "border-red text-red"; + return "border-cyan text-cyan"; +} diff --git a/apps/game/src/ui/EventFeed.tsx b/apps/game/src/ui/EventFeed.tsx new file mode 100644 index 0000000..14fc952 --- /dev/null +++ b/apps/game/src/ui/EventFeed.tsx @@ -0,0 +1,37 @@ +import { Panel } from "@void-nav/ui"; +import type { Timestamp } from "spacetimedb"; +import type { ServerEvent } from "../module_bindings/types"; + +export function EventFeed({ events }: { events: ServerEvent[] }) { + return ( + +
+

Server Events

+ {events.length} +
+
+ {events.length > 0 ? ( + events.map((event) => ( +
+
+ {event.eventType} + {formatTime(event.at)} +
+

{event.message}

+
+ )) + ) : ( +

+ No subscribed server events yet. +

+ )} +
+
+ ); +} + +function formatTime(value?: Timestamp | string) { + if (!value) return "Pending"; + if (typeof value === "string") return value; + return value.toDate?.().toLocaleTimeString() ?? value.toString?.() ?? "Timestamp"; +} diff --git a/apps/game/src/ui/TargetPanel.tsx b/apps/game/src/ui/TargetPanel.tsx new file mode 100644 index 0000000..76a9443 --- /dev/null +++ b/apps/game/src/ui/TargetPanel.tsx @@ -0,0 +1,59 @@ +import { Button, Panel } from "@void-nav/ui"; +import type { PointOfInterest, Ship } from "../module_bindings/types"; + +export function TargetPanel({ + ship, + pois, + selected, + onSelect, +}: { + ship?: Ship; + pois: PointOfInterest[]; + selected?: PointOfInterest; + onSelect: (poiId: string) => void; +}) { + const current = pois.find((poi) => poi.poiId === ship?.currentPoiId); + + return ( + +
+
+

Selected Target

+

{selected?.name ?? "No target selected"}

+
+ + {ship?.flightMode ?? "offline"} + +
+ +
+ + +
+ +
+ {pois.map((poi) => ( + + ))} +
+
+ ); +} + +function InfoRow({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/apps/game/src/ui/WalletPanel.tsx b/apps/game/src/ui/WalletPanel.tsx new file mode 100644 index 0000000..489d0cb --- /dev/null +++ b/apps/game/src/ui/WalletPanel.tsx @@ -0,0 +1,15 @@ +import { Panel } from "@void-nav/ui"; +import type { Wallet } from "../module_bindings/types"; + +export function WalletPanel({ wallet }: { wallet?: Wallet }) { + return ( + +

Wallet

+
{formatIsk(wallet?.isk ?? 0n)}
+
+ ); +} + +function formatIsk(value: bigint) { + return `${value.toLocaleString()} ISK`; +} diff --git a/apps/game/tsconfig.json b/apps/game/tsconfig.json new file mode 100644 index 0000000..d8df7cf --- /dev/null +++ b/apps/game/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/apps/game/tsconfig.node.json b/apps/game/tsconfig.node.json new file mode 100644 index 0000000..3adda81 --- /dev/null +++ b/apps/game/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/game/vite.config.ts b/apps/game/vite.config.ts new file mode 100644 index 0000000..b0044df --- /dev/null +++ b/apps/game/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [tailwindcss()], + esbuild: { + jsx: "automatic", + jsxImportSource: "react", + }, +}); diff --git a/apps/site/index.html b/apps/site/index.html new file mode 100644 index 0000000..80ae253 --- /dev/null +++ b/apps/site/index.html @@ -0,0 +1,12 @@ + + + + + + VOID::NAV + + +
+ + + diff --git a/apps/site/package.json b/apps/site/package.json new file mode 100644 index 0000000..2986b5c --- /dev/null +++ b/apps/site/package.json @@ -0,0 +1,26 @@ +{ + "name": "@void-nav/site", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "tsc --noEmit && vite build", + "check": "tsc --noEmit", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@tailwindcss/vite": "^4.3.0", + "@void-nav/ui": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.1", + "tailwindcss": "^4.3.0", + "vite": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "typescript": "^5.8.3" + } +} diff --git a/apps/site/src/main.tsx b/apps/site/src/main.tsx new file mode 100644 index 0000000..9d0af5a --- /dev/null +++ b/apps/site/src/main.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { LandingPage } from "./pages/LandingPage"; +import { NotFoundPage } from "./pages/NotFoundPage"; +import "./styles/tailwind.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + } /> + } /> + + + , +); diff --git a/apps/site/src/pages/LandingPage.tsx b/apps/site/src/pages/LandingPage.tsx new file mode 100644 index 0000000..b259479 --- /dev/null +++ b/apps/site/src/pages/LandingPage.tsx @@ -0,0 +1,47 @@ +import { Button, Panel } from "@void-nav/ui"; + +const docsUrl = import.meta.env.VITE_DOCS_URL ?? "http://localhost:5173/docs"; +const gameUrl = import.meta.env.VITE_GAME_URL ?? "http://localhost:5175"; + +export function LandingPage() { + return ( +
+
+
+

Persistent browser space game

+

VOID::NAV

+

+ A multiplayer industrial space game built around pilot identity, station life, ship fitting, + market pressure, and a persistent galaxy backend. +

+
+ + +
+
+ + +
+

Current build focus

+

From prototype hub to live game shell

+
+
+ + + + +
+
+
+
+ ); +} + +function StatusLine({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/apps/site/src/pages/NotFoundPage.tsx b/apps/site/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..a38b056 --- /dev/null +++ b/apps/site/src/pages/NotFoundPage.tsx @@ -0,0 +1,14 @@ +import { Button, Panel } from "@void-nav/ui"; + +export function NotFoundPage() { + return ( +
+ +

Route unavailable

+

Page not found

+

The public site only serves the landing page right now.

+ +
+
+ ); +} diff --git a/apps/site/src/styles/tailwind.css b/apps/site/src/styles/tailwind.css new file mode 100644 index 0000000..150f69b --- /dev/null +++ b/apps/site/src/styles/tailwind.css @@ -0,0 +1,8 @@ +@import "tailwindcss"; +@import "@void-nav/ui/styles.css"; + +@source "../../../packages/ui/src"; + +#root { + min-height: 100vh; +} diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json new file mode 100644 index 0000000..d8df7cf --- /dev/null +++ b/apps/site/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/apps/site/tsconfig.node.json b/apps/site/tsconfig.node.json new file mode 100644 index 0000000..3adda81 --- /dev/null +++ b/apps/site/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/site/vite.config.ts b/apps/site/vite.config.ts new file mode 100644 index 0000000..b0044df --- /dev/null +++ b/apps/site/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [tailwindcss()], + esbuild: { + jsx: "automatic", + jsxImportSource: "react", + }, +}); diff --git a/css/base.css b/archive/legacy-static/css/base.css similarity index 100% rename from css/base.css rename to archive/legacy-static/css/base.css diff --git a/css/components.css b/archive/legacy-static/css/components.css similarity index 100% rename from css/components.css rename to archive/legacy-static/css/components.css diff --git a/css/layout.css b/archive/legacy-static/css/layout.css similarity index 100% rename from css/layout.css rename to archive/legacy-static/css/layout.css diff --git a/css/tokens.css b/archive/legacy-static/css/tokens.css similarity index 100% rename from css/tokens.css rename to archive/legacy-static/css/tokens.css diff --git a/game-hud.html b/archive/legacy-static/game-hud.html similarity index 100% rename from game-hud.html rename to archive/legacy-static/game-hud.html diff --git a/public/docs/gap-analysis.md b/archive/legacy-static/gap-analysis.md similarity index 100% rename from public/docs/gap-analysis.md rename to archive/legacy-static/gap-analysis.md diff --git a/gdd-docs-hub.html.artifact.json b/archive/legacy-static/gdd-docs-hub.html.artifact.json similarity index 100% rename from gdd-docs-hub.html.artifact.json rename to archive/legacy-static/gdd-docs-hub.html.artifact.json diff --git a/js/app.js b/archive/legacy-static/js/app.js similarity index 100% rename from js/app.js rename to archive/legacy-static/js/app.js diff --git a/js/components/sidebar.js b/archive/legacy-static/js/components/sidebar.js similarity index 100% rename from js/components/sidebar.js rename to archive/legacy-static/js/components/sidebar.js diff --git a/js/components/topbar.js b/archive/legacy-static/js/components/topbar.js similarity index 100% rename from js/components/topbar.js rename to archive/legacy-static/js/components/topbar.js diff --git a/js/demos/bounty.js b/archive/legacy-static/js/demos/bounty.js similarity index 100% rename from js/demos/bounty.js rename to archive/legacy-static/js/demos/bounty.js diff --git a/js/demos/chat.js b/archive/legacy-static/js/demos/chat.js similarity index 100% rename from js/demos/chat.js rename to archive/legacy-static/js/demos/chat.js diff --git a/js/demos/combat.js b/archive/legacy-static/js/demos/combat.js similarity index 100% rename from js/demos/combat.js rename to archive/legacy-static/js/demos/combat.js diff --git a/js/demos/fitting.js b/archive/legacy-static/js/demos/fitting.js similarity index 100% rename from js/demos/fitting.js rename to archive/legacy-static/js/demos/fitting.js diff --git a/js/demos/galaxy.js b/archive/legacy-static/js/demos/galaxy.js similarity index 100% rename from js/demos/galaxy.js rename to archive/legacy-static/js/demos/galaxy.js diff --git a/js/demos/gamehud.js b/archive/legacy-static/js/demos/gamehud.js similarity index 100% rename from js/demos/gamehud.js rename to archive/legacy-static/js/demos/gamehud.js diff --git a/js/demos/market.js b/archive/legacy-static/js/demos/market.js similarity index 100% rename from js/demos/market.js rename to archive/legacy-static/js/demos/market.js diff --git a/js/demos/movement.js b/archive/legacy-static/js/demos/movement.js similarity index 100% rename from js/demos/movement.js rename to archive/legacy-static/js/demos/movement.js diff --git a/js/demos/progression.js b/archive/legacy-static/js/demos/progression.js similarity index 100% rename from js/demos/progression.js rename to archive/legacy-static/js/demos/progression.js diff --git a/js/demos/refining.js b/archive/legacy-static/js/demos/refining.js similarity index 100% rename from js/demos/refining.js rename to archive/legacy-static/js/demos/refining.js diff --git a/js/demos/starmap.js b/archive/legacy-static/js/demos/starmap.js similarity index 100% rename from js/demos/starmap.js rename to archive/legacy-static/js/demos/starmap.js diff --git a/js/demos/zora.js b/archive/legacy-static/js/demos/zora.js similarity index 100% rename from js/demos/zora.js rename to archive/legacy-static/js/demos/zora.js diff --git a/js/fake-backend.js b/archive/legacy-static/js/fake-backend.js similarity index 100% rename from js/fake-backend.js rename to archive/legacy-static/js/fake-backend.js diff --git a/js/lib/three-helpers.js b/archive/legacy-static/js/lib/three-helpers.js similarity index 100% rename from js/lib/three-helpers.js rename to archive/legacy-static/js/lib/three-helpers.js diff --git a/js/loader.js b/archive/legacy-static/js/loader.js similarity index 100% rename from js/loader.js rename to archive/legacy-static/js/loader.js diff --git a/js/pages/agents.js b/archive/legacy-static/js/pages/agents.js similarity index 100% rename from js/pages/agents.js rename to archive/legacy-static/js/pages/agents.js diff --git a/js/pages/architecture.js b/archive/legacy-static/js/pages/architecture.js similarity index 100% rename from js/pages/architecture.js rename to archive/legacy-static/js/pages/architecture.js diff --git a/js/pages/backend.js b/archive/legacy-static/js/pages/backend.js similarity index 100% rename from js/pages/backend.js rename to archive/legacy-static/js/pages/backend.js diff --git a/js/pages/demo-gallery.js b/archive/legacy-static/js/pages/demo-gallery.js similarity index 100% rename from js/pages/demo-gallery.js rename to archive/legacy-static/js/pages/demo-gallery.js diff --git a/js/pages/economy.js b/archive/legacy-static/js/pages/economy.js similarity index 100% rename from js/pages/economy.js rename to archive/legacy-static/js/pages/economy.js diff --git a/js/pages/gameplay.js b/archive/legacy-static/js/pages/gameplay.js similarity index 100% rename from js/pages/gameplay.js rename to archive/legacy-static/js/pages/gameplay.js diff --git a/js/pages/overview.js b/archive/legacy-static/js/pages/overview.js similarity index 100% rename from js/pages/overview.js rename to archive/legacy-static/js/pages/overview.js diff --git a/js/pages/risks.js b/archive/legacy-static/js/pages/risks.js similarity index 100% rename from js/pages/risks.js rename to archive/legacy-static/js/pages/risks.js diff --git a/js/pages/roadmap.js b/archive/legacy-static/js/pages/roadmap.js similarity index 100% rename from js/pages/roadmap.js rename to archive/legacy-static/js/pages/roadmap.js diff --git a/js/pages/ship-ai.js b/archive/legacy-static/js/pages/ship-ai.js similarity index 100% rename from js/pages/ship-ai.js rename to archive/legacy-static/js/pages/ship-ai.js diff --git a/js/pages/ships.js b/archive/legacy-static/js/pages/ships.js similarity index 100% rename from js/pages/ships.js rename to archive/legacy-static/js/pages/ships.js diff --git a/js/pages/social.js b/archive/legacy-static/js/pages/social.js similarity index 100% rename from js/pages/social.js rename to archive/legacy-static/js/pages/social.js diff --git a/js/pages/techstack.js b/archive/legacy-static/js/pages/techstack.js similarity index 100% rename from js/pages/techstack.js rename to archive/legacy-static/js/pages/techstack.js diff --git a/js/router.js b/archive/legacy-static/js/router.js similarity index 100% rename from js/router.js rename to archive/legacy-static/js/router.js diff --git a/js/state.js b/archive/legacy-static/js/state.js similarity index 100% rename from js/state.js rename to archive/legacy-static/js/state.js diff --git a/public/assets/mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx b/archive/legacy-static/mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx similarity index 100% rename from public/assets/mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx rename to archive/legacy-static/mpg7uppn-eve_like_multiplayer_prototype_design_doc.docx diff --git a/public/assets/mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png b/archive/legacy-static/mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png similarity index 100% rename from public/assets/mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png rename to archive/legacy-static/mpgzmjy2-drawing-2026-05-22T14-01-06-091Z.png diff --git a/public/assets/mph38gxn-drawing-2026-05-22T15-42-07-328Z.png b/archive/legacy-static/mph38gxn-drawing-2026-05-22T15-42-07-328Z.png similarity index 100% rename from public/assets/mph38gxn-drawing-2026-05-22T15-42-07-328Z.png rename to archive/legacy-static/mph38gxn-drawing-2026-05-22T15-42-07-328Z.png diff --git a/public/assets/mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png b/archive/legacy-static/mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png similarity index 100% rename from public/assets/mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png rename to archive/legacy-static/mpj6kt4n-drawing-2026-05-24T02-51-14-351Z.png diff --git a/package.json b/package.json index 932ec64..999c3a4 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,16 @@ { - "name": "space-game", + "name": "void-nav", "private": true, "version": "0.1.0", "type": "module", "scripts": { - "dev": "vite --host 0.0.0.0", - "build": "tsc --noEmit && vite build", - "preview": "vite preview --host 0.0.0.0" - }, - "dependencies": { - "@react-three/drei": "^9.122.0", - "@react-three/fiber": "^8.17.10", - "@tailwindcss/vite": "^4.3.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.30.1", - "tailwindcss": "^4.3.0", - "three": "^0.160.0", - "vite": "^7.0.0" - }, - "devDependencies": { - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.7", - "@types/three": "^0.160.0", - "typescript": "^5.8.3" + "dev:docs": "pnpm --filter @void-nav/docs dev --port 5173", + "dev:site": "pnpm --filter @void-nav/site dev --port 5174", + "dev:game": "pnpm --filter @void-nav/game dev --port 5175", + "dev:db": "spacetime dev --server local", + "build": "pnpm -r --if-present build", + "check": "pnpm -r --if-present check", + "generate:bindings": "spacetime generate" }, "packageManager": "pnpm@9.15.0" } diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..9872da6 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,23 @@ +{ + "name": "@void-nav/ui", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./styles.css": "./src/styles.css" + }, + "scripts": { + "check": "tsc --noEmit" + }, + "peerDependencies": { + "react": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.23", + "typescript": "^5.8.3" + } +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 0000000..af6b576 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,4 @@ +export { Button } from "./primitives/Button"; +export type { ButtonProps } from "./primitives/Button"; +export { Panel } from "./primitives/Panel"; +export type { PanelProps } from "./primitives/Panel"; diff --git a/packages/ui/src/primitives/Button.tsx b/packages/ui/src/primitives/Button.tsx new file mode 100644 index 0000000..1a63be3 --- /dev/null +++ b/packages/ui/src/primitives/Button.tsx @@ -0,0 +1,55 @@ +import type { ButtonHTMLAttributes, AnchorHTMLAttributes, ReactNode } from "react"; + +type ButtonTone = "primary" | "secondary" | "ghost"; + +type BaseProps = { + children: ReactNode; + className?: string; + tone?: ButtonTone; +}; + +type NativeButtonProps = BaseProps & + ButtonHTMLAttributes & { + href?: never; + }; + +type AnchorButtonProps = BaseProps & + AnchorHTMLAttributes & { + href: string; + }; + +export type ButtonProps = NativeButtonProps | AnchorButtonProps; + +const tones: Record = { + primary: "border-accent bg-accent text-bg hover:bg-accent-hover", + secondary: "border-border bg-surface-raised text-fg hover:border-border-light hover:bg-surface-hover", + ghost: "border-transparent bg-transparent text-cyan hover:border-border-light hover:bg-surface", +}; + +export function Button({ children, className = "", tone = "secondary", ...props }: ButtonProps) { + const classes = [ + "inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-55", + tones[tone], + className, + ] + .filter(Boolean) + .join(" "); + + if ("href" in props) { + const anchorProps = props as Omit; + + return ( + + {children} + + ); + } + + const buttonProps = props as Omit; + + return ( + + ); +} diff --git a/packages/ui/src/primitives/Panel.tsx b/packages/ui/src/primitives/Panel.tsx new file mode 100644 index 0000000..29d50fb --- /dev/null +++ b/packages/ui/src/primitives/Panel.tsx @@ -0,0 +1,22 @@ +import type { HTMLAttributes, ReactNode } from "react"; + +export type PanelProps = HTMLAttributes & { + as?: "article" | "section" | "div"; + children: ReactNode; +}; + +export function Panel({ as: Tag = "section", children, className = "", ...props }: PanelProps) { + return ( + + {children} + + ); +} diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css new file mode 100644 index 0000000..771e290 --- /dev/null +++ b/packages/ui/src/styles.css @@ -0,0 +1,75 @@ +@theme { + --color-bg: #080c14; + --color-bg-subtle: #0b1120; + --color-surface: #0f1623; + --color-surface-raised: #162032; + --color-surface-hover: #1c2d45; + --color-fg: #d4dce8; + --color-fg-bright: #f1f5f9; + --color-fg-dim: #94a3b8; + --color-muted: #5a6b82; + --color-border: #1c2a3f; + --color-border-light: #253550; + --color-accent: #f0a030; + --color-accent-hover: #fbbf24; + --color-cyan: #22d3ee; + --color-red: #ef4444; + --color-green: #22c55e; + --color-purple: #a78bfa; + + --font-display: "SF Pro Display", -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + --font-body: "SF Pro Text", -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + --font-mono: "JetBrains Mono", "Fira Code", ui-monospace, Menlo, monospace; +} + +@layer base { + :root { + --bg: var(--color-bg); + --bg-subtle: var(--color-bg-subtle); + --surface: var(--color-surface); + --surface-raised: var(--color-surface-raised); + --surface-hover: var(--color-surface-hover); + --fg: var(--color-fg); + --fg-bright: var(--color-fg-bright); + --fg-dim: var(--color-fg-dim); + --muted: var(--color-muted); + --border: var(--color-border); + --border-light: var(--color-border-light); + --accent: var(--color-accent); + --accent-hover: var(--color-accent-hover); + --cyan: var(--color-cyan); + --red: var(--color-red); + --green: var(--color-green); + --purple: var(--color-purple); + } + + *, *::before, *::after { + box-sizing: border-box; + } + + html { + font-size: 14px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + } + + body { + margin: 0; + min-height: 100vh; + background: var(--bg); + color: var(--fg); + font-family: var(--font-body); + line-height: 1.6; + } + + a { + color: inherit; + text-decoration: none; + } + + button, + input { + font: inherit; + } +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..5b0010b --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2020"], + "types": ["react"], + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 428ac81..af824fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,9 @@ settings: importers: - .: + .: {} + + apps/docs: dependencies: '@react-three/drei': specifier: ^9.122.0 @@ -49,6 +51,109 @@ importers: specifier: ^5.8.3 version: 5.9.3 + apps/game: + dependencies: + '@react-three/drei': + specifier: ^9.122.0 + version: 9.122.0(@react-three/fiber@8.18.0(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1))(@types/react@18.3.29)(@types/three@0.160.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1)(use-sync-external-store@1.6.0(react@18.3.1)) + '@react-three/fiber': + specifier: ^8.17.10 + version: 8.18.0(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.160.1) + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)) + '@void-nav/ui': + specifier: workspace:* + version: link:../../packages/ui + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + spacetimedb: + specifier: ^2.3.0 + version: 2.3.0(react@18.3.1) + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 + three: + specifier: ^0.160.0 + version: 0.160.1 + vite: + specifier: ^7.0.0 + version: 7.3.3(jiti@2.7.0)(lightningcss@1.32.0) + devDependencies: + '@types/react': + specifier: ^18.3.23 + version: 18.3.29 + '@types/react-dom': + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.29) + '@types/three': + specifier: ^0.160.0 + version: 0.160.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + apps/site: + dependencies: + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)) + '@void-nav/ui': + specifier: workspace:* + version: link:../../packages/ui + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.30.1 + version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 + vite: + specifier: ^7.0.0 + version: 7.3.3(jiti@2.7.0)(lightningcss@1.32.0) + devDependencies: + '@types/react': + specifier: ^18.3.23 + version: 18.3.29 + '@types/react-dom': + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.29) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + packages/ui: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + devDependencies: + '@types/react': + specifier: ^18.3.23 + version: 18.3.29 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + services/spacetimedb: + dependencies: + spacetimedb: + specifier: ^2.3.0 + version: 2.3.0(react@18.3.1) + devDependencies: + typescript: + specifier: ^5.8.3 + version: 5.9.3 + packages: '@babel/runtime@7.29.7': @@ -631,6 +736,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hls.js@1.6.16: resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} @@ -761,6 +869,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -779,12 +891,20 @@ packages: potpack@1.0.2: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + promise-worker-transferable@1.0.4: resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + react-composer@5.0.3: resolution: {integrity: sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==} peerDependencies: @@ -839,6 +959,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + scheduler@0.21.0: resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} @@ -857,6 +981,29 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + spacetimedb@2.3.0: + resolution: {integrity: sha512-AleiQ+UkcgC9aRe0ArRRh77c8DI6lM2lBBw1wlVzlYpev6Aiem5aLiduPbACxyECrdwngfurDR211ZTmbh+etw==} + peerDependencies: + '@angular/core': '>=17.0.0' + '@tanstack/react-query': ^5.0.0 + react: ^18.0.0 || ^19.0.0-0 || ^19.0.0 + svelte: ^4.0.0 || ^5.0.0 + undici: ^6.19.2 + vue: ^3.3.0 + peerDependenciesMeta: + '@angular/core': + optional: true + '@tanstack/react-query': + optional: true + react: + optional: true + svelte: + optional: true + undici: + optional: true + vue: + optional: true + stats-gl@2.4.2: resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} peerDependencies: @@ -866,6 +1013,10 @@ packages: stats.js@0.17.0: resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + suspend-react@0.1.3: resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} peerDependencies: @@ -917,6 +1068,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + url-polyfill@1.1.14: + resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -1485,6 +1639,8 @@ snapshots: graceful-fs@4.2.11: {} + headers-polyfill@4.0.3: {} + hls.js@1.6.16: {} ieee754@1.2.1: {} @@ -1582,6 +1738,8 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + path-key@3.1.1: {} picocolors@1.1.1: {} @@ -1596,6 +1754,8 @@ snapshots: potpack@1.0.2: {} + prettier@3.8.3: {} + promise-worker-transferable@1.0.4: dependencies: is-promise: 2.2.2 @@ -1607,6 +1767,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + pure-rand@7.0.1: {} + react-composer@5.0.3(react@18.3.1): dependencies: prop-types: 15.8.1 @@ -1681,6 +1843,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.4 fsevents: 2.3.3 + safe-stable-stringify@2.5.0: {} + scheduler@0.21.0: dependencies: loose-envify: 1.4.0 @@ -1697,6 +1861,19 @@ snapshots: source-map-js@1.2.1: {} + spacetimedb@2.3.0(react@18.3.1): + dependencies: + base64-js: 1.5.1 + headers-polyfill: 4.0.3 + object-inspect: 1.13.4 + prettier: 3.8.3 + pure-rand: 7.0.1 + safe-stable-stringify: 2.5.0 + statuses: 2.0.2 + url-polyfill: 1.1.14 + optionalDependencies: + react: 18.3.1 + stats-gl@2.4.2(@types/three@0.160.0)(three@0.160.1): dependencies: '@types/three': 0.160.0 @@ -1704,6 +1881,8 @@ snapshots: stats.js@0.17.0: {} + statuses@2.0.2: {} + suspend-react@0.1.3(react@18.3.1): dependencies: react: 18.3.1 @@ -1757,6 +1936,8 @@ snapshots: typescript@5.9.3: {} + url-polyfill@1.1.14: {} + use-sync-external-store@1.6.0(react@18.3.1): dependencies: react: 18.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..1d8028e --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - "apps/*" + - "packages/*" + - "services/*" diff --git a/services/spacetimedb/package.json b/services/spacetimedb/package.json new file mode 100644 index 0000000..d05f892 --- /dev/null +++ b/services/spacetimedb/package.json @@ -0,0 +1,12 @@ +{ + "name": "@void-nav/spacetimedb", + "private": true, + "version": "0.1.0", + "type": "module", + "dependencies": { + "spacetimedb": "^2.3.0" + }, + "devDependencies": { + "typescript": "^5.8.3" + } +} diff --git a/services/spacetimedb/src/index.ts b/services/spacetimedb/src/index.ts new file mode 100644 index 0000000..fc665e3 --- /dev/null +++ b/services/spacetimedb/src/index.ts @@ -0,0 +1,595 @@ +import { schema, table, t } from "spacetimedb/server"; + +const STARTER_SYSTEM_ID = "solace"; +const STARTER_STATION_ID = "solace-prime"; +const STARTER_STATION_POI_ID = "poi-solace-prime"; +const STARTER_BELT_POI_ID = "poi-solace-belt-alpha"; +const APPROACH_DURATION_MS = 5000n; +const MINING_DURATION_MS = 6000n; +const STARTER_WALLET_ISK = 25000n; +const STARTER_CARGO_CAPACITY = 2500n; +const MINING_YIELD_QUANTITY = 1000n; +const VELDSPAR_ITEM_ID = "ore-veldspar"; +const VELDSPAR_ITEM_NAME = "Veldspar"; +const VELDSPAR_UNIT_PRICE = 12n; + +const player = table( + { name: "player", public: true }, + { + identity: t.identity().primaryKey(), + display_name: t.string(), + created_at: t.timestamp(), + updated_at: t.timestamp(), + last_connected_at: t.timestamp(), + is_connected: t.bool(), + }, +); + +const ship = table( + { name: "ship", public: true }, + { + ship_id: t.u64().primaryKey().autoInc(), + owner_identity: t.identity().index("btree"), + ship_name: t.string(), + hull_type: t.string(), + current_system_id: t.string(), + docked_station_id: t.string(), + current_poi_id: t.string(), + selected_poi_id: t.string(), + flight_mode: t.string(), + x: t.f32(), + y: t.f32(), + z: t.f32(), + cargo_capacity: t.u64(), + }, +); + +const system = table( + { name: "system", public: true }, + { + system_id: t.string().primaryKey(), + name: t.string(), + security_level: t.f32(), + }, +); + +const station = table( + { name: "station", public: true }, + { + station_id: t.string().primaryKey(), + system_id: t.string().index("btree"), + name: t.string(), + }, +); + +const point_of_interest = table( + { name: "point_of_interest", public: true }, + { + poi_id: t.string().primaryKey(), + system_id: t.string().index("btree"), + name: t.string(), + poi_type: t.string(), + x: t.f32(), + y: t.f32(), + z: t.f32(), + station_id: t.string(), + }, +); + +const cargo_item = table( + { name: "cargo_item", public: true }, + { + cargo_item_id: t.u64().primaryKey().autoInc(), + owner_identity: t.identity().index("btree"), + ship_id: t.u64().index("btree"), + item_id: t.string(), + item_name: t.string(), + category: t.string(), + quantity: t.u64(), + unit_price: t.u64(), + }, +); + +const wallet = table( + { name: "wallet", public: true }, + { + owner_identity: t.identity().primaryKey(), + isk: t.u64(), + updated_at: t.timestamp(), + }, +); + +const ship_operation = table( + { name: "ship_operation", public: true }, + { + ship_id: t.u64().primaryKey(), + operation_type: t.string(), + target_poi_id: t.string(), + started_at: t.timestamp(), + duration_ms: t.u64(), + completes_at_ms: t.u64(), + }, +); + +const server_event = table( + { name: "server_event", public: true }, + { + event_id: t.u64().primaryKey().autoInc(), + at: t.timestamp(), + actor_identity: t.identity().index("btree"), + event_type: t.string(), + message: t.string(), + }, +); + +const spacetimedb = schema({ + player, + ship, + system, + station, + point_of_interest, + cargo_item, + wallet, + ship_operation, + server_event, +}); +export default spacetimedb; + +export const seedWorld = spacetimedb.reducer({}, (ctx) => { + seedStarterWorld(ctx); + writeEvent(ctx, "world_seeded", "Starter system and station are available."); +}); + +export const connectPlayer = spacetimedb.reducer({ displayName: t.string() }, (ctx, { displayName }) => { + seedStarterWorld(ctx); + + const trimmedName = normalizeDisplayName(displayName); + const existingPlayer = ctx.db.player.identity.find(ctx.sender); + const timestamp = ctx.timestamp; + + if (existingPlayer) { + ctx.db.player.identity.update({ + ...existingPlayer, + display_name: trimmedName, + updated_at: timestamp, + last_connected_at: timestamp, + is_connected: true, + }); + } else { + ctx.db.player.insert({ + identity: ctx.sender, + display_name: trimmedName, + created_at: timestamp, + updated_at: timestamp, + last_connected_at: timestamp, + is_connected: true, + }); + } + + const existingShip = findStarterShip(ctx, ctx.sender); + if (!existingShip) { + ctx.db.ship.insert({ + ship_id: 0n, + owner_identity: ctx.sender, + ship_name: `${trimmedName}'s Ibis`, + hull_type: "Starter Frigate", + current_system_id: STARTER_SYSTEM_ID, + docked_station_id: STARTER_STATION_ID, + current_poi_id: STARTER_STATION_POI_ID, + selected_poi_id: "", + flight_mode: "docked", + x: 0, + y: 0, + z: 0, + cargo_capacity: STARTER_CARGO_CAPACITY, + }); + } + + if (!ctx.db.wallet.owner_identity.find(ctx.sender)) { + ctx.db.wallet.insert({ + owner_identity: ctx.sender, + isk: STARTER_WALLET_ISK, + updated_at: timestamp, + }); + } + + writeEvent(ctx, "player_connected", `${trimmedName} connected to VOID::NAV.`); +}); + +export const renamePlayer = spacetimedb.reducer({ displayName: t.string() }, (ctx, { displayName }) => { + const existingPlayer = ctx.db.player.identity.find(ctx.sender); + const trimmedName = normalizeDisplayName(displayName); + + if (!existingPlayer) { + throw new Error("Player must connect before renaming."); + } + + ctx.db.player.identity.update({ + ...existingPlayer, + display_name: trimmedName, + updated_at: ctx.timestamp, + }); + + writeEvent(ctx, "player_renamed", `Pilot renamed to ${trimmedName}.`); +}); + +export const ping = spacetimedb.reducer({}, (ctx) => { + writeEvent(ctx, "ping", "Client ping reached the SpacetimeDB module."); +}); + +export const undock = spacetimedb.reducer({}, (ctx) => { + const ship = requirePlayerShip(ctx); + + if (ship.docked_station_id.length === 0) { + throw new Error("Ship is already undocked."); + } + + ctx.db.ship.ship_id.update({ + ...ship, + docked_station_id: "", + flight_mode: "flight", + }); + + writeEvent(ctx, "ship_undocked", `${ship.ship_name} undocked from ${ship.docked_station_id}.`); +}); + +export const selectTarget = spacetimedb.reducer({ poiId: t.string() }, (ctx, { poiId }) => { + const ship = requirePlayerShip(ctx); + const poi = requirePoiInCurrentSystem(ctx, ship, poiId); + + ctx.db.ship.ship_id.update({ + ...ship, + selected_poi_id: poi.poi_id, + }); + + writeEvent(ctx, "target_selected", `${ship.ship_name} selected ${poi.name}.`); +}); + +export const startApproach = spacetimedb.reducer({}, (ctx) => { + const ship = requirePlayerShip(ctx); + + if (ship.docked_station_id.length > 0) { + throw new Error("Ship must be undocked before approaching a target."); + } + + if (ship.selected_poi_id.length === 0) { + throw new Error("Select a target before starting approach."); + } + + const target = requirePoiInCurrentSystem(ctx, ship, ship.selected_poi_id); + if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) { + throw new Error("Ship already has an active operation."); + } + + const nowMs = ctx.timestamp.toMillis(); + ctx.db.ship_operation.insert({ + ship_id: ship.ship_id, + operation_type: "approach", + target_poi_id: target.poi_id, + started_at: ctx.timestamp, + duration_ms: APPROACH_DURATION_MS, + completes_at_ms: nowMs + APPROACH_DURATION_MS, + }); + + ctx.db.ship.ship_id.update({ + ...ship, + flight_mode: "approaching", + }); + + writeEvent(ctx, "approach_started", `${ship.ship_name} started approach to ${target.name}.`); +}); + +export const completeApproach = spacetimedb.reducer({}, (ctx) => { + const ship = requirePlayerShip(ctx); + const operation = requireOperation(ctx, ship.ship_id, "approach"); + requireElapsed(ctx, operation.completes_at_ms, "Approach is still in progress."); + const target = requirePoiInCurrentSystem(ctx, ship, operation.target_poi_id); + + ctx.db.ship_operation.ship_id.delete(ship.ship_id); + ctx.db.ship.ship_id.update({ + ...ship, + current_poi_id: target.poi_id, + flight_mode: "flight", + x: target.x, + y: target.y, + z: target.z, + }); + + writeEvent(ctx, "approach_completed", `${ship.ship_name} arrived at ${target.name}.`); +}); + +export const dock = spacetimedb.reducer({}, (ctx) => { + const ship = requirePlayerShip(ctx); + + if (ship.docked_station_id.length > 0) { + throw new Error("Ship is already docked."); + } + + if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) { + throw new Error("Cannot dock while an operation is active."); + } + + const currentPoi = requireCurrentPoi(ctx, ship); + if (currentPoi.station_id.length === 0) { + throw new Error("Current point of interest is not dockable."); + } + + ctx.db.ship.ship_id.update({ + ...ship, + docked_station_id: currentPoi.station_id, + flight_mode: "docked", + }); + + writeEvent(ctx, "ship_docked", `${ship.ship_name} docked at ${currentPoi.name}.`); +}); + +export const startMining = spacetimedb.reducer({}, (ctx) => { + const ship = requirePlayerShip(ctx); + + if (ship.docked_station_id.length > 0) { + throw new Error("Ship must be undocked before mining."); + } + + if (ctx.db.ship_operation.ship_id.find(ship.ship_id)) { + throw new Error("Ship already has an active operation."); + } + + const currentPoi = requireCurrentPoi(ctx, ship); + if (currentPoi.poi_type !== "asteroid_belt") { + throw new Error("Mining requires being at an asteroid belt."); + } + + const freeCapacity = getFreeCargoCapacity(ctx, ship); + if (freeCapacity <= 0n) { + throw new Error("Cargo hold is full."); + } + + const nowMs = ctx.timestamp.toMillis(); + ctx.db.ship_operation.insert({ + ship_id: ship.ship_id, + operation_type: "mining", + target_poi_id: currentPoi.poi_id, + started_at: ctx.timestamp, + duration_ms: MINING_DURATION_MS, + completes_at_ms: nowMs + MINING_DURATION_MS, + }); + + ctx.db.ship.ship_id.update({ + ...ship, + flight_mode: "mining", + }); + + writeEvent(ctx, "mining_started", `${ship.ship_name} started mining ${currentPoi.name}.`); +}); + +export const completeMiningCycle = spacetimedb.reducer({}, (ctx) => { + const ship = requirePlayerShip(ctx); + const operation = requireOperation(ctx, ship.ship_id, "mining"); + requireElapsed(ctx, operation.completes_at_ms, "Mining cycle is still in progress."); + + const currentPoi = requireCurrentPoi(ctx, ship); + if (currentPoi.poi_type !== "asteroid_belt") { + throw new Error("Mining can only complete at an asteroid belt."); + } + + const freeCapacity = getFreeCargoCapacity(ctx, ship); + if (freeCapacity <= 0n) { + throw new Error("Cargo hold is full."); + } + + const quantity = minBigint(MINING_YIELD_QUANTITY, freeCapacity); + const existingCargo = findCargoItem(ctx, ship.ship_id, VELDSPAR_ITEM_ID); + + if (existingCargo) { + ctx.db.cargo_item.cargo_item_id.update({ + ...existingCargo, + quantity: existingCargo.quantity + quantity, + unit_price: VELDSPAR_UNIT_PRICE, + }); + } else { + ctx.db.cargo_item.insert({ + cargo_item_id: 0n, + owner_identity: ctx.sender, + ship_id: ship.ship_id, + item_id: VELDSPAR_ITEM_ID, + item_name: VELDSPAR_ITEM_NAME, + category: "ore", + quantity, + unit_price: VELDSPAR_UNIT_PRICE, + }); + } + + ctx.db.ship_operation.ship_id.delete(ship.ship_id); + ctx.db.ship.ship_id.update({ + ...ship, + flight_mode: "flight", + }); + + writeEvent(ctx, "mining_completed", `${ship.ship_name} mined ${quantity.toString()} ${VELDSPAR_ITEM_NAME}.`); +}); + +export const sellOreToNpcMarket = spacetimedb.reducer( + { itemId: t.string(), quantity: t.u64() }, + (ctx, { itemId, quantity }) => { + const ship = requirePlayerShip(ctx); + + if (ship.docked_station_id.length === 0) { + throw new Error("Ship must be docked before selling ore."); + } + + if (quantity <= 0n) { + throw new Error("Sell quantity must be greater than zero."); + } + + const cargo = findCargoItem(ctx, ship.ship_id, itemId); + if (!cargo || cargo.category !== "ore") { + throw new Error("Ore cargo item not found."); + } + + if (cargo.quantity < quantity) { + throw new Error("Cannot sell more ore than the ship is carrying."); + } + + const playerWallet = ctx.db.wallet.owner_identity.find(ctx.sender); + if (!playerWallet) { + throw new Error("Player wallet was not found."); + } + + if (cargo.quantity === quantity) { + ctx.db.cargo_item.cargo_item_id.delete(cargo.cargo_item_id); + } else { + ctx.db.cargo_item.cargo_item_id.update({ + ...cargo, + quantity: cargo.quantity - quantity, + }); + } + + const payout = quantity * cargo.unit_price; + ctx.db.wallet.owner_identity.update({ + ...playerWallet, + isk: playerWallet.isk + payout, + updated_at: ctx.timestamp, + }); + + writeEvent(ctx, "ore_sold", `Sold ${quantity.toString()} ${cargo.item_name} for ${payout.toString()} ISK.`); + }, +); + +function seedStarterWorld(ctx: ReducerContextLike) { + if (!ctx.db.system.system_id.find(STARTER_SYSTEM_ID)) { + ctx.db.system.insert({ + system_id: STARTER_SYSTEM_ID, + name: "Solace", + security_level: 0.9, + }); + } + + if (!ctx.db.station.station_id.find(STARTER_STATION_ID)) { + ctx.db.station.insert({ + station_id: STARTER_STATION_ID, + system_id: STARTER_SYSTEM_ID, + name: "Solace Prime Orbital", + }); + } + + if (!ctx.db.point_of_interest.poi_id.find(STARTER_STATION_POI_ID)) { + ctx.db.point_of_interest.insert({ + poi_id: STARTER_STATION_POI_ID, + system_id: STARTER_SYSTEM_ID, + name: "Solace Prime Orbital", + poi_type: "station", + x: 0, + y: 0, + z: 0, + station_id: STARTER_STATION_ID, + }); + } + + if (!ctx.db.point_of_interest.poi_id.find(STARTER_BELT_POI_ID)) { + ctx.db.point_of_interest.insert({ + poi_id: STARTER_BELT_POI_ID, + system_id: STARTER_SYSTEM_ID, + name: "Solace Belt Alpha", + poi_type: "asteroid_belt", + x: 34, + y: -2, + z: -18, + station_id: "", + }); + } +} + +function findStarterShip(ctx: ReducerContextLike, ownerIdentity: unknown) { + return Array.from(ctx.db.ship.iter()).find((row) => identitiesEqual(row.owner_identity, ownerIdentity)); +} + +function requirePlayerShip(ctx: ReducerContextLike) { + const playerShip = findStarterShip(ctx, ctx.sender); + if (!playerShip) { + throw new Error("Player must connect before controlling a ship."); + } + return playerShip; +} + +function requirePoiInCurrentSystem(ctx: ReducerContextLike, ship: ReturnType, poiId: string) { + const poi = ctx.db.point_of_interest.poi_id.find(poiId); + if (!poi) { + throw new Error(`Point of interest not found: ${poiId}.`); + } + + if (poi.system_id !== ship.current_system_id) { + throw new Error("Selected point of interest is not in the ship's current system."); + } + + return poi; +} + +function requireCurrentPoi(ctx: ReducerContextLike, ship: ReturnType) { + if (ship.current_poi_id.length === 0) { + throw new Error("Ship is not at a point of interest."); + } + + return requirePoiInCurrentSystem(ctx, ship, ship.current_poi_id); +} + +function requireOperation(ctx: ReducerContextLike, shipId: bigint, operationType: string) { + const operation = ctx.db.ship_operation.ship_id.find(shipId); + if (!operation) { + throw new Error(`No active ${operationType} operation found.`); + } + + if (operation.operation_type !== operationType) { + throw new Error(`Active operation is ${operation.operation_type}, not ${operationType}.`); + } + + return operation; +} + +function requireElapsed(ctx: ReducerContextLike, completesAtMs: bigint, message: string) { + if (ctx.timestamp.toMillis() < completesAtMs) { + throw new Error(message); + } +} + +function findCargoItem(ctx: ReducerContextLike, shipId: bigint, itemId: string) { + return Array.from(ctx.db.cargo_item.iter()).find((row) => row.ship_id === shipId && row.item_id === itemId); +} + +function getShipCargoQuantity(ctx: ReducerContextLike, shipId: bigint) { + return Array.from(ctx.db.cargo_item.iter()) + .filter((row) => row.ship_id === shipId) + .reduce((total, row) => total + row.quantity, 0n); +} + +function getFreeCargoCapacity(ctx: ReducerContextLike, ship: ReturnType) { + const usedCapacity = getShipCargoQuantity(ctx, ship.ship_id); + return ship.cargo_capacity > usedCapacity ? ship.cargo_capacity - usedCapacity : 0n; +} + +function minBigint(left: bigint, right: bigint) { + return left < right ? left : right; +} + +function identitiesEqual(left: unknown, right: unknown) { + if (left && right && typeof left === "object" && "isEqual" in left && typeof left.isEqual === "function") { + return left.isEqual(right); + } + return left === right; +} + +function writeEvent(ctx: ReducerContextLike, eventType: string, message: string) { + ctx.db.server_event.insert({ + event_id: 0n, + at: ctx.timestamp, + actor_identity: ctx.sender, + event_type: eventType, + message, + }); +} + +function normalizeDisplayName(displayName: string) { + const trimmedName = displayName.trim(); + return trimmedName.length > 0 ? trimmedName.slice(0, 32) : "New Pilot"; +} + +type ReducerContextLike = Parameters[1]>[0]; diff --git a/services/spacetimedb/tsconfig.json b/services/spacetimedb/tsconfig.json new file mode 100644 index 0000000..b596636 --- /dev/null +++ b/services/spacetimedb/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2020"], + "jsx": "preserve" + }, + "include": ["src"] +} diff --git a/spacetime.json b/spacetime.json new file mode 100644 index 0000000..83fc78b --- /dev/null +++ b/spacetime.json @@ -0,0 +1,13 @@ +{ + "database": "void-nav-dev", + "module-path": "./services/spacetimedb", + "generate": [ + { + "language": "typescript", + "out-dir": "./apps/game/src/module_bindings" + } + ], + "dev": { + "run": "pnpm --filter @void-nav/game dev --port 5175" + } +} diff --git a/src/layouts/SiteLayout.tsx b/src/layouts/SiteLayout.tsx deleted file mode 100644 index d07038b..0000000 --- a/src/layouts/SiteLayout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Outlet } from "react-router-dom"; - -export function SiteLayout() { - return ; -} diff --git a/src/pages/ApplicationPage.tsx b/src/pages/ApplicationPage.tsx deleted file mode 100644 index b357ead..0000000 --- a/src/pages/ApplicationPage.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Link } from "react-router-dom"; - -export function ApplicationPage() { - return ( -
-
-
Future game shell
-

Application

-

Game implementation pending

-

- The playable game will live here once development begins. Existing design - documentation, demos, and the HUD style reference are available under the documentation area. -

- Open documentation -
-
- ); -} diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx deleted file mode 100644 index 23fda35..0000000 --- a/src/pages/LandingPage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Link } from "react-router-dom"; - -export function LandingPage() { - return ( -
-
-
EVE-inspired multiplayer prototype
-

VOID::NAV

-

- A browser-based space game design hub for the persistent galaxy, economy, - HUD, backend, and prototype systems that will become the playable game. -

-
- Read Documentation - Open Application -
-
- Demo gallery - Roadmap - Gap analysis -
-
-
- ); -} diff --git a/tsconfig.json b/tsconfig.base.json similarity index 91% rename from tsconfig.json rename to tsconfig.base.json index 5ec64a5..442bbd8 100644 --- a/tsconfig.json +++ b/tsconfig.base.json @@ -15,6 +15,5 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" - }, - "include": ["src", "vite.config.ts"] + } }