Skip to main content

nightshade_renderer/
particles.rs

1//! Particle system component definitions.
2
3use nalgebra_glm::{Vec3, Vec4};
4
5#[derive(Debug, Clone)]
6pub struct ParticleTextureUpload {
7    pub slot: u32,
8    pub rgba_data: Vec<u8>,
9    pub width: u32,
10    pub height: u32,
11}
12
13/// Effect type for categorizing particle behaviors.
14#[derive(
15    Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, enum2schema::Schema,
16)]
17#[schema(string_enum)]
18pub enum EmitterType {
19    /// Burst-style pyrotechnic effects.
20    Firework,
21    /// Continuous flame-like effects with upward drift.
22    Fire,
23    /// Slow, expanding, non-emissive particles.
24    Smoke,
25    /// Fast, small, bright particles with strong gravity.
26    Sparks,
27    /// Continuous stream following a moving emitter.
28    Trail,
29}
30
31/// Spatial volume from which particles spawn.
32#[derive(
33    Debug,
34    Clone,
35    Copy,
36    PartialEq,
37    Default,
38    serde::Serialize,
39    serde::Deserialize,
40    enum2schema::Schema,
41)]
42pub enum EmitterShape {
43    /// Single point emission.
44    #[default]
45    Point,
46    /// Emit from random points within a sphere.
47    Sphere { radius: f32 },
48    /// Emit within a cone pointed along the emitter direction.
49    Cone { angle: f32, height: f32 },
50    /// Emit from random points within a box.
51    Box { half_extents: Vec3 },
52}
53
54/// Color and alpha values sampled over particle lifetime.
55///
56/// Colors are interpolated linearly between keyframes. The time value
57/// ranges from 0.0 (particle birth) to 1.0 (particle death).
58#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, enum2schema::Schema)]
59pub struct ColorGradient {
60    /// Keyframes as (normalized_time, rgba_color) pairs.
61    pub colors: Vec<(f32, Vec4)>,
62}
63
64impl Default for ColorGradient {
65    fn default() -> Self {
66        Self {
67            colors: vec![
68                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
69                (1.0, Vec4::new(1.0, 1.0, 1.0, 0.0)),
70            ],
71        }
72    }
73}
74
75impl ColorGradient {
76    /// Hot to cool gradient for firework trails.
77    pub fn firework_trail() -> Self {
78        Self {
79            colors: vec![
80                (0.0, Vec4::new(1.0, 1.0, 0.95, 1.0)),
81                (0.15, Vec4::new(1.0, 0.95, 0.7, 1.0)),
82                (0.4, Vec4::new(1.0, 0.7, 0.3, 0.85)),
83                (0.7, Vec4::new(1.0, 0.4, 0.1, 0.5)),
84                (1.0, Vec4::new(0.6, 0.15, 0.0, 0.0)),
85            ],
86        }
87    }
88
89    /// Standard firework burst gradient with custom base color.
90    pub fn firework_explosion(base_color: Vec3) -> Self {
91        Self {
92            colors: vec![
93                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
94                (
95                    0.05,
96                    Vec4::new(
97                        (base_color.x + 1.0) * 0.5,
98                        (base_color.y + 1.0) * 0.5,
99                        (base_color.z + 1.0) * 0.5,
100                        1.0,
101                    ),
102                ),
103                (
104                    0.15,
105                    Vec4::new(base_color.x, base_color.y, base_color.z, 1.0),
106                ),
107                (
108                    0.4,
109                    Vec4::new(base_color.x, base_color.y, base_color.z, 0.95),
110                ),
111                (
112                    0.65,
113                    Vec4::new(
114                        base_color.x * 0.85,
115                        base_color.y * 0.85,
116                        base_color.z * 0.85,
117                        0.7,
118                    ),
119                ),
120                (
121                    0.85,
122                    Vec4::new(
123                        base_color.x * 0.5,
124                        base_color.y * 0.5,
125                        base_color.z * 0.5,
126                        0.3,
127                    ),
128                ),
129                (1.0, Vec4::new(0.1, 0.1, 0.1, 0.0)),
130            ],
131        }
132    }
133
134    /// Long-lived willow-style drooping effect.
135    pub fn firework_willow(base_color: Vec3) -> Self {
136        Self {
137            colors: vec![
138                (0.0, Vec4::new(1.0, 1.0, 0.95, 1.0)),
139                (
140                    0.1,
141                    Vec4::new(base_color.x, base_color.y, base_color.z, 1.0),
142                ),
143                (
144                    0.3,
145                    Vec4::new(base_color.x, base_color.y, base_color.z, 0.9),
146                ),
147                (
148                    0.6,
149                    Vec4::new(
150                        base_color.x * 0.7,
151                        base_color.y * 0.8,
152                        base_color.z * 0.6,
153                        0.6,
154                    ),
155                ),
156                (
157                    0.85,
158                    Vec4::new(
159                        base_color.x * 0.4,
160                        base_color.y * 0.5,
161                        base_color.z * 0.3,
162                        0.25,
163                    ),
164                ),
165                (1.0, Vec4::new(0.1, 0.1, 0.05, 0.0)),
166            ],
167        }
168    }
169
170    pub fn firework_glitter() -> Self {
171        Self {
172            colors: vec![
173                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
174                (0.2, Vec4::new(1.0, 1.0, 0.9, 1.0)),
175                (0.5, Vec4::new(1.0, 0.95, 0.7, 0.9)),
176                (0.8, Vec4::new(1.0, 0.85, 0.5, 0.5)),
177                (1.0, Vec4::new(0.8, 0.6, 0.2, 0.0)),
178            ],
179        }
180    }
181
182    pub fn firework_crackle() -> Self {
183        Self {
184            colors: vec![
185                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
186                (0.1, Vec4::new(1.0, 0.95, 0.8, 1.0)),
187                (0.3, Vec4::new(1.0, 0.8, 0.4, 0.9)),
188                (0.6, Vec4::new(1.0, 0.5, 0.1, 0.6)),
189                (1.0, Vec4::new(0.4, 0.1, 0.0, 0.0)),
190            ],
191        }
192    }
193
194    pub fn flash_burst() -> Self {
195        Self {
196            colors: vec![
197                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
198                (0.15, Vec4::new(1.0, 1.0, 0.95, 1.0)),
199                (0.4, Vec4::new(1.0, 0.95, 0.8, 0.7)),
200                (0.7, Vec4::new(1.0, 0.9, 0.6, 0.3)),
201                (1.0, Vec4::new(1.0, 0.8, 0.4, 0.0)),
202            ],
203        }
204    }
205
206    pub fn comet_trail() -> Self {
207        Self {
208            colors: vec![
209                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
210                (0.1, Vec4::new(1.0, 0.98, 0.9, 1.0)),
211                (0.25, Vec4::new(1.0, 0.9, 0.6, 0.95)),
212                (0.5, Vec4::new(1.0, 0.7, 0.3, 0.8)),
213                (0.75, Vec4::new(1.0, 0.4, 0.1, 0.5)),
214                (1.0, Vec4::new(0.5, 0.1, 0.0, 0.0)),
215            ],
216        }
217    }
218
219    pub fn palm_frond(base_color: Vec3) -> Self {
220        Self {
221            colors: vec![
222                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
223                (
224                    0.08,
225                    Vec4::new(base_color.x, base_color.y, base_color.z, 1.0),
226                ),
227                (
228                    0.25,
229                    Vec4::new(base_color.x, base_color.y, base_color.z, 0.95),
230                ),
231                (
232                    0.5,
233                    Vec4::new(
234                        base_color.x * 0.9,
235                        base_color.y * 0.9,
236                        base_color.z * 0.9,
237                        0.85,
238                    ),
239                ),
240                (
241                    0.75,
242                    Vec4::new(
243                        base_color.x * 0.7,
244                        base_color.y * 0.7,
245                        base_color.z * 0.7,
246                        0.5,
247                    ),
248                ),
249                (
250                    1.0,
251                    Vec4::new(
252                        base_color.x * 0.3,
253                        base_color.y * 0.3,
254                        base_color.z * 0.3,
255                        0.0,
256                    ),
257                ),
258            ],
259        }
260    }
261
262    pub fn crossette() -> Self {
263        Self {
264            colors: vec![
265                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
266                (0.1, Vec4::new(1.0, 0.95, 0.85, 1.0)),
267                (0.3, Vec4::new(1.0, 0.85, 0.6, 0.9)),
268                (0.6, Vec4::new(1.0, 0.6, 0.2, 0.7)),
269                (1.0, Vec4::new(0.6, 0.2, 0.0, 0.0)),
270            ],
271        }
272    }
273
274    pub fn strobe() -> Self {
275        Self {
276            colors: vec![
277                (0.0, Vec4::new(1.0, 1.0, 1.0, 1.0)),
278                (0.5, Vec4::new(1.0, 1.0, 1.0, 1.0)),
279                (1.0, Vec4::new(0.8, 0.8, 0.8, 0.0)),
280            ],
281        }
282    }
283
284    pub fn fire() -> Self {
285        Self {
286            colors: vec![
287                (0.0, Vec4::new(1.0, 1.0, 0.9, 1.0)),
288                (0.2, Vec4::new(1.0, 0.9, 0.4, 1.0)),
289                (0.4, Vec4::new(1.0, 0.5, 0.1, 0.9)),
290                (0.7, Vec4::new(0.9, 0.2, 0.0, 0.6)),
291                (1.0, Vec4::new(0.3, 0.05, 0.0, 0.0)),
292            ],
293        }
294    }
295
296    pub fn smoke() -> Self {
297        Self {
298            colors: vec![
299                (0.0, Vec4::new(0.4, 0.4, 0.45, 0.0)),
300                (0.1, Vec4::new(0.5, 0.5, 0.55, 0.6)),
301                (0.4, Vec4::new(0.55, 0.55, 0.6, 0.5)),
302                (0.7, Vec4::new(0.6, 0.6, 0.65, 0.3)),
303                (1.0, Vec4::new(0.65, 0.65, 0.7, 0.0)),
304            ],
305        }
306    }
307
308    pub fn sparks() -> Self {
309        Self {
310            colors: vec![
311                (0.0, Vec4::new(1.0, 1.0, 0.8, 1.0)),
312                (0.3, Vec4::new(1.0, 0.8, 0.2, 1.0)),
313                (0.7, Vec4::new(1.0, 0.4, 0.0, 0.7)),
314                (1.0, Vec4::new(0.5, 0.1, 0.0, 0.0)),
315            ],
316        }
317    }
318
319    /// Converts to GPU-compatible format (max 8 keyframes).
320    pub fn to_gpu_data(&self) -> [[f32; 8]; 8] {
321        let mut data = [[0.0f32; 8]; 8];
322        for (index, (time, color)) in self.colors.iter().take(8).enumerate() {
323            data[index] = [*time, color.x, color.y, color.z, color.w, 0.0, 0.0, 0.0];
324        }
325        data
326    }
327
328    /// Returns the number of keyframes (capped at 8 for GPU).
329    pub fn color_count(&self) -> u32 {
330        self.colors.len().min(8) as u32
331    }
332}
333
334/// Particle emitter component.
335///
336/// Spawns particles continuously at `spawn_rate` or in bursts of `burst_count`.
337/// Each particle is simulated independently with its own velocity, gravity, and lifetime.
338#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, enum2schema::Schema)]
339pub struct ParticleEmitter {
340    /// Effect type for shader selection and behavior hints.
341    pub emitter_type: EmitterType,
342    /// Volume from which particles are emitted.
343    pub shape: EmitterShape,
344    /// Emitter position in local space (added to entity transform).
345    pub position: Vec3,
346    /// Primary emission direction (for directional shapes).
347    pub direction: Vec3,
348    /// Particles spawned per second (0 for burst-only).
349    pub spawn_rate: f32,
350    /// Number of particles to spawn immediately when enabled.
351    pub burst_count: u32,
352    /// Minimum particle lifetime in seconds.
353    pub particle_lifetime_min: f32,
354    /// Maximum particle lifetime in seconds.
355    pub particle_lifetime_max: f32,
356    /// Minimum initial speed along emission direction.
357    pub initial_velocity_min: f32,
358    /// Maximum initial speed along emission direction.
359    pub initial_velocity_max: f32,
360    /// Random angular spread from direction (radians).
361    pub velocity_spread: f32,
362    /// Acceleration applied each frame (typically downward for gravity).
363    pub gravity: Vec3,
364    /// Velocity damping factor (0 = no damping, 1 = full stop immediately).
365    pub drag: f32,
366    /// Particle size at birth.
367    pub size_start: f32,
368    /// Particle size at death.
369    pub size_end: f32,
370    /// Color and alpha over particle lifetime.
371    pub color_gradient: ColorGradient,
372    /// Emission multiplier for HDR bloom.
373    pub emissive_strength: f32,
374    /// Whether the emitter is active.
375    pub enabled: bool,
376    /// Internal accumulator for fractional particle spawning.
377    pub accumulated_spawn: f32,
378    /// If true, fires burst once then disables.
379    pub one_shot: bool,
380    /// Whether the one-shot burst has already fired.
381    pub has_fired: bool,
382    /// Strength of noise-based position perturbation.
383    pub turbulence_strength: f32,
384    /// Frequency of turbulence noise sampling.
385    pub turbulence_frequency: f32,
386    /// Texture array layer index (0 = procedural shape, 1+ = texture from array).
387    pub texture_index: u32,
388    /// Keyframed size over particle lifetime. When non-empty,
389    /// overrides the linear `size_start` to `size_end` interpolation.
390    /// Each entry is (normalized_time, size).
391    #[serde(default)]
392    pub size_curve: Vec<(f32, f32)>,
393    /// Keyframed opacity over particle lifetime. When non-empty,
394    /// multiplies the gradient alpha by the sampled value.
395    /// Each entry is (normalized_time, opacity).
396    #[serde(default)]
397    pub opacity_curve: Vec<(f32, f32)>,
398    /// Sub-emitter spawn behavior on particle death. The named entry
399    /// references a prefab path in the prefab cache. Empty = no sub-emitter.
400    #[serde(default)]
401    pub death_sub_emitter: Option<String>,
402    /// Spawn a sub-emitter every `trail_spawn_interval` seconds along
403    /// the particle's path. 0 disables trails.
404    #[serde(default)]
405    pub trail_sub_emitter: Option<String>,
406    #[serde(default)]
407    pub trail_spawn_interval: f32,
408}
409
410impl Default for ParticleEmitter {
411    fn default() -> Self {
412        Self {
413            emitter_type: EmitterType::Fire,
414            shape: EmitterShape::Point,
415            position: Vec3::zeros(),
416            direction: Vec3::new(0.0, 1.0, 0.0),
417            spawn_rate: 100.0,
418            burst_count: 0,
419            particle_lifetime_min: 1.0,
420            particle_lifetime_max: 2.0,
421            initial_velocity_min: 1.0,
422            initial_velocity_max: 3.0,
423            velocity_spread: 0.3,
424            gravity: Vec3::new(0.0, -9.81, 0.0),
425            drag: 0.1,
426            size_start: 0.1,
427            size_end: 0.0,
428            color_gradient: ColorGradient::default(),
429            emissive_strength: 1.0,
430            enabled: true,
431            accumulated_spawn: 0.0,
432            one_shot: false,
433            has_fired: false,
434            turbulence_strength: 0.0,
435            turbulence_frequency: 1.0,
436            texture_index: 0,
437            size_curve: Vec::new(),
438            opacity_curve: Vec::new(),
439            death_sub_emitter: None,
440            trail_sub_emitter: None,
441            trail_spawn_interval: 0.0,
442        }
443    }
444}
445
446impl ParticleEmitter {
447    /// Creates a rising firework shell with trailing sparks.
448    pub fn firework_shell(position: Vec3, velocity: Vec3) -> Self {
449        Self {
450            emitter_type: EmitterType::Firework,
451            shape: EmitterShape::Point,
452            position,
453            direction: velocity.normalize(),
454            spawn_rate: 120.0,
455            burst_count: 0,
456            particle_lifetime_min: 0.15,
457            particle_lifetime_max: 0.4,
458            initial_velocity_min: velocity.magnitude() * 0.05,
459            initial_velocity_max: velocity.magnitude() * 0.15,
460            velocity_spread: 0.25,
461            gravity: Vec3::new(0.0, -4.0, 0.0),
462            drag: 0.1,
463            size_start: 0.15,
464            size_end: 0.03,
465            color_gradient: ColorGradient::firework_trail(),
466            emissive_strength: 5.0,
467            enabled: true,
468            accumulated_spawn: 0.0,
469            one_shot: false,
470            has_fired: false,
471            turbulence_strength: 0.2,
472            turbulence_frequency: 2.0,
473            texture_index: 0,
474            size_curve: Vec::new(),
475            opacity_curve: Vec::new(),
476            death_sub_emitter: None,
477            trail_sub_emitter: None,
478            trail_spawn_interval: 0.0,
479        }
480    }
481
482    /// Creates a spherical firework burst.
483    pub fn firework_explosion(position: Vec3, color: Vec3, particle_count: u32) -> Self {
484        Self {
485            emitter_type: EmitterType::Firework,
486            shape: EmitterShape::Sphere { radius: 0.5 },
487            position,
488            direction: Vec3::new(0.0, 1.0, 0.0),
489            spawn_rate: 0.0,
490            burst_count: particle_count,
491            particle_lifetime_min: 2.0,
492            particle_lifetime_max: 3.5,
493            initial_velocity_min: 12.0,
494            initial_velocity_max: 25.0,
495            velocity_spread: std::f32::consts::PI,
496            gravity: Vec3::new(0.0, -4.0, 0.0),
497            drag: 0.4,
498            size_start: 0.25,
499            size_end: 0.04,
500            color_gradient: ColorGradient::firework_explosion(color),
501            emissive_strength: 8.0,
502            enabled: true,
503            accumulated_spawn: 0.0,
504            one_shot: true,
505            has_fired: false,
506            turbulence_strength: 0.3,
507            turbulence_frequency: 1.5,
508            texture_index: 0,
509            size_curve: Vec::new(),
510            opacity_curve: Vec::new(),
511            death_sub_emitter: None,
512            trail_sub_emitter: None,
513            trail_spawn_interval: 0.0,
514        }
515    }
516
517    pub fn firework_willow(position: Vec3, color: Vec3, particle_count: u32) -> Self {
518        Self {
519            emitter_type: EmitterType::Firework,
520            shape: EmitterShape::Sphere { radius: 0.3 },
521            position,
522            direction: Vec3::new(0.0, 1.0, 0.0),
523            spawn_rate: 0.0,
524            burst_count: particle_count,
525            particle_lifetime_min: 3.5,
526            particle_lifetime_max: 5.5,
527            initial_velocity_min: 8.0,
528            initial_velocity_max: 18.0,
529            velocity_spread: std::f32::consts::PI,
530            gravity: Vec3::new(0.0, -6.0, 0.0),
531            drag: 0.15,
532            size_start: 0.18,
533            size_end: 0.02,
534            color_gradient: ColorGradient::firework_willow(color),
535            emissive_strength: 6.0,
536            enabled: true,
537            accumulated_spawn: 0.0,
538            one_shot: true,
539            has_fired: false,
540            turbulence_strength: 0.1,
541            turbulence_frequency: 0.8,
542            texture_index: 0,
543            size_curve: Vec::new(),
544            opacity_curve: Vec::new(),
545            death_sub_emitter: None,
546            trail_sub_emitter: None,
547            trail_spawn_interval: 0.0,
548        }
549    }
550
551    pub fn firework_chrysanthemum(position: Vec3, color: Vec3, particle_count: u32) -> Self {
552        Self {
553            emitter_type: EmitterType::Firework,
554            shape: EmitterShape::Sphere { radius: 0.2 },
555            position,
556            direction: Vec3::new(0.0, 1.0, 0.0),
557            spawn_rate: 0.0,
558            burst_count: particle_count,
559            particle_lifetime_min: 2.5,
560            particle_lifetime_max: 4.0,
561            initial_velocity_min: 15.0,
562            initial_velocity_max: 30.0,
563            velocity_spread: std::f32::consts::PI,
564            gravity: Vec3::new(0.0, -2.5, 0.0),
565            drag: 0.6,
566            size_start: 0.2,
567            size_end: 0.05,
568            color_gradient: ColorGradient::firework_explosion(color),
569            emissive_strength: 10.0,
570            enabled: true,
571            accumulated_spawn: 0.0,
572            one_shot: true,
573            has_fired: false,
574            turbulence_strength: 0.15,
575            turbulence_frequency: 1.2,
576            texture_index: 0,
577            size_curve: Vec::new(),
578            opacity_curve: Vec::new(),
579            death_sub_emitter: None,
580            trail_sub_emitter: None,
581            trail_spawn_interval: 0.0,
582        }
583    }
584
585    pub fn firework_ring(position: Vec3, color: Vec3, particle_count: u32) -> Self {
586        Self {
587            emitter_type: EmitterType::Firework,
588            shape: EmitterShape::Point,
589            position,
590            direction: Vec3::new(0.0, 0.0, 1.0),
591            spawn_rate: 0.0,
592            burst_count: particle_count,
593            particle_lifetime_min: 2.0,
594            particle_lifetime_max: 3.0,
595            initial_velocity_min: 18.0,
596            initial_velocity_max: 22.0,
597            velocity_spread: 0.15,
598            gravity: Vec3::new(0.0, -1.5, 0.0),
599            drag: 0.35,
600            size_start: 0.22,
601            size_end: 0.03,
602            color_gradient: ColorGradient::firework_explosion(color),
603            emissive_strength: 9.0,
604            enabled: true,
605            accumulated_spawn: 0.0,
606            one_shot: true,
607            has_fired: false,
608            turbulence_strength: 0.05,
609            turbulence_frequency: 1.0,
610            texture_index: 0,
611            size_curve: Vec::new(),
612            opacity_curve: Vec::new(),
613            death_sub_emitter: None,
614            trail_sub_emitter: None,
615            trail_spawn_interval: 0.0,
616        }
617    }
618
619    pub fn firework_glitter(position: Vec3, particle_count: u32) -> Self {
620        Self {
621            emitter_type: EmitterType::Sparks,
622            shape: EmitterShape::Sphere { radius: 2.0 },
623            position,
624            direction: Vec3::new(0.0, 1.0, 0.0),
625            spawn_rate: 0.0,
626            burst_count: particle_count,
627            particle_lifetime_min: 0.8,
628            particle_lifetime_max: 2.5,
629            initial_velocity_min: 1.0,
630            initial_velocity_max: 5.0,
631            velocity_spread: std::f32::consts::PI,
632            gravity: Vec3::new(0.0, -8.0, 0.0),
633            drag: 0.05,
634            size_start: 0.12,
635            size_end: 0.02,
636            color_gradient: ColorGradient::firework_glitter(),
637            emissive_strength: 12.0,
638            enabled: true,
639            accumulated_spawn: 0.0,
640            one_shot: true,
641            has_fired: false,
642            turbulence_strength: 0.8,
643            turbulence_frequency: 4.0,
644            texture_index: 0,
645            size_curve: Vec::new(),
646            opacity_curve: Vec::new(),
647            death_sub_emitter: None,
648            trail_sub_emitter: None,
649            trail_spawn_interval: 0.0,
650        }
651    }
652
653    pub fn firework_crackle(position: Vec3, particle_count: u32) -> Self {
654        Self {
655            emitter_type: EmitterType::Sparks,
656            shape: EmitterShape::Sphere { radius: 1.5 },
657            position,
658            direction: Vec3::new(0.0, 1.0, 0.0),
659            spawn_rate: 0.0,
660            burst_count: particle_count,
661            particle_lifetime_min: 0.3,
662            particle_lifetime_max: 0.8,
663            initial_velocity_min: 3.0,
664            initial_velocity_max: 10.0,
665            velocity_spread: std::f32::consts::PI,
666            gravity: Vec3::new(0.0, -12.0, 0.0),
667            drag: 0.02,
668            size_start: 0.08,
669            size_end: 0.01,
670            color_gradient: ColorGradient::firework_crackle(),
671            emissive_strength: 15.0,
672            enabled: true,
673            accumulated_spawn: 0.0,
674            one_shot: true,
675            has_fired: false,
676            turbulence_strength: 1.2,
677            turbulence_frequency: 6.0,
678            texture_index: 0,
679            size_curve: Vec::new(),
680            opacity_curve: Vec::new(),
681            death_sub_emitter: None,
682            trail_sub_emitter: None,
683            trail_spawn_interval: 0.0,
684        }
685    }
686
687    pub fn flash_burst(position: Vec3) -> Self {
688        Self {
689            emitter_type: EmitterType::Firework,
690            shape: EmitterShape::Point,
691            position,
692            direction: Vec3::new(0.0, 1.0, 0.0),
693            spawn_rate: 0.0,
694            burst_count: 50,
695            particle_lifetime_min: 0.1,
696            particle_lifetime_max: 0.25,
697            initial_velocity_min: 2.0,
698            initial_velocity_max: 8.0,
699            velocity_spread: std::f32::consts::PI,
700            gravity: Vec3::new(0.0, 0.0, 0.0),
701            drag: 0.0,
702            size_start: 1.5,
703            size_end: 0.3,
704            color_gradient: ColorGradient::flash_burst(),
705            emissive_strength: 25.0,
706            enabled: true,
707            accumulated_spawn: 0.0,
708            one_shot: true,
709            has_fired: false,
710            turbulence_strength: 0.0,
711            turbulence_frequency: 0.0,
712            texture_index: 0,
713            size_curve: Vec::new(),
714            opacity_curve: Vec::new(),
715            death_sub_emitter: None,
716            trail_sub_emitter: None,
717            trail_spawn_interval: 0.0,
718        }
719    }
720
721    pub fn comet_shell(position: Vec3, velocity: Vec3) -> Self {
722        Self {
723            emitter_type: EmitterType::Firework,
724            shape: EmitterShape::Point,
725            position,
726            direction: velocity.normalize(),
727            spawn_rate: 200.0,
728            burst_count: 0,
729            particle_lifetime_min: 0.2,
730            particle_lifetime_max: 0.5,
731            initial_velocity_min: velocity.magnitude() * 0.02,
732            initial_velocity_max: velocity.magnitude() * 0.08,
733            velocity_spread: 0.15,
734            gravity: Vec3::new(0.0, -2.0, 0.0),
735            drag: 0.05,
736            size_start: 0.25,
737            size_end: 0.04,
738            color_gradient: ColorGradient::comet_trail(),
739            emissive_strength: 8.0,
740            enabled: true,
741            accumulated_spawn: 0.0,
742            one_shot: false,
743            has_fired: false,
744            turbulence_strength: 0.1,
745            turbulence_frequency: 1.5,
746            texture_index: 0,
747            size_curve: Vec::new(),
748            opacity_curve: Vec::new(),
749            death_sub_emitter: None,
750            trail_sub_emitter: None,
751            trail_spawn_interval: 0.0,
752        }
753    }
754
755    pub fn palm_explosion(position: Vec3, color: Vec3, particle_count: u32) -> Self {
756        Self {
757            emitter_type: EmitterType::Firework,
758            shape: EmitterShape::Sphere { radius: 0.2 },
759            position,
760            direction: Vec3::new(0.0, 1.0, 0.0),
761            spawn_rate: 0.0,
762            burst_count: particle_count,
763            particle_lifetime_min: 3.0,
764            particle_lifetime_max: 5.0,
765            initial_velocity_min: 20.0,
766            initial_velocity_max: 35.0,
767            velocity_spread: 0.4,
768            gravity: Vec3::new(0.0, -8.0, 0.0),
769            drag: 0.25,
770            size_start: 0.22,
771            size_end: 0.03,
772            color_gradient: ColorGradient::palm_frond(color),
773            emissive_strength: 8.0,
774            enabled: true,
775            accumulated_spawn: 0.0,
776            one_shot: true,
777            has_fired: false,
778            turbulence_strength: 0.2,
779            turbulence_frequency: 1.0,
780            texture_index: 0,
781            size_curve: Vec::new(),
782            opacity_curve: Vec::new(),
783            death_sub_emitter: None,
784            trail_sub_emitter: None,
785            trail_spawn_interval: 0.0,
786        }
787    }
788
789    pub fn crossette_burst(position: Vec3, color: Vec3, particle_count: u32) -> Self {
790        Self {
791            emitter_type: EmitterType::Sparks,
792            shape: EmitterShape::Sphere { radius: 0.1 },
793            position,
794            direction: Vec3::new(0.0, 1.0, 0.0),
795            spawn_rate: 0.0,
796            burst_count: particle_count,
797            particle_lifetime_min: 0.5,
798            particle_lifetime_max: 1.2,
799            initial_velocity_min: 15.0,
800            initial_velocity_max: 25.0,
801            velocity_spread: std::f32::consts::PI,
802            gravity: Vec3::new(0.0, -5.0, 0.0),
803            drag: 0.3,
804            size_start: 0.15,
805            size_end: 0.02,
806            color_gradient: ColorGradient::firework_explosion(color),
807            emissive_strength: 12.0,
808            enabled: true,
809            accumulated_spawn: 0.0,
810            one_shot: true,
811            has_fired: false,
812            turbulence_strength: 0.5,
813            turbulence_frequency: 3.0,
814            texture_index: 0,
815            size_curve: Vec::new(),
816            opacity_curve: Vec::new(),
817            death_sub_emitter: None,
818            trail_sub_emitter: None,
819            trail_spawn_interval: 0.0,
820        }
821    }
822
823    pub fn strobe_effect(position: Vec3, particle_count: u32) -> Self {
824        Self {
825            emitter_type: EmitterType::Sparks,
826            shape: EmitterShape::Sphere { radius: 3.0 },
827            position,
828            direction: Vec3::new(0.0, -1.0, 0.0),
829            spawn_rate: 0.0,
830            burst_count: particle_count,
831            particle_lifetime_min: 1.5,
832            particle_lifetime_max: 3.0,
833            initial_velocity_min: 0.5,
834            initial_velocity_max: 2.0,
835            velocity_spread: std::f32::consts::PI,
836            gravity: Vec3::new(0.0, -6.0, 0.0),
837            drag: 0.1,
838            size_start: 0.1,
839            size_end: 0.02,
840            color_gradient: ColorGradient::strobe(),
841            emissive_strength: 20.0,
842            enabled: true,
843            accumulated_spawn: 0.0,
844            one_shot: true,
845            has_fired: false,
846            turbulence_strength: 0.3,
847            turbulence_frequency: 2.0,
848            texture_index: 0,
849            size_curve: Vec::new(),
850            opacity_curve: Vec::new(),
851            death_sub_emitter: None,
852            trail_sub_emitter: None,
853            trail_spawn_interval: 0.0,
854        }
855    }
856
857    pub fn trailing_sparks(position: Vec3, velocity: Vec3) -> Self {
858        Self {
859            emitter_type: EmitterType::Sparks,
860            shape: EmitterShape::Point,
861            position,
862            direction: (-velocity).normalize(),
863            spawn_rate: 80.0,
864            burst_count: 0,
865            particle_lifetime_min: 0.2,
866            particle_lifetime_max: 0.6,
867            initial_velocity_min: 1.0,
868            initial_velocity_max: 4.0,
869            velocity_spread: 0.5,
870            gravity: Vec3::new(0.0, -15.0, 0.0),
871            drag: 0.05,
872            size_start: 0.06,
873            size_end: 0.01,
874            color_gradient: ColorGradient::firework_glitter(),
875            emissive_strength: 10.0,
876            enabled: true,
877            accumulated_spawn: 0.0,
878            one_shot: false,
879            has_fired: false,
880            turbulence_strength: 0.8,
881            turbulence_frequency: 5.0,
882            texture_index: 0,
883            size_curve: Vec::new(),
884            opacity_curve: Vec::new(),
885            death_sub_emitter: None,
886            trail_sub_emitter: None,
887            trail_spawn_interval: 0.0,
888        }
889    }
890
891    /// Creates a continuous fire effect.
892    pub fn fire(position: Vec3) -> Self {
893        Self {
894            emitter_type: EmitterType::Fire,
895            shape: EmitterShape::Cone {
896                angle: 0.3,
897                height: 0.1,
898            },
899            position,
900            direction: Vec3::new(0.0, 1.0, 0.0),
901            spawn_rate: 200.0,
902            burst_count: 0,
903            particle_lifetime_min: 0.5,
904            particle_lifetime_max: 1.5,
905            initial_velocity_min: 2.0,
906            initial_velocity_max: 5.0,
907            velocity_spread: 0.4,
908            gravity: Vec3::new(0.0, 2.0, 0.0),
909            drag: 0.5,
910            size_start: 0.3,
911            size_end: 0.1,
912            color_gradient: ColorGradient::fire(),
913            emissive_strength: 4.0,
914            enabled: true,
915            accumulated_spawn: 0.0,
916            one_shot: false,
917            has_fired: false,
918            turbulence_strength: 1.5,
919            turbulence_frequency: 3.0,
920            texture_index: 0,
921            size_curve: Vec::new(),
922            opacity_curve: Vec::new(),
923            death_sub_emitter: None,
924            trail_sub_emitter: None,
925            trail_spawn_interval: 0.0,
926        }
927    }
928
929    /// Creates a continuous smoke plume.
930    pub fn smoke(position: Vec3) -> Self {
931        Self {
932            emitter_type: EmitterType::Smoke,
933            shape: EmitterShape::Cone {
934                angle: 0.1,
935                height: 0.2,
936            },
937            position,
938            direction: Vec3::new(0.0, 1.0, 0.0),
939            spawn_rate: 50.0,
940            burst_count: 0,
941            particle_lifetime_min: 2.5,
942            particle_lifetime_max: 4.5,
943            initial_velocity_min: 0.8,
944            initial_velocity_max: 1.8,
945            velocity_spread: 0.15,
946            gravity: Vec3::new(0.0, 0.6, 0.0),
947            drag: 0.3,
948            size_start: 0.4,
949            size_end: 1.2,
950            color_gradient: ColorGradient::smoke(),
951            emissive_strength: 0.0,
952            enabled: true,
953            accumulated_spawn: 0.0,
954            one_shot: false,
955            has_fired: false,
956            turbulence_strength: 0.3,
957            turbulence_frequency: 0.4,
958            texture_index: 0,
959            size_curve: Vec::new(),
960            opacity_curve: Vec::new(),
961            death_sub_emitter: None,
962            trail_sub_emitter: None,
963            trail_spawn_interval: 0.0,
964        }
965    }
966
967    /// Creates a continuous spark fountain.
968    pub fn sparks(position: Vec3) -> Self {
969        Self {
970            emitter_type: EmitterType::Sparks,
971            shape: EmitterShape::Point,
972            position,
973            direction: Vec3::new(0.0, 1.0, 0.0),
974            spawn_rate: 50.0,
975            burst_count: 0,
976            particle_lifetime_min: 0.3,
977            particle_lifetime_max: 0.8,
978            initial_velocity_min: 5.0,
979            initial_velocity_max: 10.0,
980            velocity_spread: 0.8,
981            gravity: Vec3::new(0.0, -15.0, 0.0),
982            drag: 0.1,
983            size_start: 0.05,
984            size_end: 0.01,
985            color_gradient: ColorGradient::sparks(),
986            emissive_strength: 6.0,
987            enabled: true,
988            accumulated_spawn: 0.0,
989            one_shot: false,
990            has_fired: false,
991            turbulence_strength: 0.0,
992            turbulence_frequency: 1.0,
993            texture_index: 0,
994            size_curve: Vec::new(),
995            opacity_curve: Vec::new(),
996            death_sub_emitter: None,
997            trail_sub_emitter: None,
998            trail_spawn_interval: 0.0,
999        }
1000    }
1001}
1002
1003/// Runtime statistics for the particle system.
1004#[derive(Debug, Clone, Copy, Default)]
1005pub struct ParticleSystemStats {
1006    /// Currently alive particles across all emitters.
1007    pub active_particles: u32,
1008    /// Total particles spawned since startup.
1009    pub total_spawned: u64,
1010    /// Total particles that have reached end of life.
1011    pub total_died: u64,
1012}