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<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)),
))
}
/// 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<P> {
if self.0.is_empty() {
return None;
}
if self.0.len() == 1 && at == self.0.iter().nth(0).unwrap().0.0 {
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 {
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<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.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))
}
}