From a7796a1394d29ced4d3b3aa5d012499f02031980 Mon Sep 17 00:00:00 2001 From: francy51 Date: Thu, 4 Jun 2026 10:00:43 -0400 Subject: [PATCH] Split Rust game src into modules; update AGENTS.md for dual toolchain - Refactor apps/game/src/main.rs into modules: state.rs, camera.rs, ui/, gameplay/ - Add gameplay/galaxy_creation.rs scaffold (stubs only, no new logic) - Update root AGENTS.md: separate TS workspace vs Rust game commands - Update apps/AGENTS.md: docs/game/site use two toolchains, not one - Add apps/game/AGENTS.md: build commands, module layout, naming conventions, Plugin pattern --- AGENTS.md | 51 ++++-- apps/AGENTS.md | 29 ++-- apps/game/AGENTS.md | 78 ++++++++++ apps/game/src/camera.rs | 5 + apps/game/src/gameplay/galaxy_creation.rs | 18 +++ apps/game/src/gameplay/mod.rs | 1 + apps/game/src/main.rs | 182 ++-------------------- apps/game/src/state.rs | 11 ++ apps/game/src/ui/main_menu.rs | 156 +++++++++++++++++++ apps/game/src/ui/mod.rs | 1 + 10 files changed, 337 insertions(+), 195 deletions(-) create mode 100644 apps/game/AGENTS.md create mode 100644 apps/game/src/camera.rs create mode 100644 apps/game/src/gameplay/galaxy_creation.rs create mode 100644 apps/game/src/gameplay/mod.rs create mode 100644 apps/game/src/state.rs create mode 100644 apps/game/src/ui/main_menu.rs create mode 100644 apps/game/src/ui/mod.rs diff --git a/AGENTS.md b/AGENTS.md index 7082189..e2a4ed8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,37 +6,54 @@ Single-player open-world space exploration RPG. Windward Horizon in space with F | Path | Purpose | |------|---------| -| `apps/docs` | Living design docs, interactive demos, and vertical-slice prototype | -| `apps/game` | Playable game shell connected to SpacetimeDB | -| `apps/site` | Public landing page | -| `packages/ui` | Shared UI primitives and design tokens | -| `services/spacetimedb` | SpacetimeDB TypeScript backend module | +| `apps/docs` | Living design docs, interactive demos, and vertical-slice prototype (Vite + React) | +| `apps/game` | Playable game client — **Rust + Bevy** (independent of the JS/TS workspace) | +| `apps/site` | Public landing page (Vite + React) | +| `packages/ui` | Shared UI primitives and design tokens for the TS apps | +| `services/spacetimedb` | SpacetimeDB TypeScript backend module (authoritative game state) | | `scripts/` | Dev tooling (dev.sh startup script) | | `archive/` | Legacy pre-monorepo files kept for reference | -| `src/module_bindings/` | Empty; generated bindings go to `apps/game/src/module_bindings` | ## Key Files - `package.json` — pnpm workspace scripts (`dev`, `build`, `check`, `generate:bindings`) -- `pnpm-workspace.yaml` — workspace glob patterns +- `pnpm-workspace.yaml` — workspace glob patterns (covers TS apps only) - `spacetime.json` — SpacetimeDB module path and binding generation config -- `tsconfig.base.json` — Shared TypeScript config (ES2020, React JSX) +- `tsconfig.base.json` — Shared TypeScript config (ES2020, React JSX) — does not apply to `apps/game` +- `apps/game/Cargo.toml` — Rust manifest for the game binary ## Commands +### JS/TS workspace (docs, site, packages/ui, services) + ```bash -pnpm dev # Full stack: SpacetimeDB + game (port 5175) +pnpm dev # Full stack: SpacetimeDB + docs/site pnpm dev:docs # Docs only (port 5173) pnpm dev:site # Site only (port 5174) -pnpm dev:game # Game frontend only (requires running SpacetimeDB) -pnpm build # Build all apps -pnpm check # Typecheck all packages +pnpm build # Build all TS packages +pnpm check # Typecheck all TS packages pnpm generate:bindings # Regenerate SpacetimeDB TypeScript bindings ``` -## Conventions +### Rust game (`apps/game`) -- SpacetimeDB is the persistence layer from day 1 — no localStorage for game state -- All UI uses Tailwind CSS v4 with custom design tokens from `packages/ui` -- 3D rendering uses React Three Fiber + Drei + Three.js -- Module bindings are auto-generated — never edit `module_bindings/` by hand +```bash +cd apps/game +cargo run # Build + run the game binary +cargo check # Fast typecheck +cargo build # Build without running +cargo clippy # Lint +``` + +## Architecture Notes + +- **Two independent toolchains coexist**: the pnpm/TS workspace (docs, site, UI lib, SpacetimeDB backend) and the Cargo/Rust game client. They share no code. +- **SpacetimeDB** remains the authoritative persistence layer. The game client will connect to it via the SpacetimeDB Rust SDK when wired up (TS bindings generated by `pnpm generate:bindings` are consumed by the docs prototype, not the game). +- **Naming follows RFC 344**: see `apps/game/AGENTS.md` for Rust/Bevy conventions. + +## Conventions (TS apps) + +- SpacetimeDB is the persistence layer from day 1 — no localStorage for game state in the docs prototype +- TS apps use Tailwind CSS v4 with custom design tokens from `packages/ui` +- 3D rendering in `apps/docs` uses React Three Fiber + Drei + Three.js +- Module bindings are auto-generated — never edit generated bindings by hand diff --git a/apps/AGENTS.md b/apps/AGENTS.md index e3c8364..ca09df2 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -1,16 +1,23 @@ # apps/ — Application Packages -Three Vite + React apps, each independently buildable and dev-serveable. +Three independent applications. **Note**: they do not share a toolchain. -| App | Port | Package | Description | -|-----|------|---------|-------------| -| `docs` | 5173 | `@void-nav/docs` | Living design docs, 16 doc pages, 14 interactive demos, game-slice prototype | -| `game` | 5175 | `@void-nav/game` | Playable single-player game client connected to SpacetimeDB backend | -| `site` | 5174 | `@void-nav/site` | Public landing page / marketing site | +| App | Toolchain | Port / Binary | Description | +|-----|-----------|---------------|-------------| +| `docs` | Vite + React + TS | 5173 | Living design docs, 16 doc pages, 14 interactive demos, game-slice prototype | +| `game` | Rust + Bevy | `cargo run` | Single-player game client. Standalone — does not depend on the pnpm workspace | +| `site` | Vite + React + TS | 5174 | Public landing page / marketing site | -## Shared +## TS apps (`docs`, `site`) -- All use Tailwind CSS v4 and `@void-nav/ui` for shared primitives -- All use Vite 7 as build tool -- All use React 18 with TypeScript 5.8 -- `docs` and `game` also use Three.js + React Three Fiber for 3D scenes +- Vite 7, React 18, TypeScript 5.8 +- Tailwind CSS v4 with `@void-nav/ui` shared primitives +- `docs` also uses Three.js + React Three Fiber for 3D scenes +- Managed via pnpm workspace scripts (`pnpm dev:docs`, `pnpm dev:site`, etc.) + +## Rust app (`game`) + +- Bevy 0.16, Rust edition 2021 +- Built and run via `cargo` (no Vite, no pnpm scripts) +- See [`apps/game/AGENTS.md`](./game/AGENTS.md) for module layout, naming conventions, and build commands +- Will connect to SpacetimeDB via the Rust SDK when persistence is wired up diff --git a/apps/game/AGENTS.md b/apps/game/AGENTS.md new file mode 100644 index 0000000..a3846dd --- /dev/null +++ b/apps/game/AGENTS.md @@ -0,0 +1,78 @@ +# apps/game — Bevy/Rust Game Client + +Single-player space exploration RPG built with [Bevy 0.16](https://bevyengine.org). +Crate name: `void-nav`. + +This app is **independent of the pnpm/TS workspace** — no Vite, no React, no Tailwind. It builds with plain `cargo` from this directory. + +## Commands + +```bash +cargo run # Build + launch the game +cargo check # Fast typecheck (no codegen) +cargo build # Build without running +cargo clippy # Lint +cargo test # Run unit tests +``` + +## Module Layout + +``` +src/ +├── main.rs # App builder: plugins, state init, system registration +├── state.rs # AppState enum (MainMenu, GalaxyCreation, CharacterCreation, InGame, Options) +├── camera.rs # Camera spawn +├── ui/ # UI screens (menus, HUD, etc.) +│ ├── mod.rs +│ └── main_menu.rs +└── gameplay/ # Non-UI gameplay systems + ├── mod.rs + └── galaxy_creation.rs +``` + +### When to add a file vs a folder + +- **Default**: one file per feature (e.g. `main_menu.rs`, `galaxy_creation.rs`). +- **Promote to a folder** when the file exceeds ~300 lines, mixes UI + logic + data, or needs shared private helpers. +- A promoted folder should expose its public API via `mod.rs` and ideally bundle its systems into a Bevy `Plugin` (see pattern below). + +### Plugin pattern (recommended once a folder exists) + +```rust +// src/gameplay/galaxy_creation/mod.rs +pub struct GalaxyCreationPlugin; + +impl Plugin for GalaxyCreationPlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(AppState::GalaxyCreation), setup_galaxy_creation) + .add_systems(OnExit(AppState::GalaxyCreation), despawn_galaxy_creation) + .add_systems(Update, ( + /* update systems */ + ).run_if(in_state(AppState::GalaxyCreation))); + } +} +``` + +Then `main.rs` collapses to `app.add_plugins((..., GalaxyCreationPlugin))`. + +## Naming Conventions + +Follows [Rust RFC 344](https://rust-lang.github.io/api-guidelines/naming.html). Bevy layers these additional conventions: + +| Item | Convention | Example | +|---|---|---| +| Files, modules, directories | `snake_case` | `galaxy_creation.rs`, `main_menu.rs` | +| Crate name | `snake_case` | `void-nav` | +| Structs, enums, enum variants | `UpperCamelCase` | `AppState`, `GalaxyCreation` | +| Components | `UpperCamelCase` (suffix optional) | `Player`, `MainMenuUi` | +| Resources | `UpperCamelCase` | `ClearColor`, `Time` | +| States | `UpperCamelCase` + `State` suffix | `AppState`, `GameState` | +| Plugins | `UpperCamelCase` + `Plugin` suffix | `GalaxyCreationPlugin` | +| Functions, systems, locals | `snake_case`, verb-first | `spawn_camera`, `setup_main_menu` | +| Constants, statics | `SCREAMING_SNAKE_CASE` | `MAX_HEALTH` | + +## Architecture Notes + +- **State machine driven**: each `AppState` variant has its own UI/systems wired via `OnEnter` / `OnExit` / `Update` (latter guarded by `in_state`). +- **Persistence**: SpacetimeDB will be the authoritative backend (via the Rust SDK, not the TS bindings). Not yet wired up. +- **No `module_bindings/` here**: TS bindings live in the TS workspace and are consumed by the docs prototype. diff --git a/apps/game/src/camera.rs b/apps/game/src/camera.rs new file mode 100644 index 0000000..2489ebf --- /dev/null +++ b/apps/game/src/camera.rs @@ -0,0 +1,5 @@ +use bevy::prelude::*; + +pub fn spawn_camera(mut commands: Commands) { + commands.spawn(Camera2d); +} diff --git a/apps/game/src/gameplay/galaxy_creation.rs b/apps/game/src/gameplay/galaxy_creation.rs new file mode 100644 index 0000000..46442ff --- /dev/null +++ b/apps/game/src/gameplay/galaxy_creation.rs @@ -0,0 +1,18 @@ +use bevy::prelude::*; + +// ── Markers ───────────────────────────────────────────────────────────────── + +#[derive(Component)] +pub struct GalaxyCreationUi; + +// ── Galaxy Creation Screen ────────────────────────────────────────────────── + +pub fn setup_galaxy_creation(_commands: Commands) { + // TODO: spawn galaxy creation UI +} + +pub fn despawn_galaxy_creation(mut commands: Commands, query: Query>) { + for entity in &query { + commands.entity(entity).despawn(); + } +} diff --git a/apps/game/src/gameplay/mod.rs b/apps/game/src/gameplay/mod.rs new file mode 100644 index 0000000..e2a7d61 --- /dev/null +++ b/apps/game/src/gameplay/mod.rs @@ -0,0 +1 @@ +pub mod galaxy_creation; diff --git a/apps/game/src/main.rs b/apps/game/src/main.rs index d7544a9..414362e 100644 --- a/apps/game/src/main.rs +++ b/apps/game/src/main.rs @@ -1,176 +1,24 @@ +mod camera; +mod gameplay; +mod state; +mod ui; + use bevy::prelude::*; +use gameplay::galaxy_creation; +use state::AppState; +use ui::main_menu; + fn main() { App::new() .add_plugins(DefaultPlugins) .insert_resource(ClearColor(Color::srgb(0.02, 0.02, 0.06))) .init_state::() - .add_systems(Startup, spawn_camera) - .add_systems(OnEnter(AppState::MainMenu), setup_main_menu) - .add_systems(OnExit(AppState::MainMenu), despawn_main_menu) - .add_systems(Update, main_menu_buttons) + .add_systems(Startup, camera::spawn_camera) + .add_systems(OnEnter(AppState::MainMenu), main_menu::setup_main_menu) + .add_systems(OnExit(AppState::MainMenu), main_menu::despawn_main_menu) + .add_systems(Update, main_menu::main_menu_buttons) + .add_systems(OnEnter(AppState::GalaxyCreation), galaxy_creation::setup_galaxy_creation) + .add_systems(OnExit(AppState::GalaxyCreation), galaxy_creation::despawn_galaxy_creation) .run(); } - -// ── States ────────────────────────────────────────────────────────────────── - -#[derive(Clone, Eq, PartialEq, Debug, Hash, States, Default)] -enum AppState { - #[default] - MainMenu, - InGame, - Options, -} - -// ── Markers ───────────────────────────────────────────────────────────────── - -#[derive(Component)] -struct MainMenuUi; - -#[derive(Component)] -enum MenuButton { - Start, - Options, - Exit, -} - -// ── Camera ────────────────────────────────────────────────────────────────── - -fn spawn_camera(mut commands: Commands) { - commands.spawn(Camera2d); -} - -// ── Main Menu ─────────────────────────────────────────────────────────────── - -fn setup_main_menu(mut commands: Commands) { - let button_style = Style { - width: Val::Px(280.0), - height: Val::Px(64.0), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }; - - let button_text_font = TextFont { - font_size: 28.0, - ..default() - }; - - commands.spawn(( - Node { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - display: Display::Flex, - flex_direction: FlexDirection::Column, - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - row_gap: Val::Px(24.0), - ..default() - }, - BackgroundColor(Color::srgb(0.02, 0.02, 0.06)), - MainMenuUi, - )).with_children(|parent| { - // Title - parent.spawn(( - Text::new("VOID::NAV"), - TextFont { - font_size: 72.0, - ..default() - }, - TextColor(Color::srgb(0.7, 0.85, 1.0)), - Node { - margin: UiRect::bottom(Val::Px(48.0)), - ..default() - }, - )); - - // Subtitle - parent.spawn(( - Text::new("A space exploration RPG"), - TextFont { - font_size: 18.0, - ..default() - }, - TextColor(Color::srgb(0.4, 0.5, 0.6)), - Node { - margin: UiRect::bottom(Val::Px(32.0)), - ..default() - }, - )); - - // Start Game button - spawn_menu_button( - &mut parent.spawn_empty(), - "Start Game", - MenuButton::Start, - &button_style, - &button_text_font, - ); - - // Options button - spawn_menu_button( - &mut parent.spawn_empty(), - "Options", - MenuButton::Options, - &button_style, - &button_text_font, - ); - - // Exit button - spawn_menu_button( - &mut parent.spawn_empty(), - "Exit", - MenuButton::Exit, - &button_style, - &button_text_font, - ); - }); -} - -fn spawn_menu_button( - cmd: &mut EntityCommands, - label: &str, - marker: MenuButton, - style: &Style, - text_font: &TextFont, -) { - cmd.insert(( - Button, - style.clone(), - BackgroundColor(Color::srgb(0.08, 0.1, 0.18)), - BorderColor(Color::srgb(0.3, 0.45, 0.7)), - BorderRadius::all(Val::Px(8.0)), - marker, - )) - .with_children(|btn| { - btn.spawn(( - Text::new(label), - text_font.clone(), - TextColor(Color::srgb(0.75, 0.85, 1.0)), - )); - }); -} - -fn despawn_main_menu(mut commands: Commands, query: Query>) { - for entity in &query { - commands.entity(entity).despawn_recursive(); - } -} - -// ── Button Interaction ────────────────────────────────────────────────────── - -fn main_menu_buttons( - mut next_state: ResMut>, - mut exit: EventWriter, - interaction_query: Query<(&Interaction, &MenuButton), Changed>, -) { - for (interaction, button) in &interaction_query { - if *interaction == Interaction::Pressed { - match button { - MenuButton::Start => next_state.set(AppState::InGame), - MenuButton::Options => next_state.set(AppState::Options), - MenuButton::Exit => exit.send(AppExit::Success), - } - } - } -} diff --git a/apps/game/src/state.rs b/apps/game/src/state.rs new file mode 100644 index 0000000..d731f5a --- /dev/null +++ b/apps/game/src/state.rs @@ -0,0 +1,11 @@ +use bevy::prelude::*; + +#[derive(Clone, Eq, PartialEq, Debug, Hash, States, Default)] +pub enum AppState { + #[default] + MainMenu, + GalaxyCreation, + CharacterCreation, + InGame, + Options, +} diff --git a/apps/game/src/ui/main_menu.rs b/apps/game/src/ui/main_menu.rs new file mode 100644 index 0000000..e46aa0d --- /dev/null +++ b/apps/game/src/ui/main_menu.rs @@ -0,0 +1,156 @@ +use bevy::prelude::*; + +use crate::state::AppState; + +// ── Markers ───────────────────────────────────────────────────────────────── + +#[derive(Component)] +pub struct MainMenuUi; + +#[derive(Component)] +pub enum MenuButton { + ContinueGame, //Needed for later once save game functionality is implemented, should have a check to see if a save game exists and only show this button if it does + NewGame, + Options, + Exit, +} + +// ── Main Menu ─────────────────────────────────────────────────────────────── + +pub fn setup_main_menu(mut commands: Commands) { + let button_style = Node { + width: Val::Px(280.0), + height: Val::Px(64.0), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }; + + let button_text_font = TextFont { + font_size: 28.0, + ..default() + }; + + commands + .spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + row_gap: Val::Px(24.0), + ..default() + }, + BackgroundColor(Color::srgb(0.02, 0.02, 0.06)), + MainMenuUi, + )) + .with_children(|parent| { + // Title + parent.spawn(( + Text::new("VOID::NAV"), + TextFont { + font_size: 72.0, + ..default() + }, + TextColor(Color::srgb(0.7, 0.85, 1.0)), + Node { + margin: UiRect::bottom(Val::Px(48.0)), + ..default() + }, + )); + + // Subtitle + parent.spawn(( + Text::new("A space exploration RPG"), + TextFont { + font_size: 18.0, + ..default() + }, + TextColor(Color::srgb(0.4, 0.5, 0.6)), + Node { + margin: UiRect::bottom(Val::Px(32.0)), + ..default() + }, + )); + + // Start Game button + spawn_menu_button( + &mut parent.spawn_empty(), + "Start Game", + MenuButton::NewGame, + &button_style, + &button_text_font, + ); + + // Options button + spawn_menu_button( + &mut parent.spawn_empty(), + "Options", + MenuButton::Options, + &button_style, + &button_text_font, + ); + + // Exit button + spawn_menu_button( + &mut parent.spawn_empty(), + "Exit", + MenuButton::Exit, + &button_style, + &button_text_font, + ); + }); +} + +fn spawn_menu_button( + cmd: &mut EntityCommands, + label: &str, + marker: MenuButton, + style: &Node, + text_font: &TextFont, +) { + cmd.insert(( + Button, + style.clone(), + BackgroundColor(Color::srgb(0.08, 0.1, 0.18)), + BorderColor(Color::srgb(0.3, 0.45, 0.7)), + BorderRadius::all(Val::Px(8.0)), + marker, + )) + .with_children(|btn| { + btn.spawn(( + Text::new(label), + text_font.clone(), + TextColor(Color::srgb(0.75, 0.85, 1.0)), + )); + }); +} + +pub fn despawn_main_menu(mut commands: Commands, query: Query>) { + for entity in &query { + commands.entity(entity).despawn(); + } +} + +// ── Button Interaction ────────────────────────────────────────────────────── + +pub fn main_menu_buttons( + mut next_state: ResMut>, + mut exit: EventWriter, + interaction_query: Query<(&Interaction, &MenuButton), Changed>, +) { + for (interaction, button) in &interaction_query { + if *interaction == Interaction::Pressed { + match button { + MenuButton::ContinueGame => next_state.set(AppState::InGame), // Placeholder for now + MenuButton::NewGame => next_state.set(AppState::GalaxyCreation), + MenuButton::Options => next_state.set(AppState::Options), + MenuButton::Exit => { + exit.write(AppExit::Success); + } + } + } + } +} diff --git a/apps/game/src/ui/mod.rs b/apps/game/src/ui/mod.rs new file mode 100644 index 0000000..515f306 --- /dev/null +++ b/apps/game/src/ui/mod.rs @@ -0,0 +1 @@ +pub mod main_menu;