twgame 0.11.0

DDNet physics implementation
Documentation
use crate::entities::tee::TEE_PROXIMITY;
use crate::entities::Tee;
use crate::ids::PlayerUid;
use crate::map::{HookHit, TuneZone};
use crate::state::GameState;
use crate::tuning::Tuning;
use std::fmt;
use std::fmt::Write as _;
use twgame_core::normalize;
use twgame_core::twsnap::enums::HookState;
use twgame_core::twsnap::enums::Sound;
use twgame_core::twsnap::flags::TeeFlags;
use twgame_core::twsnap::items::Tee as SnapTee;
use twgame_core::twsnap::time::{Duration, Instant};
use twgame_core::twsnap::vec2_from_bits;
use vek::num_traits::SaturatingSub;
use vek::Vec2;

#[derive(Clone, Debug)]
pub struct Hook {
    state: HookState,
    tele_base: Option<Vec2<f32>>,
    pos: Vec2<f32>,
    direction: Vec2<f32>,
    attached_tee: Option<PlayerUid>,
    /// keep track on how long to keep the hook
    duration: Duration,
    endless_hook: bool,
}

impl Hook {
    pub fn new() -> Hook {
        Hook {
            state: HookState::Idle,
            tele_base: None,
            pos: Vec2::zero(),
            direction: Vec2::zero(),
            attached_tee: None,
            // how long to keep on hooking
            duration: Duration::T0MS,
            endless_hook: false,
        }
    }

    fn hook_tick(&self) -> Duration {
        if self.duration == Duration::T0MS {
            Duration::T0MS
        } else {
            Duration::T120MS.saturating_sub(&self.duration)
        }
    }

    pub fn format_save_1(&self, f: &mut String) -> fmt::Result {
        let hook_pos_x = self.pos.x as i32;
        let hook_pos_y = self.pos.y as i32;
        let hook_dir_x = self.direction.x;
        let hook_dir_y = self.direction.y;
        let hook_tele_base = self.tele_base.unwrap_or(Vec2::zero());
        let hook_tick = self.hook_tick().ticks();
        let hook_state = self.state as i32;
        write!(
            f,
            "\t\
            {hook_pos_x}\t{hook_pos_y}\t{hook_dir_x:.6}\t{hook_dir_y:.6}\t\
            {hook_tele_base_x}\t{hook_tele_base_y}\t{hook_tick}\t{hook_state}",
            hook_tele_base_x = hook_tele_base.x,
            hook_tele_base_y = hook_tele_base.y,
        )
    }

    pub fn format_save_2(&self, f: &mut String) -> fmt::Result {
        // TODO: save/load hooked player
        let hooked_player = -1;
        let new_hook = self.tele_base.is_some() as i32;
        write!(f, "\t{hooked_player}\t{new_hook}")
    }

    pub fn load_1(&mut self, tee_info: &mut std::str::Split<char>) {
        self.pos.x = tee_info.next().unwrap().parse::<i32>().unwrap() as f32;
        self.pos.y = tee_info.next().unwrap().parse::<i32>().unwrap() as f32;
        self.direction.x = tee_info.next().unwrap().parse::<f32>().unwrap();
        self.direction.y = tee_info.next().unwrap().parse::<f32>().unwrap();
        self.tele_base = Some(Vec2::new(
            tee_info.next().unwrap().parse::<i32>().unwrap() as f32,
            tee_info.next().unwrap().parse::<i32>().unwrap() as f32,
        ));
        let _hook_tick = tee_info.next().unwrap().parse::<i32>().unwrap();
        let hook_state = tee_info.next().unwrap().parse::<i32>().unwrap();
        self.state = HookState::from(hook_state);
    }

    pub fn load_2(&mut self, tee_info: &mut std::str::Split<char>) {
        let _hooked_player = tee_info.next().unwrap().parse::<i32>().unwrap();
        let new_hook = tee_info.next().unwrap_or("0").parse::<i32>().unwrap();
        if new_hook == 0 {
            self.tele_base = None;
        }
    }

    /// returns true if hook was dispatched
    pub fn on_hook_input(
        &mut self,
        tuning: &Tuning,
        pos: Vec2<f32>,
        target_dir: Vec2<f32>,
    ) -> bool {
        if self.state == HookState::Idle {
            self.state = HookState::Flying;
            self.pos = pos + target_dir * TEE_PROXIMITY * 1.5;
            self.direction = target_dir;
            self.attached_tee = None;
            self.duration = Duration::from_secs_f32(tuning.hook_duration)
                .decrement()
                .unwrap_or(Duration::T0MS)
                .decrement()
                .unwrap_or(Duration::T0MS);
            return true;
        }
        false
    }

