use bevy::color::{Color, LinearRgba}; use bevy::math::Vec2; use bevy::prelude::Component; use ordered_float::OrderedFloat; use rand::Rng; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::ops::Bound::{Excluded, Included, Unbounded}; #[derive(Deserialize, Serialize, Component, Clone)] pub struct ParticleEffect { // -- lifetime / spawning -- // /// Particle lifetime in seconds pub lifetime_seconds: RandF32, /// Delay inbetween each batch of particles spawned pub batch_spawn_delay_seconds: RandF32, /// Number of distinct particles spawned per batch pub particles_in_batch: RandUsize, // -- velocity -- // /// Initial linear velocity added to the particle's velocity when it is spawned pub initial_linear_velocity: RandVec2, /// Initial angular velocity added to the particle's rotation when it is spawned pub initial_angular_velocity: RandF32, // -- scale -- // // Scale curve over the lifetime of the particle pub scale: LifetimeCurve, // -- color -- // // Color curve over the lifetime of the particle pub color: LifetimeCurve, } #[derive(Serialize, Deserialize, Clone)] pub struct LifetimeCurve(pub BTreeMap, P>); impl LifetimeCurve

{ pub fn new<'a>(points: impl IntoIterator) -> Self where P: 'a, { Self( points .into_iter() .map(|u| (OrderedFloat(u.0), u.1)) .collect::>(), ) } /// Sample for the value at T. Returns None if the curve has no points, or if T is outside /// the domain of the points specified. #[must_use] pub fn sample(&self, at: f32) -> Option

{ if self.0.is_empty() { return None; } if self.0.len() == 1 && (at - self.0.iter().nth(0).unwrap().0.0).abs() < 0.01 { return Some(*self.0.iter().nth(0).unwrap().1); } // Find A, the point "to the left" of `at` let mut r1 = self.0.range((Unbounded, Included(OrderedFloat(at)))); let (a_key, a_val) = r1.next_back()?; let (b_key, b_val) = if (at - self.0.iter().last().unwrap().0.0).abs() < 0.01 { self.0.iter().last().unwrap() } else { // Find B, the point "to the right" of `at` let mut r2 = self.0.range((Excluded(OrderedFloat(at)), Unbounded)); r2.next()? }; if a_key == b_key && b_key == self.0.iter().last().unwrap().0 { return Some(*b_val); } // Calculate `t` (value from 0 - 1 indicating our progress from A to B) let t = (at - **a_key) / *(b_key - a_key); // Lerp Some(a_val.lerp(b_val, t)) } /// Given an input time value, use the ends of the spline's time to clamp /// the input value. #[must_use] pub fn clamp_time(&self, time: f32) -> Option { let first = self.0.iter().nth(0)?; let last = self.0.iter().last()?; Some(time.clamp(first.0.0, last.0.0)) } } pub trait Lerp { #[must_use] fn lerp(&self, other: &Self, t: f32) -> Self; } impl Lerp for f32 { fn lerp(&self, other: &Self, t: f32) -> Self { (1.0 - t) * self + t * other } } impl Lerp for Color { fn lerp(&self, other: &Self, t: f32) -> Self { let a: LinearRgba = (*self).into(); let b: LinearRgba = (*other).into(); Color::from(a * (1.0 - t) + b * t) } } #[derive(Deserialize, Serialize, Clone, Copy)] pub struct RandF32 { pub value: f32, pub randomness: f32, } impl RandF32 { pub fn sample(&self, rng: &mut impl Rng) -> f32 { rng.random_range(self.value - self.randomness..self.value + self.randomness) } } #[derive(Deserialize, Serialize, Clone, Copy)] pub struct RandUsize { pub value: usize, pub randomness: usize, } impl RandUsize { pub fn sample(&self, rng: &mut impl Rng) -> usize { let lower_bound = self.value.saturating_sub(self.randomness); rng.random_range(lower_bound..self.value + self.randomness) } } #[derive(Deserialize, Serialize, Clone, Copy)] pub struct RandVec2 { pub x: RandF32, pub y: RandF32, } impl RandVec2 { pub fn sample(&self, rng: &mut impl Rng) -> Vec2 { Vec2::new(self.x.sample(rng), self.y.sample(rng)) } }