shadowengine2d 2.0.0

A comprehensive 2D game engine built in Rust with ECS, rendering, audio, assets, animations, and scene management
Documentation
// Particle system module for ShadowEngine2D
// Version 2.0
// Author: ShadowEngine Team

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) {
        // Burst emission
        if let Some(burst_count) = self.burst.take() {
            for _ in 0..burst_count {
                self.particles.push(self.spawn_particle());
            }
        }
        // Continuous emission
        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;
        }
        // Update particles
        for particle in &mut self.particles {
            particle.update(delta_time, self.gravity, self.custom_update.as_deref());
        }
        // Remove dead particles
        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 } => {
                // For cone, randomize velocity direction within angle
            }
        }
        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,
            );
        }
    }
}