twgpu 0.4.0

Render Teeworlds and DDNet maps
Documentation
use std::f32::consts::PI;
use std::iter;

use crate::shared::{Clock, Rng};
use crate::sprites::{
    ParticleData, ParticleSprite, SpriteBuilder, SpriteContainer, SpriteTextures, SpritesData,
AtlasSpriteBuilding, GameSprite, TeeStorage, TextureToken};
use fixed::traits::{Fixed, FromFixed};
use vek::{Lerp, Rgba, Vec2};

use super::SpriteVertex;


impl Clock {
    pub fn lerp_f32<T: Fixed>(&self, from: T, to: T) -> f32 {
        let frac = self.frac();
        f32::lerp_unclamped(f32::from_fixed(from), f32::from_fixed(to), frac)
    }

    pub fn lerp_vec<T: Fixed>(&self, from: Vec2<T>, to: Vec2<T>) -> Vec2<f32> {
        Vec2::new(self.lerp_f32(from.x, to.x), self.lerp_f32(from.y, to.y))
    }
}

pub(crate) fn calc_angle(vec: Vec2<f32>) -> f32 {
    if vec.x == 0. && vec.y == 0. {
        0.
    } else if vec.x == 0. {
        if vec.y < 0. {
            -PI / 2.
        } else {
            PI / 2.
        }
    } else {
        let mut angle = (vec.y / vec.x).atan();
        if vec.x < 0. {
            angle += PI
        }
        angle
    }
}

pub(crate) fn solid_at(map: &twgame::Map, pos: Vec2<f32>) -> bool {
    let i32_pos = Vec2::new(pos.x as i32, pos.y as i32);
    map.is_solid(i32_pos)
}

pub struct SpriteRenderContext<'a> {
    pub clock: &'a Clock,
    pub from_snap: &'a twsnap::Snap,
    pub to_snap: &'a twsnap::Snap,
    pub tees: &'a mut TeeStorage,
    pub map: &'a twgame::Map,
    pub sprites: &'a mut SpritesData,
    pub particles: &'a mut ParticleData,
    pub textures: &'a SpriteTextures,
    pub rng: &'a mut Rng,
}

/// The functions return `true`, once all sprites are rendered
pub trait GenerateSprites<T>: Sized {
    fn interpolate_sprites_fg(&mut self, from: &T, to: &T);

    /// Only implement, if a second render pass is required.
    #[allow(unused_variables)]
    fn interpolate_sprites_bg(&mut self, from: &T, to: &T) {}

    fn interpolate_sprites(&mut self, from: &T, to: &T) {
        self.interpolate_sprites_bg(from, to);
        self.interpolate_sprites_fg(from, to);
    }
}

struct SnapSpriteGenerator<'a> {
    projectiles: CombineIter<'a, twsnap::items::Projectile>,
    map_projectiles: CombineIter<'a, twsnap::items::MapProjectile>,
    pickups: CombineIter<'a, twsnap::items::Pickup>,
    pvp_flags: CombineIter<'a, twsnap::items::PvpFlag>,
    lasers: CombineIter<'a, twsnap::items::Laser>,
    players: CombineIter<'a, twsnap::items::Player>,
}

impl<'a> SnapSpriteGenerator<'a> {
    fn new(from: &'a twsnap::Snap, to: &'a twsnap::Snap) -> Self {
        Self {
            projectiles: CombineIter::new(&from.projectiles, &to.projectiles),
            map_projectiles: CombineIter::new(&from.map_projectiles, &to.map_projectiles),
            pickups: CombineIter::new(&from.pickups, &to.pickups),
            pvp_flags: CombineIter::new(&from.pvp_flags, &to.pvp_flags),
            lasers: CombineIter::new(&from.lasers, &to.lasers),
            players: CombineIter::new(&from.players, &to.players),
        }
    }
}

struct CombineIter<'a, T> {
    from_items: twsnap::Iter<'a, T>,
    to_items: &'a twsnap::OrderedMap<T>,
}

