outfly/src/actor.rs

1128 lines
40 KiB
Rust

// ▄████████▄ + ███ + ▄█████████ ███ +
// ███▀ ▀███ + + ███ ███▀ + ███ + +
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
// + + + ███
// + ▀████████████████████████████████████████████████████▀
//
// 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;
}
}