Files
Space-Game/apps/game/src/gameplay/in_system/flight.rs
francy51 30b6678569 feat(camera): comprehensive camera system (C1)
Rebuild the camera around a single persistent MainCamera and the
Camera System design-doc model: two framing modes (Orbit = inspection/
docked tactical, Follow = track the ship) plus Cinematic as a boolean
overlay (free rotation + HUD hide + live gameplay), not a third mode.

Critical fixes (gap analysis A1-A3):
- A1: scenes now *reconfigure* the one startup camera instead of spawning
  a second one; despawn_in_system_scene no longer destroys MainCamera, so
  the session never loses its camera. .single_mut() on MainCamera now
  succeeds during flight.
- A2: Cinematic is a real overlay — toggle (KeyC), free-look rotation in
  any framing, HUD hidden while active, gameplay keeps running.
- A3: removed dead FollowCamera component + setup_follow_camera; tracking
  is unified in track_camera_target.

Gaps B1-B6:
- B1: adopted doc model (Cinematic overlay, not exclusive mode).
- B2: canonical isometric tactical_rotation() baseline.
- B3: smooth reframing via OrbitFocusGoal exponential-damp tween.
- B4: non-zero CameraState defaults.
- B5: consolidated the three orbit-control impls into one (dropped the
  starting_base local control + its Euler variant).
- B6: track_camera_target keeps OrbitCamera.target synced to the focus
  entity every frame.

Docked view now frames the actual station at a tactical iso distance.
cargo check + clippy clean for all newly-authored code; net -10 lines
(more dead code removed than added). 42 tests pass.
2026-06-16 20:05:31 -04:00

192 lines
6.5 KiB
Rust

//! Flight state and controls for in-system gameplay.
//!
//! Handles the transition from docked to active flight mode, including
//! ship controls, camera transitions, and flight state management.
//!
//! Navigation is point-and-click tactical mode: select targets and click
//! "Approach" to auto-navigate. No WASD controls.
use bevy::prelude::*;
use crate::camera::{CameraState, CameraMode, OrbitFocusGoal};
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)]
pub struct FlightState {
pub is_flying: bool,
pub current_speed: f32,
}
/// Event fired when the player docks at a station.
#[derive(Event, Debug, Clone)]
pub struct DockEvent {
pub ship: Entity,
pub station: Entity,
}
/// Plugin for flight controls and state management.
pub struct FlightControlsPlugin;
impl Plugin for FlightControlsPlugin {
fn build(&self, app: &mut App) {
app.add_event::<DockEvent>()
.add_systems(
Update,
(
handle_undock,
handle_docking,
flight_input_system,
).run_if(in_state(crate::state::AppState::InGame)),
);
}
}
/// Handle undock event: transition from docked to flight mode.
fn handle_undock(
mut commands: Commands,
mut events: EventReader<UndockEvent>,
mut docked_state: ResMut<DockedState>,
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);
// Find player ship
let Ok(player_entity) = player_query.single() else {
bevy::log::warn!("No docked player ship found");
continue;
};
// Remove docked component
commands.entity(player_entity).remove::<Docked>();
// Add flight state with initial velocity away from station
let initial_speed = 10.0; // Start with slow drift
commands.entity(player_entity).insert((
FlightState {
is_flying: true,
current_speed: initial_speed,
},
Velocity(Vec3::new(0.0, 0.0, -1.0) * initial_speed),
MoveTarget(Vec3::new(0.0, 0.0, -50.0)), // Initial target away from station
super::SelectedTarget {
entity: Entity::PLACEHOLDER,
kind: super::TargetKind::Manual,
name: "None".to_string(),
},
));
// Update docked state resource
docked_state.undock();
// Transition camera to follow mode: a tactical iso view tracking the
// ship. Arm a focus goal so the distance eases from the docked framing
// to the follow distance instead of hard-snapping. The orbit target is
// left to `track_camera_target`, which locks it to the ship.
camera_state.mode = CameraMode::Follow;
camera_state.target_entity = Some(player_entity);
camera_state.follow_distance = 45.0; // Higher for tactical view
camera_state.follow_height = 35.0; // Isometric angle
focus_goal.arm(
None,
Some(camera_state.follow_distance),
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();
// }
bevy::log::info!("Transitioned to flight mode");
}
}
/// Handle docking event: transition from flight to docked mode.
fn handle_docking(
mut commands: Commands,
mut events: EventReader<DockEvent>,
mut docked_state: ResMut<DockedState>,
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);
let Ok(identifiable) = identifiable_query.get(event.station) else {
bevy::log::warn!("Docking target has no Identifiable component");
continue;
};
// Add docked component
commands.entity(event.ship).insert(Docked {
station_entity: event.station,
});
// Remove flight state
commands.entity(event.ship)
.remove::<FlightState>();
// Update docked state resource
docked_state.dock_at(event.station);
// Transition camera back to the docked tactical framing (Orbit on the
// station), easing the distance in.
camera_state.mode = CameraMode::Orbit;
camera_state.target_entity = Some(event.station);
focus_goal.arm(
None,
Some(crate::camera::DOCKED_FRAMING_DISTANCE),
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());
bevy::log::info!("Docked at {}", identifiable.display_name);
}
}
/// Flight input system: point-and-click navigation only.
///
/// This system handles updating the flight state based on movement.
/// Actual navigation is handled by click-to-move (setting MoveTarget)
/// and the kinematic systems (steer_to_target, integrate_velocity).
///
/// This is a minimal system - the real action happens in the movement systems.
fn flight_input_system(
mut player_query: Query<(&Velocity, &mut FlightState), With<PlayerShip>>,
docked_state: Res<DockedState>,
) {
// Only process flight input when not docked
if docked_state.is_docked {
return;
}
let Ok((velocity, mut flight_state)) = player_query.single_mut() else {
return;
};
// Update current speed from actual velocity
flight_state.current_speed = velocity.0.length();
}