impl<'a, T> Iterator for CombineIter<'a, T> {
    type Item = (&'a T, &'a T);

    fn next(&mut self) -> Option<Self::Item> {
        let (key, from) = self.from_items.next()?;
        Some(match self.to_items.get(key) {
            None => (from, from),
            Some(to) => (from, to),
        })
    }
}

impl<'a, T> CombineIter<'a, T> {
    fn new(from: &'a twsnap::OrderedMap<T>, to: &'a twsnap::OrderedMap<T>) -> Self {
        Self {
            from_items: from.iter(),
            to_items: to,
        }
    }
}

struct BgFgIter<'a, T> {
    bg: CombineIter<'a, T>,
    fg: CombineIter<'a, T>,
}

fn render_iter<'a, T: 'a, ITER, SPRITES: SpriteContainer>(
    ctx: &mut SpriteRenderContext<'a>,
    iter: &'a mut ITER,
    _sprites: &mut SPRITES,
) where
    SpriteRenderContext<'a>: GenerateSprites<T>,
    ITER: Iterator<Item = (&'a T, &'a T)>,
{
    for (from, to) in iter {
        ctx.interpolate_sprites(from, to);
    }
}

impl SpriteRenderContext<'_> {
    fn iter_generator<T, F>(&mut self, bgfg: &mut BgFgIter<T>, sprite_cache: &mut Vec<[SpriteVertex; 4]>, texture_cache: &mut Vec<TextureToken>)
    where Self: GenerateSprites<T>,
    F: FnMut(&[SpriteVertex; 4], TextureToken) -> bool {
        while let Some((from, to)) = bgfg.bg.next() {
            let mut too_full = false;
            self.interpolate_sprites_bg(from, to);
            if too_full {
                return;
            }
        }
        while let Some((from, to)) = bgfg.fg.next() {
            let mut too_full = false;
            self.interpolate_sprites_bg(from, to);
            if too_full {
                return;
            }
        }
    }
}

impl<'a> GenerateSprites<twsnap::Snap> for SpriteRenderContext<'a> {
    fn interpolate_sprites_fg(&mut self, from: &twsnap::Snap, to: &twsnap::Snap) {
        self.interpolate_sprites(&from.projectiles, &to.projectiles);
        self.interpolate_sprites(&from.map_projectiles, &to.map_projectiles);
        self.interpolate_sprites(&from.pickups, &to.pickups);
        self.interpolate_sprites(&from.pvp_flags, &to.pvp_flags);
        self.interpolate_sprites(&from.lasers, &to.lasers);
        self.interpolate_sprites(&from.players, &to.players);
    }
}

impl<'a> GenerateSprites<twsnap::items::Projectile> for SpriteRenderContext<'a> {
    fn interpolate_sprites_fg(
        &mut self,
        from: &twsnap::items::Projectile,
        _to: &twsnap::items::Projectile,
    ) {
        // Weird calculations going on here in the original implementation
        let time = self.clock.current_tick() as f32;
        let position = self.map.projectile_position_at(from, time);
        let previous_position = self.map.projectile_position_at(from, time - 0.01);
        let vel = position - previous_position;
        let angle = if from.kind == twsnap::enums::ActiveWeapon::Grenade {
            let time_alive =
                self.clock.current_time() as f32 - from.start_tick.snap_tick() as f32 / 50.;
            time_alive * PI * 2. * 2.
        } else if vel.magnitude() > 0.0000003125 {
            // 0.0001 / 32
            calc_angle(vel)
        } else {
            0.
        };
        GameSprite::projectile_of(from.kind)
            .sprite(position, self.textures.game_skin)
            .rotate(angle)
            .render(self.sprites);

        if from.kind == twsnap::enums::ActiveWeapon::Grenade {
            if self.particles.effect_50hz {
                self.particles
                    .smoke_trail(position, vel, self.textures, self.rng)
            }
        } else if self.particles.effect_100hz {
            self.particles
                .bullet_trail(position, self.textures, self.rng)
        }
    }
}

