use crate::ecs::Component;
use crate::math::{Color, Vec2};
#[derive(Clone, Debug)]
pub struct Particle {
pub position: Vec2,
pub velocity: Vec2,
pub acceleration: Vec2,
pub color: Color,
pub size: f32,
pub lifetime: f32,
pub max_lifetime: f32,
pub fade_out: bool,
}
impl Particle {
pub fn new(position: Vec2, velocity: Vec2, color: Color, lifetime: f32) -> Self {
Self {
position,
velocity,
acceleration: Vec2::ZERO,
color,
size: 4.0,
lifetime,
max_lifetime: lifetime,
fade_out: true,
}
}
pub fn update(&mut self, dt: f32) -> bool {
self.lifetime -= dt;
if self.lifetime <= 0.0 {
return false; }
self.velocity += self.acceleration * dt;
self.position += self.velocity * dt;
if self.fade_out {
let life_ratio = self.lifetime / self.max_lifetime;
self.color.a = life_ratio;
}
true }
}
#[derive(Clone, Debug)]
pub struct ParticleEmitter {
pub position: Vec2,
pub emission_rate: f32,
pub particle_lifetime: f32,
pub velocity_min: Vec2,
pub velocity_max: Vec2,
pub color_start: Color,
pub color_end: Color,
pub size_min: f32,
pub size_max: f32,
pub gravity: Vec2,
pub spread_angle: f32,
pub direction: Vec2,
pub burst_count: u32,
pub is_burst: bool,
pub loop_emission: bool,
pub active: bool,
emission_timer: f32,
}
impl ParticleEmitter {
pub fn new(position: Vec2) -> Self {
Self {
position,
emission_rate: 10.0,
particle_lifetime: 1.0,
velocity_min: Vec2::new(-50.0, -50.0),
velocity_max: Vec2::new(50.0, 50.0),
color_start: Color::WHITE,
color_end: Color::WHITE,
size_min: 4.0,
size_max: 8.0,
gravity: Vec2::new(0.0, 100.0),
spread_angle: std::f32::consts::PI * 2.0,
direction: Vec2::new(0.0, -1.0),
burst_count: 0,
is_burst: false,
loop_emission: true,
active: true,
emission_timer: 0.0,
}
}
pub fn with_rate(mut self, rate: f32) -> Self {
self.emission_rate = rate;
self
}
pub fn with_lifetime(mut self, lifetime: f32) -> Self {
self.particle_lifetime = lifetime;
self
}
pub fn with_velocity(mut self, min: Vec2, max: Vec2) -> Self {
self.velocity_min = min;
self.velocity_max = max;
self
}
pub fn with_color(mut self, start: Color, end: Color) -> Self {
self.color_start = start;
self.color_end = end;
self
}
pub fn with_size(mut self, min: f32, max: f32) -> Self {
self.size_min = min;
self.size_max = max;
self
}
pub fn with_gravity(mut self, gravity: Vec2) -> Self {
self.gravity = gravity;
self
}
pub fn with_direction(mut self, direction: Vec2, spread: f32) -> Self {
self.direction = direction.normalize();
self.spread_angle = spread;
self
}
pub fn as_burst(mut self, count: u32) -> Self {
self.is_burst = true;
self.burst_count = count;
self.loop_emission = false;
self
}
pub fn emit_particle(&self) -> Particle {
use rand::Rng;
let mut rng = rand::thread_rng();
let angle = if self.spread_angle > 0.0 {
let base_angle = self.direction.y.atan2(self.direction.x);
base_angle + rng.gen_range(-self.spread_angle / 2.0..self.spread_angle / 2.0)
} else {
self.direction.y.atan2(self.direction.x)
};
let speed = rng.gen_range(
self.velocity_min.length()..self.velocity_max.length()
);
let velocity = Vec2::new(angle.cos(), angle.sin()) * speed;
let size = rng.gen_range(self.size_min..self.size_max);
let color = self.color_start;
let mut particle = Particle::new(self.position, velocity, color, self.particle_lifetime);
particle.size = size;
particle.acceleration = self.gravity;
particle
}
}
impl Component for ParticleEmitter {}
pub struct ParticleSystem {
particles: Vec<Particle>,
}
impl ParticleSystem {
pub fn new() -> Self {
Self {
particles: Vec::new(),
}
}
pub fn update(&mut self, emitters: &mut [&mut ParticleEmitter], dt: f32) {
self.particles.retain_mut(|p| p.update(dt));
for emitter in emitters {
if !emitter.active {
continue;
}
if emitter.is_burst {
if emitter.burst_count > 0 {
for _ in 0..emitter.burst_count {
self.particles.push(emitter.emit_particle());
}
emitter.burst_count = 0;
if !emitter.loop_emission {
emitter.active = false;
}
}
} else {
emitter.emission_timer += dt;
let emission_interval = 1.0 / emitter.emission_rate;
while emitter.emission_timer >= emission_interval {
self.particles.push(emitter.emit_particle());
emitter.emission_timer -= emission_interval;
}
}
}
}
pub fn particles(&self) -> &[Particle] {
&self.particles
}
pub fn particle_count(&self) -> usize {
self.particles.len()
}
pub fn clear(&mut self) {
self.particles.clear();
}
}
impl Default for ParticleSystem {
fn default() -> Self {
Self::new()
}
}
impl ParticleEmitter {
pub fn explosion(position: Vec2) -> Self {
Self::new(position)
.with_rate(100.0)
.with_lifetime(0.5)
.with_velocity(Vec2::new(100.0, 100.0), Vec2::new(300.0, 300.0))
.with_color(Color::rgb(1.0, 0.5, 0.0), Color::rgb(1.0, 0.0, 0.0))
.with_size(6.0, 12.0)
.with_gravity(Vec2::new(0.0, 200.0))
.as_burst(50)
}
pub fn fire(position: Vec2) -> Self {
Self::new(position)
.with_rate(30.0)
.with_lifetime(1.0)
.with_direction(Vec2::new(0.0, -1.0), 0.5)
.with_velocity(Vec2::new(20.0, 50.0), Vec2::new(40.0, 100.0))
.with_color(Color::rgb(1.0, 0.8, 0.0), Color::rgb(1.0, 0.2, 0.0))
.with_size(4.0, 8.0)
.with_gravity(Vec2::new(0.0, -50.0))
}
pub fn smoke(position: Vec2) -> Self {
Self::new(position)
.with_rate(15.0)
.with_lifetime(2.0)
.with_direction(Vec2::new(0.0, -1.0), 0.3)
.with_velocity(Vec2::new(10.0, 30.0), Vec2::new(20.0, 60.0))
.with_color(Color::rgb(0.5, 0.5, 0.5), Color::rgb(0.3, 0.3, 0.3))
.with_size(6.0, 12.0)
.with_gravity(Vec2::new(0.0, -20.0))
}
pub fn sparkles(position: Vec2) -> Self {
Self::new(position)
.with_rate(20.0)
.with_lifetime(0.8)
.with_velocity(Vec2::new(50.0, 50.0), Vec2::new(150.0, 150.0))
.with_color(Color::rgb(1.0, 1.0, 0.5), Color::WHITE)
.with_size(2.0, 4.0)
.with_gravity(Vec2::new(0.0, 100.0))
}
pub fn trail(position: Vec2) -> Self {
Self::new(position)
.with_rate(50.0)
.with_lifetime(0.3)
.with_velocity(Vec2::ZERO, Vec2::new(10.0, 10.0))
.with_color(Color::CYAN, Color::BLUE)
.with_size(3.0, 6.0)
.with_gravity(Vec2::ZERO)
}
}