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.
192 lines
6.5 KiB
Rust
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();
|
|
}
|