870 lines
32 KiB
Rust
870 lines
32 KiB
Rust
// ▄████████▄ + ███ + ▄█████████ ███ +
|
|
// ███▀ ▀███ + + ███ ███▀ + ███ + +
|
|
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
|
|
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
|
|
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
|
|
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
|
|
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
|
|
// + + + ███
|
|
// + ▀████████████████████████████████████████████████████▀
|
|
//
|
|
// This module manages the game's viewport, handles camera- and
|
|
// movement-related keyboard input, and provides some camera-
|
|
// related computation functions.
|
|
|
|
use crate::prelude::*;
|
|
use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings};
|
|
use bevy::core_pipeline::tonemapping::Tonemapping;
|
|
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
|
use bevy::pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap};
|
|
use bevy::prelude::*;
|
|
use bevy::transform::TransformSystem;
|
|
use bevy::window::PrimaryWindow;
|
|
use bevy_xpbd_3d::plugins::sync;
|
|
use bevy_xpbd_3d::prelude::*;
|
|
use std::collections::HashMap;
|
|
|
|
pub const INITIAL_ZOOM_LEVEL: f64 = 10.0;
|
|
pub const MAX_DIST_FOR_MATCH_VELOCITY: f64 = 10000.0;
|
|
|
|
pub struct CameraPlugin;
|
|
|
|
impl Plugin for CameraPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_systems(Startup, setup_camera);
|
|
app.add_systems(Update, handle_input.run_if(in_control).run_if(game_running));
|
|
app.add_systems(
|
|
Update,
|
|
update_map_only_object_visibility
|
|
.run_if(alive)
|
|
.run_if(game_running),
|
|
);
|
|
app.add_systems(
|
|
PostUpdate,
|
|
manage_player_actor
|
|
.run_if(game_running)
|
|
.in_set(sync::SyncSet::PositionToTransform),
|
|
);
|
|
app.add_systems(
|
|
PostUpdate,
|
|
sync_camera_to_player
|
|
.run_if(game_running)
|
|
.after(PhysicsSet::Sync)
|
|
.after(apply_input_to_player)
|
|
.before(TransformSystem::TransformPropagate),
|
|
);
|
|
app.add_systems(
|
|
PostUpdate,
|
|
update_mapcam_center
|
|
.run_if(game_running)
|
|
.before(sync::position_to_transform)
|
|
.in_set(sync::SyncSet::PositionToTransform),
|
|
);
|
|
app.add_systems(
|
|
Update,
|
|
update_map_camera.run_if(in_control).run_if(game_running),
|
|
);
|
|
app.add_systems(Update, update_fov.run_if(alive).run_if(game_running));
|
|
app.add_systems(PreUpdate, apply_input_to_player.run_if(game_running));
|
|
app.insert_resource(MapCam::default());
|
|
|
|
// To center the renderer origin on the player camera,
|
|
// 1. Disable bevy_xpbd's position->transform sync function
|
|
app.insert_resource(sync::SyncConfig {
|
|
position_to_transform: true,
|
|
transform_to_position: false,
|
|
});
|
|
// 2. Add own position->transform sync function
|
|
app.add_systems(
|
|
PostUpdate,
|
|
position_to_transform
|
|
.run_if(game_running)
|
|
.after(sync::position_to_transform)
|
|
.in_set(sync::SyncSet::PositionToTransform),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct ShowOnlyInMap {
|
|
pub min_distance: f64,
|
|
pub distance_to_id: String,
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
pub struct MapCam {
|
|
pub initialized: bool,
|
|
pub zoom_level: f64,
|
|
pub target_zoom_level: f64,
|
|
pub pitch: f64,
|
|
pub yaw: f64,
|
|
pub offset_x: f64,
|
|
pub offset_z: f64,
|
|
pub center: DVec3,
|
|
pub center_on_entity: Option<Entity>,
|
|
}
|
|
impl Default for MapCam {
|
|
fn default() -> Self {
|
|
Self {
|
|
initialized: false,
|
|
zoom_level: 2.0,
|
|
target_zoom_level: INITIAL_ZOOM_LEVEL,
|
|
pitch: PI * 0.3,
|
|
yaw: 0.0,
|
|
offset_x: 0.0,
|
|
offset_z: 0.0,
|
|
center: DVec3::new(0.0, 0.0, 0.0),
|
|
center_on_entity: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn setup_camera(mut commands: Commands, settings: Res<var::Settings>) {
|
|
// Add player
|
|
commands.spawn((
|
|
Camera3dBundle {
|
|
camera: Camera {
|
|
hdr: true, // HDR is required for bloom
|
|
clear_color: ClearColorConfig::Custom(Color::BLACK),
|
|
..default()
|
|
},
|
|
tonemapping: Tonemapping::TonyMcMapface,
|
|
transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
..default()
|
|
},
|
|
BloomSettings {
|
|
composite_mode: BloomCompositeMode::EnergyConserving,
|
|
..default()
|
|
},
|
|
));
|
|
|
|
// Add Light from the Sun
|
|
commands.spawn(DirectionalLightBundle {
|
|
directional_light: DirectionalLight {
|
|
illuminance: 2000.0,
|
|
shadows_enabled: settings.shadows_sun,
|
|
..default()
|
|
},
|
|
transform: Transform::from_rotation(Quat::from_rotation_y(PI32 / 2.0)),
|
|
cascade_shadow_config: CascadeShadowConfigBuilder {
|
|
num_cascades: 4,
|
|
minimum_distance: 0.1,
|
|
maximum_distance: 5000.0,
|
|
..default()
|
|
}
|
|
.into(),
|
|
..default()
|
|
});
|
|
|
|
commands.insert_resource(DirectionalLightShadowMap {
|
|
size: settings.shadowmap_resolution,
|
|
});
|
|
}
|
|
|
|
pub fn sync_camera_to_player(
|
|
settings: Res<var::Settings>,
|
|
mut q_camera: Query<&mut Transform, (With<Camera>, Without<actor::PlayerCamera>)>,
|
|
q_playercam: Query<(&actor::Actor, &Transform), (With<actor::PlayerCamera>, Without<Camera>)>,
|
|
) {
|
|
if settings.map_active || q_camera.is_empty() || q_playercam.is_empty() {
|
|
return;
|
|
}
|
|
let mut camera_transform = q_camera.get_single_mut().unwrap();
|
|
let (actor, player_transform) = q_playercam.get_single().unwrap();
|
|
|
|
// Rotation
|
|
let rotation = player_transform.rotation * Quat::from_array([0.0, -1.0, 0.0, 0.0]);
|
|
|
|
// Translation
|
|
if settings.third_person {
|
|
camera_transform.translation = player_transform.translation
|
|
+ rotation * (actor.camdistance * Vec3::new(0.0, 0.2, 1.0));
|
|
camera_transform.rotation = rotation * Quat::from_euler(EulerRot::XYZ, -0.02, 0.0, 0.0);
|
|
} else {
|
|
camera_transform.translation = player_transform.translation;
|
|
camera_transform.rotation = rotation;
|
|
}
|
|
}
|
|
|
|
pub fn update_map_camera(
|
|
settings: Res<var::Settings>,
|
|
mut mapcam: ResMut<MapCam>,
|
|
mut q_camera: Query<&mut Transform, (With<Camera>, Without<actor::PlayerCamera>)>,
|
|
q_playercam: Query<(Entity, &Transform), (With<actor::PlayerCamera>, Without<Camera>)>,
|
|
q_target: Query<
|
|
(Entity, &Transform),
|
|
(
|
|
With<hud::IsTargeted>,
|
|
Without<Camera>,
|
|
Without<actor::PlayerCamera>,
|
|
),
|
|
>,
|
|
q_target_changed: Query<(), Changed<hud::IsTargeted>>,
|
|
mut mouse_events: EventReader<MouseMotion>,
|
|
mut er_mousewheel: EventReader<MouseWheel>,
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
) {
|
|
if !settings.map_active || q_camera.is_empty() || q_playercam.is_empty() {
|
|
return;
|
|
}
|
|
let mut camera_transform = q_camera.get_single_mut().unwrap();
|
|
let (player_entity, player_trans) = q_playercam.get_single().unwrap();
|
|
let (target_entity, target_trans) = if let Ok(target) = q_target.get_single() {
|
|
target
|
|
} else {
|
|
(player_entity, player_trans)
|
|
};
|
|
mapcam.center_on_entity = Some(target_entity);
|
|
|
|
// Get mouse movement
|
|
let mut mouse_delta = Vec2::ZERO;
|
|
for mouse_event in mouse_events.read() {
|
|
mouse_delta += mouse_event.delta;
|
|
}
|
|
// NOTE: we need to subtract a bit from PI/2, otherwise the "up"
|
|
// direction parameter for the Transform.look_at function is ambiguous
|
|
// at the extreme values and the orientation will flicker back/forth.
|
|
let min_zoom: f64 = target_trans.scale.x as f64 * 2.0;
|
|
let max_zoom: f64 = 17e18; // at this point, camera starts glitching
|
|
mapcam.pitch = (mapcam.pitch
|
|
+ mouse_delta.y as f64 / 180.0 * settings.mouse_sensitivity as f64)
|
|
.clamp(-PI / 2.0 + EPSILON, PI / 2.0 - EPSILON);
|
|
mapcam.yaw += mouse_delta.x as f64 / 180.0 * settings.mouse_sensitivity as f64;
|
|
|
|
// Reset movement offset if target changes
|
|
if !q_target_changed.is_empty() {
|
|
mapcam.offset_x = 0.0;
|
|
mapcam.offset_z = 0.0;
|
|
}
|
|
|
|
// Get keyboard movement
|
|
let mut offset_x: f64 = 0.0;
|
|
let mut offset_z: f64 = 0.0;
|
|
if keyboard_input.pressed(settings.key_forward) {
|
|
offset_x -= 1.0;
|
|
}
|
|
if keyboard_input.pressed(settings.key_back) {
|
|
offset_x += 1.0;
|
|
}
|
|
if keyboard_input.pressed(settings.key_right) {
|
|
offset_z -= 1.0;
|
|
}
|
|
if keyboard_input.pressed(settings.key_left) {
|
|
offset_z += 1.0;
|
|
}
|
|
if keyboard_input.pressed(settings.key_stop) {
|
|
mapcam.offset_x = 0.0;
|
|
mapcam.offset_z = 0.0;
|
|
}
|
|
|
|
// Update zoom level
|
|
if !mapcam.initialized {
|
|
let factor: f64 = if target_trans == player_trans {
|
|
7.0
|
|
} else {
|
|
1.0
|
|
};
|
|
mapcam.target_zoom_level *= target_trans.scale.x as f64 * factor;
|
|
mapcam.zoom_level *= target_trans.scale.x as f64 * factor;
|
|
mapcam.initialized = true;
|
|
}
|
|
let mut change_zoom: f64 = 0.0;
|
|
if keyboard_input.pressed(settings.key_map_zoom_out) {
|
|
change_zoom += 0.5;
|
|
}
|
|
if keyboard_input.pressed(settings.key_map_zoom_in) {
|
|
change_zoom -= 0.5;
|
|
}
|
|
for wheel_event in er_mousewheel.read() {
|
|
change_zoom -= wheel_event.y as f64 * 3.0;
|
|
}
|
|
mapcam.target_zoom_level =
|
|
(mapcam.target_zoom_level * 1.1f64.powf(change_zoom)).clamp(min_zoom, max_zoom);
|
|
let zoom_speed = 0.05; // should be between 0.0001 (slow) and 1.0 (instant)
|
|
mapcam.zoom_level = (zoom_speed * mapcam.target_zoom_level
|
|
+ (1.0 - zoom_speed) * mapcam.zoom_level)
|
|
.clamp(min_zoom, max_zoom);
|
|
|
|
// Update point of view
|
|
let pov_rotation =
|
|
DQuat::from_euler(EulerRot::XYZ, 0.0, mapcam.yaw as f64, mapcam.pitch as f64);
|
|
let point_of_view = pov_rotation * (mapcam.zoom_level as f64 * DVec3::new(1.0, 0.0, 0.0));
|
|
|
|
// Update movement offset
|
|
let mut direction = pov_rotation * DVec3::new(offset_x, 0.0, offset_z);
|
|
let speed = direction.length();
|
|
direction.y = 0.0;
|
|
let direction = speed * direction.normalize_or_zero();
|
|
|
|
mapcam.offset_x += 0.01 * (direction.x * mapcam.zoom_level);
|
|
mapcam.offset_z += 0.01 * (direction.z * mapcam.zoom_level);
|
|
|
|
// Apply updates to camera
|
|
camera_transform.translation = point_of_view.as_vec3();
|
|
camera_transform.look_at(Vec3::ZERO, Vec3::Y);
|
|
}
|
|
|
|
pub fn update_mapcam_center(
|
|
mut mapcam: ResMut<MapCam>,
|
|
settings: Res<var::Settings>,
|
|
q_pos: Query<&Position>,
|
|
) {
|
|
if !settings.map_active {
|
|
return;
|
|
}
|
|
if let Some(entity) = mapcam.center_on_entity {
|
|
if let Ok(pos) = q_pos.get(entity) {
|
|
let offset = DVec3::new(mapcam.offset_x, 0.0, mapcam.offset_z);
|
|
mapcam.center = **pos + offset;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_fov(
|
|
q_player: Query<&actor::ExperiencesGForce, With<actor::Player>>,
|
|
mouse_input: Res<ButtonInput<MouseButton>>,
|
|
mut settings: ResMut<var::Settings>,
|
|
mut q_camera: Query<&mut Projection, With<Camera>>,
|
|
) {
|
|
if let (Ok(gforce), Ok(mut projection)) = (q_player.get_single(), q_camera.get_single_mut()) {
|
|
let fov: f32;
|
|
if mouse_input.pressed(settings.key_zoom) {
|
|
fov = settings.zoom_fov.to_radians();
|
|
if !settings.is_zooming {
|
|
settings.is_zooming = true;
|
|
}
|
|
} else {
|
|
fov = (gforce.visual_effect.clamp(0.0, 1.0) * settings.fov_highspeed + settings.fov)
|
|
.to_radians();
|
|
if settings.is_zooming {
|
|
settings.is_zooming = false;
|
|
}
|
|
};
|
|
*projection = Projection::Perspective(PerspectiveProjection {
|
|
fov: fov,
|
|
..default()
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn handle_input(
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
settings: Res<var::Settings>,
|
|
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
|
|
mut ew_game: EventWriter<GameEvent>,
|
|
) {
|
|
if keyboard_input.just_pressed(settings.key_camera) {
|
|
ew_game.send(GameEvent::SetThirdPerson(Toggle));
|
|
}
|
|
if keyboard_input.just_pressed(settings.key_map) {
|
|
ew_game.send(GameEvent::SetMap(Toggle));
|
|
}
|
|
if keyboard_input.just_pressed(settings.key_rotation_stabilizer) {
|
|
ew_game.send(GameEvent::SetRotationStabilizer(Toggle));
|
|
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
|
|
}
|
|
}
|
|
|
|
fn manage_player_actor(
|
|
mut commands: Commands,
|
|
settings: Res<var::Settings>,
|
|
mut q_playercam: Query<&mut Visibility, With<actor::PlayerCamera>>,
|
|
mut q_hiddenplayer: Query<
|
|
(
|
|
Entity,
|
|
&mut Visibility,
|
|
&mut Position,
|
|
&mut Rotation,
|
|
&mut LinearVelocity,
|
|
&mut AngularVelocity,
|
|
Option<&mut actor::ExperiencesGForce>,
|
|
Option<&actor::JustNowEnteredVehicle>,
|
|
),
|
|
(With<actor::Player>, Without<actor::PlayerCamera>),
|
|
>,
|
|
q_ride: Query<
|
|
(
|
|
&Transform,
|
|
&Position,
|
|
&Rotation,
|
|
&LinearVelocity,
|
|
&AngularVelocity,
|
|
),
|
|
(With<actor::PlayerDrivesThis>, Without<actor::Player>),
|
|
>,
|
|
) {
|
|
for mut vis in &mut q_playercam {
|
|
if settings.third_person || settings.map_active {
|
|
*vis = Visibility::Inherited;
|
|
} else {
|
|
*vis = Visibility::Hidden;
|
|
}
|
|
}
|
|
for (entity, mut vis, mut pos, mut rot, mut v, mut angv, mut gforce, entering) in
|
|
&mut q_hiddenplayer
|
|
{
|
|
// If we are riding a vehicle, place the player at the position where
|
|
// it would be after exiting the vehicle.
|
|
// I would rather place it in the center of the vehicle, but at the time
|
|
// of writing, I couldn't set the position/rotation of the player *during*
|
|
// exiting the vehicle, so I'm doing it here instead, as a workaround.
|
|
*vis = Visibility::Hidden;
|
|
if let Ok((ride_trans, ride_pos, ride_rot, ride_v, ride_angv)) = q_ride.get_single() {
|
|
pos.0 = ride_pos.0
|
|
+ DVec3::from(ride_trans.rotation * Vec3::new(0.0, 0.0, ride_trans.scale.z * 2.0));
|
|
rot.0 = ride_rot.0 * DQuat::from_array([-1.0, 0.0, 0.0, 0.0]);
|
|
*v = ride_v.clone();
|
|
*angv = ride_angv.clone();
|
|
|
|
// I really don't want people to die from the g-forces of entering
|
|
// vehicles at high relative speed, even though they probably should.
|
|
if let (Some(gforce), Some(_)) = (&mut gforce, entering) {
|
|
gforce.last_linear_velocity = v.0;
|
|
commands
|
|
.entity(entity)
|
|
.remove::<actor::JustNowEnteredVehicle>();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn apply_input_to_player(
|
|
time: Res<Time>,
|
|
mut commands: Commands,
|
|
settings: Res<var::Settings>,
|
|
jupiter_pos: Res<game::JupiterPos>,
|
|
windows: Query<&Window, With<PrimaryWindow>>,
|
|
mut mouse_events: EventReader<MouseMotion>,
|
|
key_input: Res<ButtonInput<KeyCode>>,
|
|
q_audiosinks: Query<(&audio::Sfx, &AudioSink)>,
|
|
q_target: Query<&LinearVelocity, (With<hud::IsTargeted>, Without<actor::PlayerCamera>)>,
|
|
q_closest: Query<
|
|
(&Position, &LinearVelocity),
|
|
(
|
|
Without<hud::IsTargeted>,
|
|
Without<visual::IsEffect>,
|
|
Without<actor::PlayerCamera>,
|
|
Without<actor::Player>,
|
|
Without<actor::PlayerDrivesThis>,
|
|
),
|
|
>,
|
|
mut q_playercam: Query<
|
|
(
|
|
Entity,
|
|
&Transform,
|
|
&mut actor::Engine,
|
|
&Position,
|
|
&mut LinearVelocity,
|
|
&mut ExternalTorque,
|
|
Option<&actor::PlayerDrivesThis>,
|
|
),
|
|
(With<actor::PlayerCamera>, Without<Camera>),
|
|
>,
|
|
mut ew_effect: EventWriter<visual::SpawnEffectEvent>,
|
|
) {
|
|
if settings.map_active || !settings.in_control() {
|
|
return;
|
|
}
|
|
let dt = time.delta_seconds();
|
|
let mut play_thruster_sound = false;
|
|
let mut axis_input: DVec3 = DVec3::ZERO;
|
|
|
|
let (win_res_x, win_res_y): (f32, f32);
|
|
let mut focused = true;
|
|
if let Ok(window) = &windows.get_single() {
|
|
focused = window.focused;
|
|
win_res_x = window.resolution.width();
|
|
win_res_y = window.resolution.height();
|
|
} else {
|
|
win_res_x = 1920.0;
|
|
win_res_y = 1050.0;
|
|
}
|
|
|
|
if let Ok((player_entity, player_transform, mut engine, pos, mut v, mut torque, bike)) =
|
|
q_playercam.get_single_mut()
|
|
{
|
|
let target_v: DVec3 = if let Ok(target) = q_target.get_single() {
|
|
target.0
|
|
} else {
|
|
let mut closest_distance = MAX_DIST_FOR_MATCH_VELOCITY;
|
|
let mut closest_velocity = None;
|
|
for (testpos, v) in q_closest.iter() {
|
|
let distance = (pos.0 - testpos.0).length();
|
|
if distance < closest_distance {
|
|
closest_velocity = Some(v);
|
|
closest_distance = distance;
|
|
}
|
|
}
|
|
if closest_velocity.is_some() {
|
|
closest_velocity.unwrap().0
|
|
} else {
|
|
let relative_pos = pos.0 - jupiter_pos.0;
|
|
nature::orbital_velocity(relative_pos, nature::JUPITER_MASS)
|
|
}
|
|
};
|
|
// Handle key input
|
|
if focused {
|
|
if key_input.pressed(settings.key_forward) || settings.cruise_control_active {
|
|
axis_input.z += 1.2;
|
|
}
|
|
if key_input.pressed(settings.key_back) {
|
|
axis_input.z -= 1.2;
|
|
}
|
|
if key_input.pressed(settings.key_right) {
|
|
axis_input.x -= 1.2;
|
|
}
|
|
if key_input.pressed(settings.key_left) {
|
|
axis_input.x += 1.2;
|
|
}
|
|
if key_input.pressed(settings.key_up) {
|
|
axis_input.y += 1.2;
|
|
}
|
|
if key_input.pressed(settings.key_down) {
|
|
axis_input.y -= 1.2;
|
|
}
|
|
if key_input.pressed(settings.key_stop) {
|
|
let stop_direction = (target_v - v.0).normalize();
|
|
if stop_direction.length_squared() > 0.3 {
|
|
axis_input += 1.0
|
|
* DVec3::from(
|
|
player_transform.rotation.inverse() * stop_direction.as_vec3(),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
if settings.cruise_control_active {
|
|
axis_input.z += 1.2;
|
|
}
|
|
}
|
|
// In typical games we would normalize the input vector so that diagonal movement is as
|
|
// fast as forward or sideways movement. But here, we merely clamp each direction to an
|
|
// absolute maximum of 1, since every thruster can be used separately. If the forward
|
|
// thrusters and the leftward thrusters are active at the same time, then of course the
|
|
// total diagonal acceleration is faster than the forward acceleration alone.
|
|
axis_input = axis_input.clamp(DVec3::splat(-1.0), DVec3::splat(1.0));
|
|
|
|
// Apply movement update
|
|
let forward_factor = engine.current_warmup
|
|
* (if axis_input.z > 0.0 {
|
|
engine.thrust_forward
|
|
} else {
|
|
engine.thrust_back
|
|
});
|
|
let right_factor = engine.thrust_sideways * engine.current_warmup;
|
|
let up_factor = engine.thrust_sideways * engine.current_warmup;
|
|
let factor = DVec3::new(right_factor as f64, up_factor as f64, forward_factor as f64);
|
|
let boost = if bike.is_none() {
|
|
engine.current_boost_factor
|
|
} else {
|
|
1.0
|
|
};
|
|
|
|
if axis_input.length_squared() > 0.003 {
|
|
engine.currently_firing = true;
|
|
let acceleration_global: DVec3 =
|
|
DVec3::from(player_transform.rotation * (axis_input * factor).as_vec3());
|
|
let mut acceleration_total: DVec3 =
|
|
(actor::ENGINE_SPEED_FACTOR * dt) as f64 * boost * acceleration_global;
|
|
let threshold = 1e-5;
|
|
if key_input.pressed(settings.key_stop) {
|
|
// Decelerate (or match velocity to target_v)
|
|
engine.currently_matching_velocity = true;
|
|
let dv = v.0 - target_v;
|
|
for i in 0..3 {
|
|
if dv[i].abs() < threshold {
|
|
v[i] = target_v[i];
|
|
} else if dv[i].signum() != (dv[i] + acceleration_total[i]).signum() {
|
|
// Almost stopped, but we overshot v=0
|
|
v[i] = target_v[i];
|
|
acceleration_total[i] = 0.0;
|
|
}
|
|
}
|
|
} else {
|
|
engine.currently_matching_velocity = false;
|
|
}
|
|
// TODO: handle mass
|
|
v.0 += acceleration_total;
|
|
engine.current_warmup =
|
|
(engine.current_warmup + dt / engine.warmup_seconds).clamp(0.0, 1.0);
|
|
play_thruster_sound = true;
|
|
|
|
// Visual effect
|
|
if bike.is_none() && acceleration_total.length_squared() > 1e-4 {
|
|
let thruster_direction = acceleration_total.normalize();
|
|
let thruster_pos = pos.0 - 0.3 * thruster_direction;
|
|
let thruster_v = v.0 - boost * 5.0 * thruster_direction;
|
|
ew_effect.send(visual::SpawnEffectEvent {
|
|
duration: 2.0,
|
|
class: visual::Effects::ThrusterParticle(
|
|
Position::from(thruster_pos),
|
|
LinearVelocity::from(thruster_v),
|
|
),
|
|
});
|
|
}
|
|
} else {
|
|
engine.currently_firing = false;
|
|
engine.currently_matching_velocity = false;
|
|
engine.current_warmup =
|
|
(engine.current_warmup - dt / engine.warmup_seconds).clamp(0.0, 1.0);
|
|
}
|
|
|
|
// Handle mouse input and mouse-like key bindings
|
|
let mut play_reactionwheel_sound = false;
|
|
let mut mouse_delta = Vec2::ZERO;
|
|
let mut pitch_yaw_rot = Vec3::ZERO;
|
|
let sensitivity_factor = if settings.is_zooming {
|
|
settings.zoom_sensitivity_factor
|
|
} else {
|
|
1.0
|
|
};
|
|
let mouseless_sensitivity = 8.0 * sensitivity_factor;
|
|
let mouseless_rotation_sensitivity = 40.0 * sensitivity_factor;
|
|
if key_input.pressed(settings.key_mouseup) {
|
|
pitch_yaw_rot[0] -= mouseless_sensitivity;
|
|
}
|
|
if key_input.pressed(settings.key_mousedown) {
|
|
pitch_yaw_rot[0] += mouseless_sensitivity;
|
|
}
|
|
if key_input.pressed(settings.key_mouseleft) {
|
|
pitch_yaw_rot[1] += mouseless_sensitivity;
|
|
}
|
|
if key_input.pressed(settings.key_mouseright) {
|
|
pitch_yaw_rot[1] -= mouseless_sensitivity;
|
|
}
|
|
if key_input.pressed(settings.key_rotateleft) {
|
|
pitch_yaw_rot[2] -= mouseless_rotation_sensitivity;
|
|
}
|
|
if key_input.pressed(settings.key_rotateright) {
|
|
pitch_yaw_rot[2] += mouseless_rotation_sensitivity;
|
|
}
|
|
for mouse_event in mouse_events.read() {
|
|
mouse_delta += mouse_event.delta;
|
|
}
|
|
if mouse_delta != Vec2::ZERO {
|
|
if key_input.pressed(settings.key_rotate) {
|
|
pitch_yaw_rot[2] += 1000.0 * mouse_delta.x / win_res_x;
|
|
} else {
|
|
pitch_yaw_rot[0] += 1000.0 * mouse_delta.y / win_res_y;
|
|
pitch_yaw_rot[1] -= 1000.0 * mouse_delta.x / win_res_x;
|
|
}
|
|
}
|
|
|
|
let mouse_speed = pitch_yaw_rot.length();
|
|
let mouse_moving = mouse_speed > EPSILON32;
|
|
|
|
if mouse_moving {
|
|
play_reactionwheel_sound = true;
|
|
pitch_yaw_rot *=
|
|
settings.mouse_sensitivity * sensitivity_factor * engine.reaction_wheels;
|
|
torque.apply_torque(DVec3::from(
|
|
player_transform.rotation
|
|
* Vec3::new(pitch_yaw_rot[0], pitch_yaw_rot[1], pitch_yaw_rot[2]),
|
|
));
|
|
}
|
|
|
|
if settings.rotation_stabilizer_active || key_input.pressed(settings.key_stop) {
|
|
commands
|
|
.entity(player_entity)
|
|
.try_insert(actor::WantsMaxRotation(mouse_speed as f64 * 0.1));
|
|
} else {
|
|
commands
|
|
.entity(player_entity)
|
|
.remove::<actor::WantsMaxRotation>();
|
|
}
|
|
|
|
let mut sinks: HashMap<audio::Sfx, &AudioSink> = HashMap::new();
|
|
for (sfx, sink) in &q_audiosinks {
|
|
sinks.insert(*sfx, sink);
|
|
}
|
|
|
|
// Play sound effects
|
|
if let Some(sink) = sinks.get(&audio::Sfx::ElectricMotor) {
|
|
let reactionwheel_volume = 1.5;
|
|
let volume = sink.volume();
|
|
let speed = sink.speed();
|
|
let action = pitch_yaw_rot.length_squared().powf(0.2) * 0.0005;
|
|
if play_reactionwheel_sound && !settings.mute_sfx && bike.is_some() {
|
|
sink.set_volume(
|
|
settings.volume_sfx * reactionwheel_volume * (volume + action).clamp(0.0, 1.0),
|
|
);
|
|
sink.set_speed((speed + action * 0.2).clamp(0.2, 0.5));
|
|
sink.play()
|
|
} else {
|
|
if volume <= 0.01 {
|
|
sink.pause()
|
|
} else {
|
|
sink.set_volume((volume - 0.01).clamp(0.0, 1.0));
|
|
sink.set_speed((speed - 0.03).clamp(0.2, 0.5));
|
|
}
|
|
}
|
|
}
|
|
let sinks = vec![
|
|
(
|
|
1.0,
|
|
boost as f32,
|
|
actor::EngineType::Monopropellant,
|
|
sinks.get(&audio::Sfx::Thruster),
|
|
),
|
|
(
|
|
1.0,
|
|
1.0,
|
|
actor::EngineType::Ion,
|
|
sinks.get(&audio::Sfx::Ion),
|
|
),
|
|
];
|
|
let seconds_to_max_vol = 0.05;
|
|
let seconds_to_min_vol = 0.05;
|
|
for sink_data in sinks {
|
|
if let (vol_boost, speed, engine_type, Some(sink)) = sink_data {
|
|
if settings.mute_sfx {
|
|
sink.pause();
|
|
} else {
|
|
let volume = sink.volume();
|
|
let maxvol = settings.volume_sfx * vol_boost;
|
|
if engine.engine_type == engine_type {
|
|
if play_thruster_sound {
|
|
sink.set_speed(speed);
|
|
sink.play();
|
|
if volume < maxvol {
|
|
sink.set_volume(
|
|
(volume + dt / seconds_to_max_vol).clamp(0.0, maxvol),
|
|
);
|
|
}
|
|
} else {
|
|
sink.set_volume((volume - dt / seconds_to_min_vol).clamp(0.0, maxvol));
|
|
}
|
|
} else if volume > 0.0 {
|
|
sink.set_volume((volume - dt / seconds_to_min_vol).clamp(0.0, maxvol));
|
|
}
|
|
if volume < 0.0001 {
|
|
sink.pause();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_map_only_object_visibility(
|
|
settings: Res<var::Settings>,
|
|
q_camera: Query<&Transform, With<Camera>>,
|
|
q_player: Query<&Position, With<actor::PlayerCamera>>,
|
|
mut q_onlyinmap: Query<(&mut Visibility, &ShowOnlyInMap), Without<Camera>>,
|
|
id2pos: Res<game::Id2Pos>,
|
|
) {
|
|
if q_camera.is_empty() || q_player.is_empty() {
|
|
return;
|
|
}
|
|
let cam: &Transform = q_camera.get_single().unwrap();
|
|
let player_pos: &Position = q_player.get_single().unwrap();
|
|
let cam_pos: Vec3 = cam.translation + player_pos.as_vec3();
|
|
for (mut vis, onlyinmap) in &mut q_onlyinmap {
|
|
if settings.map_active && settings.hud_active {
|
|
if let Some(pos) = id2pos.0.get(&onlyinmap.distance_to_id) {
|
|
let dist = cam_pos.distance(pos.as_vec3());
|
|
if dist >= onlyinmap.min_distance as f32 {
|
|
*vis = Visibility::Inherited;
|
|
} else {
|
|
*vis = Visibility::Hidden;
|
|
}
|
|
} else {
|
|
error!(
|
|
"Failed get position of actor ID '{}'",
|
|
&onlyinmap.distance_to_id
|
|
);
|
|
*vis = Visibility::Hidden;
|
|
}
|
|
} else {
|
|
*vis = Visibility::Hidden;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find the closest world object that the player is looking at
|
|
#[inline]
|
|
pub fn find_closest_target<TargetSpecifier>(
|
|
objects: Vec<(TargetSpecifier, &Transform)>,
|
|
camera_transform: &Transform,
|
|
) -> (Option<TargetSpecifier>, f32)
|
|
where
|
|
TargetSpecifier: Clone,
|
|
{
|
|
let mut closest_entity: Option<TargetSpecifier> = None;
|
|
let mut closest_distance: f32 = f32::MAX;
|
|
let target_vector: Vec3 =
|
|
(camera_transform.rotation * Vec3::new(0.0, 0.0, -1.0)).normalize_or_zero();
|
|
for (entity, trans) in objects {
|
|
// Use Transform instead of Position because we're basing this
|
|
// not on the player mesh but on the camera, which doesn't have a position.
|
|
let (angular_diameter, angle, distance) =
|
|
calc_angular_diameter_known_target_vector(trans, camera_transform, &target_vector);
|
|
if angle <= angular_diameter.clamp(0.01, PI32) {
|
|
// It's in the field of view!
|
|
//commands.entity(entity).insert(IsTargeted);
|
|
let distance_to_surface = distance - trans.scale.x;
|
|
if distance_to_surface < closest_distance {
|
|
closest_distance = distance_to_surface;
|
|
closest_entity = Some(entity);
|
|
}
|
|
}
|
|
}
|
|
return (closest_entity, closest_distance);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn calc_angular_diameter_known_target_vector(
|
|
target: &Transform,
|
|
camera: &Transform,
|
|
target_vector: &Vec3,
|
|
) -> (f32, f32, f32) {
|
|
let pos_vector: Vec3 = (target.translation - camera.translation).normalize_or_zero();
|
|
let cosine_of_angle: f32 = target_vector.dot(pos_vector);
|
|
let angle: f32 = cosine_of_angle.acos();
|
|
let distance: f32 = target.translation.distance(camera.translation);
|
|
let leeway: f32 = 1.3;
|
|
let angular_diameter: f32 = if distance > 0.0 {
|
|
// Angular Diameter
|
|
leeway * (target.scale[0] / distance).asin()
|
|
} else {
|
|
0.0
|
|
};
|
|
return (angular_diameter, angle, distance);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn calc_angular_diameter(target: &Transform, camera: &Transform) -> (f32, f32, f32) {
|
|
let target_vector: Vec3 = (camera.rotation * Vec3::new(0.0, 0.0, -1.0)).normalize_or_zero();
|
|
return calc_angular_diameter_known_target_vector(target, camera, &target_vector);
|
|
}
|
|
|
|
// An extension of bevy_xpbd_3d::plugins::position_to_transform that adjusts
|
|
// the rendering position to center entities at the player camera.
|
|
// This avoids rendering glitches when very far away from the origin.
|
|
pub fn position_to_transform(
|
|
mapcam: Res<MapCam>,
|
|
settings: Res<var::Settings>,
|
|
q_player: Query<&Position, With<actor::PlayerCamera>>,
|
|
mut q_trans: Query<
|
|
(&'static mut Transform, &'static Position, &'static Rotation),
|
|
Without<Parent>,
|
|
>,
|
|
) {
|
|
let center: DVec3 = if settings.map_active {
|
|
mapcam.center
|
|
} else if let Ok(player_pos) = q_player.get_single() {
|
|
**player_pos
|
|
} else {
|
|
return;
|
|
};
|
|
|
|
for (mut transform, pos, rot) in &mut q_trans {
|
|
transform.translation = Vec3::new(
|
|
(pos.x - center.x) as f32,
|
|
(pos.y - center.y) as f32,
|
|
(pos.z - center.z) as f32,
|
|
);
|
|
transform.rotation = rot.as_quat();
|
|
}
|
|
}
|