outfly/src/actor.rs

536 lines
19 KiB
Rust
Raw Normal View History

2024-04-21 16:23:40 +00:00
// ▄████████▄ + ███ + ▄█████████ ███ +
// ███▀ ▀███ + + ███ ███▀ + ███ + +
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
2024-04-21 17:34:00 +00:00
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
2024-04-21 16:23:40 +00:00
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
// + + + ███
// + ▀████████████████████████████████████████████████████▀
2024-04-23 15:33:36 +00:00
//
// This module manages the internal states of individual characters,
// such as their resources, the damage they receive, and interactions
2024-05-12 21:57:21 +00:00
// between characters and with vehicles.
2024-04-23 15:45:40 +00:00
//
// This module should never handle any visual aspects directly.
2024-04-21 16:23:40 +00:00
2024-03-17 22:49:50 +00:00
use bevy::prelude::*;
2024-03-29 15:58:42 +00:00
use bevy_xpbd_3d::prelude::*;
2024-05-12 20:17:17 +00:00
use crate::prelude::*;
2024-03-29 01:40:55 +00:00
pub const ENGINE_SPEED_FACTOR: f32 = 30.0;
2024-04-14 19:51:59 +00:00
const MAX_TRANSMISSION_DISTANCE: f32 = 100.0;
2024-04-17 13:24:00 +00:00
const MAX_INTERACT_DISTANCE: f32 = 50.0;
2024-03-17 22:49:50 +00:00
pub struct ActorPlugin;
impl Plugin for ActorPlugin {
fn build(&self, app: &mut App) {
2024-03-19 15:14:12 +00:00
app.add_systems(FixedUpdate, (
update_physics_lifeforms,
2024-05-08 00:35:36 +00:00
update_power,
handle_wants_maxrotation,
handle_wants_maxvelocity,
2024-03-19 15:14:12 +00:00
));
2024-04-30 22:58:58 +00:00
app.add_systems(PostUpdate, handle_gforce
.after(PhysicsSet::Sync)
.after(sync::position_to_transform));
app.add_systems(Update, (
handle_input,
2024-03-29 15:58:42 +00:00
handle_collisions,
handle_damage,
));
app.add_systems(PostUpdate, (
handle_vehicle_enter_exit,
));
2024-03-28 15:02:36 +00:00
app.add_event::<VehicleEnterExitEvent>();
2024-03-17 22:49:50 +00:00
}
}
#[derive(Copy, Clone)]
pub enum DamageType {
Unknown,
Mental,
Trauma,
2024-05-12 20:30:53 +00:00
GForce,
Asphyxiation,
//Poison,
//Radiation,
//Freeze,
//Burn,
}
2024-03-28 15:02:36 +00:00
#[derive(Event)]
pub struct VehicleEnterExitEvent {
vehicle: Entity,
driver: Entity,
is_entering: bool,
is_player: bool
}
2024-03-17 22:49:50 +00:00
#[derive(Component)]
pub struct Actor {
pub id: String,
pub name: Option<String>,
pub camdistance: f32,
2024-03-17 22:49:50 +00:00
}
impl Default for Actor {
fn default() -> Self {
Self {
id: "".to_string(),
name: None,
2024-04-29 23:26:59 +00:00
camdistance: 7.5,
2024-03-17 22:49:50 +00:00
}
}
}
#[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,
}
}
}
2024-04-05 23:11:11 +00:00
#[derive(Component)]
pub struct ExperiencesGForce {
pub gforce: f32,
pub damage_threshold: f32,
2024-04-08 00:36:47 +00:00
pub visual_effect_threshold: f32,
pub visual_effect: f32,
2024-04-05 23:11:11 +00:00
pub last_linear_velocity: DVec3,
pub ignore_gforce_seconds: f32,
2024-04-05 23:11:11 +00:00
}
impl Default for ExperiencesGForce { fn default() -> Self { Self {
gforce: 0.0,
damage_threshold: 100.0,
2024-04-08 00:36:47 +00:00
visual_effect_threshold: 20.0,
visual_effect: 0.0,
2024-04-05 23:11:11 +00:00
last_linear_velocity: DVec3::splat(0.0),
ignore_gforce_seconds: 0.0,
2024-04-05 23:11:11 +00:00
}}}
#[derive(Component)] pub struct Player; // Attached to the suit of the player
2024-05-07 16:52:38 +00:00
#[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
2024-04-17 11:55:39 +00:00
#[derive(Component)] pub struct JustNowEnteredVehicle;
2024-03-28 15:02:36 +00:00
#[derive(Component)] pub struct ActorEnteringVehicle;
#[derive(Component)] pub struct ActorVehicleBeingEntered;
#[derive(Component)] pub struct PlayersFlashLight;
#[derive(Component)] pub struct WantsMaxRotation(pub f64);
#[derive(Component)] pub struct WantsMaxVelocity(pub f64);
#[derive(Component)] pub struct Identifier(pub String);
2024-03-17 22:49:50 +00:00
#[derive(Component)]
pub struct LifeForm {
2024-03-20 17:37:42 +00:00
pub is_alive: bool,
2024-03-17 22:49:50 +00:00
pub adrenaline: f32,
pub adrenaline_baseline: f32,
pub adrenaline_jolt: f32,
}
impl Default for LifeForm { fn default() -> Self { Self {
2024-03-20 17:37:42 +00:00
is_alive: true,
2024-03-17 22:49:50 +00:00
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,
}}}
2024-03-28 13:10:10 +00:00
#[derive(Copy, Clone, PartialEq)]
pub enum EngineType {
Monopropellant,
Rocket,
2024-03-29 03:36:20 +00:00
Ion,
2024-03-28 13:10:10 +00:00
}
#[derive(Component)]
pub struct Engine {
pub thrust_forward: f32,
pub thrust_back: f32,
pub thrust_sideways: f32,
pub reaction_wheels: f32,
2024-03-28 13:10:10 +00:00
pub engine_type: EngineType,
2024-03-28 23:01:17 +00:00
pub warmup_seconds: f32,
pub current_warmup: f32, // between 0.0 and 1.0
}
impl Default for Engine {
fn default() -> Self {
Self {
thrust_forward: 1.0,
thrust_back: 1.0,
thrust_sideways: 1.0,
reaction_wheels: 1.0,
2024-03-28 13:10:10 +00:00
engine_type: EngineType::Monopropellant,
2024-03-28 23:01:17 +00:00
warmup_seconds: 1.5,
current_warmup: 0.0,
}
}
}
2024-03-17 22:49:50 +00:00
#[derive(Component)]
pub struct Suit {
pub oxygen: f32,
pub oxygen_max: f32,
pub integrity: f32, // [0.0 - 1.0]
2024-03-17 22:49:50 +00:00
}
impl Default for Suit { fn default() -> Self { SUIT_SIMPLE } }
const SUIT_SIMPLE: Suit = Suit {
2024-03-18 22:53:52 +00:00
oxygen: nature::OXY_D,
oxygen_max: nature::OXY_D,
2024-03-30 18:39:53 +00:00
integrity: 1.0,
2024-03-17 22:49:50 +00:00
};
2024-05-08 00:35:36 +00:00
#[derive(Component)]
pub struct Battery {
pub power: f32, // Watt-seconds
pub capacity: f32, // Watt-seconds
pub reactor: f32, // Watt (production)
}
impl Default for Battery {
fn default() -> Self {
Self {
power: 10e3 * 3600.0,
capacity: 10e3 * 3600.0, // 10kWh
reactor: 2000e3, // 2MW
}
}
}
pub fn update_power(
time: Res<Time>,
2024-05-12 20:17:17 +00:00
mut settings: ResMut<Settings>,
2024-05-08 00:35:36 +00:00
mut q_battery: Query<(&mut Battery, Option<&Player>)>,
mut q_flashlight: Query<&mut Visibility, With<PlayersFlashLight>>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
) {
let d = time.delta_seconds();
for (mut battery, player) in &mut q_battery {
if player.is_some() && settings.flashlight_active {
battery.power -= 4000000.0 * d;
if battery.power <= 0.0 {
settings.flashlight_active = false;
2024-05-08 04:42:35 +00:00
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
2024-05-08 00:35:36 +00:00
for mut flashlight_vis in &mut q_flashlight {
*flashlight_vis = Visibility::Hidden;
}
}
}
battery.power = (battery.power + battery.reactor * d)
.clamp(0.0, battery.capacity);
}
}
2024-03-19 15:14:12 +00:00
pub fn update_physics_lifeforms(
2024-03-17 22:49:50 +00:00
time: Res<Time>,
mut query: Query<(&mut LifeForm, &mut HitPoints, &mut Suit, &LinearVelocity)>,
2024-03-17 22:49:50 +00:00
) {
let d = time.delta_seconds();
for (mut lifeform, mut hp, mut suit, velocity) in query.iter_mut() {
2024-03-17 22:49:50 +00:00
if lifeform.adrenaline_jolt.abs() > 1e-3 {
lifeform.adrenaline_jolt *= 0.99;
}
else {
lifeform.adrenaline_jolt = 0.0
}
let speed = velocity.length();
2024-03-28 22:13:59 +00:00
if speed > 1000.0 {
lifeform.adrenaline += 0.001;
}
2024-03-17 22:49:50 +00:00
lifeform.adrenaline = (lifeform.adrenaline - 0.0001 + lifeform.adrenaline_jolt * 0.01).clamp(0.0, 1.0);
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;
}
2024-03-17 22:49:50 +00:00
}
}
pub fn handle_input(
mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
2024-05-12 20:17:17 +00:00
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, &Transform), (With<actor::Vehicle>, Without<actor::Player>, Without<Camera>)>,
mut ew_conv: EventWriter<chat::StartConversationEvent>,
2024-03-28 15:02:36 +00:00
mut ew_vehicle: EventWriter<VehicleEnterExitEvent>,
2024-05-12 21:57:21 +00:00
mut ew_playerdies: EventWriter<game::PlayerDiesEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
2024-03-28 15:02:36 +00:00
q_player_drives: Query<Entity, With<PlayerDrivesThis>>,
) {
2024-04-05 18:01:44 +00:00
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
2024-04-05 18:01:44 +00:00
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()});
}
}
2024-03-28 15:02:36 +00:00
// Entering Vehicles
if q_player_drives.is_empty() {
let objects: Vec<(Entity, &Transform)> = q_vehicles
.iter()
.collect();
if let (Some(entity), dist) = camera::find_closest_target::<Entity>(objects, camtrans) {
if dist <= MAX_INTERACT_DISTANCE {
commands.entity(entity).insert(ActorVehicleBeingEntered);
2024-04-05 18:01:44 +00:00
commands.entity(player_entity).insert(ActorEnteringVehicle);
ew_vehicle.send(VehicleEnterExitEvent{
vehicle: entity,
2024-04-05 18:01:44 +00:00
driver: player_entity,
is_entering: q_player_drives.is_empty(),
is_player: true,
});
2024-03-28 15:02:36 +00:00
}
}
}
}
else if keyboard_input.just_pressed(settings.key_vehicle) {
2024-03-28 15:02:36 +00:00
// Exiting Vehicles
2024-04-05 18:01:44 +00:00
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,
is_entering: false,
is_player: true,
});
break;
}
}
else if keyboard_input.just_pressed(settings.key_flashlight) {
for mut flashlight_vis in &mut q_flashlight {
2024-05-08 04:42:35 +00:00
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;
}
}
}
2024-04-11 18:47:11 +00:00
else if keyboard_input.just_pressed(settings.key_restart) {
settings.god_mode = false;
2024-05-12 21:57:21 +00:00
ew_playerdies.send(game::PlayerDiesEvent(DamageType::Mental));
2024-04-11 18:47:11 +00:00
}
}
2024-03-28 15:02:36 +00:00
pub fn handle_vehicle_enter_exit(
mut commands: Commands,
2024-05-12 20:17:17 +00:00
mut settings: ResMut<Settings>,
2024-03-28 15:02:36 +00:00
mut er_vehicle: EventReader<VehicleEnterExitEvent>,
mut q_playerflashlight: Query<&mut Visibility, (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>)>,
2024-03-28 15:02:36 +00:00
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
) {
for event in er_vehicle.read() {
for (driver, mut driver_vis, driver_collider) in q_drivers.iter_mut() {
2024-03-28 15:02:36 +00:00
if driver == event.driver {
for (vehicle, mut vehicle_component, mut vehicle_vis) in q_vehicles.iter_mut() {
if !event.is_player {
continue;
}
2024-03-28 15:02:36 +00:00
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::<Collider>();
2024-04-17 11:55:39 +00:00
commands.entity(driver).insert(JustNowEnteredVehicle);
commands.entity(vehicle).insert(PlayerCamera);
commands.entity(vehicle).insert(PlayerDrivesThis);
if let Ok(mut flashlight_vis) = q_playerflashlight.get_single_mut() {
*flashlight_vis = Visibility::Hidden;
2024-05-08 00:35:36 +00:00
settings.flashlight_active = false;
}
2024-03-28 15:02:36 +00:00
}
else {
// Exiting Vehicle
if let Some(collider) = &vehicle_component.stored_drivers_collider {
commands.entity(driver).insert(collider.clone());
2024-03-28 15:02:36 +00:00
}
commands.entity(driver).insert(RigidBody::Dynamic);
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
commands.entity(vehicle).remove::<PlayerCamera>();
commands.entity(driver).insert(PlayerCamera);
commands.entity(vehicle).remove::<PlayerDrivesThis>();
2024-04-03 10:27:43 +00:00
*vehicle_vis = Visibility::Visible;
2024-03-28 15:02:36 +00:00
}
}
}
}
}
}
}
2024-03-29 15:58:42 +00:00
fn handle_collisions(
mut collision_event_reader: EventReader<CollisionStarted>,
2024-03-29 15:58:42 +00:00
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
q_player: Query<Entity, With<PlayerCollider>>,
2024-04-05 23:11:11 +00:00
mut q_player_lifeform: Query<(&mut LifeForm, &mut Suit), With<Player>>,
2024-03-29 15:58:42 +00:00
) {
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 {
2024-03-29 15:58:42 +00:00
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Crash));
2024-04-01 04:24:29 +00:00
lifeform.adrenaline_jolt += 0.1;
2024-05-07 22:32:20 +00:00
suit.integrity = (suit.integrity - 0.03).max(0.0);
2024-03-29 15:58:42 +00:00
}
}
}
}
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();
2024-05-12 22:48:41 +00:00
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.01).clamp(1.001, 1.1)) as f64;
v_ang.0 *= angular_slowdown;
}
}
}
fn handle_wants_maxvelocity(
time: Res<Time>,
mut query: Query<(&mut LinearVelocity, &Engine, &WantsMaxVelocity)>,
) {
let dt = time.delta_seconds();
for (mut v, engine, maxv) in &mut query {
let total = v.0.length();
2024-05-12 22:48:41 +00:00
if total <= maxv.0 + EPSILON {
if total > maxv.0 {
v.0 = DVec3::splat(0.0);
}
// already not moving
continue;
}
// 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 * -v.0;
v.0 += acceleration;
2024-05-12 22:48:41 +00:00
if v.0.length() + EPSILON < acceleration.length() {
v.0 = DVec3::splat(0.0);
}
}
}
2024-04-05 00:58:02 +00:00
fn handle_damage(
2024-05-12 21:57:21 +00:00
mut ew_playerdies: EventWriter<game::PlayerDiesEvent>,
mut q_hp: Query<(&mut HitPoints, Option<&Player>), Changed<HitPoints>>,
2024-05-12 20:17:17 +00:00
settings: Res<Settings>,
) {
for (mut hp, player_maybe) in &mut q_hp {
if player_maybe.is_some() {
2024-04-07 23:44:36 +00:00
if !settings.god_mode {
hp.current -= hp.damage;
}
if hp.current <= 0.0 {
2024-05-12 21:57:21 +00:00
ew_playerdies.send(game::PlayerDiesEvent(hp.damagetype));
}
}
2024-04-07 23:44:36 +00:00
else {
hp.current -= hp.damage;
}
hp.damage = 0.0;
}
}
2024-04-05 23:11:11 +00:00
fn handle_gforce(
time: Res<Time>,
mut q_actor: Query<(&LinearVelocity, &mut HitPoints, &mut ExperiencesGForce)>,
) {
let dt = time.delta_seconds();
2024-04-07 23:53:56 +00:00
let factor = 1.0 / dt / nature::EARTH_GRAVITY;
2024-04-05 23:11:11 +00:00
for (v, mut hp, mut gforce) in &mut q_actor {
2024-04-07 23:53:56 +00:00
gforce.gforce = factor * (v.0 - gforce.last_linear_velocity).length() as f32;
gforce.last_linear_velocity = v.0;
if gforce.ignore_gforce_seconds > 0.0 {
gforce.ignore_gforce_seconds -= dt;
continue;
}
2024-04-05 23:11:11 +00:00
if gforce.gforce > gforce.damage_threshold {
hp.damage += (gforce.gforce - gforce.damage_threshold).powf(2.0) / 3000.0;
2024-05-12 20:30:53 +00:00
hp.damagetype = DamageType::GForce;
2024-04-05 23:11:11 +00:00
}
2024-04-08 00:36:47 +00:00
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
}
2024-04-05 23:11:11 +00:00
}
}