impl<'a> GenerateSprites<twsnap::items::MapProjectile> for SpriteRenderContext<'a> {
    fn interpolate_sprites_fg(
        &mut self,
        from: &twsnap::items::MapProjectile,
        _to: &twsnap::items::MapProjectile,
    ) {
        // Weird calculations going on here in the original implementation
        let time = self.clock.current_tick() as f32;
        let position = self.map.map_projectile_position_at(from, time);
        let angle = if from.kind == twsnap::enums::ActiveWeapon::Grenade {
            let time_alive =
                self.clock.current_time() as f32 - from.start_tick.snap_tick() as f32 / 50.;
            time_alive * PI * 2. * 2.
        } else {
            let previous_position = self.map.map_projectile_position_at(from, time - 0.001);
            let vel = position - previous_position;
            if vel.magnitude() > 0.00001 {
                // Does this need to be normalized first?
                calc_angle(vel)
            } else {
                0.
            }
        };
        GameSprite::from(from.kind)
            .sprite(position, self.textures.game_skin)
            .rotate(angle)
            .render(self.sprites);
    }
}

impl<'a> GenerateSprites<twsnap::items::Pickup> for SpriteRenderContext<'a> {
    fn interpolate_sprites_fg(&mut self, from: &twsnap::items::Pickup, to: &twsnap::items::Pickup) {
        let mut position = self.clock.lerp_vec(from.pos, to.pos);
        // All pickups move in small circles
        let offset_angle = position.x + position.y + self.clock.current_tick() as f32 / 50. * 2.;
        let offset_direction = Vec2::new(offset_angle.cos(), offset_angle.sin());
        position += offset_direction * 2.5 / 32.;
        let game_sprite = GameSprite::from(from.kind);
        if game_sprite == GameSprite::Ninja {
            if self.particles.effect_50hz {
                self.particles
                    .ninja_shine(position, Vec2::new(3., 0.3125), self.textures, self.rng)
            }
            position.x -= 10. / 32.;
        }
        game_sprite
            .sprite(position, self.textures.game_skin)
            .render(self.sprites);
    }
}

impl<'a> GenerateSprites<twsnap::items::Laser> for SpriteRenderContext<'a> {
    fn interpolate_sprites_fg(&mut self, from: &twsnap::items::Laser, _to: &twsnap::items::Laser) {
        use twsnap::enums::LaserGunType;
        use twsnap::enums::LaserType;
        let outline_color = match from.kind {
            LaserType::Shotgun => Rgba::new(31, 24, 11, 255),
            LaserType::Dragger(_) | LaserType::Door => Rgba::new(0, 34, 25, 255),
            LaserType::Freeze
            | LaserType::Plasma
            | LaserType::Gun(LaserGunType::Freeze)
            | LaserType::Gun(LaserGunType::Expfreeze) => Rgba::new(33, 30, 46, 255),
            _ => Rgba::new(18, 18, 63, 255),
        }
        .az::<f32>()
            / 255.;
        let inside_color = match from.kind {
            LaserType::Shotgun => Rgba::new(145, 106, 64, 255),
            LaserType::Dragger(_) | LaserType::Door => Rgba::new(68, 194, 163, 255),
            LaserType::Freeze
            | LaserType::Plasma
            | LaserType::Gun(LaserGunType::Freeze)
            | LaserType::Gun(LaserGunType::Expfreeze) => Rgba::new(123, 114, 144, 255),
            _ => Rgba::new(127, 127, 255, 255),
        }
        .az::<f32>()
            / 255.;
        // TODO: make independant of tick speed
        let ticks = self.clock.current_tick() as f32 - from.start_tick.snap_tick() as f32;
        let ms = ticks / 50. * 1000.;
        // TODO: use tune config
        const LASER_BOUNCE_DELAY: f32 = 150.;
        // The clamp *might* not be required
        let progress = (ms / LASER_BOUNCE_DELAY).clamp(0., 1.);
        let inverse_progress = 1. - progress;
        SpriteBuilder::line(
            from.from.az(),
            from.to.az(),
            inverse_progress * 14. / 32.,
            self.textures.blank_texture(),
        )
        .multiply_color(outline_color)
        .render(self.sprites);
        SpriteBuilder::line(
            from.from.az(),
            from.to.az(),
            inverse_progress * 10. / 32.,
            self.textures.blank_texture(),
        )
        .multiply_color(inside_color)
        .render(self.sprites);

        let particle = match self.clock.current_tick() as i64 % 3 {
            0 => ParticleSprite::Splat1,
            1 => ParticleSprite::Splat2,
            2 => ParticleSprite::Splat3,
            _ => unreachable!(),
        };
        particle
            .sprite(from.to.az(), self.textures.particles)
            .rotate(self.clock.current_tick() as f32)
            .multiply_color(outline_color)
            .render(self.sprites);
        particle
            .sprite(from.to.az(), self.textures.particles)
            .rotate(self.clock.current_tick() as f32)
            .scale(5. / 6.)
            .multiply_color(inside_color)
            .render(self.sprites);
    }
}

