✨ 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:
@@ -7,6 +7,53 @@ use crate::ui::util::cursor_over_ui;
|
||||
#[derive(Component)]
|
||||
pub struct MainCamera;
|
||||
|
||||
/// Camera mode determines how the camera behaves.
|
||||
/// - Orbit: Free-look inspection around a target (Galaxy view, docked inspection)
|
||||
/// - Follow: Tracks behind a moving entity (player ship during flight)
|
||||
/// - Cinematic: Fixed cinematic shot (docked view, cutscenes)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub enum CameraMode {
|
||||
#[default]
|
||||
Orbit,
|
||||
Follow,
|
||||
Cinematic,
|
||||
}
|
||||
|
||||
impl CameraMode {
|
||||
pub fn is_orbit(&self) -> bool {
|
||||
matches!(self, Self::Orbit)
|
||||
}
|
||||
|
||||
pub fn is_follow(&self) -> bool {
|
||||
matches!(self, Self::Follow)
|
||||
}
|
||||
|
||||
pub fn is_cinematic(&self) -> bool {
|
||||
matches!(self, Self::Cinematic)
|
||||
}
|
||||
}
|
||||
|
||||
/// Global camera state resource. Controls which camera mode is active
|
||||
/// and which entity the camera should follow (if any).
|
||||
#[derive(Resource, Default, Debug)]
|
||||
pub struct CameraState {
|
||||
pub mode: CameraMode,
|
||||
pub target_entity: Option<Entity>,
|
||||
pub follow_distance: f32,
|
||||
pub follow_height: f32,
|
||||
}
|
||||
|
||||
/// Follow camera component. Attached to the MainCamera when in Follow mode,
|
||||
/// this configures how the camera tracks its target.
|
||||
#[derive(Component, Debug, Clone)]
|
||||
pub struct FollowCamera {
|
||||
pub target: Entity,
|
||||
pub distance: f32,
|
||||
pub height: f32,
|
||||
pub stiffness: f32,
|
||||
pub damping: f32,
|
||||
}
|
||||
|
||||
/// Orbit-style camera: rotates around `target` at `distance`, controlled by mouse.
|
||||
/// Used for inspection scenes like Galaxy where there is no player to follow.
|
||||
///
|
||||
@@ -73,7 +120,10 @@ const ORBIT_MAX_DISTANCE: f32 = 1500.0;
|
||||
///
|
||||
/// Drag is suppressed when the cursor is over any UI node — otherwise clicking
|
||||
/// buttons or panels would also rotate the camera.
|
||||
///
|
||||
/// Only runs when camera mode is Orbit.
|
||||
pub fn orbit_camera_control(
|
||||
camera_state: Res<CameraState>,
|
||||
mouse_input: Res<ButtonInput<MouseButton>>,
|
||||
primary_window: Query<&Window, With<PrimaryWindow>>,
|
||||
mut mouse_motion: EventReader<bevy::input::mouse::MouseMotion>,
|
||||
@@ -81,6 +131,12 @@ pub fn orbit_camera_control(
|
||||
mut query: Query<(&mut Transform, &mut OrbitCamera), With<MainCamera>>,
|
||||
ui_nodes: Query<(&bevy::ui::ComputedNode, &GlobalTransform)>,
|
||||
) {
|
||||
// Only run orbit controls in Orbit mode
|
||||
if camera_state.mode != CameraMode::Orbit {
|
||||
mouse_motion.clear();
|
||||
scroll_events.clear();
|
||||
return;
|
||||
}
|
||||
let Ok((mut transform, mut orbit)) = query.single_mut() else {
|
||||
// Drain pending input to avoid stale buildup when there's no camera.
|
||||
mouse_motion.clear();
|
||||
@@ -162,3 +218,102 @@ pub fn apply_orbit_reset(
|
||||
orbit.reset();
|
||||
commands.remove_resource::<ResetOrbitCamera>();
|
||||
}
|
||||
|
||||
/// Follow camera system. Tracks behind the target entity (player ship) during flight.
|
||||
/// The camera maintains a fixed distance and height behind the target, smoothly
|
||||
/// interpolating to the ideal position each frame.
|
||||
pub fn follow_camera_system(
|
||||
time: Res<Time>,
|
||||
camera_state: Res<CameraState>,
|
||||
mut camera_query: Query<&mut Transform, With<MainCamera>>,
|
||||
target_query: Query<&GlobalTransform>,
|
||||
follow_cam_query: Query<&FollowCamera, With<MainCamera>>,
|
||||
) {
|
||||
// Only run when in follow mode
|
||||
if camera_state.mode != CameraMode::Follow {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(target_entity) = camera_state.target_entity else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(mut camera_transform) = camera_query.single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(target_transform) = target_query.get(target_entity) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dt = time.delta_secs();
|
||||
|
||||
// Get target's forward direction (negative Z is forward in Bevy)
|
||||
let target_rotation = target_transform.rotation();
|
||||
let target_forward = target_rotation * Vec3::NEG_Z;
|
||||
let target_up = target_rotation * Vec3::Y;
|
||||
|
||||
// Calculate ideal camera position: behind and above the target
|
||||
let follow_distance = camera_state.follow_distance;
|
||||
let follow_height = camera_state.follow_height;
|
||||
|
||||
// Position behind the ship: target position - forward * distance + up * height
|
||||
let target_pos = target_transform.translation();
|
||||
let ideal_position = target_pos - target_forward * follow_distance + target_up * follow_height;
|
||||
|
||||
// Get stiffness from FollowCamera component if it exists, otherwise use default
|
||||
let stiffness = follow_cam_query
|
||||
.single()
|
||||
.map(|fc| fc.stiffness)
|
||||
.unwrap_or(3.0);
|
||||
|
||||
// Smoothly interpolate current position to ideal position
|
||||
// Using exponential lerp: current = current + (ideal - current) * stiffness * dt
|
||||
let lerp_factor = (stiffness * dt).min(1.0);
|
||||
camera_transform.translation = camera_transform.translation
|
||||
.lerp(ideal_position, lerp_factor);
|
||||
|
||||
// Look at the target (slightly above center to look at ship body, not feet)
|
||||
let look_target = target_pos + target_up * (follow_height * 0.5);
|
||||
let look_dir = (look_target - camera_transform.translation).normalize();
|
||||
|
||||
// Smoothly rotate to look at target
|
||||
let ideal_look = Transform::IDENTITY.looking_to(look_dir, Vec3::Y);
|
||||
camera_transform.rotation = camera_transform.rotation
|
||||
.slerp(ideal_look.rotation, lerp_factor);
|
||||
}
|
||||
|
||||
/// Initialize the follow camera when transitioning to follow mode.
|
||||
/// This system adds the FollowCamera component to the MainCamera entity.
|
||||
pub fn setup_follow_camera(
|
||||
mut commands: Commands,
|
||||
camera_state: Res<CameraState>,
|
||||
camera_query: Query<Entity, With<MainCamera>>,
|
||||
follow_cam_query: Query<&FollowCamera, With<MainCamera>>,
|
||||
) {
|
||||
// Only run when we just switched to follow mode and don't have FollowCamera component yet
|
||||
if camera_state.mode != CameraMode::Follow {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(target_entity) = camera_state.target_entity else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(camera_entity) = camera_query.single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check if FollowCamera already exists, if so don't add it again
|
||||
if follow_cam_query.single().is_ok() {
|
||||
return;
|
||||
}
|
||||
|
||||
commands.entity(camera_entity).insert(FollowCamera {
|
||||
target: target_entity,
|
||||
distance: camera_state.follow_distance,
|
||||
height: camera_state.follow_height,
|
||||
stiffness: 3.0,
|
||||
damping: 0.5,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user