diff --git a/apps/game/src/gameplay/in_system/flight.rs b/apps/game/src/gameplay/in_system/flight.rs index 7fdea4c..174daa2 100644 --- a/apps/game/src/gameplay/in_system/flight.rs +++ b/apps/game/src/gameplay/in_system/flight.rs @@ -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, mut focus_goal: ResMut, player_query: Query, With)>, - // 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, mut focus_goal: ResMut, 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::(); + 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); } diff --git a/apps/game/src/gameplay/in_system/flight_ui.rs b/apps/game/src/gameplay/in_system/flight_ui.rs index fc89d51..4877099 100644 --- a/apps/game/src/gameplay/in_system/flight_ui.rs +++ b/apps/game/src/gameplay/in_system/flight_ui.rs @@ -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>, @@ -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>) { - for entity in &query { - commands.entity(entity).despawn(); - } -} diff --git a/apps/game/src/gameplay/in_system/hud.rs b/apps/game/src/gameplay/in_system/hud.rs new file mode 100644 index 0000000..e00ce7d --- /dev/null +++ b/apps/game/src/gameplay/in_system/hud.rs @@ -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>, + flying_player: Query<&FlightState, With>, + docked_panels: Query>, + flight_panels: Query>, +) { + 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>, + flight_panels: Query>, +) { + for entity in &docked_panels { + commands.entity(entity).despawn(); + } + for entity in &flight_panels { + commands.entity(entity).despawn(); + } +} diff --git a/apps/game/src/gameplay/in_system/mod.rs b/apps/game/src/gameplay/in_system/mod.rs index 456d4f6..72ceb5a 100644 --- a/apps/game/src/gameplay/in_system/mod.rs +++ b/apps/game/src/gameplay/in_system/mod.rs @@ -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, player_query: Query>, - durations: Res, - mut operation_events: EventWriter, selected_target_query: Query<&SelectedTarget, With>, + 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, + docked: Res, + mut undock_events: EventWriter, +) { + 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( diff --git a/apps/game/src/gameplay/in_system/operations.rs b/apps/game/src/gameplay/in_system/operations.rs index d3583db..c24d78f 100644 --- a/apps/game/src/gameplay/in_system/operations.rs +++ b/apps/game/src/gameplay/in_system/operations.rs @@ -83,13 +83,12 @@ impl Plugin for TimedOperationPlugin { fn monitor_operations( time: Res