use crate::shared::{Clock, Rng};
use crate::sprites::{
solid_at, ExtraSprite, ParticleSprite, SpriteBuilder, SpriteGenerator, SpriteTextures,
SpriteVertex, TextureToken,
};
use std::f32::consts::PI;
use std::ops::{Index, IndexMut};
use twgame::Map;
use twsnap::{Snap, SnapId};
use vek::az::Az;
use vek::{Lerp, Rgba, Vec2};
pub struct Particles {
last_time: f64,
friction_fraction: f64,
pub effect_100hz: bool,
pub effect_50hz: bool,
pub effect_5hz: bool,
last_effect_100hz: f64,
last_effect_50hz: f64,
last_effect_5hz: f64,
particles: [Vec<Particle>; ParticleGroup::COUNT],
}
#[derive(Debug, Copy, Clone)]
pub enum ParticleGroup {
Trails,
Explosions,
Extra,
General,
}
#[derive(Debug, Copy, Clone)]
pub struct Particle {
pub texture: TextureToken,
pub position: Vec2<f32>,
pub velocity: Vec2<f32>,
pub life_span: f32,
pub size_progression: [f32; 2],
pub alpha_progression: Option<[f32; 2]>,
pub rotation: f32,
pub rotation_speed: f32,
pub gravity: f32,
pub friction: f32,
pub collides: bool,
pub color: Rgba<u8>,
time_alive: f32,
}
impl Default for Particle {
fn default() -> Self {
Self {
texture: TextureToken::INVALID,
position: Vec2::zero(),
velocity: Vec2::zero(),
life_span: 0.,
size_progression: [1., 1.],
alpha_progression: None,
rotation: 0.,
rotation_speed: 0.,
gravity: 0.,
friction: 0.,
collides: true,
color: Rgba::white(),
time_alive: 0.,
}
}
}
impl Particle {
pub fn update(
&mut self,
map: &Map,
rng: &mut Rng,
time_delta: f32,
friction_steps: u32,
) -> bool {
self.time_alive += time_delta;
if self.time_alive > self.life_span {
return false;
}
self.velocity.y += self.gravity * time_delta;
for _ in 0..friction_steps {
self.velocity *= self.friction;
}
let mut position_delta = self.velocity * time_delta;
if self.collides && solid_at(map, self.position + position_delta) {
let collides_horizontal =
solid_at(map, self.position + Vec2::new(position_delta.x, 0.));
let collides_vertical = solid_at(map, self.position + Vec2::new(0., position_delta.y));
let elasticity = rng.f32_range(0.1, 1.);
match (collides_horizontal, collides_vertical) {
(true, false) => position_delta.x *= -elasticity,
(false, true) => position_delta.y *= -elasticity,
_ => position_delta *= -elasticity,
}
self.velocity = position_delta / time_delta;
}
self.position += position_delta;
self.rotation += self.rotation_speed * time_delta;
true }
pub fn to_sprite(&self) -> ([SpriteVertex; 4], TextureToken) {
let frac = self.time_alive / self.life_span;
let size = f32::lerp_unclamped(self.size_progression[0], self.size_progression[1], frac);
let size = Vec2::broadcast(size);
let mut color = self.color;
if let Some(vals) = self.alpha_progression {
color.a = f32::lerp_unclamped(vals[0], vals[1], frac).az::<u8>();
}
SpriteBuilder::new(self.position, size, self.texture)
.rotate(self.rotation)
.multiply_color(color)
.finish()
}
}
impl ParticleGroup {
const COUNT: usize = 4;
pub const ALL: [Self; Self::COUNT] =
[Self::Trails, Self::Explosions, Self::Extra, Self::General];
}
impl<T> Index<ParticleGroup> for [T; 4] {
type Output = T;
fn index(&self, index: ParticleGroup) -> &Self::Output {
self.get(index as usize).unwrap()
}
}
impl<T> IndexMut<ParticleGroup> for [T; 4] {
fn index_mut(&mut self, index: ParticleGroup) -> &mut Self::Output {
self.get_mut(index as usize).unwrap()
}
}
impl Particles {
pub fn new(time: f64) -> Self {
Self {
last_time: time,
friction_fraction: 0.,
effect_100hz: true,
effect_50hz: true,
effect_5hz: true,
last_effect_100hz: time,
last_effect_50hz: time,
last_effect_5hz: time,
particles: Default::default(),
}
}
pub fn update(&mut self, clock: &Clock, map: &Map, rng: &mut Rng) {
let time = clock.current_time();
let time_delta = time - self.last_time;
if time_delta < 0. {
return;
}
self.last_time = time;
if time - self.last_effect_5hz >= 0.2 {
self.effect_5hz = true;
self.last_effect_5hz = time;
} else {
self.effect_5hz = false;
}
if time - self.last_effect_50hz >= 0.02 {
self.effect_50hz = true;
self.last_effect_50hz = time;
} else {
self.effect_50hz = false;
}
if time - self.last_effect_100hz >= 0.01 {
self.effect_100hz = true;
self.last_effect_100hz = time;
} else {
self.effect_100hz = false;
}
self.friction_fraction += time_delta;
let friction_steps = if self.friction_fraction > 2. {
self.friction_fraction = 0.;
40
} else {
let mut friction_steps = 0;
while self.friction_fraction >= 0.05 {
friction_steps += 1;
self.friction_fraction -= 0.05;
}
friction_steps
};
for group in ParticleGroup::ALL {
self.particles[group]
.retain_mut(|particle| particle.update(map, rng, time_delta as f32, friction_steps))
}
}
pub fn get_group(&self, group: ParticleGroup) -> &[Particle] {
&self.particles[group]
}
}
impl SpriteGenerator for &[Particle] {
fn generate<F>(&mut self, mut f: F) -> bool
where
F: FnMut(&[SpriteVertex; 4], TextureToken) -> bool,
{
while let Some((first, remaining)) = self.split_first() {
*self = remaining;
let (sprite, texture) = first.to_sprite();
if !f(&sprite, texture) {
return false;
}
}
true
}
}
impl Particles {
pub fn air_jump(&mut self, position: Vec2<f32>, textures: &SpriteTextures, rng: &mut Rng) {
let particle = Particle {
texture: textures.particles.select(ParticleSprite::AirJump),
position: position + Vec2::new(0., 0.5),
velocity: Vec2::new(0., 200. / 32.),
life_span: 0.5,
size_progression: [1.5, 0.],
rotation: rng.angle(),
rotation_speed: PI * 2.,
gravity: 500. / 32.,
friction: 0.7,
..Particle::default()
};
let offset = Vec2::new(6. / 32., 0.);
self.particles[ParticleGroup::General].push(Particle {
position: particle.position - offset,
..particle
});
self.particles[ParticleGroup::General].push(Particle {
position: particle.position + offset,
..particle
});
}
pub fn ninja_shine(
&mut self,
position: Vec2<f32>,
size: Vec2<f32>,
textures: &SpriteTextures,
rng: &mut Rng,
) {
self.particles[ParticleGroup::General].push(Particle {
texture: textures.particles.select(ParticleSprite::Slice),
position: position
+ size * Vec2::new(rng.f32_range(-0.5, 0.5), rng.f32_range(-0.5, 0.5)),
life_span: 0.5,
size_progression: [0.5, 0.],
rotation: rng.angle(),
rotation_speed: PI * 2.,
gravity: 500. / 32.,
friction: 0.9,
..Particle::default()
})
}
pub fn freezing_flakes(
&mut self,
position: Vec2<f32>,
textures: &SpriteTextures,
rng: &mut Rng,
) {
let size = rng.f32_range(0.25, 0.75);
self.particles[ParticleGroup::Extra].push(Particle {
texture: textures.extras.select(ExtraSprite::Snowflake),
position: position + Vec2::new(rng.f32_range(-0.5, 0.5), rng.f32_range(-0.5, 0.5)),
life_span: 1.5,
size_progression: [size, size * 0.5],
alpha_progression: Some([255., 0.]),
rotation: rng.angle(),
rotation_speed: PI,
gravity: rng.f32_max(250. / 32.),
friction: 0.9,
..Particle::default()
})
}
pub fn smoke_trail(
&mut self,
position: Vec2<f32>,
velocity: Vec2<f32>,
textures: &SpriteTextures,
rng: &mut Rng,
) {
self.particles[ParticleGroup::Trails].push(Particle {
texture: textures.particles.select(ParticleSprite::Smoke),
position,
velocity: velocity + rng.direction() * 50. / 32.,
life_span: rng.f32_range(0.5, 1.),
size_progression: [rng.f32_range(0.375, 0.625), 0.],
gravity: -rng.f32_max(500. / 32.),
friction: 0.7,
..Particle::default()
})
}
pub fn skid_trail(
&mut self,
position: Vec2<f32>,
velocity: Vec2<f32>,
textures: &SpriteTextures,
rng: &mut Rng,
) {
self.particles[ParticleGroup::General].push(Particle {
texture: textures.particles.select(ParticleSprite::Smoke),
position,
velocity: velocity + rng.direction() * 50. / 32.,
life_span: rng.f32_range(0.5, 1.),
size_progression: [rng.f32_range(24., 36.) / 32., 0.],
gravity: rng.f32_max(500. / 32.),
friction: 0.7,
color: Rgba::new(191, 191, 191, 255), ..Particle::default()
})
}
pub fn bullet_trail(&mut self, position: Vec2<f32>, textures: &SpriteTextures, rng: &mut Rng) {
self.particles[ParticleGroup::Trails].push(Particle {
texture: textures.particles.select(ParticleSprite::Ball),
position,
life_span: rng.f32_range(0.25, 0.5),
size_progression: [0.25, 0.],
friction: 0.7,
..Particle::default()
})
}
pub fn player_spawn(&mut self, position: Vec2<f32>, textures: &SpriteTextures, rng: &mut Rng) {
for _ in 0..32 {
self.particles[ParticleGroup::General].push(Particle {
texture: textures.particles.select(ParticleSprite::Shell),
position,
velocity: rng.direction() * rng.f32().powi(3) * 600. / 32.,
life_span: rng.f32_range(0.3, 0.6),
size_progression: [rng.f32_range(2., 3.), 0.],
rotation: rng.angle(),
rotation_speed: rng.f32(),
gravity: -rng.f32_max(400. / 32.),
friction: 0.7,
color: Rgba::new(0xb5, 0x50, 0xcb, 0xff),
..Particle::default()
})
}
}
pub fn player_death(
&mut self,
position: Vec2<f32>,
player: SnapId,
snap: &Snap,
textures: &SpriteTextures,
rng: &mut Rng,
) {
let tee_color = match snap.players.get(player) {
Some(player) => player.color_body.to_rgba(),
None => Rgba::white(),
};
let color = ((tee_color * rng.f32_range(0.75, 1.)) * 255.).az();
let particle_sprite = match rng.u32() % 3 {
0 => ParticleSprite::Splat1,
1 => ParticleSprite::Splat2,
2 => ParticleSprite::Splat3,
_ => unreachable!(),
};
for _ in 0..64 {
self.particles[ParticleGroup::General].push(Particle {
texture: textures.particles.select(particle_sprite),
position,
velocity: rng.direction() * rng.f32_range(0.1, 1.1) * 900. / 32.,
life_span: rng.f32_range(0.3, 0.6),
size_progression: [rng.f32_range(0.75, 1.25), 0.],
rotation: rng.angle(),
rotation_speed: rng.f32_range(-0.5, 0.5) * PI,
gravity: 800. / 32.,
friction: 0.8,
color,
..Particle::default()
})
}
}
pub fn explosion(&mut self, position: Vec2<f32>, textures: &SpriteTextures, rng: &mut Rng) {
self.particles[ParticleGroup::Explosions].push(Particle {
texture: textures.particles.select(ParticleSprite::Explosion),
position,
life_span: 0.4,
size_progression: [150. / 32., 0.],
rotation: rng.angle(),
..Particle::default()
});
for _ in 0..24 {
let lerped_color = Rgba::lerp_unclamped(
Rgba::new(0.75, 0.75, 0.75, 1.),
Rgba::new(0.5, 0.5, 0.5, 1.),
rng.f32(),
);
let color = (lerped_color * 255.).az();
self.particles[ParticleGroup::General].push(Particle {
texture: textures.particles.select(ParticleSprite::Smoke),
position,
velocity: rng.direction() * rng.f32_range(1., 1.2) * 1000. / 32.,
life_span: rng.f32_range(0.5, 0.9),
size_progression: [rng.f32_range(1., 1.25), 0.],
gravity: -rng.f32_max(800. / 32.),
friction: 0.4,
color,
..Particle::default()
});
}
}
pub fn hammer_hit(&mut self, position: Vec2<f32>, textures: &SpriteTextures, rng: &mut Rng) {
self.particles[ParticleGroup::Explosions].push(Particle {
texture: textures.particles.select(ParticleSprite::Hit),
position,
life_span: 0.3,
size_progression: [120. / 32., 0.],
rotation: rng.angle(),
..Particle::default()
});
}
}