Rename galaxy_creation to galaxy; add character creation skeleton

- Rename GalaxyCreationPlugin -> GalaxyPlugin, AppState::GalaxyCreation
  -> AppState::Galaxy, module path galaxy_creation -> galaxy (folder
  rename preserves git history via git mv).
- Rename GalaxyCreationSpawned -> GalaxySpawned, despawn_galaxy_creation
  -> despawn_galaxy.
- Update AGENTS.md module layout, plugin pattern example, and naming
  table for the new names.
- Add apps/game/src/gameplay/character_creation/ skeleton: placeholder
  UI with Confirm (-> InGame) and Back (-> Galaxy) buttons plus Escape
  shortcut. Wired into main.rs via CharacterCreationPlugin.
This commit is contained in:
2026-06-07 17:11:56 -04:00
parent 031a674bd0
commit 75f58bcd54
15 changed files with 247 additions and 43 deletions

View File

@@ -20,40 +20,44 @@ cargo test # Run unit tests
``` ```
src/ src/
├── main.rs # App builder: plugins, state init, system registration ├── 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 ├── camera.rs # Camera spawn
├── ui/ # UI screens (menus, HUD, etc.) ├── ui/ # UI screens (menus, HUD, etc.)
│ ├── mod.rs │ ├── mod.rs
│ └── main_menu.rs │ └── main_menu.rs
└── gameplay/ # Non-UI gameplay systems └── gameplay/ # Non-UI gameplay systems
├── mod.rs ├── 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 ### 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. - **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). - 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) ### Plugin pattern (recommended once a folder exists)
```rust ```rust
// src/gameplay/galaxy_creation/mod.rs // src/gameplay/galaxy/mod.rs
pub struct GalaxyCreationPlugin; pub struct GalaxyPlugin;
impl Plugin for GalaxyCreationPlugin { impl Plugin for GalaxyPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(OnEnter(AppState::GalaxyCreation), setup_galaxy_creation) app.add_systems(OnEnter(AppState::Galaxy), setup_galaxy)
.add_systems(OnExit(AppState::GalaxyCreation), despawn_galaxy_creation) .add_systems(OnExit(AppState::Galaxy), despawn_galaxy)
.add_systems(Update, ( .add_systems(Update, (
/* update systems */ /* 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 ## Naming Conventions
@@ -61,13 +65,13 @@ Follows [Rust RFC 344](https://rust-lang.github.io/api-guidelines/naming.html).
| Item | Convention | Example | | 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` | | 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` | | Components | `UpperCamelCase` (suffix optional) | `Player`, `MainMenuUi` |
| Resources | `UpperCamelCase` | `ClearColor`, `Time` | | Resources | `UpperCamelCase` | `ClearColor`, `Time` |
| States | `UpperCamelCase` + `State` suffix | `AppState`, `GameState` | | 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` | | Functions, systems, locals | `snake_case`, verb-first | `spawn_camera`, `setup_main_menu` |
| Constants, statics | `SCREAMING_SNAKE_CASE` | `MAX_HEALTH` | | Constants, statics | `SCREAMING_SNAKE_CASE` | `MAX_HEALTH` |

View File

@@ -8,7 +8,7 @@ use crate::ui::util::cursor_over_ui;
pub struct MainCamera; pub struct MainCamera;
/// Orbit-style camera: rotates around `target` at `distance`, controlled by mouse. /// 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 /// 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)` /// with no gimbal lock. The base orientation is camera at `target + (0, 0, distance)`

View File

@@ -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<Entity, With<CharacterCreationSpawned>>,
) {
for entity in &query {
// Bevy 0.16: despawn() is recursive by default.
commands.entity(entity).despawn();
}
}
// ── Input ───────────────────────────────────────────────────────────────────
fn escape_to_galaxy(
keys: Res<ButtonInput<KeyCode>>,
mut next_state: ResMut<NextState<AppState>>,
) {
if keys.just_pressed(KeyCode::Escape) {
next_state.set(AppState::Galaxy);
}
}
fn confirm_button_handler(
mut next_state: ResMut<NextState<AppState>>,
query: Query<(&Interaction, &CharacterCreationButton), Changed<Interaction>>,
) {
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<NextState<AppState>>,
query: Query<(&Interaction, &CharacterCreationButton), Changed<Interaction>>,
) {
for (interaction, button) in &query {
if *interaction == Interaction::Pressed && matches!(button, CharacterCreationButton::Back) {
next_state.set(AppState::Galaxy);
}
}
}

View File

@@ -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 //! Provides spatial reference for the galaxy inspection scene. Drawn as three
//! thin emissive cylinders — mesh-based rather than gizmos so they participate //! thin emissive cylinders — mesh-based rather than gizmos so they participate
//! in normal scene rendering (occlusion, depth). //! 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 /// Length of each axis arrow. Picked to be visible from the default orbit
/// distance (~420) without dominating the scene. /// 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 /// Cylinder radius — same scale as connection lines so axes feel like part of
/// the galaxy scaffold. /// the galaxy scaffold.
const AXIS_RADIUS: f32 = 0.35; const AXIS_RADIUS: f32 = 0.35;

View File

@@ -1,4 +1,4 @@
//! Galaxy creation inspection scene. //! Galaxy inspection scene.
//! //!
//! Procedural spiral galaxy viewer with editable parameters. The 3D scene is //! Procedural spiral galaxy viewer with editable parameters. The 3D scene is
//! regenerated whenever [`GalaxyParams`] changes (via the `generation` counter); //! regenerated whenever [`GalaxyParams`] changes (via the `generation` counter);
@@ -26,19 +26,19 @@ pub use contents::{SystemContents, SystemContext, SystemSummary};
pub use params::{GalaxyParams, SelectedStar}; pub use params::{GalaxyParams, SelectedStar};
use params::{CORE_COUNT, NEAREST_NEIGHBOR_CONNECTIONS, SPACING_ATTEMPTS}; 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) { fn build(&self, app: &mut App) {
app.init_resource::<GalaxyParams>() app.init_resource::<GalaxyParams>()
.init_resource::<SelectedStar>() .init_resource::<SelectedStar>()
.add_systems( .add_systems(
OnEnter(AppState::GalaxyCreation), OnEnter(AppState::Galaxy),
(setup_galaxy_scene, ui::setup_galaxy_ui), (setup_galaxy_scene, ui::setup_galaxy_ui),
) )
.add_systems( .add_systems(
OnExit(AppState::GalaxyCreation), OnExit(AppState::Galaxy),
(despawn_galaxy_creation, reset_selection), (despawn_galaxy, reset_selection),
) )
.add_systems( .add_systems(
Update, Update,
@@ -55,7 +55,7 @@ impl Plugin for GalaxyCreationPlugin {
orbits::advance_orbital_paths, orbits::advance_orbital_paths,
) )
.chain() .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 /// 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. /// despawned on state exit. Applies to both 3D scene roots and UI panel roots.
#[derive(Component)] #[derive(Component)]
pub struct GalaxyCreationSpawned; pub struct GalaxySpawned;
/// Tag for the 3D scene root only — despawned on regeneration, so we can /// Tag for the 3D scene root only — despawned on regeneration, so we can
/// rebuild the galaxy without disturbing the UI panels. /// rebuild the galaxy without disturbing the UI panels.
@@ -349,7 +349,7 @@ fn spawn_galaxy_scene(
// Parent group so all galaxy contents despawn together. // Parent group so all galaxy contents despawn together.
commands commands
.spawn((Transform::default(), GalaxyScene, GalaxyCreationSpawned)) .spawn((Transform::default(), GalaxyScene, GalaxySpawned))
.with_children(|parent| { .with_children(|parent| {
// XYZ reference axes through the origin. // XYZ reference axes through the origin.
axes::spawn_axes(parent, meshes, materials); axes::spawn_axes(parent, meshes, materials);
@@ -467,9 +467,9 @@ fn build_connections(systems: &[GeneratedSystem]) -> Vec<(usize, usize)> {
// ── Lifecycle systems ─────────────────────────────────────────────────────── // ── Lifecycle systems ───────────────────────────────────────────────────────
fn despawn_galaxy_creation( fn despawn_galaxy(
mut commands: Commands, mut commands: Commands,
query: Query<Entity, With<GalaxyCreationSpawned>>, query: Query<Entity, With<GalaxySpawned>>,
) { ) {
for entity in &query { for entity in &query {
// Bevy 0.16: despawn() is recursive by default. // Bevy 0.16: despawn() is recursive by default.

View File

@@ -10,8 +10,9 @@ use bevy::window::PrimaryWindow;
use super::StarSystem; use super::StarSystem;
use crate::camera::MainCamera; use crate::camera::MainCamera;
use crate::gameplay::galaxy_creation::params::SelectedStar; use crate::gameplay::galaxy::params::SelectedStar;
use crate::gameplay::galaxy_creation::ui::GalaxyInfoPanel; use crate::gameplay::galaxy::ui::GalaxyInfoPanel;
use crate::gameplay::galaxy::SystemContents;
use crate::ui::util::cursor_over_ui; use crate::ui::util::cursor_over_ui;
// ── Tunables ──────────────────────────────────────────────────────────────── // ── Tunables ────────────────────────────────────────────────────────────────
@@ -163,6 +164,8 @@ pub fn refresh_info_panel(
)); ));
spawn_info_line(parent, "Faction", sys.faction); spawn_info_line(parent, "Faction", sys.faction);
spawn_info_line(parent, "Security", &format!("{:.2}", sys.security)); 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); spawn_info_line(parent, "ID", &sys.id);
} }
None => { None => {

View File

@@ -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). //! info panel (right).
//! //!
//! Bevy 0.16 does not ship a native Slider widget, so each parameter is a //! Bevy 0.16 does not ship a native Slider widget, so each parameter is a
@@ -7,8 +7,8 @@
use bevy::prelude::*; use bevy::prelude::*;
use super::GalaxyCreationSpawned; use super::GalaxySpawned;
use crate::gameplay::galaxy_creation::params::*; use crate::gameplay::galaxy::params::*;
// ── Markers ───────────────────────────────────────────────────────────────── // ── Markers ─────────────────────────────────────────────────────────────────
@@ -82,7 +82,7 @@ fn spawn_control_panel(commands: &mut Commands) {
BorderColor(PANEL_BORDER), BorderColor(PANEL_BORDER),
BorderRadius::all(Val::Px(8.0)), BorderRadius::all(Val::Px(8.0)),
GalaxyControlPanel, GalaxyControlPanel,
GalaxyCreationSpawned, GalaxySpawned,
)) ))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent.spawn((
@@ -292,7 +292,7 @@ fn spawn_info_panel_empty(commands: &mut Commands) {
BorderColor(PANEL_BORDER), BorderColor(PANEL_BORDER),
BorderRadius::all(Val::Px(8.0)), BorderRadius::all(Val::Px(8.0)),
GalaxyInfoPanel, GalaxyInfoPanel,
GalaxyCreationSpawned, GalaxySpawned,
)) ))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent.spawn((
@@ -440,6 +440,6 @@ pub fn refresh_control_panel_values(
// ── State wiring ──────────────────────────────────────────────────────────── // ── State wiring ────────────────────────────────────────────────────────────
// //
// `setup_galaxy_ui` runs on OnEnter(GalaxyCreation) via the plugin in `mod.rs`. // `setup_galaxy_ui` runs on OnEnter(Galaxy) via the plugin in `mod.rs`.
// Despawning happens through the shared `GalaxyCreationSpawned` marker in the // Despawning happens through the shared `GalaxySpawned` marker in the
// plugin's OnExit handler — no separate UI cleanup needed. // plugin's OnExit handler — no separate UI cleanup needed.

View File

@@ -1,4 +1,5 @@
pub mod galaxy_creation; pub mod character_creation;
pub mod galaxy;
pub mod movement; pub mod movement;
pub mod physics; pub mod physics;
pub mod star_map; pub mod star_map;

View File

@@ -7,7 +7,8 @@ use bevy::prelude::*;
use camera::orbit_camera_control; use camera::orbit_camera_control;
use gameplay::{ use gameplay::{
galaxy_creation::GalaxyCreationPlugin, character_creation::CharacterCreationPlugin,
galaxy::GalaxyPlugin,
movement::MovementPlugin, movement::MovementPlugin,
physics::PhysicsPlugin, physics::PhysicsPlugin,
star_map::StarMapPlugin, star_map::StarMapPlugin,
@@ -30,7 +31,7 @@ fn main() {
// follow camera instead (not yet implemented). // follow camera instead (not yet implemented).
.add_systems( .add_systems(
Update, 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(OnEnter(AppState::MainMenu), main_menu::setup_main_menu)
.add_systems(OnExit(AppState::MainMenu), main_menu::despawn_main_menu) .add_systems(OnExit(AppState::MainMenu), main_menu::despawn_main_menu)
@@ -38,8 +39,9 @@ fn main() {
.add_plugins(( .add_plugins((
MovementPlugin, MovementPlugin,
PhysicsPlugin, PhysicsPlugin,
GalaxyCreationPlugin, GalaxyPlugin,
StarMapPlugin, StarMapPlugin,
CharacterCreationPlugin,
)) ))
.run(); .run();
} }

View File

@@ -4,7 +4,7 @@ use bevy::prelude::*;
pub enum AppState { pub enum AppState {
#[default] #[default]
MainMenu, MainMenu,
GalaxyCreation, Galaxy,
CharacterCreation, CharacterCreation,
InGame, InGame,
Options, Options,

View File

@@ -145,7 +145,7 @@ pub fn main_menu_buttons(
if *interaction == Interaction::Pressed { if *interaction == Interaction::Pressed {
match button { match button {
MenuButton::ContinueGame => next_state.set(AppState::InGame), // Placeholder for now 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::Options => next_state.set(AppState::Options),
MenuButton::Exit => { MenuButton::Exit => {
exit.write(AppExit::Success); exit.write(AppExit::Success);