use std::collections::BTreeMap;
use std::ops::Bound::{Included, Unbounded};
use bevy::color::{Color, LinearRgba};
use bevy::math::Vec2;
use bevy::prelude::Component;
use ordered_float::{FloatCore, OrderedFloat};
use rand::Rng;
use serde::{Deserialize, Serialize};
#[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<f32>,
// -- color -- //
// Color curve over the lifetime of the particle
pub color: LifetimeCurve<Color>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct LifetimeCurve<P: Lerp>(pub BTreeMap<OrderedFloat<f32>, P>);
impl<P: Lerp + Copy> LifetimeCurve<P> {
pub fn new<'a>(points: impl IntoIterator<Item = &'a (f32, P)>) -> Self where P: 'a {
Self(BTreeMap::from_iter(points.into_iter().map(|u| (OrderedFloat(u.0), u.1.clone()))))
}
/// 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.
pub fn sample(&self, at: f32) -> Option<P> {
if self.0.is_empty() { return None; }
// Find A, the point "to the left" of `at`
let mut r1 = self.0.range((Included(OrderedFloat(at)), Unbounded));
let (a_key, a_val) = r1.next()?;
// Find B, the point "to the right" of `at`
let mut r2 = self.0.range((Unbounded, Included(OrderedFloat(at))));
let (b_key, b_val) = r2.next()?;
// 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.
pub fn clamp_time(&self, time: f32) -> Option<f32> {
let Some(first) = self.0.iter().nth(0) else { return None; };
let Some(last) = self.0.iter().last() else { return None; };
Some(time.clamp(first.0.0, last.0.0))
}
}
pub trait Lerp {
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.checked_sub(self.randomness).unwrap_or(0);
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))
}
}