// ▄████████▄ + ███ + ▄█████████ ███ + // ███▀ ▀███ + + ███ ███▀ + ███ + + // ███ + ███ ███ ███ █████████ ███ ███ ███ ███ // ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███ // ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███ // ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███ // ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████ // + + + ███ // + ▀████████████████████████████████████████████████████▀ // // This module manages the internal states of individual characters, // such as their resources, the damage they receive, and interactions // between characters and with vehicles. // // This module should never handle any visual aspects directly. use crate::prelude::*; use bevy::prelude::*; use bevy_xpbd_3d::prelude::*; use std::collections::HashMap; pub const ENGINE_SPEED_FACTOR: f32 = 30.0; const MAX_TRANSMISSION_DISTANCE: f32 = 100.0; const MAX_INTERACT_DISTANCE: f32 = 50.0; pub const POWER_DRAIN_THRUSTER: [f32; 3] = [3000.0, 3000.0, 0.0]; pub const THRUSTER_BOOST_FACTOR: [f64; 3] = [3.0, 3.0, 0.0]; pub const POWER_DRAIN_FLASHLIGHT: [f32; 4] = [200.0, 1500.0, 2500.0, 10000.0]; pub const FLASHLIGHT_INTENSITY: [f32; 4] = [10e6, 400e6, 2e9, 100e9]; // in lumens pub const POWER_DRAIN_LIGHTAMP: [f32; 4] = [0.0, 200.0, 400.0, 1400.0]; pub const POWER_DRAIN_AR: f32 = 300.0; pub const POWER_GAIN_REACTOR: [f32; 3] = [0.0, 2000.0, 10000.0]; pub struct ActorPlugin; impl Plugin for ActorPlugin { fn build(&self, app: &mut App) { app.add_systems( FixedUpdate, ( update_physics_lifeforms.run_if(game_running), update_power.run_if(game_running), handle_gravity.run_if(game_running), handle_wants_rotation_change.run_if(game_running), handle_wants_maxrotation.run_if(game_running), handle_wants_maxvelocity .run_if(game_running) .run_if(any_with_component::<WantsMaxVelocity>), handle_wants_lookat.run_if(game_running).run_if(alive), ), ); app.add_systems( PreUpdate, handle_wants_acceleration .run_if(game_running) .run_if(alive) .after(PhysicsSet::Sync) .after(sync::position_to_transform), ); app.add_systems( PostUpdate, handle_gforce .run_if(game_running) .run_if(alive) .after(PhysicsSet::Sync) .after(sync::position_to_transform) .after(handle_wants_acceleration), ); app.add_systems( Update, ( handle_input.run_if(in_control).run_if(game_running), handle_collisions.run_if(game_running), handle_damage.run_if(game_running), ), ); app.add_systems( PostUpdate, (handle_vehicle_enter_exit.run_if(game_running),), ); app.add_event::<VehicleEnterExitEvent>(); } } #[derive(Copy, Clone)] pub enum DamageType { Unknown, Mental, Trauma, GForce, Asphyxiation, Depressurization, //Poison, Radiation, //Freeze, //Burn, } #[derive(Event)] pub struct VehicleEnterExitEvent { vehicle: Entity, driver: Entity, name: Option<String>, is_entering: bool, is_player: bool, } #[derive(Component)] pub struct Actor { pub id: String, pub name: Option<String>, pub camdistance: f32, } impl Default for Actor { fn default() -> Self { Self { id: "".to_string(), name: None, camdistance: 7.5, } } } #[derive(Component)] pub struct HitPoints { pub current: f32, pub max: f32, pub damage: f32, pub damagetype: DamageType, } impl Default for HitPoints { fn default() -> Self { Self { current: 100.0, max: 100.0, damage: 0.0, damagetype: DamageType::Unknown, } } } #[derive(Component)] pub struct ExperiencesGForce { pub gforce: f32, pub damage_threshold: f32, pub visual_effect_threshold: f32, pub visual_effect: f32, pub last_linear_velocity: DVec3, pub gravitational_component: DVec3, pub ignore_gforce_seconds: f32, } impl Default for ExperiencesGForce { fn default() -> Self { Self { gforce: 0.0, damage_threshold: 100.0, visual_effect_threshold: 20.0, visual_effect: 0.0, last_linear_velocity: DVec3::ZERO, gravitational_component: DVec3::ZERO, ignore_gforce_seconds: 0.01, } } } #[derive(Component, Default)] pub struct WantsAcceleration { pub direction: DVec3, pub brake: bool, } #[derive(Component)] pub struct Player; // Attached to the suit of the player #[derive(Component)] pub struct PlayerCollider; // Attached to the collider of the suit of the player #[derive(Component)] pub struct PlayerDrivesThis; // Attached to the entered vehicle #[derive(Component)] pub struct PlayerCamera; // Attached to the actor to use as point of view #[derive(Component)] pub struct JustNowEnteredVehicle; #[derive(Component)] pub struct ActorEnteringVehicle; #[derive(Component)] pub struct ActorVehicleBeingEntered; #[derive(Component)] pub struct HiddenInsideVehicle; #[derive(Component)] pub struct MessageOnVehicleEntry(pub String); #[derive(Component)] pub struct PlayersFlashLight; #[derive(Component)] pub struct MirrorLight; #[derive(Component)] pub struct WantsMaxRotation(pub f64); #[derive(Component)] pub struct WantsMaxVelocity(pub f64); #[derive(Component)] pub struct WantsToLookAt(pub String); #[derive(Component)] pub struct WantsRotationChange(pub Vec3); // Vec3 = (pitch, yaw, rot) #[derive(Component)] pub struct WantsMatchVelocityWith(pub String); #[derive(Component)] pub struct Identifier(pub String); #[derive(Component)] pub struct OrbitsJupiter; #[derive(Component)] pub struct LifeForm { pub is_alive: bool, pub is_radioactively_damaged: bool, pub adrenaline: f32, pub adrenaline_baseline: f32, pub adrenaline_jolt: f32, } impl Default for LifeForm { fn default() -> Self { Self { is_alive: true, is_radioactively_damaged: false, adrenaline: 0.3, adrenaline_baseline: 0.3, adrenaline_jolt: 0.0, } } } #[derive(Component)] pub struct Vehicle { stored_drivers_collider: Option<Collider>, } impl Default for Vehicle { fn default() -> Self { Self { stored_drivers_collider: None, } } } #[derive(Copy, Clone, PartialEq)] pub enum EngineType { Monopropellant, Ion, } #[derive(Component, Copy, Clone)] pub struct Engine { pub thrust_forward: f32, pub thrust_back: f32, pub thrust_sideways: f32, pub reaction_wheels: f32, pub engine_type: EngineType, pub warmup_seconds: f32, pub current_warmup: f32, // between 0.0 and 1.0 pub current_boost_factor: f64, pub currently_firing: bool, pub currently_matching_velocity: bool, } impl Default for Engine { fn default() -> Self { Self { thrust_forward: 1.0, thrust_back: 1.0, thrust_sideways: 1.0, reaction_wheels: 1.0, engine_type: EngineType::Monopropellant, warmup_seconds: 1.5, current_warmup: 0.0, current_boost_factor: 1.0, currently_firing: false, currently_matching_velocity: false, } } } #[derive(Component)] pub struct Suit { pub oxygen: f32, pub oxygen_max: f32, pub integrity: f32, // [0.0 - 1.0] } impl Default for Suit { fn default() -> Self { SUIT_SIMPLE } } const SUIT_SIMPLE: Suit = Suit { oxygen: nature::OXY_D, oxygen_max: nature::OXY_D, integrity: 1.0, }; #[derive(Component)] pub struct Battery { pub power: f32, // Watt-seconds pub capacity: f32, // Watt-seconds pub overloaded_recovering: bool, } impl Default for Battery { fn default() -> Self { Self { power: 10.0 * 3600.0, capacity: 10.0 * 3600.0, // 10Wh overloaded_recovering: false, } } } pub fn update_power( time: Res<Time>, mut settings: ResMut<Settings>, prefs: Res<Preferences>, mut q_battery: Query<(&mut Battery, &mut Engine), With<Player>>, mut q_flashlight: Query<&mut Visibility, With<PlayersFlashLight>>, q_bike: Query<&PlayerDrivesThis>, mut ew_sfx: EventWriter<audio::PlaySfxEvent>, mut ew_game: EventWriter<game::GameEvent>, ) { let mut power_down = false; let d = time.delta_seconds(); let inside_vehicle = !q_bike.is_empty(); for (mut battery, mut engine) in &mut q_battery { if !inside_vehicle && !settings.god_mode { if settings.flashlight_active { battery.power -= POWER_DRAIN_FLASHLIGHT[prefs.flashlight_power] * d; // 2.4MW if battery.power <= 0.0 { power_down = true; settings.flashlight_active = false; for mut flashlight_vis in &mut q_flashlight { *flashlight_vis = Visibility::Hidden; } } } if settings.hud_active { let mut hud_drain = POWER_DRAIN_AR; hud_drain += POWER_DRAIN_LIGHTAMP[prefs.light_amp]; battery.power -= hud_drain * d; if battery.power <= 0.0 { power_down = true; ew_game.send(GameEvent::SetAR(Turn::Off)); for mut flashlight_vis in &mut q_flashlight { *flashlight_vis = Visibility::Hidden; } } } let drain = POWER_DRAIN_THRUSTER[prefs.thruster_boost]; let boosting = !battery.overloaded_recovering && prefs.thruster_boost != 2 && (prefs.thruster_boost == 1 || engine.currently_matching_velocity); if boosting { if battery.power > drain * d * 100.0 { engine.current_boost_factor = THRUSTER_BOOST_FACTOR[prefs.thruster_boost]; if engine.currently_firing { battery.power -= drain * d; } } else { power_down = true; battery.overloaded_recovering = true; engine.current_boost_factor = 1.0; } } else { engine.current_boost_factor = 1.0; } if power_down { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::PowerDown)); } } let reactor = POWER_GAIN_REACTOR[settings.reactor_state]; battery.power = (battery.power + reactor * d).clamp(0.0, battery.capacity); if battery.overloaded_recovering && battery.power > battery.capacity * 0.5 { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::PowerUp)); battery.overloaded_recovering = false; } } } pub fn update_physics_lifeforms( time: Res<Time>, settings: Res<Settings>, id2pos: Res<game::Id2Pos>, mut query: Query<( &mut LifeForm, &mut HitPoints, &mut Suit, &LinearVelocity, &Position, Option<&Player>, )>, q_bike: Query<&PlayerDrivesThis>, ) { let d = time.delta_seconds(); let inside_vehicle = !q_bike.is_empty(); for (mut lifeform, mut hp, mut suit, velocity, pos, player) in query.iter_mut() { if lifeform.adrenaline_jolt.abs() > 1e-3 { lifeform.adrenaline_jolt *= 0.99; } else { lifeform.adrenaline_jolt = 0.0 } let speed = velocity.length(); if speed > 1000.0 { lifeform.adrenaline += 0.001; } lifeform.adrenaline = (lifeform.adrenaline - 0.0001 + lifeform.adrenaline_jolt * 0.01).clamp(0.0, 1.0); if player.is_some() { lifeform.is_radioactively_damaged = if !inside_vehicle && settings.reactor_state == 2 { true } else if let Some(pos_jupiter) = id2pos.0.get(cmd::ID_JUPITER) { pos_jupiter.distance(pos.0) < 140_000_000.0 } else { false }; if lifeform.is_radioactively_damaged { hp.damage += 0.3 * d; hp.damagetype = DamageType::Radiation; } } let mut oxygen_drain = nature::OXY_S; let integr_threshold = 0.5; if suit.integrity < integr_threshold { // The oxygen drain from suit integrity scales with (2 - 2x)^4, // which is a function with ~16 at x=0 and 0 at x=1. // Furthermore, x is divided by the integrity threshold (e.g. 0.5) // to squeeze the function horizontally, and change the range for // the x parameter from [0-1] to [0-integritythreshold] // // 16 |. // |. // |'. // | '. // | '.. // |______''.... // x=0 x=1 let drain_scaling = (2.0 - 2.0 * suit.integrity / integr_threshold).powf(4.0); oxygen_drain += suit.oxygen * 0.01 * drain_scaling; } suit.oxygen = (suit.oxygen - oxygen_drain * d).clamp(0.0, suit.oxygen_max); if suit.oxygen <= 0.0 { hp.damage += 1.0 * d; hp.damagetype = DamageType::Asphyxiation; } } } pub fn handle_input( mut commands: Commands, keyboard_input: Res<ButtonInput<KeyCode>>, mut settings: ResMut<Settings>, q_talker: Query<(&chat::Talker, &Transform), (Without<actor::Player>, Without<Camera>)>, player: Query<Entity, With<actor::Player>>, q_camera: Query<&Transform, With<Camera>>, mut q_flashlight: Query<&mut Visibility, With<PlayersFlashLight>>, q_vehicles: Query< (Entity, &Actor, &Transform, Option<&MessageOnVehicleEntry>), ( With<actor::Vehicle>, Without<actor::Player>, Without<Camera>, ), >, mut ew_conv: EventWriter<chat::StartConversationEvent>, mut ew_vehicle: EventWriter<VehicleEnterExitEvent>, mut ew_sfx: EventWriter<audio::PlaySfxEvent>, mut log: ResMut<hud::Log>, q_player_drives: Query<Entity, With<PlayerDrivesThis>>, ) { if q_camera.is_empty() || player.is_empty() { return; } let camtrans = q_camera.get_single().unwrap(); let player_entity = player.get_single().unwrap(); if keyboard_input.just_pressed(settings.key_interact) { let mut done = false; // Talking to people let objects: Vec<(chat::Talker, &Transform)> = q_talker .iter() .map(|(talker, transform)| (talker.clone(), transform)) .collect(); // TODO: replace Transform.translation with Position if let (Some(talker), dist) = camera::find_closest_target::<chat::Talker>(objects, camtrans) { if dist <= MAX_TRANSMISSION_DISTANCE { ew_conv.send(chat::StartConversationEvent { talker: talker.clone(), }); done = true; } } // Entering Vehicles if !done && q_player_drives.is_empty() { // Sort vehicles by their distance to the player let objects: Vec<((Entity, &Actor, Option<&MessageOnVehicleEntry>), &Transform)> = q_vehicles .iter() .map(|(entity, actor, transform, msg)| ((entity, actor, msg), transform)) .collect(); // Get the vehicle with shortest distance if let (Some((entity, actor, msg)), dist) = camera::find_closest_target::<(Entity, &Actor, Option<&MessageOnVehicleEntry>)>( objects, camtrans, ) { if dist <= MAX_INTERACT_DISTANCE { commands.entity(entity).insert(ActorVehicleBeingEntered); commands.entity(player_entity).insert(ActorEnteringVehicle); ew_vehicle.send(VehicleEnterExitEvent { vehicle: entity, driver: player_entity, name: actor.name.clone(), is_entering: q_player_drives.is_empty(), is_player: true, }); if let Some(msg) = msg { log.warning(msg.0.clone()); } } } } } else if keyboard_input.just_pressed(settings.key_vehicle) { // Exiting Vehicles for vehicle_entity in &q_player_drives { commands .entity(vehicle_entity) .insert(ActorVehicleBeingEntered); commands.entity(player_entity).insert(ActorEnteringVehicle); ew_vehicle.send(VehicleEnterExitEvent { vehicle: vehicle_entity, driver: player_entity, name: None, is_entering: false, is_player: true, }); break; } } else if keyboard_input.just_pressed(settings.key_flashlight) { for mut flashlight_vis in &mut q_flashlight { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch)); if *flashlight_vis == Visibility::Hidden { *flashlight_vis = Visibility::Visible; settings.flashlight_active = true; } else { *flashlight_vis = Visibility::Hidden; settings.flashlight_active = false; } } } else if keyboard_input.just_pressed(settings.key_cruise_control) { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch)); settings.cruise_control_active ^= true; } } pub fn handle_vehicle_enter_exit( mut commands: Commands, mut er_vehicle: EventReader<VehicleEnterExitEvent>, mut ew_achievement: EventWriter<game::AchievementEvent>, mut q_playerflashlight: Query< &mut Transform, ( With<PlayersFlashLight>, Without<ActorVehicleBeingEntered>, Without<ActorEnteringVehicle>, ), >, mut q_drivers: Query< (Entity, &mut Visibility, Option<&Collider>), ( Without<ActorVehicleBeingEntered>, With<ActorEnteringVehicle>, ), >, mut q_vehicles: Query< (Entity, &mut Vehicle, &mut Visibility), ( With<ActorVehicleBeingEntered>, Without<ActorEnteringVehicle>, ), >, mut ew_sfx: EventWriter<audio::PlaySfxEvent>, ) { for event in er_vehicle.read() { for (driver, mut driver_vis, driver_collider) in q_drivers.iter_mut() { if driver == event.driver { for (vehicle, mut vehicle_component, mut vehicle_vis) in q_vehicles.iter_mut() { if !event.is_player { continue; } if vehicle == event.vehicle { if event.is_entering { // Entering Vehicle if let Some(collider) = driver_collider { vehicle_component.stored_drivers_collider = Some(collider.clone()); } commands.entity(driver).remove::<RigidBody>(); *driver_vis = Visibility::Hidden; //seems to have no effect... ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::EnterVehicle)); commands.entity(driver).remove::<PlayerCamera>(); commands.entity(driver).remove::<WantsRotationChange>(); commands.entity(driver).remove::<Collider>(); commands.entity(driver).remove::<WantsAcceleration>(); commands.entity(driver).insert(JustNowEnteredVehicle); commands.entity(driver).insert(HiddenInsideVehicle); commands .entity(vehicle) .insert(WantsAcceleration::default()); commands.entity(vehicle).remove::<hud::IsTargeted>(); commands.entity(vehicle).insert(PlayerCamera); commands.entity(vehicle).insert(PlayerDrivesThis); commands.entity(vehicle).insert(WantsMaxRotation(0.0)); if let Ok(mut flashlight_trans) = q_playerflashlight.get_single_mut() { flashlight_trans.rotation = Quat::from_rotation_y(0f32); flashlight_trans.translation = Vec3::new(0.0, 0.0, 0.0); } if let Some(vehicle_name) = &event.name { ew_achievement.send(game::AchievementEvent::RideVehicle( vehicle_name.clone(), )); } } else { // Exiting Vehicle if let Some(collider) = &vehicle_component.stored_drivers_collider { commands.entity(driver).insert(collider.clone()); } if let Ok(mut flashlight_trans) = q_playerflashlight.get_single_mut() { flashlight_trans.rotation = Quat::from_rotation_y(180f32.to_radians()); flashlight_trans.translation = Vec3::new(0.0, 0.0, 1.0); } commands.entity(driver).remove::<HiddenInsideVehicle>(); commands.entity(driver).insert(WantsAcceleration::default()); commands.entity(driver).insert(RigidBody::Dynamic); ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::ExitVehicle)); commands.entity(vehicle).remove::<WantsMaxRotation>(); commands.entity(vehicle).remove::<PlayerCamera>(); commands.entity(driver).insert(PlayerCamera); commands.entity(vehicle).remove::<PlayerDrivesThis>(); commands.entity(vehicle).remove::<WantsAcceleration>(); commands.entity(vehicle).remove::<WantsRotationChange>(); *vehicle_vis = Visibility::Visible; } } } } } } } fn handle_collisions( mut collision_event_reader: EventReader<CollisionStarted>, mut ew_sfx: EventWriter<audio::PlaySfxEvent>, q_player: Query<Entity, With<PlayerCollider>>, mut q_player_lifeform: Query<(&mut LifeForm, &mut Suit), With<Player>>, ) { if let (Ok(player), Ok((mut lifeform, mut suit))) = (q_player.get_single(), q_player_lifeform.get_single_mut()) { for CollisionStarted(entity1, entity2) in collision_event_reader.read() { if *entity1 == player || *entity2 == player { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Crash)); lifeform.adrenaline_jolt += 0.1; suit.integrity = (suit.integrity - 0.03).max(0.0); } } } } fn handle_wants_maxrotation( //time: Res<Time>, mut query: Query<(&mut AngularVelocity, &Engine, &WantsMaxRotation)>, ) { //let d = time.delta_seconds(); for (mut v_ang, engine, maxrot) in &mut query { let total = v_ang.0.length(); if total <= maxrot.0 + EPSILON { if total > maxrot.0 { v_ang.0 = DVec3::splat(0.0); } } else { let angular_slowdown: f64 = (2.0 - engine.reaction_wheels.powf(0.05).clamp(1.001, 1.1)) as f64; v_ang.0 *= angular_slowdown; } } } /// Slows down NPC's movement until they reach their target velocity. fn handle_wants_maxvelocity( time: Res<Time>, mut query: Query<( &Position, &mut LinearVelocity, &Engine, &WantsMaxVelocity, Option<&OrbitsJupiter>, Option<&WantsMatchVelocityWith>, )>, id2v: Res<game::Id2V>, jupiter_pos: Res<game::JupiterPos>, ) { let dt = time.delta_seconds(); for (pos, mut v, engine, maxv, orbits_jupiter, matchwith) in &mut query { let target_velocity = if let Some(matchwith) = matchwith { if let Some(target_v) = id2v.0.get(&matchwith.0) { *target_v } else { warn!("Can't match velocity with nonexisting ID {}", matchwith.0); continue; } } else if orbits_jupiter.is_some() { let relative_pos = pos.0 - jupiter_pos.0; nature::orbital_velocity(relative_pos, nature::JUPITER_MASS) } else { DVec3::ZERO }; let relative_velocity = v.0 - target_velocity; let relative_speed = relative_velocity.length(); if relative_speed <= maxv.0 + EPSILON { // it's already pretty close to the target if relative_speed > maxv.0 { // but not quite the target, so let's set it to the target v.0 = target_velocity; } } else { // slow it down a little bit // TODO: respect engine parameters for different thrusts for different directions let avg_thrust = (engine.thrust_forward + engine.thrust_back + engine.thrust_sideways) / 3.0; let acceleration = (avg_thrust * dt) as f64 * -relative_velocity; v.0 += acceleration; if v.0.length() + EPSILON < acceleration.length() { v.0 = target_velocity; } } } } fn handle_wants_rotation_change(mut q_actor: Query<(&mut Rotation, &mut WantsRotationChange)>) { for (mut rot, mut change_rot) in &mut q_actor { if change_rot.0 == Vec3::ZERO { continue; } let actual_change_rot = change_rot.0.clamp_length_max(0.10); for axis in 0..3 { let original = change_rot.0[axis]; change_rot.0[axis] = 0.90 * change_rot.0[axis] - actual_change_rot[axis]; if original.signum() != change_rot.0[axis].signum() { change_rot.0[axis] = 0.0; } } let change = DQuat::from_euler( EulerRot::XYZ, actual_change_rot[0] as f64, actual_change_rot[1] as f64, actual_change_rot[2] as f64, ); **rot = **rot * change; } } fn handle_wants_acceleration( time: Res<Time>, settings: Res<var::Settings>, jupiter_pos: Res<game::JupiterPos>, q_audiosinks: Query<(&audio::Sfx, &AudioSink)>, mut q_actor: Query< ( Entity, &Transform, &Position, &mut LinearVelocity, Option<&mut Engine>, Option<&WantsAcceleration>, Option<&hud::IsTargeted>, Option<&PlayerCamera>, ), (Without<visual::IsEffect>, Without<HiddenInsideVehicle>), >, mut ew_effect: EventWriter<visual::SpawnEffectEvent>, ) { let dt = time.delta_seconds(); // Vector elements: (Entity, is_player, pos) let mut request_closest: Vec<(Entity, bool, DVec3)> = vec![]; let mut closest_map: HashMap<Entity, DVec3> = HashMap::new(); // First, determine whether any actor wants to brake (=match velocity) for (entity, _, pos, _, _, accel, _, is_player) in &mut q_actor { if accel.is_some() && accel.unwrap().brake { request_closest.push((entity, is_player.is_some(), pos.0.clone())); } } // If an actor is braking, find out relative to what it wants to brake for (entity, is_player, pos) in &request_closest { let mut target_v: Option<DVec3> = None; // First, if this is the player, check whether they targeted anything // so we can match velocity to the target. if *is_player { for (_, _, _, v, _, _, is_target, _) in &q_actor { if is_target.is_some() { target_v = Some(v.0); break; } } } // If not, simply look for the closest object and match velocity to that. if target_v.is_none() { let mut closest_distance = camera::MAX_DIST_FOR_MATCH_VELOCITY; for (testentity, _, testpos, v, _, _, _, _) in &q_actor { if *entity != testentity { let distance = (*pos - testpos.0).length(); if distance < closest_distance { target_v = Some(v.0); closest_distance = distance; } } } } // Last resort: Match velocity to the orbital velocity around Jupiter if target_v.is_none() { let relative_pos = *pos - jupiter_pos.0; target_v = Some(nature::orbital_velocity(relative_pos, nature::JUPITER_MASS)); } if let Some(target_v) = target_v { closest_map.insert(*entity, target_v); } } // Finally, apply the requested acceleration to the actor's velocity let mut play_thruster_sound = false; let mut players_engine: Option<Engine> = None; for (entity, trans, pos, mut v, engine, accel, _, is_player) in &mut q_actor { let mut thruster_on = false; if let (Some(mut engine), Some(accel)) = (engine, accel) { let mut delta_v = DVec3::ZERO; let mut allow_fullstop = false; let boost = engine.current_boost_factor; engine.currently_matching_velocity = false; if accel.brake { if let Some(target_v) = closest_map.get(&entity) { let stop_direction = (*target_v - v.0).as_vec3(); if stop_direction.length_squared() > 0.003 { delta_v = (trans.rotation.inverse() * stop_direction.normalize()).as_dvec3(); engine.currently_matching_velocity = true; thruster_on = true; // is this redundant? } } } if accel.direction != DVec3::ZERO { // Player is pressing AWSD keys. // When braking AND accelerating, boost acceleration a bit to // overcome the braking and cause a slow acceleration: let brake_factor = if accel.brake { 1.10 } else { 1.0 }; delta_v += accel.direction.normalize() * brake_factor; } else if accel.brake { // Player is only pressing space. allow_fullstop = true; } if delta_v.length_squared() > 0.003 { // Engine is firing! thruster_on = true; engine.currently_firing = true; engine.current_warmup = (engine.current_warmup + dt / engine.warmup_seconds).clamp(0.0, 1.0); delta_v = delta_v.clamp(DVec3::splat(-1.0), DVec3::splat(1.0)); // Adjust acceleration to what the engine can actually provide let factor_forward = if accel.direction.z > 0.0 { engine.thrust_forward } else { engine.thrust_back }; let factor_right = engine.thrust_sideways; let factor_up = engine.thrust_sideways; let engine_factor = Vec3::new(factor_right, factor_up, factor_forward).as_dvec3() * engine.current_warmup as f64 * ENGINE_SPEED_FACTOR as f64 * engine.current_boost_factor; let final_accel = (trans.rotation * (delta_v * engine_factor).as_vec3() * dt).as_dvec3(); // Apply acceleration to velocity if allow_fullstop { if let Some(target_v) = closest_map.get(&entity) { // Prevent overshooting when matching velocity, which // would result in oscillating acceleration back and forth for axis in 0..3 { let original = v[axis]; let target = target_v[axis]; v[axis] += final_accel[axis]; if (original - target).signum() != (v[axis] - target).signum() { v[axis] = target; } } } } else { **v += final_accel; } // Visual effect if engine.engine_type == EngineType::Monopropellant { let thruster_direction = final_accel.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 is not firing engine.current_warmup = (engine.current_warmup - dt / engine.warmup_seconds).clamp(0.0, 1.0); engine.currently_firing = false; } if is_player.is_some() { play_thruster_sound = thruster_on; players_engine = Some((*engine).clone()); } } } // Play sound effects for player acceleration let engine = if let Some(engine) = players_engine { engine } else { warn!("Failed to retrieve player's engine type for playing SFX"); Engine::default() }; let mut sinks: HashMap<audio::Sfx, &AudioSink> = HashMap::new(); for (sfx, sink) in &q_audiosinks { sinks.insert(*sfx, sink); } let sinks = vec![ ( 1.0, engine.current_boost_factor as f32, actor::EngineType::Monopropellant, sinks.get(&audio::Sfx::Thruster), ), ( 0.6, 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(); } } } } } fn handle_wants_lookat( mut query: Query< ( &Position, &mut Rotation, &Transform, &WantsToLookAt, Option<&visual::IsEffect>, ), Without<Camera>, >, q_playercam: Query<&Position, With<PlayerCamera>>, q_cam: Query<&Transform, With<Camera>>, id2pos: Res<game::Id2Pos>, ) { let player_pos = if let Ok(player_pos) = q_playercam.get_single() { player_pos } else { return; }; let cam_pos = if let Ok(cam_trans) = q_cam.get_single() { cam_trans.translation.as_dvec3() + player_pos.0 } else { return; }; // TODO: use ExternalTorque rather than hard-resetting the rotation for (pos, mut rot, trans, target_id, is_effect) in &mut query { let target_pos: DVec3 = if target_id.0 == cmd::ID_SPECIAL_PLAYERCAM { cam_pos } else if let Some(target_pos) = id2pos.0.get(&target_id.0) { *target_pos } else { continue; }; let up = if is_effect.is_some() { // trans.up() sometimes crashes with thruster particle effects Dir3::Y } else { if trans.translation.length_squared() > 1e-6 { trans.up() } else { Dir3::Y } }; rot.0 = look_at_quat(**pos, target_pos, up.as_dvec3()); } } fn handle_damage( mut ew_playerdies: EventWriter<game::PlayerDiesEvent>, mut q_hp: Query<(&mut HitPoints, Option<&Player>), Changed<HitPoints>>, settings: Res<Settings>, ) { for (mut hp, player_maybe) in &mut q_hp { if player_maybe.is_some() { if !settings.god_mode { hp.current -= hp.damage; } if hp.current <= 0.0 { ew_playerdies.send(game::PlayerDiesEvent(hp.damagetype)); } } else { hp.current -= hp.damage; } hp.damage = 0.0; } } fn handle_gforce( time: Res<Time>, mut q_actor: Query<(&LinearVelocity, &mut HitPoints, &mut ExperiencesGForce)>, ) { let dt = time.delta_seconds(); let factor = 1.0 / dt / nature::EARTH_GRAVITY; for (v, mut hp, mut gforce) in &mut q_actor { gforce.gforce = factor * (v.0 - gforce.last_linear_velocity - gforce.gravitational_component).length() as f32; gforce.last_linear_velocity = v.0; gforce.gravitational_component = DVec3::ZERO; if gforce.ignore_gforce_seconds > 0.0 { gforce.ignore_gforce_seconds -= dt; continue; } if gforce.gforce > gforce.damage_threshold { hp.damage += (gforce.gforce - gforce.damage_threshold).powf(2.0) / 3000.0; hp.damagetype = DamageType::GForce; } if gforce.visual_effect > 0.0001 { gforce.visual_effect *= 0.984; } else if gforce.visual_effect > 0.0 { gforce.visual_effect = 0.0; } if gforce.gforce > gforce.visual_effect_threshold { gforce.visual_effect += (gforce.gforce - gforce.visual_effect_threshold).powf(2.0) / 300000.0 } } } fn handle_gravity( time: Res<Time>, mut q_pos: Query< ( &Position, &mut LinearVelocity, Option<&mut ExperiencesGForce>, ), With<OrbitsJupiter>, >, jupiter_pos: Res<game::JupiterPos>, ) { let dt = time.delta_seconds() as f64; // this assumes prograde orbits for every object for (pos, mut v, gforce_maybe) in &mut q_pos { let relative_pos = pos.0 - jupiter_pos.0; let accel = dt * nature::gravitational_acceleration(relative_pos, nature::JUPITER_MASS); if let Some(mut gforce) = gforce_maybe { gforce.gravitational_component += accel; } v.0 += accel; } }