twgame 0.11.0

DDNet physics implementation
Documentation
use crate::entities::tee::TEE_PROXIMITY;
use crate::entities::{SpawnOrder, SpawnOrderEntity, SpawnableEntity};
use crate::ids::PlayerUid;
use crate::map::{coord, TuneZone};
use crate::state::{closest_point_on_line, BugKind, GameState};
use crate::{Bug, SnapOuter};
use std::ops::Not;
use twgame_core::normalize;
use twgame_core::twsnap::enums::LaserType;
use twgame_core::twsnap::enums::Sound;
use twgame_core::twsnap::time::{Duration, Instant};
use twgame_core::twsnap::{items, vec2_from_bits, Snap, SnapId};
use vek::Vec2;

// TODO: separate LaserType into MapLaserType and LaserType in TwSnap and use their LaserType here
#[derive(Debug, Clone, Copy)]
pub enum Kind {
    Rifle,
    Shotgun,
}

impl From<Kind> for LaserType {
    fn from(val: Kind) -> Self {
        match val {
            Kind::Rifle => LaserType::Rifle,
            Kind::Shotgun => LaserType::Shotgun,
        }
    }
}

#[derive(Debug, Clone)]
pub struct Laser {
    snap_id: SnapId,
    spawn_order: SpawnOrder,
    /// store tele out for next tick
    tele_pos: Option<Vec2<f32>>,
    from: Vec2<f32>,
    pos: Vec2<f32>,
    prev_pos: Vec2<f32>,
    dir: Vec2<f32>,
    energy: f32,
    tune_zone: TuneZone,
    bounces: i32,
    eval_tick: Instant,
    owner: PlayerUid,
    kind: Kind,
    zero_energy_bounce_in_last_tick: bool,
    is_marked_for_destroy: bool,
}

