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/
├── 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` |

View File

@@ -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)`

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
//! 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;

View File

@@ -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::<GalaxyParams>()
.init_resource::<SelectedStar>()
.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<Entity, With<GalaxyCreationSpawned>>,
query: Query<Entity, With<GalaxySpawned>>,
) {
for entity in &query {
// Bevy 0.16: despawn() is recursive by default.

View File

@@ -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 => {

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).
//!
//! 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.

View File

@@ -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;

View File

@@ -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();
}

View File

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

View File

@@ -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);