diff --git a/apps/game/AGENTS.md b/apps/game/AGENTS.md index a3846dd..b390bc2 100644 --- a/apps/game/AGENTS.md +++ b/apps/game/AGENTS.md @@ -20,40 +20,44 @@ cargo test # Run unit tests ``` src/ ├── main.rs # App builder: plugins, state init, system registration -├── state.rs # AppState enum (MainMenu, GalaxyCreation, CharacterCreation, InGame, Options) +├── state.rs # AppState enum (MainMenu, Galaxy, 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 + ├── character_creation/ # Character creation scene (skeleton) + ├── galaxy/ # Procedural galaxy inspection scene + ├── movement/ # Ship movement (kinematic + orbital) + ├── physics/ # Physics primitives (mass, gravity, etc.) + └── star_map/ # Star map data + visualization ``` ### When to add a file vs a folder -- **Default**: one file per feature (e.g. `main_menu.rs`, `galaxy_creation.rs`). +- **Default**: one file per feature (e.g. `main_menu.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; +// src/gameplay/galaxy/mod.rs +pub struct GalaxyPlugin; -impl Plugin for GalaxyCreationPlugin { +impl Plugin for GalaxyPlugin { fn build(&self, app: &mut App) { - app.add_systems(OnEnter(AppState::GalaxyCreation), setup_galaxy_creation) - .add_systems(OnExit(AppState::GalaxyCreation), despawn_galaxy_creation) + app.add_systems(OnEnter(AppState::Galaxy), setup_galaxy) + .add_systems(OnExit(AppState::Galaxy), despawn_galaxy) .add_systems(Update, ( /* update systems */ - ).run_if(in_state(AppState::GalaxyCreation))); + ).run_if(in_state(AppState::Galaxy))); } } ``` -Then `main.rs` collapses to `app.add_plugins((..., GalaxyCreationPlugin))`. +Then `main.rs` collapses to `app.add_plugins((..., GalaxyPlugin))`. ## Naming Conventions @@ -61,13 +65,13 @@ Follows [Rust RFC 344](https://rust-lang.github.io/api-guidelines/naming.html). | Item | Convention | Example | |---|---|---| -| Files, modules, directories | `snake_case` | `galaxy_creation.rs`, `main_menu.rs` | +| Files, modules, directories | `snake_case` | `galaxy`, `main_menu` | | Crate name | `snake_case` | `void-nav` | -| Structs, enums, enum variants | `UpperCamelCase` | `AppState`, `GalaxyCreation` | +| Structs, enums, enum variants | `UpperCamelCase` | `AppState`, `Galaxy` | | Components | `UpperCamelCase` (suffix optional) | `Player`, `MainMenuUi` | | Resources | `UpperCamelCase` | `ClearColor`, `Time` | | States | `UpperCamelCase` + `State` suffix | `AppState`, `GameState` | -| Plugins | `UpperCamelCase` + `Plugin` suffix | `GalaxyCreationPlugin` | +| Plugins | `UpperCamelCase` + `Plugin` suffix | `GalaxyPlugin` | | Functions, systems, locals | `snake_case`, verb-first | `spawn_camera`, `setup_main_menu` | | Constants, statics | `SCREAMING_SNAKE_CASE` | `MAX_HEALTH` | diff --git a/apps/game/src/camera.rs b/apps/game/src/camera.rs index 2ede3d7..76ebe2b 100644 --- a/apps/game/src/camera.rs +++ b/apps/game/src/camera.rs @@ -8,7 +8,7 @@ use crate::ui::util::cursor_over_ui; pub struct MainCamera; /// Orbit-style camera: rotates around `target` at `distance`, controlled by mouse. -/// Used for inspection scenes like GalaxyCreation where there is no player to follow. +/// Used for inspection scenes like Galaxy where there is no player to follow. /// /// Orientation is stored as a quaternion (`rotation`) to allow true 360° motion /// with no gimbal lock. The base orientation is camera at `target + (0, 0, distance)` diff --git a/apps/game/src/gameplay/character_creation/mod.rs b/apps/game/src/gameplay/character_creation/mod.rs new file mode 100644 index 0000000..29d4ab1 --- /dev/null +++ b/apps/game/src/gameplay/character_creation/mod.rs @@ -0,0 +1,194 @@ +//! Character creation scene. +//! +//! Barebones skeleton: spawns a placeholder UI on enter, despawns on exit, +//! and wires a `Confirm` button that transitions to [`AppState::InGame`] and +//! a `Back` button (or Escape) that returns to [`AppState::Galaxy`]. +//! +//! Intended to grow into the real character creator — name, portrait, stats, +//! starting ship, background / origin, etc. Each major aspect should split +//! into its own submodule (e.g. `ui.rs`, `params.rs`, `presets.rs`) once it +//! outgrows this file (see the module-layout guidelines in `apps/game/AGENTS.md`). + +use bevy::prelude::*; + +use crate::state::AppState; + +pub struct CharacterCreationPlugin; + +impl Plugin for CharacterCreationPlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(AppState::CharacterCreation), setup_character_creation) + .add_systems( + OnExit(AppState::CharacterCreation), + despawn_character_creation, + ) + .add_systems( + Update, + ( + escape_to_galaxy, + confirm_button_handler, + back_button_handler, + ) + .run_if(in_state(AppState::CharacterCreation)), + ); + } +} + +// ── Markers ───────────────────────────────────────────────────────────────── + +/// Tag for everything spawned during character creation so it can be cleanly +/// despawned on state exit. +#[derive(Component)] +pub struct CharacterCreationSpawned; + +#[derive(Component)] +pub enum CharacterCreationButton { + /// Finalize the character and begin the game. + Confirm, + /// Discard and return to the galaxy selection screen. + Back, +} + +// ── Setup ─────────────────────────────────────────────────────────────────── + +fn setup_character_creation(mut commands: Commands) { + let button_style = Node { + width: Val::Px(220.0), + height: Val::Px(56.0), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }; + let button_font = TextFont { + font_size: 24.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)), + CharacterCreationSpawned, + )) + .with_children(|parent| { + parent.spawn(( + Text::new("Character Creation"), + TextFont { + font_size: 56.0, + ..default() + }, + TextColor(Color::srgb(0.7, 0.85, 1.0)), + Node { + margin: UiRect::bottom(Val::Px(16.0)), + ..default() + }, + )); + + parent.spawn(( + Text::new("TODO: name, portrait, stats, background, starting ship"), + TextFont { + font_size: 18.0, + ..default() + }, + TextColor(Color::srgb(0.4, 0.5, 0.6)), + Node { + margin: UiRect::bottom(Val::Px(48.0)), + ..default() + }, + )); + + spawn_button( + &mut parent.spawn_empty(), + "Confirm", + CharacterCreationButton::Confirm, + &button_style, + &button_font, + ); + spawn_button( + &mut parent.spawn_empty(), + "Back", + CharacterCreationButton::Back, + &button_style, + &button_font, + ); + }); +} + +fn spawn_button( + cmd: &mut EntityCommands, + label: &str, + marker: CharacterCreationButton, + 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)), + )); + }); +} + +// ── Lifecycle ─────────────────────────────────────────────────────────────── + +fn despawn_character_creation( + mut commands: Commands, + query: Query>, +) { + for entity in &query { + // Bevy 0.16: despawn() is recursive by default. + commands.entity(entity).despawn(); + } +} + +// ── Input ─────────────────────────────────────────────────────────────────── + +fn escape_to_galaxy( + keys: Res>, + mut next_state: ResMut>, +) { + if keys.just_pressed(KeyCode::Escape) { + next_state.set(AppState::Galaxy); + } +} + +fn confirm_button_handler( + mut next_state: ResMut>, + query: Query<(&Interaction, &CharacterCreationButton), Changed>, +) { + for (interaction, button) in &query { + if *interaction == Interaction::Pressed && matches!(button, CharacterCreationButton::Confirm) { + // TODO: persist the configured character before entering the game. + next_state.set(AppState::InGame); + } + } +} + +fn back_button_handler( + mut next_state: ResMut>, + query: Query<(&Interaction, &CharacterCreationButton), Changed>, +) { + for (interaction, button) in &query { + if *interaction == Interaction::Pressed && matches!(button, CharacterCreationButton::Back) { + next_state.set(AppState::Galaxy); + } + } +} diff --git a/apps/game/src/gameplay/galaxy_creation/axes.rs b/apps/game/src/gameplay/galaxy/axes.rs similarity index 93% rename from apps/game/src/gameplay/galaxy_creation/axes.rs rename to apps/game/src/gameplay/galaxy/axes.rs index 600df14..28053ed 100644 --- a/apps/game/src/gameplay/galaxy_creation/axes.rs +++ b/apps/game/src/gameplay/galaxy/axes.rs @@ -1,14 +1,14 @@ -//! World-space XYZ axis indicator (red/green/blue cylinders through the origin). +//! World-space XYZ axis indicator (grey cylinders through the origin). //! //! Provides spatial reference for the galaxy inspection scene. Drawn as three //! thin emissive cylinders — mesh-based rather than gizmos so they participate //! in normal scene rendering (occlusion, depth). -use bevy::prelude::*; +use bevy::{math::InvalidDirectionError::Infinite, prelude::*}; /// Length of each axis arrow. Picked to be visible from the default orbit /// distance (~420) without dominating the scene. -const AXIS_LENGTH: f32 = 60.0; +const AXIS_LENGTH: f32 = 100000.00; /// Cylinder radius — same scale as connection lines so axes feel like part of /// the galaxy scaffold. const AXIS_RADIUS: f32 = 0.35; diff --git a/apps/game/src/gameplay/galaxy_creation/contents.rs b/apps/game/src/gameplay/galaxy/contents.rs similarity index 100% rename from apps/game/src/gameplay/galaxy_creation/contents.rs rename to apps/game/src/gameplay/galaxy/contents.rs diff --git a/apps/game/src/gameplay/galaxy_creation/mod.rs b/apps/game/src/gameplay/galaxy/mod.rs similarity index 97% rename from apps/game/src/gameplay/galaxy_creation/mod.rs rename to apps/game/src/gameplay/galaxy/mod.rs index b2155a3..976738c 100644 --- a/apps/game/src/gameplay/galaxy_creation/mod.rs +++ b/apps/game/src/gameplay/galaxy/mod.rs @@ -1,4 +1,4 @@ -//! Galaxy creation inspection scene. +//! Galaxy inspection scene. //! //! Procedural spiral galaxy viewer with editable parameters. The 3D scene is //! regenerated whenever [`GalaxyParams`] changes (via the `generation` counter); @@ -26,19 +26,19 @@ pub use contents::{SystemContents, SystemContext, SystemSummary}; pub use params::{GalaxyParams, SelectedStar}; use params::{CORE_COUNT, NEAREST_NEIGHBOR_CONNECTIONS, SPACING_ATTEMPTS}; -pub struct GalaxyCreationPlugin; +pub struct GalaxyPlugin; -impl Plugin for GalaxyCreationPlugin { +impl Plugin for GalaxyPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .add_systems( - OnEnter(AppState::GalaxyCreation), + OnEnter(AppState::Galaxy), (setup_galaxy_scene, ui::setup_galaxy_ui), ) .add_systems( - OnExit(AppState::GalaxyCreation), - (despawn_galaxy_creation, reset_selection), + OnExit(AppState::Galaxy), + (despawn_galaxy, reset_selection), ) .add_systems( Update, @@ -55,7 +55,7 @@ impl Plugin for GalaxyCreationPlugin { orbits::advance_orbital_paths, ) .chain() - .run_if(in_state(AppState::GalaxyCreation)), + .run_if(in_state(AppState::Galaxy)), ); } } @@ -65,7 +65,7 @@ impl Plugin for GalaxyCreationPlugin { /// Tag for *anything* spawned during galaxy creation so it can be cleanly /// despawned on state exit. Applies to both 3D scene roots and UI panel roots. #[derive(Component)] -pub struct GalaxyCreationSpawned; +pub struct GalaxySpawned; /// Tag for the 3D scene root only — despawned on regeneration, so we can /// rebuild the galaxy without disturbing the UI panels. @@ -349,7 +349,7 @@ fn spawn_galaxy_scene( // Parent group so all galaxy contents despawn together. commands - .spawn((Transform::default(), GalaxyScene, GalaxyCreationSpawned)) + .spawn((Transform::default(), GalaxyScene, GalaxySpawned)) .with_children(|parent| { // XYZ reference axes through the origin. axes::spawn_axes(parent, meshes, materials); @@ -467,9 +467,9 @@ fn build_connections(systems: &[GeneratedSystem]) -> Vec<(usize, usize)> { // ── Lifecycle systems ─────────────────────────────────────────────────────── -fn despawn_galaxy_creation( +fn despawn_galaxy( mut commands: Commands, - query: Query>, + query: Query>, ) { for entity in &query { // Bevy 0.16: despawn() is recursive by default. diff --git a/apps/game/src/gameplay/galaxy_creation/orbits.rs b/apps/game/src/gameplay/galaxy/orbits.rs similarity index 100% rename from apps/game/src/gameplay/galaxy_creation/orbits.rs rename to apps/game/src/gameplay/galaxy/orbits.rs diff --git a/apps/game/src/gameplay/galaxy_creation/params.rs b/apps/game/src/gameplay/galaxy/params.rs similarity index 100% rename from apps/game/src/gameplay/galaxy_creation/params.rs rename to apps/game/src/gameplay/galaxy/params.rs diff --git a/apps/game/src/gameplay/galaxy_creation/poi.rs b/apps/game/src/gameplay/galaxy/poi.rs similarity index 100% rename from apps/game/src/gameplay/galaxy_creation/poi.rs rename to apps/game/src/gameplay/galaxy/poi.rs diff --git a/apps/game/src/gameplay/galaxy_creation/selection.rs b/apps/game/src/gameplay/galaxy/selection.rs similarity index 97% rename from apps/game/src/gameplay/galaxy_creation/selection.rs rename to apps/game/src/gameplay/galaxy/selection.rs index d079429..8177f9a 100644 --- a/apps/game/src/gameplay/galaxy_creation/selection.rs +++ b/apps/game/src/gameplay/galaxy/selection.rs @@ -10,8 +10,9 @@ use bevy::window::PrimaryWindow; use super::StarSystem; use crate::camera::MainCamera; -use crate::gameplay::galaxy_creation::params::SelectedStar; -use crate::gameplay::galaxy_creation::ui::GalaxyInfoPanel; +use crate::gameplay::galaxy::params::SelectedStar; +use crate::gameplay::galaxy::ui::GalaxyInfoPanel; +use crate::gameplay::galaxy::SystemContents; use crate::ui::util::cursor_over_ui; // ── Tunables ──────────────────────────────────────────────────────────────── @@ -163,6 +164,8 @@ pub fn refresh_info_panel( )); spawn_info_line(parent, "Faction", sys.faction); spawn_info_line(parent, "Security", &format!("{:.2}", sys.security)); + spawn_info_line(parent, "POIs", &format!("{}", sys.poi_count)); + spawn_info_line(parent, "ID", &sys.id); } None => { diff --git a/apps/game/src/gameplay/galaxy_creation/ui.rs b/apps/game/src/gameplay/galaxy/ui.rs similarity index 97% rename from apps/game/src/gameplay/galaxy_creation/ui.rs rename to apps/game/src/gameplay/galaxy/ui.rs index 7d81ec7..c89eecf 100644 --- a/apps/game/src/gameplay/galaxy_creation/ui.rs +++ b/apps/game/src/gameplay/galaxy/ui.rs @@ -1,4 +1,4 @@ -//! Galaxy creation UI: parameter slider panel (left) and selected-system +//! Galaxy UI: parameter slider panel (left) and selected-system //! info panel (right). //! //! Bevy 0.16 does not ship a native Slider widget, so each parameter is a @@ -7,8 +7,8 @@ use bevy::prelude::*; -use super::GalaxyCreationSpawned; -use crate::gameplay::galaxy_creation::params::*; +use super::GalaxySpawned; +use crate::gameplay::galaxy::params::*; // ── Markers ───────────────────────────────────────────────────────────────── @@ -82,7 +82,7 @@ fn spawn_control_panel(commands: &mut Commands) { BorderColor(PANEL_BORDER), BorderRadius::all(Val::Px(8.0)), GalaxyControlPanel, - GalaxyCreationSpawned, + GalaxySpawned, )) .with_children(|parent| { parent.spawn(( @@ -292,7 +292,7 @@ fn spawn_info_panel_empty(commands: &mut Commands) { BorderColor(PANEL_BORDER), BorderRadius::all(Val::Px(8.0)), GalaxyInfoPanel, - GalaxyCreationSpawned, + GalaxySpawned, )) .with_children(|parent| { parent.spawn(( @@ -440,6 +440,6 @@ pub fn refresh_control_panel_values( // ── State wiring ──────────────────────────────────────────────────────────── // -// `setup_galaxy_ui` runs on OnEnter(GalaxyCreation) via the plugin in `mod.rs`. -// Despawning happens through the shared `GalaxyCreationSpawned` marker in the +// `setup_galaxy_ui` runs on OnEnter(Galaxy) via the plugin in `mod.rs`. +// Despawning happens through the shared `GalaxySpawned` marker in the // plugin's OnExit handler — no separate UI cleanup needed. diff --git a/apps/game/src/gameplay/mod.rs b/apps/game/src/gameplay/mod.rs index f68f0c4..0b7a59c 100644 --- a/apps/game/src/gameplay/mod.rs +++ b/apps/game/src/gameplay/mod.rs @@ -1,4 +1,5 @@ -pub mod galaxy_creation; +pub mod character_creation; +pub mod galaxy; pub mod movement; pub mod physics; pub mod star_map; diff --git a/apps/game/src/main.rs b/apps/game/src/main.rs index fe5607b..956cb51 100644 --- a/apps/game/src/main.rs +++ b/apps/game/src/main.rs @@ -7,7 +7,8 @@ use bevy::prelude::*; use camera::orbit_camera_control; use gameplay::{ - galaxy_creation::GalaxyCreationPlugin, + character_creation::CharacterCreationPlugin, + galaxy::GalaxyPlugin, movement::MovementPlugin, physics::PhysicsPlugin, star_map::StarMapPlugin, @@ -30,7 +31,7 @@ fn main() { // follow camera instead (not yet implemented). .add_systems( Update, - orbit_camera_control.run_if(in_state(AppState::GalaxyCreation)), + orbit_camera_control.run_if(in_state(AppState::Galaxy)), ) .add_systems(OnEnter(AppState::MainMenu), main_menu::setup_main_menu) .add_systems(OnExit(AppState::MainMenu), main_menu::despawn_main_menu) @@ -38,8 +39,9 @@ fn main() { .add_plugins(( MovementPlugin, PhysicsPlugin, - GalaxyCreationPlugin, + GalaxyPlugin, StarMapPlugin, + CharacterCreationPlugin, )) .run(); } diff --git a/apps/game/src/state.rs b/apps/game/src/state.rs index d731f5a..fa16e3c 100644 --- a/apps/game/src/state.rs +++ b/apps/game/src/state.rs @@ -4,7 +4,7 @@ use bevy::prelude::*; pub enum AppState { #[default] MainMenu, - GalaxyCreation, + Galaxy, CharacterCreation, InGame, Options, diff --git a/apps/game/src/ui/main_menu.rs b/apps/game/src/ui/main_menu.rs index e46aa0d..1745a02 100644 --- a/apps/game/src/ui/main_menu.rs +++ b/apps/game/src/ui/main_menu.rs @@ -145,7 +145,7 @@ pub fn main_menu_buttons( if *interaction == Interaction::Pressed { match button { MenuButton::ContinueGame => next_state.set(AppState::InGame), // Placeholder for now - MenuButton::NewGame => next_state.set(AppState::GalaxyCreation), + MenuButton::NewGame => next_state.set(AppState::Galaxy), MenuButton::Options => next_state.set(AppState::Options), MenuButton::Exit => { exit.write(AppExit::Success);