feat(game): restore in-system HUD with reactive station↔flight panel swap (U1)
Basic UI Shell: bring back the in-game flight HUD and station info panel, and implement the missing "station mode panel swap." Implementation: - New `in_system/hud.rs` with a reactive, idempotent `sync_hud_panels` system: it shows the station info panel while the player is `Docked` and the flight HUD while in `FlightState`, spawning/despawning only when the on-screen panel disagrees with the player's state. This replaces the old fragile event-driven spawn/despawn scattered through the transition systems. - Re-enabled the docked info panel (`ui.rs`) and flight HUD (`flight_ui.rs`), restructured both as small corner-anchored overlays with NO full-screen root so they only block camera orbit input when the cursor is directly over a panel (orbit_camera_control suppresses input over any UI node). - Made the docked panel info-only (actions live in the contextual action panel, which adapts to docked/flight modes). - Fixed the broken undock chain: `start_undocking`/`start_travel` now take a real `now_ms` (was hardcoded `0.0`, completing instantly) and a new `complete_operations` bridge maps `OperationCompletedEvent::Undocking` back to `UndockEvent` so `handle_undock` actually runs. - Wired `ActionType::Dock` to fire a `DockEvent` for a targeted station / habitable planet so the flight→docked swap is reachable too. - `handle_docking` now also clears Velocity/MoveTarget/SelectedTarget so the docked ship stops cleanly. - Fixed deprecated Bevy 0.16 APIs (single_mut, EventWriter::write, despawn) and removed now-dead code, dropping total warnings 183→137. Compiles clean (`cargo check`); no new warnings in touched files.
This commit is contained in:
@@ -13,8 +13,6 @@ use crate::gameplay::movement::components::{Velocity, MoveTarget};
|
|||||||
use crate::gameplay::galaxy::Identifiable;
|
use crate::gameplay::galaxy::Identifiable;
|
||||||
use super::{DockedState, UndockEvent};
|
use super::{DockedState, UndockEvent};
|
||||||
use super::scene::{Docked, PlayerShip};
|
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.
|
/// Flight state component attached to the player ship when actively flying.
|
||||||
#[derive(Component, Debug, Clone, Default)]
|
#[derive(Component, Debug, Clone, Default)]
|
||||||
@@ -55,7 +53,6 @@ fn handle_undock(
|
|||||||
mut camera_state: ResMut<CameraState>,
|
mut camera_state: ResMut<CameraState>,
|
||||||
mut focus_goal: ResMut<OrbitFocusGoal>,
|
mut focus_goal: ResMut<OrbitFocusGoal>,
|
||||||
player_query: Query<Entity, (With<PlayerShip>, With<Docked>)>,
|
player_query: Query<Entity, (With<PlayerShip>, With<Docked>)>,
|
||||||
// docked_ui_query removed - UI no longer needed
|
|
||||||
) {
|
) {
|
||||||
for event in events.read() {
|
for event in events.read() {
|
||||||
bevy::log::info!("Handling undock from station {:?}", event.station_entity);
|
bevy::log::info!("Handling undock from station {:?}", event.station_entity);
|
||||||
@@ -102,13 +99,8 @@ fn handle_undock(
|
|||||||
Some(crate::camera::tactical_rotation()),
|
Some(crate::camera::tactical_rotation()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// UI removed - gameplay only
|
// The flight HUD is spawned reactively by `hud::sync_hud_panels`
|
||||||
// setup_flight_ui(commands.reborrow());
|
// once it sees the player's new `FlightState` component.
|
||||||
|
|
||||||
// Despawn docked UI (commented out - UI being removed)
|
|
||||||
// for entity in docked_ui_query.iter() {
|
|
||||||
// commands.entity(entity).despawn();
|
|
||||||
// }
|
|
||||||
|
|
||||||
bevy::log::info!("Transitioned to flight mode");
|
bevy::log::info!("Transitioned to flight mode");
|
||||||
}
|
}
|
||||||
@@ -122,7 +114,6 @@ fn handle_docking(
|
|||||||
mut camera_state: ResMut<CameraState>,
|
mut camera_state: ResMut<CameraState>,
|
||||||
mut focus_goal: ResMut<OrbitFocusGoal>,
|
mut focus_goal: ResMut<OrbitFocusGoal>,
|
||||||
identifiable_query: Query<&Identifiable>,
|
identifiable_query: Query<&Identifiable>,
|
||||||
// flight_ui_query removed - UI no longer needed
|
|
||||||
) {
|
) {
|
||||||
for event in events.read() {
|
for event in events.read() {
|
||||||
bevy::log::info!("Handling docking at target {:?}", event.station);
|
bevy::log::info!("Handling docking at target {:?}", event.station);
|
||||||
@@ -132,14 +123,14 @@ fn handle_docking(
|
|||||||
continue;
|
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 {
|
commands.entity(event.ship).insert(Docked {
|
||||||
station_entity: event.station,
|
station_entity: event.station,
|
||||||
});
|
});
|
||||||
|
commands
|
||||||
// Remove flight state
|
.entity(event.ship)
|
||||||
commands.entity(event.ship)
|
.remove::<(FlightState, Velocity, MoveTarget, super::SelectedTarget)>();
|
||||||
.remove::<FlightState>();
|
|
||||||
|
|
||||||
// Update docked state resource
|
// Update docked state resource
|
||||||
docked_state.dock_at(event.station);
|
docked_state.dock_at(event.station);
|
||||||
@@ -154,13 +145,8 @@ fn handle_docking(
|
|||||||
Some(crate::camera::tactical_rotation()),
|
Some(crate::camera::tactical_rotation()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// UI removed - no longer needed
|
// The station info panel is spawned reactively by
|
||||||
// Despawn flight HUD
|
// `hud::sync_hud_panels` once it sees the player's `Docked` component.
|
||||||
// for entity in flight_ui_query.iter() {
|
|
||||||
// commands.entity(entity).despawn();
|
|
||||||
// }
|
|
||||||
// Respawn docked UI
|
|
||||||
// super::ui::setup_docked_ui(commands.reborrow());
|
|
||||||
|
|
||||||
bevy::log::info!("Docked at {}", identifiable.display_name);
|
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::*;
|
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 TEXT_DIM: Color = Color::srgb(0.55, 0.65, 0.82);
|
||||||
const ACCENT: Color = Color::srgb(0.30, 0.72, 0.45);
|
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)]
|
#[derive(Component)]
|
||||||
pub struct FlightUi;
|
pub struct FlightUi;
|
||||||
|
|
||||||
@@ -22,32 +29,16 @@ pub struct SpeedDisplay;
|
|||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct StatusPanel;
|
pub struct StatusPanel;
|
||||||
|
|
||||||
/// Setup the flight HUD when entering flight mode.
|
/// Spawn the flight HUD panels.
|
||||||
pub fn setup_flight_ui(mut commands: Commands) {
|
pub fn setup_flight_ui(mut commands: Commands) {
|
||||||
bevy::log::info!("Setting up flight HUD");
|
bevy::log::info!("Setting up flight HUD");
|
||||||
|
spawn_speed_indicator(&mut commands);
|
||||||
commands
|
spawn_status_panel(&mut 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_speed_indicator(parent: &mut ChildSpawnerCommands) {
|
/// Speed indicator (bottom-left).
|
||||||
parent
|
fn spawn_speed_indicator(commands: &mut Commands) {
|
||||||
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
@@ -64,7 +55,6 @@ fn spawn_speed_indicator(parent: &mut ChildSpawnerCommands) {
|
|||||||
FlightUi,
|
FlightUi,
|
||||||
))
|
))
|
||||||
.with_children(|panel| {
|
.with_children(|panel| {
|
||||||
// Label
|
|
||||||
panel.spawn((
|
panel.spawn((
|
||||||
Text::new("SPEED"),
|
Text::new("SPEED"),
|
||||||
TextFont {
|
TextFont {
|
||||||
@@ -73,8 +63,6 @@ fn spawn_speed_indicator(parent: &mut ChildSpawnerCommands) {
|
|||||||
},
|
},
|
||||||
TextColor(TEXT_DIM),
|
TextColor(TEXT_DIM),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Speed value
|
|
||||||
panel.spawn((
|
panel.spawn((
|
||||||
Text::new("0 m/s"),
|
Text::new("0 m/s"),
|
||||||
TextFont {
|
TextFont {
|
||||||
@@ -87,8 +75,9 @@ fn spawn_speed_indicator(parent: &mut ChildSpawnerCommands) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_status_panel(parent: &mut ChildSpawnerCommands) {
|
/// Status panel (top-left).
|
||||||
parent
|
fn spawn_status_panel(commands: &mut Commands) {
|
||||||
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
@@ -106,7 +95,6 @@ fn spawn_status_panel(parent: &mut ChildSpawnerCommands) {
|
|||||||
StatusPanel,
|
StatusPanel,
|
||||||
))
|
))
|
||||||
.with_children(|panel| {
|
.with_children(|panel| {
|
||||||
// Status label
|
|
||||||
panel.spawn((
|
panel.spawn((
|
||||||
Text::new("FLIGHT MODE"),
|
Text::new("FLIGHT MODE"),
|
||||||
TextFont {
|
TextFont {
|
||||||
@@ -115,8 +103,6 @@ fn spawn_status_panel(parent: &mut ChildSpawnerCommands) {
|
|||||||
},
|
},
|
||||||
TextColor(ACCENT),
|
TextColor(ACCENT),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Flight status text
|
|
||||||
panel.spawn((
|
panel.spawn((
|
||||||
Text::new("Active"),
|
Text::new("Active"),
|
||||||
TextFont {
|
TextFont {
|
||||||
@@ -125,29 +111,18 @@ fn spawn_status_panel(parent: &mut ChildSpawnerCommands) {
|
|||||||
},
|
},
|
||||||
TextColor(TEXT_BRIGHT),
|
TextColor(TEXT_BRIGHT),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Controls hint
|
|
||||||
panel.spawn((
|
panel.spawn((
|
||||||
Node {
|
Text::new("Click: Set destination\nOrbit camera: Drag to rotate"),
|
||||||
margin: UiRect::top(Val::Px(12.0)),
|
TextFont {
|
||||||
|
font_size: 11.0,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
FlightUi,
|
TextColor(TEXT_DIM),
|
||||||
))
|
));
|
||||||
.with_children(|hint| {
|
|
||||||
hint.spawn((
|
|
||||||
Text::new("Click: Set destination\nOrbit camera: Drag to rotate"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 11.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
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(
|
pub fn update_flight_ui(
|
||||||
flight_query: Query<&FlightState>,
|
flight_query: Query<&FlightState>,
|
||||||
mut speed_query: Query<&mut Text, With<SpeedDisplay>>,
|
mut speed_query: Query<&mut Text, With<SpeedDisplay>>,
|
||||||
@@ -156,15 +131,7 @@ pub fn update_flight_ui(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update speed display
|
|
||||||
if let Ok(mut speed_text) = speed_query.single_mut() {
|
if let Ok(mut speed_text) = speed_query.single_mut() {
|
||||||
speed_text.0 = format!("{:.0} m/s", flight_state.current_speed);
|
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 docked;
|
||||||
mod flight;
|
mod flight;
|
||||||
mod flight_ui;
|
mod flight_ui;
|
||||||
|
mod hud;
|
||||||
mod operations;
|
mod operations;
|
||||||
mod scene;
|
mod scene;
|
||||||
mod target;
|
mod target;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
use bevy::ecs::system::SystemParam;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
@@ -22,7 +24,7 @@ pub use flight::{FlightState, FlightControlsPlugin};
|
|||||||
pub use scene::ActiveSystem;
|
pub use scene::ActiveSystem;
|
||||||
pub use target::{Targetable, TargetKind, SelectedTarget, TargetSelectionPlugin};
|
pub use target::{Targetable, TargetKind, SelectedTarget, TargetSelectionPlugin};
|
||||||
pub use actions::{ContextualActionPlugin, ActionType, ActionTriggeredEvent, ActionUi};
|
pub use actions::{ContextualActionPlugin, ActionType, ActionTriggeredEvent, ActionUi};
|
||||||
pub use operations::{TimedOperationPlugin, OperationKind, ActiveOperation};
|
pub use operations::TimedOperationPlugin;
|
||||||
|
|
||||||
pub struct InSystemPlugin;
|
pub struct InSystemPlugin;
|
||||||
|
|
||||||
@@ -37,29 +39,19 @@ impl Plugin for InSystemPlugin {
|
|||||||
.add_plugins(TimedOperationPlugin)
|
.add_plugins(TimedOperationPlugin)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
OnEnter(AppState::InGame),
|
OnEnter(AppState::InGame),
|
||||||
(
|
(scene::setup_in_system_view, add_targetable_to_pois).chain(),
|
||||||
scene::setup_in_system_view,
|
|
||||||
// UI removed - no longer needed
|
|
||||||
// ui::setup_docked_ui,
|
|
||||||
add_targetable_to_pois,
|
|
||||||
).chain(),
|
|
||||||
)
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
OnExit(AppState::InGame),
|
OnExit(AppState::InGame),
|
||||||
(
|
(hud::despawn_in_system_hud, scene::despawn_in_system_scene).chain(),
|
||||||
// UI removed - no longer needed
|
|
||||||
// ui::despawn_docked_ui,
|
|
||||||
// flight_ui::despawn_flight_ui,
|
|
||||||
scene::despawn_in_system_scene,
|
|
||||||
).chain(),
|
|
||||||
)
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
// UI removed - no longer needed
|
hud::sync_hud_panels,
|
||||||
// ui::refresh_docked_ui,
|
ui::refresh_docked_ui,
|
||||||
// ui::undock_button_handler,
|
flight_ui::update_flight_ui,
|
||||||
// flight_ui::update_flight_ui,
|
complete_operations,
|
||||||
handle_action_triggered,
|
handle_action_triggered,
|
||||||
)
|
)
|
||||||
.chain()
|
.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(
|
fn handle_action_triggered(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut events: EventReader<ActionTriggeredEvent>,
|
mut events: EventReader<ActionTriggeredEvent>,
|
||||||
player_query: Query<Entity, With<scene::PlayerShip>>,
|
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>>,
|
selected_target_query: Query<&SelectedTarget, With<scene::PlayerShip>>,
|
||||||
|
mut dispatch: ActionDispatch,
|
||||||
) {
|
) {
|
||||||
let Ok(player_entity) = player_query.single() else {
|
let Ok(player_entity) = player_query.single() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let now_ms = dispatch.time.elapsed_secs_f64() * 1000.0;
|
||||||
|
|
||||||
for event in events.read() {
|
for event in events.read() {
|
||||||
bevy::log::info!("Action triggered: {:?}", event.action_type);
|
bevy::log::info!("Action triggered: {:?}", event.action_type);
|
||||||
|
|
||||||
match event.action_type {
|
match event.action_type {
|
||||||
ActionType::Undock => {
|
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 => {
|
ActionType::Approach => {
|
||||||
// Get selected target
|
|
||||||
if let Ok(selected) = selected_target_query.single() {
|
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 => {
|
ActionType::Dock => {
|
||||||
// TODO: Implement docking operation
|
if let Ok(selected) = selected_target_query.single() {
|
||||||
bevy::log::info!("Dock action triggered");
|
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 => {
|
ActionType::StartMining => {
|
||||||
// TODO: Implement mining operation
|
|
||||||
bevy::log::info!("Start mining action triggered");
|
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)
|
/// Add Targetable components to spawned POIs (stations and asteroid belts)
|
||||||
/// so they can be selected by the target selection system.
|
/// so they can be selected by the target selection system.
|
||||||
fn add_targetable_to_pois(
|
fn add_targetable_to_pois(
|
||||||
|
|||||||
@@ -83,13 +83,12 @@ impl Plugin for TimedOperationPlugin {
|
|||||||
fn monitor_operations(
|
fn monitor_operations(
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut query: Query<(Entity, &mut ActiveOperation)>,
|
query: Query<(Entity, &ActiveOperation)>,
|
||||||
mut completed_events: EventWriter<OperationCompletedEvent>,
|
mut completed_events: EventWriter<OperationCompletedEvent>,
|
||||||
durations: Res<OperationDurations>,
|
|
||||||
) {
|
) {
|
||||||
let now = time.elapsed_secs_f64() * 1000.0;
|
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;
|
let elapsed = now - operation.started_at;
|
||||||
|
|
||||||
if elapsed >= operation.duration_ms {
|
if elapsed >= operation.duration_ms {
|
||||||
@@ -127,11 +126,12 @@ pub fn start_undocking(
|
|||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
player_entity: Entity,
|
player_entity: Entity,
|
||||||
durations: &OperationDurations,
|
durations: &OperationDurations,
|
||||||
mut events: &mut EventWriter<OperationStartedEvent>,
|
events: &mut EventWriter<OperationStartedEvent>,
|
||||||
|
now_ms: f64,
|
||||||
) {
|
) {
|
||||||
commands.entity(player_entity).insert(ActiveOperation {
|
commands.entity(player_entity).insert(ActiveOperation {
|
||||||
kind: OperationKind::Undocking,
|
kind: OperationKind::Undocking,
|
||||||
started_at: 0.0, // Will be set by the system
|
started_at: now_ms,
|
||||||
duration_ms: durations.undock,
|
duration_ms: durations.undock,
|
||||||
target_name: "Undocking".to_string(),
|
target_name: "Undocking".to_string(),
|
||||||
});
|
});
|
||||||
@@ -149,11 +149,12 @@ pub fn start_travel(
|
|||||||
player_entity: Entity,
|
player_entity: Entity,
|
||||||
target_name: String,
|
target_name: String,
|
||||||
durations: &OperationDurations,
|
durations: &OperationDurations,
|
||||||
mut events: &mut EventWriter<OperationStartedEvent>,
|
events: &mut EventWriter<OperationStartedEvent>,
|
||||||
|
now_ms: f64,
|
||||||
) {
|
) {
|
||||||
commands.entity(player_entity).insert(ActiveOperation {
|
commands.entity(player_entity).insert(ActiveOperation {
|
||||||
kind: OperationKind::Travel,
|
kind: OperationKind::Travel,
|
||||||
started_at: 0.0,
|
started_at: now_ms,
|
||||||
duration_ms: durations.local_travel,
|
duration_ms: durations.local_travel,
|
||||||
target_name: target_name.clone(),
|
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 bevy::prelude::*;
|
||||||
|
|
||||||
use crate::gameplay::in_system::{DockedState, UndockEvent};
|
|
||||||
use crate::gameplay::campaign::CampaignDraft;
|
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_BG: Color = Color::srgb(0.05, 0.07, 0.12);
|
||||||
const PANEL_BORDER: Color = Color::srgb(0.25, 0.40, 0.62);
|
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_BRIGHT: Color = Color::srgb(0.82, 0.90, 1.0);
|
||||||
const TEXT_DIM: Color = Color::srgb(0.55, 0.65, 0.82);
|
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 TITLE_FONT_SIZE: f32 = 24.0;
|
||||||
const SUBTITLE_FONT_SIZE: f32 = 14.0;
|
|
||||||
const BODY_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 PANEL_WIDTH: f32 = 360.0;
|
||||||
const SIDE_MARGIN: f32 = 20.0;
|
const SIDE_MARGIN: f32 = 20.0;
|
||||||
const TOP_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)]
|
#[derive(Component)]
|
||||||
pub struct DockedUi;
|
pub struct DockedUi;
|
||||||
|
|
||||||
/// Marker for the station details text.
|
/// Marker for the station details text, refreshed each frame from the
|
||||||
|
/// [`CampaignDraft`] / [`DockedState`].
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct StationDetailsText;
|
pub struct StationDetailsText;
|
||||||
|
|
||||||
/// Marker for the undock button.
|
/// Spawn the station info panel as a top-level, corner-anchored overlay.
|
||||||
#[derive(Component)]
|
///
|
||||||
pub struct UndockButton;
|
/// 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) {
|
pub fn setup_docked_ui(mut commands: 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((
|
.spawn((
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
@@ -80,7 +66,7 @@ fn spawn_station_panel(parent: &mut ChildSpawnerCommands) {
|
|||||||
TextColor(TEXT_BRIGHT),
|
TextColor(TEXT_BRIGHT),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Station details placeholder (will be updated by refresh_docked_ui)
|
// Station details (refreshed by `refresh_docked_ui`).
|
||||||
panel.spawn((
|
panel.spawn((
|
||||||
Text::new("Loading station data..."),
|
Text::new("Loading station data..."),
|
||||||
TextFont {
|
TextFont {
|
||||||
@@ -90,55 +76,25 @@ fn spawn_station_panel(parent: &mut ChildSpawnerCommands) {
|
|||||||
TextColor(TEXT_DIM),
|
TextColor(TEXT_DIM),
|
||||||
StationDetailsText,
|
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(
|
pub fn refresh_docked_ui(
|
||||||
campaign: Res<CampaignDraft>,
|
campaign: Res<CampaignDraft>,
|
||||||
docked_state: Res<DockedState>,
|
docked_state: Res<DockedState>,
|
||||||
mut query: Query<&mut Text, With<StationDetailsText>>,
|
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() {
|
if !campaign.is_changed() && !docked_state.is_changed() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut text = match query.get_single_mut() {
|
let Ok(mut text) = query.single_mut() else {
|
||||||
Ok(t) => t,
|
return;
|
||||||
Err(_) => return,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(starting_base) = &campaign.starting_base else {
|
let Some(starting_base) = &campaign.starting_base else {
|
||||||
@@ -151,22 +107,25 @@ pub fn refresh_docked_ui(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the system
|
// Find the system.
|
||||||
let system_index = match galaxy.systems.iter().position(|s| s.id == starting_base.system_id) {
|
let Some(system_index) = galaxy
|
||||||
Some(idx) => idx,
|
.systems
|
||||||
None => {
|
.iter()
|
||||||
text.0 = format!("System {} not found.", starting_base.system_id);
|
.position(|s| s.id == starting_base.system_id)
|
||||||
return;
|
else {
|
||||||
}
|
text.0 = format!("System {} not found.", starting_base.system_id);
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let system = &galaxy.systems[system_index];
|
let system = &galaxy.systems[system_index];
|
||||||
let contents = &galaxy.contents[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 = 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);
|
let station_pop = station.map(|s| s.population).unwrap_or(0);
|
||||||
|
|
||||||
text.0 = format!(
|
text.0 = format!(
|
||||||
@@ -177,32 +136,6 @@ pub fn refresh_docked_ui(
|
|||||||
system.id,
|
system.id,
|
||||||
system.faction,
|
system.faction,
|
||||||
system.security,
|
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