Split character creation into mod/params/ui submodules

Promote the monolithic character_creation/mod.rs into a folder per
apps/game/AGENTS.md: lays out the picker UI in ui.rs, holds presets
and the editable CharacterDraft resource in params.rs, and keeps
mod.rs as the plugin + lifecycle shell.

Re-exports CharacterDraft / Origin / StartingShip at the module root
for the (yet-to-be-wired) persistence layer.
This commit is contained in:
2026-06-07 22:19:52 -04:00
parent 8a966b9584
commit b372a75a84
3 changed files with 793 additions and 131 deletions

View File

@@ -1,13 +1,22 @@
//! 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`].
//! Layout / picker UI lives in [`ui`]; presets and the editable draft
//! resource live in [`params`]. This module wires the Bevy plugin, owns the
//! spawn / despawn lifecycle, and handles the Escape → Galaxy shortcut.
//!
//! 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`).
//! Persistence is not yet wired up — on Confirm, the current draft is logged
//! and the app transitions to [`AppState::InGame`]. The TODO is to persist it
//! to SpacetimeDB once the Rust SDK is integrated.
mod params;
mod ui;
// Public API surface for the (yet-to-be-wired) persistence layer / InGame
// plugin. Currently unused outside this module — `#[allow]` keeps the names
// exported so consumers can `use crate::gameplay::character_creation::Origin`
// without us remembering to re-add the re-export the day they're needed.
#[allow(unused_imports)]
pub use params::{CharacterDraft, Origin, StartingShip};
use bevy::prelude::*;
@@ -17,7 +26,11 @@ pub struct CharacterCreationPlugin;
impl Plugin for CharacterCreationPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(AppState::CharacterCreation), setup_character_creation)
app.init_resource::<CharacterDraft>()
.add_systems(
OnEnter(AppState::CharacterCreation),
ui::setup_character_creation_ui,
)
.add_systems(
OnExit(AppState::CharacterCreation),
despawn_character_creation,
@@ -26,9 +39,11 @@ impl Plugin for CharacterCreationPlugin {
Update,
(
escape_to_galaxy,
confirm_button_handler,
back_button_handler,
ui::picker_button_handler,
ui::refresh_picker_values,
ui::action_button_handler,
)
.chain()
.run_if(in_state(AppState::CharacterCreation)),
);
}
@@ -49,104 +64,6 @@ pub enum CharacterCreationButton {
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(
@@ -169,26 +86,3 @@ fn escape_to_galaxy(
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);
}
}
}