    pub fn on_no_hook_input(&mut self, tee_pos: Vec2<f32>) {
        self.pos = tee_pos; // this is just set to make the save-string correct, generally not necessary
        self.attached_tee = None;
        self.tele_base = None;
        self.state = HookState::Idle;
    }

    pub fn reset(&mut self, core_pos: Vec2<f32>) {
        self.attached_tee = None;
        self.state = HookState::Retracted;
        self.pos = core_pos;
    }

    pub fn release_if_requested(&mut self, now: Instant, game_state: &GameState) {
        let Some(tee) = self.attached_tee else {
            return;
        };
        let Some(core) = game_state.tee_cores.get_tee(tee) else {
            return;
        };
        if core.last_hook_release() == now {
            self.attached_tee = None;
            self.state = HookState::Retracted;
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub(super) fn tick(
        &mut self,
        now: Instant,
        game_state: &mut GameState,
        tune_zone: TuneZone,
        tee_uid: PlayerUid,
        tee_hook_disabled: bool,
        direction: i32,        // tee movement (left/right)
        target_dir: Vec2<f32>, // cursor direction
    ) {
        match self.state {
            HookState::Idle | HookState::Retracted => {}
            HookState::RetractStart => self.state = HookState::Retracting,
            HookState::Retracting => self.state = HookState::RetractEnd,
            HookState::RetractEnd => {
                self.state = HookState::Retracted;
                // TODO: COREEVENT_HOOK_RETRACT
            }
            HookState::Flying => {
                let tee_core = game_state.tee_cores.get_tee(tee_uid).unwrap();
                let tee_pos = tee_core.pos();
                let tuning = game_state.map.tuning(tune_zone);
                let mut new_pos = self.pos + self.direction * tuning.hook_fire_speed;
                let hook_base = self.tele_base.unwrap_or(tee_core.pos());
                if new_pos.distance(hook_base) > tuning.hook_length {
                    self.state = HookState::RetractStart;
                    // TODO: look for abuses of Bug
                    new_pos = hook_base + normalize(new_pos - hook_base) * tuning.hook_length;
                }

                let hit = game_state.map.intersect_hook(self.pos, &mut new_pos);

                // Check against other players first
                // WARNING: using magnitude2 instead of magnitude
                let mut distance_squared: f32 = 0.0;
                if tuning.player_hooking != 0.0 && !tee_core.is_solo() && !tee_hook_disabled {
                    for (uid, other_tee) in game_state
                        .tee_order
                        .id_order()
                        .tees_except(&game_state.tee_cores, tee_uid)
                        .filter(|(_, tee)| !tee.is_solo())
                    {
                        if let Some(closest_point) =
                            Tee::closed_point_on_line(self.pos, new_pos, other_tee.pos())
                        {
                            if other_tee.pos().distance(closest_point) < TEE_PROXIMITY + 2.0
                                && (self.attached_tee.is_none()
                                    || self.pos.distance_squared(other_tee.pos())
                                        < distance_squared)
                            {
                                // TODO: m_TriggeredEvents |= COREEVENT_HOOK_ATTACH_PLAYER
                                game_state
                                    .events
                                    .create_sound(tee_pos, Sound::HookAttachPlayer);
                                self.state = HookState::Grabbed;
                                self.attached_tee = Some(uid);
                                distance_squared = self.pos.distance_squared(other_tee.pos());
                            }
                        }
                    }
                }

                if self.state == HookState::Flying {
                    self.pos = new_pos;
                    match hit {
                        Some(HookHit::Collision) => {
                            // TODO: COREEVENT_HOOK_ATTACH_GROUND
                            game_state
                                .events
                                .create_sound(tee_pos, Sound::HookAttachGround);
                            self.state = HookState::Grabbed;
                        }
                        Some(HookHit::Unhookable) => {
                            // TODO: COREEVENT_HOOK_HIT_NOHOOK
                            game_state.events.create_sound(tee_pos, Sound::HookNoattach);
                            self.state = HookState::RetractStart;
                        }
                        Some(HookHit::Tele(tele_id)) => {
                            if let Some(tele_out) =
                                game_state
                                    .map
                                    .select_tele_out(now, &mut game_state.prng, tele_id)
                            {
                                self.tele_base = Some(tele_out);
                                self.pos = tele_out + target_dir * TEE_PROXIMITY * 1.5;
                                self.direction = target_dir;
                            }
                        }
                        None => {}
                    }
                }
            }
            HookState::Grabbed => {}
        }
        if self.state == HookState::Grabbed {
            // TODO: Release if currently hooked player is dead
            //
            let tee_core = game_state.tee_cores.get_tee(tee_uid).unwrap();
            let tee_pos = tee_core.pos();
            if self.attached_tee.is_none() && self.pos.distance(tee_pos) > 46.0 {
                let tuning = game_state.map.tuning(tune_zone);
                let tee_core = game_state.tee_cores.get_tee_mut(tee_uid).unwrap();
                let mut hook_vel = normalize(self.pos - tee_pos) * tuning.hook_drag_accel;
                // the hook as more power to drag you up than down.
                // this makes it easier to get on top of a platform
                if hook_vel.y > 0.0 {
                    hook_vel.y *= 0.3;
                }

                // the hook will boost it's power if the player wants to move
                // in that direction. otherwise it will dampen everything a bit
                if hook_vel.x < 0.0 && direction < 0 || hook_vel.x > 0.0 && direction > 0 {
                    hook_vel.x *= 0.95;
                } else {
                    hook_vel.x *= 0.75;
                }

                let new_vel = hook_vel + tee_core.vel();

                // check if we are under the legal limit for the hook
                if new_vel.magnitude() < tuning.hook_drag_speed
                    || new_vel.magnitude() < tee_core.vel().magnitude()
                {
                    tee_core.set_vel(new_vel); // no problem. apply
                }
            }

            if self.attached_tee.is_some() {
                // release hook (max default hook time is 1.25 s)
                if let Some(duration) = self.duration.decrement() {
                    self.duration = duration;
                } else {
                    // release hook
                    self.attached_tee = None;
                    self.state = HookState::Retracted;
                    self.pos = tee_pos; // TODO: <- necessary??, probably can remove
                }
            }
        }
    }

    pub fn handle_player_hook(
        &self,
        tee_uid: PlayerUid,
        game_state: &mut GameState,
        tune_zone: TuneZone,
    ) {
        let tuning = game_state.map.tuning(tune_zone);
        if tuning.player_hooking == 0.0 {
            return;
        }
        // retrieve hooked tee
        let Some(other_tee) = self.attached_tee.as_ref() else {
            return;
        };
        let Some([tee, other_tee]) = game_state.tee_cores.get_tees_mut([tee_uid, *other_tee])
        else {
            return;
        };
        if other_tee.is_solo() {
            return;
        }

        let distance = tee.pos().distance(other_tee.pos());
        let dir = normalize(tee.pos() - other_tee.pos());

        if distance <= TEE_PROXIMITY * 1.5 {
            return;
        }
        let accel = tuning.hook_drag_accel * (distance / tuning.hook_length);
        let drag_speed = tuning.hook_drag_speed;

        // add force to the hooked player
        other_tee.satured_impact(dir * accel * 1.5, drag_speed);

        // add a little bit of force to the tee who has the grip
        tee.satured_impact(-accel * dir * 0.25, drag_speed);
    }

    pub fn set_endless(&mut self, activated: bool) {
        self.endless_hook = activated;
    }

    pub fn has_endless(&self) -> bool {
        self.endless_hook
    }

    pub fn post_core_tick(&mut self) {
        if self.endless_hook {
            self.duration = Duration::T980MS;
        }
    }

    pub fn round(&mut self) {
        self.pos.x = self.pos.x.round() as i32 as f32;
        self.pos.y = self.pos.y.round() as i32 as f32;

        self.direction.x = (self.direction.x * 256.0).round() as i32 as f32 / 256.0;
        self.direction.y = (self.direction.y * 256.0).round() as i32 as f32 / 256.0;
    }

    pub fn snap(&self, tee: &mut SnapTee) {
        if self.endless_hook {
            tee.flags.insert(TeeFlags::ENDLESS_HOOK);
        }
        tee.hook_state = self.state;
        tee.hook_tick = self.hook_tick();
        tee.hook_pos = vec2_from_bits(self.pos.round());
        tee.hook_direction = vec2_from_bits((self.direction * 256.0).round());
        // retrieve player_id
        tee.hooked_player = self.attached_tee.map(|pid| pid.snap_id());
    }

    pub fn on_tee_swap(&mut self, pid1: PlayerUid, pid2: PlayerUid) {
        if let Some(hooked_player) = self.attached_tee.as_mut() {
            if *hooked_player == pid1 {
                *hooked_player = pid2;
            } else if *hooked_player == pid2 {
                *hooked_player = pid1;
            }
        }
    }

    pub fn reset_hooked_player(&mut self, tee_uid: PlayerUid) {
        if let Some(hooked_player) = self.attached_tee.as_ref() {
            if *hooked_player == tee_uid {
                self.attached_tee = None;
                self.state = HookState::Retracted;
            }
        }
    }
}