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