nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use crate::tui::ecs::components::{Position, Sprite, TermColor, ZIndex};
use crate::tui::ecs::world::*;
use freecs::Entity;
use rand::Rng;

#[derive(Clone)]
pub struct ParticleConfig {
    pub characters: Vec<char>,
    pub colors: Vec<TermColor>,
    pub lifetime: f64,
    pub speed_min: f64,
    pub speed_max: f64,
    pub spread: f64,
    pub direction: f64,
    pub z_index: i32,
}

impl Default for ParticleConfig {
    fn default() -> Self {
        Self {
            characters: vec!['*'],
            colors: vec![TermColor::White],
            lifetime: 1.0,
            speed_min: 1.0,
            speed_max: 3.0,
            spread: std::f64::consts::PI * 2.0,
            direction: 0.0,
            z_index: 10,
        }
    }
}

struct Particle {
    entity: Entity,
    velocity_column: f64,
    velocity_row: f64,
    remaining: f64,
}

pub struct ParticleEmitter {
    particles: Vec<Particle>,
}

impl ParticleEmitter {
    pub fn new() -> Self {
        Self {
            particles: Vec::new(),
        }
    }

    pub fn emit(
        &mut self,
        world: &mut World,
        origin_column: f64,
        origin_row: f64,
        count: usize,
        config: &ParticleConfig,
    ) {
        let mut rng = rand::rng();
        for _ in 0..count {
            let angle =
                config.direction + rng.random_range(-config.spread / 2.0..=config.spread / 2.0);
            let speed = rng.random_range(config.speed_min..=config.speed_max);
            let velocity_column = angle.cos() * speed;
            let velocity_row = angle.sin() * speed;

            let character = config.characters[rng.random_range(0..config.characters.len())];
            let color = config.colors[rng.random_range(0..config.colors.len())];

            let entity = world.spawn_entities(POSITION | SPRITE | Z_INDEX, 1)[0];
            world.set_position(
                entity,
                Position {
                    column: origin_column,
                    row: origin_row,
                },
            );
            world.set_sprite(
                entity,
                Sprite {
                    character,
                    foreground: color,
                    background: TermColor::Black,
                },
            );
            world.set_z_index(entity, ZIndex(config.z_index));

            self.particles.push(Particle {
                entity,
                velocity_column,
                velocity_row,
                remaining: config.lifetime,
            });
        }
    }

    pub fn update(&mut self, world: &mut World, delta: f64) {
        let mut expired = Vec::new();

        for (index, particle) in self.particles.iter_mut().enumerate() {
            particle.remaining -= delta;
            if particle.remaining <= 0.0 {
                expired.push(index);
                continue;
            }
            if let Some(position) = world.get_position_mut(particle.entity) {
                position.column += particle.velocity_column * delta;
                position.row += particle.velocity_row * delta;
            }
        }

        for &index in expired.iter().rev() {
            let particle = self.particles.swap_remove(index);
            world.despawn_entities(&[particle.entity]);
        }
    }

    pub fn despawn_all(&mut self, world: &mut World) {
        let entities: Vec<Entity> = self
            .particles
            .iter()
            .map(|particle| particle.entity)
            .collect();
        if !entities.is_empty() {
            world.despawn_entities(&entities);
        }
        self.particles.clear();
    }

    pub fn active_count(&self) -> usize {
        self.particles.len()
    }
}

impl Default for ParticleEmitter {
    fn default() -> Self {
        Self::new()
    }
}