1122 lines
40 KiB
Rust
1122 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) {
|
|
// 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(),
|
|
});
|
|
}
|
|
}
|
|
// Entering Vehicles
|
|
if 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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.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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|