impl Laser {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        now: Instant,
        game_state: &mut GameState,
        snap_outer: &mut SnapOuter,
        pos: Vec2<f32>,
        dir: Vec2<f32>,
        energy: f32,
        tune_zone: TuneZone,
        owner: PlayerUid,
        kind: Kind,
    ) -> Self {
        let spawn_order = SpawnOrder::new(owner, now, SpawnOrderEntity::Projectile);
        let mut laser = Self {
            snap_id: snap_outer.id_generator.next_projectile(),
            spawn_order,
            tele_pos: None,
            from: Vec2::new(0.0, 0.0),
            pos,
            prev_pos: Vec2::new(0.0, 0.0),
            dir,
            energy,
            tune_zone,
            bounces: 0,
            owner,
            eval_tick: Instant::zero(),
            kind,
            zero_energy_bounce_in_last_tick: false,
            is_marked_for_destroy: false,
        };
        laser.do_bounce(now, game_state);
        laser
    }

    fn get_tee_collision(
        &self,
        game_state: &GameState,
        to: Vec2<f32>,
    ) -> Option<(PlayerUid, Vec2<f32>)> {
        let hit_self = self.bounces != 0 || self.tele_pos.is_some();
        let hit_others = game_state
            .tee_cores
            .get_tee(self.owner)
            .map(|tee| !tee.is_solo())
            .unwrap_or(false);

        if !hit_self && !hit_others {
            return None;
        }
        if !hit_others {
            let owner = game_state.tee_cores.get_tee(self.owner)?;
            let closest_point = closest_point_on_line((self.pos, to), owner.pos())?;
            if closest_point.distance(owner.pos()) < TEE_PROXIMITY {
                Some((self.owner, closest_point))
            } else {
                None
            }
        } else {
            let ignore = hit_self.not().then_some(self.owner);
            game_state
                .tee_cores
                .intersect_tees(&game_state.tee_order, ignore, self.pos, to, 0.0)
        }
    }

    fn hit_tee(&mut self, now: Instant, game_state: &mut GameState, to: Vec2<f32>) -> bool {
        if let Some((tee_uid, pos)) = self.get_tee_collision(game_state, to) {
            self.from = self.pos;
            self.pos = pos;
            self.energy = -1.0;
            let tee = game_state.tee_cores.get_tee_mut(tee_uid).unwrap();
            match self.kind {
                Kind::Shotgun => {
                    let hit_pos = tee.pos();
                    if self.prev_pos != hit_pos {
                        tee.impact(
                            normalize(self.prev_pos - hit_pos)
                                * game_state.map.tuning(self.tune_zone).shotgun_strength,
                            false,
                        );
                    } else {
                        // TODO: allow disabling shotgun bug
                        game_state.bugs.push(Bug {
                            time: now,
                            kind: BugKind::Shotgun,
                            pos: coord::to_int(pos),
                        });
                        tee.impact(Vec2::new(-2147483648.0, -2147483648.0), false);
                    }
                }
                Kind::Rifle => {
                    tee.unfreeze();
                }
            }
            true
        } else {
            false
        }
    }

    fn do_bounce(&mut self, now: Instant, game_state: &mut GameState) {
        self.eval_tick = now;
        if self.energy < 0.0 {
            self.is_marked_for_destroy = true;
            return;
        }
        if let Some(tele_pos) = self.tele_pos {
            self.pos = tele_pos;
        }
        self.prev_pos = self.pos;
        let to = self.pos + self.dir * self.energy;

        if let Some((barrier, tele_id)) = game_state.map.intersect_laser(self.pos, to) {
            if !self.hit_tee(now, game_state, barrier) {
                self.from = self.pos;

                let (pos, dir) = game_state.map.reflect_laser(barrier, self.dir * 4.0);
                self.pos = pos;
                self.dir = normalize(dir);

                let distance = self.from.distance(self.pos);

                // let lasers bounce with zero energy for one tick, but prevent infinite bounces
                // https://github.com/ddnet/ddnet/issues/5380 - Laser does no longer reflect from walls on map Putt Putt
                if distance == 0.0 && self.zero_energy_bounce_in_last_tick {
                    self.energy = -1.0;
                } else {
                    self.energy -=
                        distance + game_state.map.tuning(self.tune_zone).laser_bounce_cost;
                }
                self.zero_energy_bounce_in_last_tick = distance == 0.0;

                if let Some(tele_id) = tele_id {
                    if let Some(tele_out) =
                        game_state
                            .map
                            .select_tele_out(now, &mut game_state.prng, tele_id)
                    {
                        self.tele_pos = Some(tele_out);
                    } else {
                        self.tele_pos = None;
                        self.bounces += 1;
                    }
                } else {
                    self.tele_pos = None;
                    self.bounces += 1;
                }

                if self.bounces > game_state.map.tuning(self.tune_zone).laser_bounce_num as i32 {
                    self.energy = -1.0;
                }
                game_state.events.create_sound(self.pos, Sound::RifleBounce);
            }
        } else if !self.hit_tee(now, game_state, to) {
            self.from = self.pos;
            self.pos = to;
            self.energy = -1.0;
        }
    }
}

impl SpawnableEntity for Laser {
    fn tick(&mut self, now: Instant, game_state: &mut GameState, _snap_outer: &mut SnapOuter) {
        // we can unwrap, because we know current tick is not before eval_tick
        if now.duration_since(self.eval_tick).unwrap()
            > Duration::from_secs_f32(
                game_state.map.tuning(self.tune_zone).laser_bounce_delay / 1000.0,
            )
        {
            self.do_bounce(now, game_state);
        }
    }

    fn snap(&self, _now: Instant, _game_state: &GameState, snapshot: &mut Snap) {
        snapshot.lasers.insert(
            self.snap_id,
            items::Laser {
                to: vec2_from_bits(self.pos),
                from: vec2_from_bits(self.from),
                start_tick: self.eval_tick,
                owner: None, // TODO: I need a `PlayerUid`, and only have a `TeeUid`
                kind: self.kind.into(),
                switch_number: 0, // TODO: store switch number for snap
                flags: Default::default(),
            },
        )
    }

    fn is_marked_for_destroy(&self) -> bool {
        self.is_marked_for_destroy
    }

    fn spawn_order(&self) -> SpawnOrder {
        self.spawn_order
    }

    fn player_uid(&self) -> PlayerUid {
        self.owner
    }

    fn on_tee_swap(&mut self, pid1: PlayerUid, pid2: PlayerUid) {
        // dont update spawn_order, because that one doesn't change
        if self.owner == pid1 {
            self.owner = pid2;
        } else if self.owner == pid2 {
            self.owner = pid1;
        }
    }
}