use bevy::math::{Vec2, Vec4};
use bevy::reflect::Reflect;
use bevy_hanabi::prelude::*;
use crate::{colors::PaletteColor, data_loader::DataFile, enums::GameSystem, InternalId};
use std::{any::Any, fmt::Write, hash::Hash};
pub const PARTICLE_DEFAULT_LIFETIME: f32 = 0.5;
pub const PARTICLE_DEFAULT_SPAWN_RATE: f32 = 25.0;
pub const PARTICLE_DEFAULT_CAPACITY: u32 = 100;
pub const PARTICLE_DEFAULT_SPEED: f32 = 2.0;
pub const PARTICLE_DEFAULT_RADIUS: f32 = 2.0;
pub const PARTICLE_DEFAULT_COLOR: Vec4 = Vec4::new(1.0, 1.0, 1.0, 1.0);
pub const PARTICLE_DEFAULT_SIZE: f32 = 0.75;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
pub struct Particle {
pub internal_id: Option<String>,
#[serde(default = "Vec::new")]
pub color_gradients: Vec<ParticleColorGradient>,
#[serde(default = "Vec::new")]
pub size_gradients: Vec<ParticleSizeGradient>,
#[serde(default = "particle_defaults::lifetime")]
pub lifetime: f32,
#[serde(default = "particle_defaults::spawn_rate")]
pub spawn_rate: f32,
#[serde(default = "particle_defaults::capacity")]
pub capacity: u32,
#[serde(default = "ParticleInitialPosition::default")]
pub initial_position: ParticleInitialPosition,
#[serde(default = "ParticleInitialVelocity::default")]
pub initial_velocity: ParticleInitialVelocity,
}
mod particle_defaults {
use super::{
PARTICLE_DEFAULT_CAPACITY, PARTICLE_DEFAULT_LIFETIME, PARTICLE_DEFAULT_SPAWN_RATE,
};
#[must_use]
pub const fn lifetime() -> f32 {
PARTICLE_DEFAULT_LIFETIME
}
#[must_use]
pub const fn spawn_rate() -> f32 {
PARTICLE_DEFAULT_SPAWN_RATE
}
#[must_use]
pub const fn capacity() -> u32 {
PARTICLE_DEFAULT_CAPACITY
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::module_name_repetitions)]
pub struct ParticleInitialVelocity {
pub center_x: f32,
pub center_y: f32,
pub speed: f32,
}
impl Default for ParticleInitialVelocity {
fn default() -> Self {
Self {
center_x: 0.0,
center_y: 0.0,
speed: PARTICLE_DEFAULT_SPEED,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::module_name_repetitions)]
pub struct ParticleInitialPosition {
pub modifier_type: PositionModifierType,
pub radius: f32,
pub shape_dimension: ShapeDimensionType,
}
impl Default for ParticleInitialPosition {
fn default() -> Self {
Self {
modifier_type: PositionModifierType::Circle,
radius: PARTICLE_DEFAULT_RADIUS,
shape_dimension: ShapeDimensionType::Volume,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, Reflect)]
#[serde(rename_all = "camelCase")]
pub enum PositionModifierType {
#[default]
Circle,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, Reflect)]
#[serde(rename_all = "camelCase")]
pub enum ShapeDimensionType {
#[default]
Volume,
Surface,
}
impl ShapeDimensionType {
#[must_use]
pub const fn as_shape_dimension(&self) -> ShapeDimension {
match self {
Self::Volume => ShapeDimension::Volume,
Self::Surface => ShapeDimension::Surface,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::module_name_repetitions)]
pub struct ParticleColorGradient {
index: f32,
color: PaletteColor,
#[serde(default = "default_alpha")]
alpha: f32,
}
const fn default_alpha() -> f32 {
1.0
}
impl Default for ParticleColorGradient {
fn default() -> Self {
Self {
index: 0.0,
color: PaletteColor::default(),
alpha: 1.0,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::module_name_repetitions)]
pub struct ParticleSizeGradient {
index: f32,
width: f32,
#[serde(default = "default_height")]
height: f32,
}
const fn default_height() -> f32 {
f32::NEG_INFINITY
}
impl Default for ParticleSizeGradient {
fn default() -> Self {
Self {
index: 0.0,
width: 1.0,
height: 1.0,
}
}
}
impl Hash for Particle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.color_gradients
.iter()
.fold(String::new(), |mut output, b| {
let _ = write!(output, "{:?}", b.color);
output
})
.hash(state);
self.capacity.hash(state);
}
}
impl InternalId for Particle {
fn update_internal_id(&mut self) {
self.internal_id = Some(self.get_internal_id());
}
#[must_use]
fn get_internal_id(&self) -> String {
if self.internal_id.is_some() {
let id = self.internal_id.clone().unwrap_or_default();
if !id.is_empty() {
return id;
}
}
format!(
"{}{}",
self.color_gradients
.iter()
.fold(String::new(), |mut output, b| {
let _ = write!(output, "{:?}", b.color);
output
}),
self.capacity
)
}
}
impl<D: Hash + InternalId + 'static> TryInto<Particle> for DataFile<D> {
type Error = ();
fn try_into(self) -> Result<Particle, Self::Error> {
if self.header.system != GameSystem::Particle {
return Err(());
}
(&self.data as &dyn Any)
.downcast_ref::<Particle>()
.cloned()
.ok_or(())
}
}
impl<D: Hash + InternalId + 'static> TryFrom<&DataFile<D>> for Particle {
type Error = ();
fn try_from(data_file: &DataFile<D>) -> Result<Self, Self::Error> {
if data_file.header.system != GameSystem::Particle {
return Err(());
}
(&data_file.data as &dyn Any)
.downcast_ref::<Self>()
.cloned()
.ok_or(())
}
}
impl Particle {
#[must_use]
pub fn get_color_gradient(&self) -> Gradient<Vec4> {
if self.color_gradients.is_empty() {
return Gradient::constant(PARTICLE_DEFAULT_COLOR);
}
let mut gradient = Gradient::new();
for color in &self.color_gradients {
let mut color_vec: Vec4 = color.color.to_color().as_rgba_f32().into();
if (color.alpha - 1.0).abs() > f32::EPSILON {
color_vec.w = color.alpha;
}
gradient.add_key(color.index.clamp(0.0, 1.0), color_vec);
}
gradient
}
#[must_use]
pub fn get_size_gradient(&self) -> Gradient<Vec2> {
if self.size_gradients.is_empty() {
return Gradient::constant(Vec2::splat(PARTICLE_DEFAULT_SIZE));
}
let mut gradient = Gradient::new();
for size in &self.size_gradients {
if size.height.is_finite() {
gradient.add_key(
size.index.clamp(0.0, 1.0),
Vec2::new(size.width, size.height),
);
} else {
gradient.add_key(
size.index.clamp(0.0, 1.0),
Vec2::new(size.width, size.width),
);
}
}
gradient
}
}