✨ feat(gameplay): implement in-system gameplay with camera modes and flight controls
Add comprehensive in-system gameplay features including: Camera System: - Orbit mode for galaxy/inspection views - Follow mode for tracking player ship during flight - Cinematic mode for docked/cutscene views - Smooth interpolation and orbit controls In-System Gameplay: - Docked state at stations with undock functionality - Flight mode with velocity and target-based navigation - Point-and-click movement via ground plane projection - Target selection system for POIs Flight Controls: - Flight state tracking with speed monitoring - Automatic camera transitions between modes - Flight HUD with speed indicator and status panel - Contextual action system for approach/dock/mining UI Updates: - Docked station panel with system information - Flight mode controls and hints - Dynamic population display This implementation provides the foundation for tactical space gameplay with smooth camera transitions and intuitive point-and-click navigation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
174
apps/game/src/gameplay/in_system/flight.rs
Normal file
174
apps/game/src/gameplay/in_system/flight.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
//! 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};
|
||||
use crate::gameplay::movement::components::{Velocity, MoveTarget};
|
||||
use crate::gameplay::galaxy::Identifiable;
|
||||
use super::{DockedState, UndockEvent};
|
||||
use super::scene::{Docked, PlayerShip};
|
||||
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>,
|
||||
player_query: Query<(Entity, &Transform), (With<PlayerShip>, With<Docked>)>,
|
||||
docked_ui_query: Query<Entity, With<super::ui::DockedUi>>,
|
||||
) {
|
||||
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 {
|
||||
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
|
||||
camera_state.mode = CameraMode::Follow;
|
||||
camera_state.target_entity = Some(player_entity);
|
||||
camera_state.follow_distance = 15.0;
|
||||
camera_state.follow_height = 5.0;
|
||||
|
||||
// Spawn flight HUD
|
||||
setup_flight_ui(commands.reborrow());
|
||||
|
||||
// Despawn docked UI
|
||||
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>,
|
||||
identifiable_query: Query<&Identifiable>,
|
||||
flight_ui_query: Query<Entity, With<super::flight_ui::FlightUi>>,
|
||||
) {
|
||||
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 to cinematic mode
|
||||
camera_state.mode = CameraMode::Cinematic;
|
||||
camera_state.target_entity = Some(event.station);
|
||||
|
||||
// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user