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)
}
}
}
}
}