Merge agent branch agent/u1-8a3c7cf2 into main (U1: Basic UI Shell)
This commit is contained in:
@@ -13,8 +13,6 @@ use crate::gameplay::movement::components::{Velocity, MoveTarget};
|
||||
use crate::gameplay::galaxy::Identifiable;
|
||||
use super::{DockedState, UndockEvent};
|
||||
use super::scene::{Docked, PlayerShip};
|
||||
// UI removed - no longer needed
|
||||
// use super::flight_ui::setup_flight_ui;
|
||||
|
||||
/// Flight state component attached to the player ship when actively flying.
|
||||
#[derive(Component, Debug, Clone, Default)]
|
||||
@@ -55,7 +53,6 @@ fn handle_undock(
|
||||
mut camera_state: ResMut<CameraState>,
|
||||
mut focus_goal: ResMut<OrbitFocusGoal>,
|
||||
player_query: Query<Entity, (With<PlayerShip>, With<Docked>)>,
|
||||
// docked_ui_query removed - UI no longer needed
|
||||
) {
|
||||
for event in events.read() {
|
||||
bevy::log::info!("Handling undock from station {:?}", event.station_entity);
|
||||
@@ -102,13 +99,8 @@ fn handle_undock(
|
||||
Some(crate::camera::tactical_rotation()),
|
||||
);
|
||||
|
||||
// UI removed - gameplay only
|
||||
// setup_flight_ui(commands.reborrow());
|
||||
|
||||
// Despawn docked UI (commented out - UI being removed)
|
||||
// for entity in docked_ui_query.iter() {
|
||||
// commands.entity(entity).despawn();
|
||||
// }
|
||||
// The flight HUD is spawned reactively by `hud::sync_hud_panels`
|
||||
// once it sees the player's new `FlightState` component.
|
||||
|
||||
bevy::log::info!("Transitioned to flight mode");
|
||||
}
|
||||
@@ -122,7 +114,6 @@ fn handle_docking(
|
||||
mut camera_state: ResMut<CameraState>,
|
||||
mut focus_goal: ResMut<OrbitFocusGoal>,
|
||||
identifiable_query: Query<&Identifiable>,
|
||||
// flight_ui_query removed - UI no longer needed
|
||||
) {
|
||||
for event in events.read() {
|
||||
bevy::log::info!("Handling docking at target {:?}", event.station);
|
||||
@@ -132,14 +123,14 @@ fn handle_docking(
|
||||
continue;
|
||||
};
|
||||
|
||||
// Add docked component
|
||||
// Add docked component and clear all flight components so the
|
||||
// reactive HUD swap hides the flight HUD (and the ship stops moving).
|
||||
commands.entity(event.ship).insert(Docked {
|
||||
station_entity: event.station,
|
||||
});
|
||||
|
||||
// Remove flight state
|
||||
commands.entity(event.ship)
|
||||
.remove::<FlightState>();
|
||||
commands
|
||||
.entity(event.ship)
|
||||
.remove::<(FlightState, Velocity, MoveTarget, super::SelectedTarget)>();
|
||||
|
||||
// Update docked state resource
|
||||
docked_state.dock_at(event.station);
|
||||
@@ -154,13 +145,8 @@ fn handle_docking(
|
||||
Some(crate::camera::tactical_rotation()),
|
||||
);
|
||||
|
||||
// UI removed - no longer needed
|
||||
// Despawn flight HUD
|
||||
// for entity in flight_ui_query.iter() {
|
||||
// commands.entity(entity).despawn();
|
||||
// }
|
||||
// Respawn docked UI
|
||||
// super::ui::setup_docked_ui(commands.reborrow());
|
||||
// The station info panel is spawned reactively by
|
||||
// `hud::sync_hud_panels` once it sees the player's `Docked` component.
|
||||
|
||||
bevy::log::info!("Docked at {}", identifiable.display_name);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
//! Flight HUD and UI elements for in-flight gameplay.
|
||||
//! Flight HUD for in-flight gameplay.
|
||||
//!
|
||||
//! Shown while the player ship has a [`FlightState`](super::FlightState)
|
||||
//! component. Spawned/despawned reactively by the [`super::hud`] module.
|
||||
//!
|
||||
//! The speed indicator (bottom-left) and status panel (top-left) are spawned
|
||||
//! as independent top-level overlays with no full-screen root, so they only
|
||||
//! block camera input when the cursor is directly over a panel.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
@@ -10,7 +17,7 @@ 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 ACCENT: Color = Color::srgb(0.30, 0.72, 0.45);
|
||||
|
||||
/// Marker for flight HUD entities.
|
||||
/// Marker for every flight HUD panel entity (spawned while flying).
|
||||
#[derive(Component)]
|
||||
pub struct FlightUi;
|
||||
|
||||
@@ -22,32 +29,16 @@ pub struct SpeedDisplay;
|
||||
#[derive(Component)]
|
||||
pub struct StatusPanel;
|
||||
|
||||
/// Setup the flight HUD when entering flight mode.
|
||||
/// Spawn the flight HUD panels.
|
||||
pub fn setup_flight_ui(mut commands: Commands) {
|
||||
bevy::log::info!("Setting up flight HUD");
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
Interaction::None, // Disable interaction - let clicks pass through
|
||||
FlightUi,
|
||||
))
|
||||
.with_children(|root| {
|
||||
// Speed indicator (bottom left)
|
||||
spawn_speed_indicator(root);
|
||||
|
||||
// Status panel (top left)
|
||||
spawn_status_panel(root);
|
||||
});
|
||||
spawn_speed_indicator(&mut commands);
|
||||
spawn_status_panel(&mut commands);
|
||||
}
|
||||
|
||||
fn spawn_speed_indicator(parent: &mut ChildSpawnerCommands) {
|
||||
parent
|
||||
/// Speed indicator (bottom-left).
|
||||
fn spawn_speed_indicator(commands: &mut Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
@@ -64,7 +55,6 @@ fn spawn_speed_indicator(parent: &mut ChildSpawnerCommands) {
|
||||
FlightUi,
|
||||
))
|
||||
.with_children(|panel| {
|
||||
// Label
|
||||
panel.spawn((
|
||||
Text::new("SPEED"),
|
||||
TextFont {
|
||||
@@ -73,8 +63,6 @@ fn spawn_speed_indicator(parent: &mut ChildSpawnerCommands) {
|
||||
},
|
||||
TextColor(TEXT_DIM),
|
||||
));
|
||||
|
||||
// Speed value
|
||||
panel.spawn((
|
||||
Text::new("0 m/s"),
|
||||
TextFont {
|
||||
@@ -87,8 +75,9 @@ fn spawn_speed_indicator(parent: &mut ChildSpawnerCommands) {
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_status_panel(parent: &mut ChildSpawnerCommands) {
|
||||
parent
|
||||
/// Status panel (top-left).
|
||||
fn spawn_status_panel(commands: &mut Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
@@ -106,7 +95,6 @@ fn spawn_status_panel(parent: &mut ChildSpawnerCommands) {
|
||||
StatusPanel,
|
||||
))
|
||||
.with_children(|panel| {
|
||||
// Status label
|
||||
panel.spawn((
|
||||
Text::new("FLIGHT MODE"),
|
||||
TextFont {
|
||||
@@ -115,8 +103,6 @@ fn spawn_status_panel(parent: &mut ChildSpawnerCommands) {
|
||||
},
|
||||
TextColor(ACCENT),
|
||||
));
|
||||
|
||||
// Flight status text
|
||||
panel.spawn((
|
||||
Text::new("Active"),
|
||||
TextFont {
|
||||
@@ -125,29 +111,18 @@ fn spawn_status_panel(parent: &mut ChildSpawnerCommands) {
|
||||
},
|
||||
TextColor(TEXT_BRIGHT),
|
||||
));
|
||||
|
||||
// Controls hint
|
||||
panel.spawn((
|
||||
Node {
|
||||
margin: UiRect::top(Val::Px(12.0)),
|
||||
Text::new("Click: Set destination\nOrbit camera: Drag to rotate"),
|
||||
TextFont {
|
||||
font_size: 11.0,
|
||||
..default()
|
||||
},
|
||||
FlightUi,
|
||||
))
|
||||
.with_children(|hint| {
|
||||
hint.spawn((
|
||||
Text::new("Click: Set destination\nOrbit camera: Drag to rotate"),
|
||||
TextFont {
|
||||
font_size: 11.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_DIM),
|
||||
));
|
||||
});
|
||||
TextColor(TEXT_DIM),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/// Update flight HUD elements with current flight state.
|
||||
/// Update the flight HUD speed readout from the player's [`FlightState`].
|
||||
pub fn update_flight_ui(
|
||||
flight_query: Query<&FlightState>,
|
||||
mut speed_query: Query<&mut Text, With<SpeedDisplay>>,
|
||||
@@ -156,15 +131,7 @@ pub fn update_flight_ui(
|
||||
return;
|
||||
};
|
||||
|
||||
// Update speed display
|
||||
if let Ok(mut speed_text) = speed_query.single_mut() {
|
||||
speed_text.0 = format!("{:.0} m/s", flight_state.current_speed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Despawn flight HUD when exiting flight mode.
|
||||
pub fn despawn_flight_ui(mut commands: Commands, query: Query<Entity, With<FlightUi>>) {
|
||||
for entity in &query {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
84
apps/game/src/gameplay/in_system/hud.rs
Normal file
84
apps/game/src/gameplay/in_system/hud.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! In-system HUD orchestration: the panel swap between the station (docked)
|
||||
//! info panel and the flight HUD.
|
||||
//!
|
||||
//! Inside a system the player is always in exactly one mode —
|
||||
//! [`Docked`](super::scene::Docked) at a station, or in active
|
||||
//! [`FlightState`](super::FlightState). This module keeps the on-screen HUD in
|
||||
//! sync with that state: the station info panel ([`super::ui`]) is shown while
|
||||
//! docked, the flight HUD ([`super::flight_ui`]) while flying. The contextual
|
||||
//! action panel ([`super::actions`]) is always present and adapts its buttons
|
||||
//! to the current mode.
|
||||
//!
|
||||
//! The swap is **reactive and idempotent**: a single [`Update`] system compares
|
||||
//! the player's mode against the panels currently on screen and only
|
||||
//! spawns/despawns when they disagree. This is more robust than scattering
|
||||
//! spawn/despawn calls through the undock/dock transition systems, because any
|
||||
//! code path that flips the player's `Docked`/`FlightState` components is
|
||||
//! automatically reflected in the UI a frame later.
|
||||
//!
|
||||
//! Both panels are spawned as small, corner-anchored overlays with no
|
||||
//! full-screen root, so they never block camera orbit input except when the
|
||||
//! cursor is directly over a panel (see `cursor_over_ui` in `camera.rs`).
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::scene::{Docked, PlayerShip};
|
||||
use super::{FlightState, flight_ui, ui};
|
||||
|
||||
/// Keep the on-screen HUD matched to the player's docking/flight mode.
|
||||
///
|
||||
/// - Docked → the station info panel ([`ui::DockedUi`]) is shown.
|
||||
/// - Flying → the flight HUD ([`flight_ui::FlightUi`]) is shown.
|
||||
///
|
||||
/// Cheap to run every frame: it only mutates the world when the on-screen
|
||||
/// panel disagrees with the player's state.
|
||||
pub fn sync_hud_panels(
|
||||
mut commands: Commands,
|
||||
docked_player: Query<&Docked, With<PlayerShip>>,
|
||||
flying_player: Query<&FlightState, With<PlayerShip>>,
|
||||
docked_panels: Query<Entity, With<ui::DockedUi>>,
|
||||
flight_panels: Query<Entity, With<flight_ui::FlightUi>>,
|
||||
) {
|
||||
let is_docked = docked_player.single().is_ok();
|
||||
let is_flying = flying_player.single().is_ok();
|
||||
|
||||
// Station info panel ↔ docked mode.
|
||||
if is_docked {
|
||||
if docked_panels.is_empty() {
|
||||
ui::setup_docked_ui(commands.reborrow());
|
||||
}
|
||||
} else {
|
||||
for entity in &docked_panels {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
// Flight HUD ↔ flight mode.
|
||||
if is_flying {
|
||||
if flight_panels.is_empty() {
|
||||
flight_ui::setup_flight_ui(commands.reborrow());
|
||||
}
|
||||
} else {
|
||||
for entity in &flight_panels {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tear down every in-system HUD panel when leaving the scene.
|
||||
///
|
||||
/// The reactive [`sync_hud_panels`] system stops running once we leave
|
||||
/// [`AppState::InGame`](crate::state::AppState::InGame), so any panel still on
|
||||
/// screen is removed explicitly here.
|
||||
pub fn despawn_in_system_hud(
|
||||
mut commands: Commands,
|
||||
docked_panels: Query<Entity, With<ui::DockedUi>>,
|
||||
flight_panels: Query<Entity, With<flight_ui::FlightUi>>,
|
||||
) {
|
||||
for entity in &docked_panels {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
for entity in &flight_panels {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@ mod actions;
|
||||
mod docked;
|
||||
mod flight;
|
||||
mod flight_ui;
|
||||
mod hud;
|
||||
mod operations;
|
||||
mod scene;
|
||||
mod target;
|
||||
mod ui;
|
||||
|
||||
use bevy::ecs::system::SystemParam;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::state::AppState;
|
||||
@@ -22,7 +24,7 @@ pub use flight::{FlightState, FlightControlsPlugin};
|
||||
pub use scene::ActiveSystem;
|
||||
pub use target::{Targetable, TargetKind, SelectedTarget, TargetSelectionPlugin};
|
||||
pub use actions::{ContextualActionPlugin, ActionType, ActionTriggeredEvent, ActionUi};
|
||||
pub use operations::{TimedOperationPlugin, OperationKind, ActiveOperation};
|
||||
pub use operations::TimedOperationPlugin;
|
||||
|
||||
pub struct InSystemPlugin;
|
||||
|
||||
@@ -37,29 +39,19 @@ impl Plugin for InSystemPlugin {
|
||||
.add_plugins(TimedOperationPlugin)
|
||||
.add_systems(
|
||||
OnEnter(AppState::InGame),
|
||||
(
|
||||
scene::setup_in_system_view,
|
||||
// UI removed - no longer needed
|
||||
// ui::setup_docked_ui,
|
||||
add_targetable_to_pois,
|
||||
).chain(),
|
||||
(scene::setup_in_system_view, add_targetable_to_pois).chain(),
|
||||
)
|
||||
.add_systems(
|
||||
OnExit(AppState::InGame),
|
||||
(
|
||||
// UI removed - no longer needed
|
||||
// ui::despawn_docked_ui,
|
||||
// flight_ui::despawn_flight_ui,
|
||||
scene::despawn_in_system_scene,
|
||||
).chain(),
|
||||
(hud::despawn_in_system_hud, scene::despawn_in_system_scene).chain(),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
// UI removed - no longer needed
|
||||
// ui::refresh_docked_ui,
|
||||
// ui::undock_button_handler,
|
||||
// flight_ui::update_flight_ui,
|
||||
hud::sync_hud_panels,
|
||||
ui::refresh_docked_ui,
|
||||
flight_ui::update_flight_ui,
|
||||
complete_operations,
|
||||
handle_action_triggered,
|
||||
)
|
||||
.chain()
|
||||
@@ -68,38 +60,77 @@ impl Plugin for InSystemPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle action triggered events from the action buttons.
|
||||
/// Bundles the resources/event-writers [`handle_action_triggered`] dispatches
|
||||
/// through, keeping its parameter list under the system-argument threshold.
|
||||
#[derive(SystemParam)]
|
||||
struct ActionDispatch<'w> {
|
||||
time: Res<'w, Time>,
|
||||
durations: Res<'w, operations::OperationDurations>,
|
||||
started: EventWriter<'w, operations::OperationStartedEvent>,
|
||||
dock: EventWriter<'w, flight::DockEvent>,
|
||||
}
|
||||
|
||||
/// Handle action triggered events from the contextual action buttons.
|
||||
///
|
||||
/// - **Undock** starts a timed operation; on completion [`complete_operations`]
|
||||
/// fires the [`UndockEvent`] that drives the docked → flight transition.
|
||||
/// - **Dock** fires a [`flight::DockEvent`] directly (instant) for the selected
|
||||
/// station / habitable planet so the flight → docked transition (and the HUD
|
||||
/// swap back to the station panel) is reachable.
|
||||
fn handle_action_triggered(
|
||||
mut commands: Commands,
|
||||
mut events: EventReader<ActionTriggeredEvent>,
|
||||
player_query: Query<Entity, With<scene::PlayerShip>>,
|
||||
durations: Res<operations::OperationDurations>,
|
||||
mut operation_events: EventWriter<operations::OperationStartedEvent>,
|
||||
selected_target_query: Query<&SelectedTarget, With<scene::PlayerShip>>,
|
||||
mut dispatch: ActionDispatch,
|
||||
) {
|
||||
let Ok(player_entity) = player_query.single() else {
|
||||
return;
|
||||
};
|
||||
let now_ms = dispatch.time.elapsed_secs_f64() * 1000.0;
|
||||
|
||||
for event in events.read() {
|
||||
bevy::log::info!("Action triggered: {:?}", event.action_type);
|
||||
|
||||
match event.action_type {
|
||||
ActionType::Undock => {
|
||||
operations::start_undocking(&mut commands, player_entity, &durations, &mut operation_events);
|
||||
operations::start_undocking(
|
||||
&mut commands,
|
||||
player_entity,
|
||||
&dispatch.durations,
|
||||
&mut dispatch.started,
|
||||
now_ms,
|
||||
);
|
||||
}
|
||||
ActionType::Approach => {
|
||||
// Get selected target
|
||||
if let Ok(selected) = selected_target_query.single() {
|
||||
operations::start_travel(&mut commands, player_entity, selected.name.clone(), &durations, &mut operation_events);
|
||||
operations::start_travel(
|
||||
&mut commands,
|
||||
player_entity,
|
||||
selected.name.clone(),
|
||||
&dispatch.durations,
|
||||
&mut dispatch.started,
|
||||
now_ms,
|
||||
);
|
||||
}
|
||||
}
|
||||
ActionType::Dock => {
|
||||
// TODO: Implement docking operation
|
||||
bevy::log::info!("Dock action triggered");
|
||||
if let Ok(selected) = selected_target_query.single() {
|
||||
match selected.kind {
|
||||
TargetKind::Station | TargetKind::HabitablePlanet => {
|
||||
bevy::log::info!("Docking at {}", selected.name);
|
||||
dispatch.dock.write(flight::DockEvent {
|
||||
ship: player_entity,
|
||||
station: selected.entity,
|
||||
});
|
||||
}
|
||||
kind => {
|
||||
bevy::log::info!("Cannot dock at {:?} target", kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ActionType::StartMining => {
|
||||
// TODO: Implement mining operation
|
||||
bevy::log::info!("Start mining action triggered");
|
||||
}
|
||||
_ => {
|
||||
@@ -109,6 +140,29 @@ fn handle_action_triggered(
|
||||
}
|
||||
}
|
||||
|
||||
/// Bridge completed timed operations back into the docked ↔ flight state
|
||||
/// machine. Today only undocking is wired — when the timed undock completes it
|
||||
/// fires the [`UndockEvent`] consumed by [`flight::handle_undock`]. Other
|
||||
/// operation kinds are logged for future work.
|
||||
fn complete_operations(
|
||||
mut completed: EventReader<operations::OperationCompletedEvent>,
|
||||
docked: Res<DockedState>,
|
||||
mut undock_events: EventWriter<UndockEvent>,
|
||||
) {
|
||||
for event in completed.read() {
|
||||
bevy::log::info!(
|
||||
"Operation completed: {:?} on entity {:?}",
|
||||
event.kind,
|
||||
event.entity
|
||||
);
|
||||
if let operations::OperationKind::Undocking = event.kind {
|
||||
undock_events.write(UndockEvent {
|
||||
station_entity: docked.station_entity.unwrap_or(Entity::PLACEHOLDER),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add Targetable components to spawned POIs (stations and asteroid belts)
|
||||
/// so they can be selected by the target selection system.
|
||||
fn add_targetable_to_pois(
|
||||
|
||||
@@ -83,13 +83,12 @@ impl Plugin for TimedOperationPlugin {
|
||||
fn monitor_operations(
|
||||
time: Res<Time>,
|
||||
mut commands: Commands,
|
||||
mut query: Query<(Entity, &mut ActiveOperation)>,
|
||||
query: Query<(Entity, &ActiveOperation)>,
|
||||
mut completed_events: EventWriter<OperationCompletedEvent>,
|
||||
durations: Res<OperationDurations>,
|
||||
) {
|
||||
let now = time.elapsed_secs_f64() * 1000.0;
|
||||
|
||||
for (entity, mut operation) in query.iter_mut() {
|
||||
for (entity, operation) in &query {
|
||||
let elapsed = now - operation.started_at;
|
||||
|
||||
if elapsed >= operation.duration_ms {
|
||||
@@ -127,11 +126,12 @@ pub fn start_undocking(
|
||||
commands: &mut Commands,
|
||||
player_entity: Entity,
|
||||
durations: &OperationDurations,
|
||||
mut events: &mut EventWriter<OperationStartedEvent>,
|
||||
events: &mut EventWriter<OperationStartedEvent>,
|
||||
now_ms: f64,
|
||||
) {
|
||||
commands.entity(player_entity).insert(ActiveOperation {
|
||||
kind: OperationKind::Undocking,
|
||||
started_at: 0.0, // Will be set by the system
|
||||
started_at: now_ms,
|
||||
duration_ms: durations.undock,
|
||||
target_name: "Undocking".to_string(),
|
||||
});
|
||||
@@ -149,11 +149,12 @@ pub fn start_travel(
|
||||
player_entity: Entity,
|
||||
target_name: String,
|
||||
durations: &OperationDurations,
|
||||
mut events: &mut EventWriter<OperationStartedEvent>,
|
||||
events: &mut EventWriter<OperationStartedEvent>,
|
||||
now_ms: f64,
|
||||
) {
|
||||
commands.entity(player_entity).insert(ActiveOperation {
|
||||
kind: OperationKind::Travel,
|
||||
started_at: 0.0,
|
||||
started_at: now_ms,
|
||||
duration_ms: durations.local_travel,
|
||||
target_name: target_name.clone(),
|
||||
});
|
||||
|
||||
@@ -1,57 +1,43 @@
|
||||
//! UI for the docked state within a station.
|
||||
//! Station info panel for the docked state.
|
||||
//!
|
||||
//! Shown while the player is [`Docked`](super::scene::Docked) at a station.
|
||||
//! This panel is info-only — all actions (undock, fitting, market, …) live in
|
||||
//! the contextual action panel ([`super::actions`]), which adapts its buttons
|
||||
//! to the current mode. The [`super::hud`] module spawns/despawns this panel
|
||||
//! reactively based on the player's docking state.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::gameplay::in_system::{DockedState, UndockEvent};
|
||||
use crate::gameplay::campaign::CampaignDraft;
|
||||
use crate::gameplay::in_system::DockedState;
|
||||
|
||||
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.28, 0.22);
|
||||
const BUTTON_BORDER: Color = Color::srgb(0.30, 0.72, 0.45);
|
||||
const BUTTON_HOVER_BG: Color = Color::srgb(0.15, 0.35, 0.28);
|
||||
|
||||
const TITLE_FONT_SIZE: f32 = 24.0;
|
||||
const SUBTITLE_FONT_SIZE: f32 = 14.0;
|
||||
const BODY_FONT_SIZE: f32 = 14.0;
|
||||
const BUTTON_FONT_SIZE: f32 = 16.0;
|
||||
|
||||
const PANEL_WIDTH: f32 = 360.0;
|
||||
const SIDE_MARGIN: f32 = 20.0;
|
||||
const TOP_MARGIN: f32 = 20.0;
|
||||
|
||||
/// Marker for UI entities spawned in the docked view.
|
||||
/// Marker for the station info panel entity (spawned while docked).
|
||||
#[derive(Component)]
|
||||
pub struct DockedUi;
|
||||
|
||||
/// Marker for the station details text.
|
||||
/// Marker for the station details text, refreshed each frame from the
|
||||
/// [`CampaignDraft`] / [`DockedState`].
|
||||
#[derive(Component)]
|
||||
pub struct StationDetailsText;
|
||||
|
||||
/// Marker for the undock button.
|
||||
#[derive(Component)]
|
||||
pub struct UndockButton;
|
||||
|
||||
/// Spawn the station info panel as a top-level, corner-anchored overlay.
|
||||
///
|
||||
/// No full-screen root: the panel is a small absolute-positioned node so it
|
||||
/// only blocks camera input when the cursor is directly over it.
|
||||
pub fn setup_docked_ui(mut commands: Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
DockedUi,
|
||||
))
|
||||
.with_children(|root| {
|
||||
spawn_station_panel(root);
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_station_panel(parent: &mut ChildSpawnerCommands) {
|
||||
parent
|
||||
.spawn((
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
@@ -80,7 +66,7 @@ fn spawn_station_panel(parent: &mut ChildSpawnerCommands) {
|
||||
TextColor(TEXT_BRIGHT),
|
||||
));
|
||||
|
||||
// Station details placeholder (will be updated by refresh_docked_ui)
|
||||
// Station details (refreshed by `refresh_docked_ui`).
|
||||
panel.spawn((
|
||||
Text::new("Loading station data..."),
|
||||
TextFont {
|
||||
@@ -90,55 +76,25 @@ fn spawn_station_panel(parent: &mut ChildSpawnerCommands) {
|
||||
TextColor(TEXT_DIM),
|
||||
StationDetailsText,
|
||||
));
|
||||
|
||||
// Spacer
|
||||
panel.spawn(Node {
|
||||
flex_grow: 1.0,
|
||||
..default()
|
||||
});
|
||||
|
||||
// Undock button
|
||||
panel.spawn((
|
||||
Button,
|
||||
Node {
|
||||
width: Val::Px(160.0),
|
||||
height: Val::Px(44.0),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
border: UiRect::all(Val::Px(1.0)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(BUTTON_BG),
|
||||
BorderColor(BUTTON_BORDER),
|
||||
BorderRadius::all(Val::Px(6.0)),
|
||||
UndockButton,
|
||||
))
|
||||
.with_children(|button| {
|
||||
button.spawn((
|
||||
Text::new("Undock"),
|
||||
TextFont {
|
||||
font_size: BUTTON_FONT_SIZE,
|
||||
..default()
|
||||
},
|
||||
TextColor(TEXT_BRIGHT),
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Refresh the station details text from the current campaign / docked state.
|
||||
///
|
||||
/// Only writes when the underlying data actually changed, so it is cheap to
|
||||
/// run every frame.
|
||||
pub fn refresh_docked_ui(
|
||||
campaign: Res<CampaignDraft>,
|
||||
docked_state: Res<DockedState>,
|
||||
mut query: Query<&mut Text, With<StationDetailsText>>,
|
||||
) {
|
||||
// Only update when something changed
|
||||
// Only update when something changed.
|
||||
if !campaign.is_changed() && !docked_state.is_changed() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut text = match query.get_single_mut() {
|
||||
Ok(t) => t,
|
||||
Err(_) => return,
|
||||
let Ok(mut text) = query.single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(starting_base) = &campaign.starting_base else {
|
||||
@@ -151,22 +107,25 @@ pub fn refresh_docked_ui(
|
||||
return;
|
||||
};
|
||||
|
||||
// Find the system
|
||||
let system_index = match galaxy.systems.iter().position(|s| s.id == starting_base.system_id) {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
text.0 = format!("System {} not found.", starting_base.system_id);
|
||||
return;
|
||||
}
|
||||
// Find the system.
|
||||
let Some(system_index) = galaxy
|
||||
.systems
|
||||
.iter()
|
||||
.position(|s| s.id == starting_base.system_id)
|
||||
else {
|
||||
text.0 = format!("System {} not found.", starting_base.system_id);
|
||||
return;
|
||||
};
|
||||
|
||||
let system = &galaxy.systems[system_index];
|
||||
let contents = &galaxy.contents[system_index];
|
||||
|
||||
// Find the docking station (highest population)
|
||||
// Docking station = highest-population station in the system.
|
||||
let station = contents.stations.iter().max_by_key(|s| s.population);
|
||||
|
||||
let station_name = station.map(|s| s.name.as_str()).unwrap_or("Unknown Station");
|
||||
let station_name = station
|
||||
.map(|s| s.name.as_str())
|
||||
.unwrap_or("Unknown Station");
|
||||
let station_pop = station.map(|s| s.population).unwrap_or(0);
|
||||
|
||||
text.0 = format!(
|
||||
@@ -177,32 +136,6 @@ pub fn refresh_docked_ui(
|
||||
system.id,
|
||||
system.faction,
|
||||
system.security,
|
||||
station_pop
|
||||
station_pop,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn undock_button_handler(
|
||||
mut commands: Commands,
|
||||
mut events: EventWriter<UndockEvent>,
|
||||
docked_state: Res<DockedState>,
|
||||
query: Query<(&Interaction, &UndockButton), Changed<Interaction>>,
|
||||
) {
|
||||
for (interaction, _) in &query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
bevy::log::info!("Undock button pressed");
|
||||
|
||||
// Fire the undock event (handled by handle_undock system)
|
||||
if let Some(station_entity) = docked_state.station_entity {
|
||||
events.send(UndockEvent { station_entity });
|
||||
} else {
|
||||
bevy::log::warn!("No station entity to undock from");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn despawn_docked_ui(mut commands: Commands, query: Query<Entity, With<DockedUi>>) {
|
||||
for entity in &query {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user