use crate::math::Position;
use crate::renderer::Renderer;
use crate::EngineResult;
use fastrand;
#[derive(Debug, Clone, Copy)]
pub struct Particle {
pub position: Position,
pub velocity: Position,
pub acceleration: Position,
pub lifetime: f32,
pub age: f32,
pub color: [f32; 4],
pub size: f32,
}
impl Particle {
pub fn is_alive(&self) -> bool {
self.age < self.lifetime
}
pub fn update(&mut self, delta_time: f32, gravity: Position, custom: Option<&dyn Fn(&mut Particle, f32)>) {
self.velocity.x += self.acceleration.x * delta_time + gravity.x * delta_time;
self.velocity.y += self.acceleration.y * delta_time + gravity.y * delta_time;
self.position.x += self.velocity.x * delta_time;
self.position.y += self.velocity.y * delta_time;
self.age += delta_time;
if let Some(cb) = custom {
cb(self, delta_time);
}
}
}
pub struct ParticleEmitter {
pub particles: Vec<Particle>,
pub position: Position,
pub emission_rate: f32,
pub particle_lifetime: f32,
pub particle_size: f32,
pub color: [f32; 4],
pub velocity: Position,
pub velocity_randomness: Position,
pub color_randomness: [f32; 4],
pub size_randomness: f32,
pub acceleration: Position,
pub gravity: Position,
pub burst: Option<usize>,
pub emission_shape: EmissionShape,
pub custom_update: Option<Box<dyn Fn(&mut Particle, f32)>>,
pub time_since_last_emit: f32,
}
#[derive(Debug, Clone, Copy)]
pub enum EmissionShape {
Point,
Circle { radius: f32 },
Cone { angle_deg: f32 },
}
impl ParticleEmitter {
pub fn new(position: Position) -> Self {
Self {
particles: Vec::new(),
position,
emission_rate: 50.0,
particle_lifetime: 1.0,
particle_size: 8.0,
color: [1.0, 1.0, 1.0, 1.0],
velocity: Position::new(0.0, -100.0),
velocity_randomness: Position::new(20.0, 20.0),
color_randomness: [0.1, 0.1, 0.1, 0.0],
size_randomness: 2.0,
acceleration: Position::new(0.0, 0.0),
gravity: Position::new(0.0, 200.0),
burst: None,
emission_shape: EmissionShape::Point,
custom_update: None,
time_since_last_emit: 0.0,
}
}
pub fn update(&mut self, delta_time: f32) {
if let Some(burst_count) = self.burst.take() {
for _ in 0..burst_count {
self.particles.push(self.spawn_particle());
}
}
self.time_since_last_emit += delta_time;
let emit_count = (self.emission_rate * self.time_since_last_emit).floor() as usize;
if emit_count > 0 {
for _ in 0..emit_count {
self.particles.push(self.spawn_particle());
}
self.time_since_last_emit = 0.0;
}
for particle in &mut self.particles {
particle.update(delta_time, self.gravity, self.custom_update.as_deref());
}
self.particles.retain(|p| p.is_alive());
}
fn spawn_particle(&self) -> Particle {
let mut rng = fastrand::Rng::new();
let mut pos = self.position;
match self.emission_shape {
EmissionShape::Point => {}
EmissionShape::Circle { radius } => {
let angle = rng.f32() * std::f32::consts::TAU;
let r = rng.f32() * radius;
pos.x += r * angle.cos();
pos.y += r * angle.sin();
}
EmissionShape::Cone { angle_deg } => {
}
}
let vel = Position::new(
self.velocity.x + rng.f32() * self.velocity_randomness.x * (if rng.bool() { 1.0 } else { -1.0 }),
self.velocity.y + rng.f32() * self.velocity_randomness.y * (if rng.bool() { 1.0 } else { -1.0 }),
);
let col = [
(self.color[0] + rng.f32() * self.color_randomness[0]).clamp(0.0, 1.0),
(self.color[1] + rng.f32() * self.color_randomness[1]).clamp(0.0, 1.0),
(self.color[2] + rng.f32() * self.color_randomness[2]).clamp(0.0, 1.0),
(self.color[3] + rng.f32() * self.color_randomness[3]).clamp(0.0, 1.0),
];
let size = (self.particle_size + rng.f32() * self.size_randomness).max(1.0);
Particle {
position: pos,
velocity: vel,
acceleration: self.acceleration,
lifetime: self.particle_lifetime,
age: 0.0,
color: col,
size,
}
}
pub fn render(&self, renderer: &mut Renderer) {
for particle in &self.particles {
renderer.draw_circle(
particle.position.x,
particle.position.y,
particle.size,
particle.color,
);
}
}
}