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:
445
apps/game/src/gameplay/galaxy/ui.rs
Normal file
445
apps/game/src/gameplay/galaxy/ui.rs
Normal 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.
|
||||
Reference in New Issue
Block a user