impl<'a> GenerateSprites<twsnap::items::PvpFlag> for SpriteRenderContext<'a> {
    fn interpolate_sprites_fg(
        &mut self,
        from: &twsnap::items::PvpFlag,
        to: &twsnap::items::PvpFlag,
    ) {
        // Might need to use the flag carriers position, if available.
        let mut position = self.clock.lerp_vec(from.pos, to.pos);
        position.y -= 42. * 3. / 4. / 32.;
        match from.pvp_team {
            twsnap::enums::PvpTeam::Red => GameSprite::RedFlag,
            twsnap::enums::PvpTeam::Blue => GameSprite::BlueFlag,
        }
        .sprite(position, self.textures.game_skin)
        .render(self.sprites);
    }
}

fn zip_iter<'a, T>(
    from: &'a twsnap::OrderedMap<T>,
    to: &'a twsnap::OrderedMap<T>,
) -> impl Iterator<Item = (&'a T, &'a T)> {
    from.iter()
        .filter_map(|(k, v1)| to.get(k).map(|v2| (v1, v2)))
}

impl SpriteRenderContext<'_> {
    /// Process the events in the `from`-Snap.
    /// Should be called whenever a new snap is introduced.
    /// Also detects events through differences between the two snaps.
    pub fn process_events(&mut self) {
        for event in &self.from_snap.events {
            match event {
                twsnap::Events::DamageIndicator(_) => {} // TODO
                twsnap::Events::Death(death) => self.particles.player_death(
                    death.pos.az(),
                    death.player,
                    self.from_snap,
                    self.textures,
                    self.rng,
                ),
                twsnap::Events::Explosion(exp) => {
                    self.particles
                        .explosion(exp.pos.az(), self.textures, self.rng)
                }
                twsnap::Events::HammerHit(hit) => {
                    self.particles
                        .hammer_hit(hit.pos.az(), self.textures, self.rng)
                }
                twsnap::Events::Sound(_) => {}
                twsnap::Events::SoundGlobal(_) => {}
                twsnap::Events::Spawn(spawn) => {
                    self.particles
                        .player_spawn(spawn.pos.az(), self.textures, self.rng)
                }
            }
        }
        for (from, to) in zip_iter(&self.from_snap.players, &self.to_snap.players) {
            if let (Some(t1), Some(t2)) = (&from.tee, &to.tee) {
                if t1.jumped_total < t2.jumped_total {
                    if self.lerp_tee(from.uid).is_none() {
                        continue;
                    }
                    let tees = self.tees.lerped_tee(from.uid).unwrap();
                    let position = self.clock.lerp_tee_vec(tees, |tee| tee.pos);
                    self.particles.air_jump(position, self.textures, self.rng)
                }
            }
        }
    }
}