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.
This commit is contained in:
2026-06-16 20:05:31 -04:00
parent aee13cb81a
commit 30b6678569
7 changed files with 420 additions and 430 deletions

View File

@@ -8,7 +8,7 @@
use bevy::prelude::*;
use crate::camera::{CameraState, CameraMode};
use crate::camera::{CameraState, CameraMode, OrbitFocusGoal};
use crate::gameplay::movement::components::{Velocity, MoveTarget};
use crate::gameplay::galaxy::Identifiable;
use super::{DockedState, UndockEvent};
@@ -53,14 +53,15 @@ fn handle_undock(
mut events: EventReader<UndockEvent>,
mut docked_state: ResMut<DockedState>,
mut camera_state: ResMut<CameraState>,
player_query: Query<(Entity, &Transform), (With<PlayerShip>, With<Docked>)>,
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, ship_transform)) = player_query.single() else {
let Ok(player_entity) = player_query.single() else {
bevy::log::warn!("No docked player ship found");
continue;
};
@@ -87,11 +88,19 @@ fn handle_undock(
// Update docked state resource
docked_state.undock();
// Transition camera to tactical follow mode (isometric view)
// 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
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());
@@ -111,6 +120,7 @@ fn handle_docking(
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
) {
@@ -134,9 +144,15 @@ fn handle_docking(
// Update docked state resource
docked_state.dock_at(event.station);
// Transition camera to cinematic mode
camera_state.mode = CameraMode::Cinematic;
// 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