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

@@ -0,0 +1,445 @@
//! 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
//! row of `Label Value [-] [+]` buttons. Each `+/-` button carries a
//! [`ParamButton`] marker identifying which field to mutate.
use bevy::prelude::*;
use super::GalaxySpawned;
use crate::gameplay::galaxy::params::*;
// ── Markers ─────────────────────────────────────────────────────────────────
#[derive(Component)]
pub struct GalaxyControlPanel;
#[derive(Component)]
pub struct GalaxyInfoPanel;
/// Identifies which parameter a `+/-` button mutates and which direction.
#[derive(Component, Clone, Copy)]
pub enum ParamButton {
SeedDecr,
SeedIncr,
CountDecr,
CountIncr,
ArmsDecr,
ArmsIncr,
VerticalArmsDecr,
VerticalArmsIncr,
SizeDecr,
SizeIncr,
TwistDecr,
TwistIncr,
VerticalTwistDecr,
VerticalTwistIncr,
/// Randomize: bump seed by 1 (equivalent to the docs "Regenerate" button).
Regenerate,
/// Reset orbit camera to default orientation.
CenterView,
}
// ── Styling constants ───────────────────────────────────────────────────────
const PANEL_BG: Color = Color::srgb(0.05, 0.07, 0.12);
const PANEL_BORDER: Color = Color::srgb(0.25, 0.40, 0.62);
const TEXT_BRIGHT: Color = Color::srgb(0.82, 0.90, 1.0);
const TEXT_DIM: Color = Color::srgb(0.55, 0.65, 0.82);
const BUTTON_BG: Color = Color::srgb(0.10, 0.14, 0.22);
const PANEL_WIDTH: f32 = 280.0;
const PANEL_PADDING: f32 = 14.0;
const TITLE_FONT_SIZE: f32 = 20.0;
const LABEL_FONT_SIZE: f32 = 15.0;
const VALUE_FONT_SIZE: f32 = 15.0;
const BUTTON_FONT_SIZE: f32 = 16.0;
const HELP_FONT_SIZE: f32 = 12.0;
// ── Setup ───────────────────────────────────────────────────────────────────
pub fn setup_galaxy_ui(mut commands: Commands) {
spawn_control_panel(&mut commands);
spawn_info_panel_empty(&mut commands);
}
fn spawn_control_panel(commands: &mut Commands) {
commands
.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(12.0),
top: Val::Px(12.0),
width: Val::Px(PANEL_WIDTH),
padding: UiRect::all(Val::Px(PANEL_PADDING)),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(6.0),
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(PANEL_BG),
BorderColor(PANEL_BORDER),
BorderRadius::all(Val::Px(8.0)),
GalaxyControlPanel,
GalaxySpawned,
))
.with_children(|parent| {
parent.spawn((
Text::new("Galaxy Parameters"),
TextFont {
font_size: TITLE_FONT_SIZE,
..default()
},
TextColor(TEXT_BRIGHT),
Node {
margin: UiRect::bottom(Val::Px(8.0)),
..default()
},
));
// Current params are read at button-press time in `param_button_handler`,
// so we don't need to thread `Res<GalaxyParams>` through setup.
spawn_param_row(parent, "Seed", "seed", ParamButton::SeedDecr, ParamButton::SeedIncr);
spawn_param_row(parent, "Systems", "count", ParamButton::CountDecr, ParamButton::CountIncr);
spawn_param_row(parent, "Disk Arms", "arms", ParamButton::ArmsDecr, ParamButton::ArmsIncr);
spawn_param_row(parent, "Vertical Arms", "varms", ParamButton::VerticalArmsDecr, ParamButton::VerticalArmsIncr);
spawn_param_row(parent, "Size", "size", ParamButton::SizeDecr, ParamButton::SizeIncr);
spawn_param_row(parent, "Twist", "twist", ParamButton::TwistDecr, ParamButton::TwistIncr);
spawn_param_row(parent, "Vertical Twist", "vtwist", ParamButton::VerticalTwistDecr, ParamButton::VerticalTwistIncr);
// Regenerate (randomize seed) button.
parent
.spawn((
Button,
Node {
width: Val::Percent(100.0),
height: Val::Px(32.0),
margin: UiRect::top(Val::Px(8.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(BUTTON_BG),
BorderColor(PANEL_BORDER),
BorderRadius::all(Val::Px(6.0)),
ParamButton::Regenerate,
))
.with_children(|btn| {
btn.spawn((
Text::new("Regenerate"),
TextFont {
font_size: BUTTON_FONT_SIZE,
..default()
},
TextColor(TEXT_BRIGHT),
));
});
// Center View (reset orbit camera) button.
parent
.spawn((
Button,
Node {
width: Val::Percent(100.0),
height: Val::Px(32.0),
margin: UiRect::top(Val::Px(4.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(BUTTON_BG),
BorderColor(PANEL_BORDER),
BorderRadius::all(Val::Px(6.0)),
ParamButton::CenterView,
))
.with_children(|btn| {
btn.spawn((
Text::new("Center View"),
TextFont {
font_size: BUTTON_FONT_SIZE,
..default()
},
TextColor(TEXT_BRIGHT),
));
});
// Help text.
parent.spawn((
Text::new("Drag to orbit (360°) • Scroll to zoom • Click a star • Esc to return"),
TextFont {
font_size: HELP_FONT_SIZE,
..default()
},
TextColor(TEXT_DIM),
Node {
margin: UiRect::top(Val::Px(10.0)),
..default()
},
));
});
}
/// Build a `[Label ........ value [-] [+]]` row. The `value_key` is stored in
/// the marker as a label for the value text child; the actual value is read
/// from `GalaxyParams` at button-press time and updated by
/// `refresh_control_panel_values` each frame.
fn spawn_param_row(
parent: &mut ChildSpawnerCommands,
label: &str,
value_key: &str,
decr: ParamButton,
incr: ParamButton,
) {
parent
.spawn(Node {
width: Val::Percent(100.0),
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(6.0),
..default()
})
.with_children(|row| {
// Label — fixed width so values align.
row.spawn((
Text::new(label),
TextFont {
font_size: LABEL_FONT_SIZE,
..default()
},
TextColor(TEXT_DIM),
Node {
width: Val::Px(118.0),
..default()
},
));
// Value — flexible width, right-aligned textually by spacer below.
row.spawn((
Text::new(""),
TextFont {
font_size: VALUE_FONT_SIZE,
..default()
},
TextColor(TEXT_BRIGHT),
Node {
flex_grow: 1.0,
..default()
},
ParamValue(value_key.to_string()),
));
// [-] button.
spawn_icon_button(row, "", decr);
// [+] button.
spawn_icon_button(row, "+", incr);
});
}
/// Marker on the value Text node so `refresh_control_panel_values` can find
/// and update it. The `String` is the param field name (e.g. "seed", "twist").
#[derive(Component)]
pub(crate) struct ParamValue(pub(crate) String);
fn spawn_icon_button(parent: &mut ChildSpawnerCommands, label: &str, marker: ParamButton) {
parent
.spawn((
Button,
Node {
width: Val::Px(26.0),
height: Val::Px(26.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(BUTTON_BG),
BorderColor(PANEL_BORDER),
BorderRadius::all(Val::Px(4.0)),
marker,
))
.with_children(|btn| {
btn.spawn((
Text::new(label),
TextFont {
font_size: BUTTON_FONT_SIZE,
..default()
},
TextColor(TEXT_BRIGHT),
));
});
}
/// Spawn the info panel in its "no selection" state. The selection module
/// replaces its contents whenever [`super::SelectedStar`] changes.
fn spawn_info_panel_empty(commands: &mut Commands) {
commands
.spawn((
Node {
position_type: PositionType::Absolute,
right: Val::Px(12.0),
top: Val::Px(12.0),
width: Val::Px(PANEL_WIDTH),
padding: UiRect::all(Val::Px(PANEL_PADDING)),
flex_direction: FlexDirection::Column,
row_gap: Val::Px(4.0),
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(PANEL_BG),
BorderColor(PANEL_BORDER),
BorderRadius::all(Val::Px(8.0)),
GalaxyInfoPanel,
GalaxySpawned,
))
.with_children(|parent| {
parent.spawn((
Text::new("Selected System"),
TextFont {
font_size: TITLE_FONT_SIZE,
..default()
},
TextColor(TEXT_BRIGHT),
Node {
margin: UiRect::bottom(Val::Px(8.0)),
..default()
},
));
parent.spawn((
Text::new("Click a star to inspect."),
TextFont {
font_size: LABEL_FONT_SIZE,
..default()
},
TextColor(TEXT_DIM),
));
});
}
// ── Button handler ──────────────────────────────────────────────────────────
pub fn param_button_handler(
mut params: ResMut<GalaxyParams>,
query: Query<(&Interaction, &ParamButton), Changed<Interaction>>,
) {
for (interaction, button) in &query {
let &Interaction::Pressed = interaction else {
continue;
};
match button {
ParamButton::SeedDecr => {
params.seed = params.seed.wrapping_sub(SEED_STEP);
params.bump_generation();
}
ParamButton::SeedIncr => {
params.seed = params.seed.wrapping_add(SEED_STEP);
params.bump_generation();
}
ParamButton::CountDecr => {
params.count = params.count.saturating_sub(COUNT_STEP).max(COUNT_MIN);
params.bump_generation();
}
ParamButton::CountIncr => {
params.count = (params.count + COUNT_STEP).min(COUNT_MAX);
params.bump_generation();
}
ParamButton::ArmsDecr => {
params.arms = params.arms.saturating_sub(1).max(ARMS_MIN);
params.bump_generation();
}
ParamButton::ArmsIncr => {
params.arms = params.arms.saturating_add(1).min(ARMS_MAX);
params.bump_generation();
}
ParamButton::VerticalArmsDecr => {
params.vertical_arms = params.vertical_arms.saturating_sub(1);
params.bump_generation();
}
ParamButton::VerticalArmsIncr => {
params.vertical_arms = params
.vertical_arms
.saturating_add(1)
.min(VERTICAL_ARMS_MAX);
params.bump_generation();
}
ParamButton::SizeDecr => {
params.size = (params.size - SIZE_STEP).max(SIZE_MIN);
params.bump_generation();
}
ParamButton::SizeIncr => {
params.size = (params.size + SIZE_STEP).min(SIZE_MAX);
params.bump_generation();
}
ParamButton::TwistDecr => {
params.twist = (params.twist - TWIST_STEP).max(TWIST_MIN);
params.bump_generation();
}
ParamButton::TwistIncr => {
params.twist = (params.twist + TWIST_STEP).min(TWIST_MAX);
params.bump_generation();
}
ParamButton::VerticalTwistDecr => {
params.vertical_twist = (params.vertical_twist - VERTICAL_TWIST_STEP).max(VERTICAL_TWIST_MIN);
params.bump_generation();
}
ParamButton::VerticalTwistIncr => {
params.vertical_twist = (params.vertical_twist + VERTICAL_TWIST_STEP).min(VERTICAL_TWIST_MAX);
params.bump_generation();
}
ParamButton::Regenerate => params.reseed_and_bump(),
// CenterView is handled by `reset_view_button_handler` (needs Commands).
ParamButton::CenterView => {}
}
}
}
/// Handle the CenterView button: insert [`ResetOrbitCamera`] so
/// [`crate::camera::apply_orbit_reset`] can snap the orbit camera back to
/// its default orientation next frame. Kept in a separate system because it
/// needs `Commands` (not just `ResMut<GalaxyParams>`).
pub fn reset_view_button_handler(
mut commands: Commands,
reset_flag: Option<Res<crate::camera::ResetOrbitCamera>>,
query: Query<(&Interaction, &ParamButton), Changed<Interaction>>,
) {
for (interaction, button) in &query {
if *interaction != Interaction::Pressed {
continue;
}
if matches!(button, ParamButton::CenterView) && reset_flag.is_none() {
commands.insert_resource(crate::camera::ResetOrbitCamera);
}
}
}
/// Refresh the displayed parameter values every frame so the UI stays in sync.
/// Bevy UI has no native data binding, so we manually patch the `Text` children
/// — cheap (≤14 nodes) and avoids re-spawning the whole panel.
pub fn refresh_control_panel_values(
params: Res<GalaxyParams>,
mut values: Query<(&ParamValue, &mut Text)>,
) {
for (marker, mut text) in &mut values {
let new = match marker.0.as_str() {
"seed" => format!("{}", params.seed),
"count" => format!("{}", params.count),
"arms" => format!("{}", params.arms),
"varms" => format!("{}", params.vertical_arms),
"size" => format!("{:.0}", params.size),
"twist" => format!("{:.1}", params.twist),
"vtwist" => format!("{:.1}", params.vertical_twist),
_ => continue,
};
if text.0 != new {
text.0 = new;
}
}
}
// ── State wiring ────────────────────────────────────────────────────────────
//
// `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.