1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
//! Particle effect details.
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};

/// Default lifetime used if not specified. This is the lifetime of the particles in seconds.
pub const PARTICLE_DEFAULT_LIFETIME: f32 = 0.5;
/// Default spawn rate used if not specified. This is the number of particles to spawn per second.
pub const PARTICLE_DEFAULT_SPAWN_RATE: f32 = 25.0;
/// Default capacity used if not specified. This is the maximum number of particles to be alive at any given time.
pub const PARTICLE_DEFAULT_CAPACITY: u32 = 100;
/// Default velocity used if not specified. This is the speed of the particles.
pub const PARTICLE_DEFAULT_SPEED: f32 = 2.0;
/// Default radius used if not specified. This is the spread of the particles.
pub const PARTICLE_DEFAULT_RADIUS: f32 = 2.0;
/// Default color used if not specified.
///
/// <div style="background-color:rgb(100%, 100%, 100%); width: 10px; padding: 10px; border: 1px solid;"></div>
pub const PARTICLE_DEFAULT_COLOR: Vec4 = Vec4::new(1.0, 1.0, 1.0, 1.0);
/// Default size that is used if not specified.
pub const PARTICLE_DEFAULT_SIZE: f32 = 0.75;

/// Details about a particle effect.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
pub struct Particle {
    /// The internal ID of the particle effect.
    pub internal_id: Option<String>,
    /// Color gradients for the particles.
    #[serde(default = "Vec::new")]
    pub color_gradients: Vec<ParticleColorGradient>,
    /// Size gradients for the particles.
    #[serde(default = "Vec::new")]
    pub size_gradients: Vec<ParticleSizeGradient>,
    /// The lifetime of the particles in seconds
    #[serde(default = "particle_defaults::lifetime")]
    pub lifetime: f32,
    /// The number of particles to spawn per second.
    #[serde(default = "particle_defaults::spawn_rate")]
    pub spawn_rate: f32,
    /// The maximum number of particles to be alive at any given time.
    ///
    /// Note: the lower the better
    #[serde(default = "particle_defaults::capacity")]
    pub capacity: u32,
    /// The initial position of the particles.
    #[serde(default = "ParticleInitialPosition::default")]
    pub initial_position: ParticleInitialPosition,
    /// The initial velocity of the particles.
    #[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
    }
}

/// Initial velocity for a particle effect.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::module_name_repetitions)]
pub struct ParticleInitialVelocity {
    /// initial velocity modifier
    pub center_x: f32,
    /// initial velocity modifier
    pub center_y: f32,
    /// the speed of the particles
    pub speed: f32,
}

impl Default for ParticleInitialVelocity {
    fn default() -> Self {
        Self {
            center_x: 0.0,
            center_y: 0.0,
            speed: PARTICLE_DEFAULT_SPEED,
        }
    }
}

/// Initial position for a particle effect.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::module_name_repetitions)]
pub struct ParticleInitialPosition {
    /// initial position modifier
    pub modifier_type: PositionModifierType,
    /// the radius of the circle
    pub radius: f32,
    /// the shape dimension for spawning particles
    pub shape_dimension: ShapeDimensionType,
}

impl Default for ParticleInitialPosition {
    fn default() -> Self {
        Self {
            modifier_type: PositionModifierType::Circle,
            radius: PARTICLE_DEFAULT_RADIUS,
            shape_dimension: ShapeDimensionType::Volume,
        }
    }
}

/// The type of position modifier for the particles.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, Reflect)]
#[serde(rename_all = "camelCase")]
pub enum PositionModifierType {
    /// A circle shape
    #[default]
    Circle,
}

/// The type of position modifier for the particles.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, Reflect)]
#[serde(rename_all = "camelCase")]
pub enum ShapeDimensionType {
    /// The entire volume of the circle
    #[default]
    Volume,
    /// Only on the edge of the circle
    Surface,
}

impl ShapeDimensionType {
    /// Convert the shape dimension type into a bevy shape dimension.
    #[must_use]
    pub const fn as_shape_dimension(&self) -> ShapeDimension {
        match self {
            Self::Volume => ShapeDimension::Volume,
            Self::Surface => ShapeDimension::Surface,
        }
    }
}

/// Color for a particle effect.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::module_name_repetitions)]
pub struct ParticleColorGradient {
    /// The index of the color gradient.
    index: f32,
    /// The color of the particle.
    color: PaletteColor,
    /// The alpha value of the particle.
    #[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,
        }
    }
}

/// Color for a particle effect.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Reflect)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::module_name_repetitions)]
pub struct ParticleSizeGradient {
    /// The index of the size gradient.
    index: f32,
    /// The size of the particle.
    width: f32,
    /// The height of the particle.
    #[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 {
    /// Update the particle's internal ID.
    fn update_internal_id(&mut self) {
        self.internal_id = Some(self.get_internal_id());
    }
    /// Get the particle's 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 {
    /// Get a gradient from the particle's color gradients.
    #[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
    }

    /// Get a gradient from the particle's size gradients.
    #[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
    }
}