Skip to main content

proof_engine/vfx/
effects.rs

1//! Pre-built VFX effect presets: explosion, fire, smoke, sparks, blood, magic, portal,
2//! lightning, water splash, dust cloud — each fully parameterised and self-contained.
3
4use glam::{Vec3, Vec4};
5use super::emitter::{
6    EmitterConfig, EmitterShape, SpawnMode, SpawnCurve, VelocityMode,
7    ColorOverLifetime, SizeOverLifetime, ParticleTag, EmitterBuilder,
8    LodController, LodLevel,
9};
10
11// ─── Effect preset trait ──────────────────────────────────────────────────────
12
13/// Common interface for all VFX presets.
14pub trait EffectPreset {
15    /// Human-readable name.
16    fn name(&self) -> &'static str;
17    /// Build all emitter configs needed for this effect.
18    fn build_emitters(&self) -> Vec<EmitterConfig>;
19    /// Suggested total duration in seconds; None = infinite/looping.
20    fn duration(&self) -> Option<f32>;
21}
22
23// ─── Explosion ────────────────────────────────────────────────────────────────
24
25/// A large explosion: fireball core, shockwave sparks, debris chunks, smoke ring.
26#[derive(Debug, Clone)]
27pub struct ExplosionEffect {
28    /// Blast radius in world units.
29    pub radius:        f32,
30    /// Power 0..1 — scales particle counts and intensity.
31    pub power:         f32,
32    /// Optional tint applied to the fireball colour.
33    pub color_tint:    Vec4,
34    /// Add dark smoke trail after the initial flash.
35    pub smoke_trail:   bool,
36    /// Number of debris chunks to launch.
37    pub debris_count:  u32,
38}
39
40impl Default for ExplosionEffect {
41    fn default() -> Self {
42        Self {
43            radius: 3.0, power: 1.0,
44            color_tint: Vec4::ONE,
45            smoke_trail: true,
46            debris_count: 16,
47        }
48    }
49}
50
51impl ExplosionEffect {
52    pub fn small()  -> Self { Self { radius: 1.2, power: 0.5, debris_count:  6, ..Default::default() } }
53    pub fn medium() -> Self { Self::default() }
54    pub fn large()  -> Self { Self { radius: 6.0, power: 1.0, debris_count: 32, ..Default::default() } }
55    pub fn nuclear()-> Self { Self { radius: 20.0, power: 1.0, debris_count: 64, smoke_trail: true, color_tint: Vec4::new(1.0, 0.9, 0.6, 1.0) } }
56}
57
58impl EffectPreset for ExplosionEffect {
59    fn name(&self) -> &'static str { "Explosion" }
60    fn duration(&self) -> Option<f32> { Some(3.0 + self.power * 2.0) }
61
62    fn build_emitters(&self) -> Vec<EmitterConfig> {
63        let r = self.radius;
64        let p = self.power;
65        let tint = self.color_tint;
66
67        // 1 — Fireball core (burst of fire particles)
68        let fireball = EmitterBuilder::new()
69            .shape(EmitterShape::Sphere { radius: r * 0.4, inner_radius: 0.0, hemisphere: false })
70            .mode(SpawnMode::Burst { count: (80.0 * p) as u32 })
71            .velocity(VelocityMode::Radial { speed_min: r * 0.8, speed_max: r * 2.5 })
72            .color(ColorOverLifetime { stops: vec![
73                (0.0, Vec4::new(1.0 * tint.x, 0.95 * tint.y, 0.4 * tint.z, 1.0)),
74                (0.2, Vec4::new(1.0 * tint.x, 0.5  * tint.y, 0.1 * tint.z, 0.9)),
75                (0.6, Vec4::new(0.4 * tint.x, 0.15 * tint.y, 0.0,           0.5)),
76                (1.0, Vec4::new(0.1,           0.1,           0.1,           0.0)),
77            ]})
78            .size_curve(SizeOverLifetime { stops: vec![(0.0, 0.0), (0.15, r * 0.6), (1.0, r * 1.2)] })
79            .size(r * 0.3, r * 0.8)
80            .lifetime(0.4 + p * 0.3, 0.9 + p * 0.6)
81            .max_particles(128)
82            .tag(ParticleTag::FIRE)
83            .build();
84
85        // 2 — Sparks
86        let sparks = EmitterBuilder::new()
87            .shape(EmitterShape::Sphere { radius: r * 0.1, inner_radius: 0.0, hemisphere: false })
88            .mode(SpawnMode::Burst { count: (60.0 * p) as u32 })
89            .velocity(VelocityMode::Radial { speed_min: r * 1.5, speed_max: r * 5.0 })
90            .color(ColorOverLifetime { stops: vec![
91                (0.0, Vec4::new(1.0, 0.9, 0.3, 1.0)),
92                (0.5, Vec4::new(1.0, 0.5, 0.1, 0.8)),
93                (1.0, Vec4::new(0.3, 0.1, 0.0, 0.0)),
94            ]})
95            .size_curve(SizeOverLifetime::shrink(r * 0.04))
96            .size(r * 0.01, r * 0.04)
97            .lifetime(0.3, 1.2)
98            .max_particles(96)
99            .tag(ParticleTag::SPARK)
100            .build();
101
102        // 3 — Smoke
103        let smoke = EmitterBuilder::new()
104            .shape(EmitterShape::Sphere { radius: r * 0.3, inner_radius: 0.0, hemisphere: true })
105            .mode(SpawnMode::BurstOverTime { count: (30.0 * p) as u32, duration: 0.5, emitted: 0 })
106            .velocity(VelocityMode::Directional {
107                direction: Vec3::Y,
108                speed_min: r * 0.3,
109                speed_max: r * 1.0,
110                spread_radians: 1.0,
111            })
112            .color(ColorOverLifetime::smoke())
113            .size_curve(SizeOverLifetime { stops: vec![(0.0, r * 0.4), (0.5, r * 1.5), (1.0, r * 2.5)] })
114            .size(r * 0.5, r * 1.2)
115            .lifetime(1.5, 3.5)
116            .max_particles(48)
117            .tag(ParticleTag::SMOKE)
118            .build();
119
120        // 4 — Debris chunks
121        let debris = EmitterBuilder::new()
122            .shape(EmitterShape::Sphere { radius: r * 0.2, inner_radius: 0.0, hemisphere: false })
123            .mode(SpawnMode::Burst { count: self.debris_count })
124            .velocity(VelocityMode::Radial { speed_min: r * 1.0, speed_max: r * 3.5 })
125            .color(ColorOverLifetime::constant(Vec4::new(0.25, 0.2, 0.15, 1.0)))
126            .size_curve(SizeOverLifetime::constant(r * 0.07))
127            .size(r * 0.04, r * 0.12)
128            .lifetime(0.8, 2.5)
129            .max_particles(64)
130            .tag(ParticleTag::DEBRIS)
131            .build();
132
133        let mut out = vec![fireball, sparks, smoke, debris];
134
135        if self.smoke_trail {
136            let trail_smoke = EmitterBuilder::new()
137                .shape(EmitterShape::Sphere { radius: r * 0.5, inner_radius: 0.0, hemisphere: true })
138                .mode(SpawnMode::BurstOverTime { count: (20.0 * p) as u32, duration: 2.0, emitted: 0 })
139                .velocity(VelocityMode::Directional {
140                    direction: Vec3::Y,
141                    speed_min: r * 0.1,
142                    speed_max: r * 0.5,
143                    spread_radians: 0.6,
144                })
145                .color(ColorOverLifetime { stops: vec![
146                    (0.0, Vec4::new(0.15, 0.12, 0.10, 0.0)),
147                    (0.1, Vec4::new(0.12, 0.10, 0.08, 0.6)),
148                    (1.0, Vec4::new(0.05, 0.05, 0.05, 0.0)),
149                ]})
150                .size_curve(SizeOverLifetime { stops: vec![(0.0, r * 0.6), (1.0, r * 2.0)] })
151                .size(r * 0.6, r * 1.4)
152                .lifetime(3.0, 6.0)
153                .max_particles(32)
154                .tag(ParticleTag::SMOKE)
155                .build();
156            out.push(trail_smoke);
157        }
158
159        out
160    }
161}
162
163// ─── Fire ─────────────────────────────────────────────────────────────────────
164
165/// Continuous fire effect with optional embers and base smoke.
166#[derive(Debug, Clone)]
167pub struct FireEffect {
168    pub width:         f32,
169    pub height:        f32,
170    pub intensity:     f32,    // 0.5 = small campfire, 1.0 = bonfire, 2.0 = inferno
171    pub color_inner:   Vec4,
172    pub color_outer:   Vec4,
173    pub embers:        bool,
174    pub base_smoke:    bool,
175}
176
177impl Default for FireEffect {
178    fn default() -> Self {
179        Self {
180            width: 0.8, height: 2.0, intensity: 1.0,
181            color_inner: Vec4::new(1.0, 0.95, 0.3, 1.0),
182            color_outer: Vec4::new(0.8, 0.2, 0.0, 1.0),
183            embers: true, base_smoke: true,
184        }
185    }
186}
187
188impl FireEffect {
189    pub fn campfire()  -> Self { Self { width: 0.5, height: 1.2, intensity: 0.6, embers: true,  base_smoke: true,  ..Default::default() } }
190    pub fn bonfire()   -> Self { Self { width: 1.5, height: 3.0, intensity: 1.5, embers: true,  base_smoke: true,  ..Default::default() } }
191    pub fn torch()     -> Self { Self { width: 0.2, height: 0.6, intensity: 0.4, embers: false, base_smoke: false, ..Default::default() } }
192    pub fn inferno()   -> Self { Self { width: 4.0, height: 6.0, intensity: 2.0, embers: true,  base_smoke: true,  ..Default::default() } }
193}
194
195impl EffectPreset for FireEffect {
196    fn name(&self) -> &'static str { "Fire" }
197    fn duration(&self) -> Option<f32> { None }
198
199    fn build_emitters(&self) -> Vec<EmitterConfig> {
200        let w = self.width;
201        let h = self.height;
202        let i = self.intensity;
203
204        let main_fire = EmitterBuilder::new()
205            .shape(EmitterShape::Disc { radius: w * 0.5, inner_radius: 0.0, arc_degrees: 360.0 })
206            .mode(SpawnMode::Continuous)
207            .curve(SpawnCurve::Constant(40.0 * i))
208            .velocity(VelocityMode::Directional {
209                direction: Vec3::Y,
210                speed_min: h * 0.4,
211                speed_max: h * 0.9,
212                spread_radians: 0.35,
213            })
214            .color(ColorOverLifetime::fire())
215            .size_curve(SizeOverLifetime::grow_shrink(w * 0.7))
216            .size(w * 0.2, w * 0.6)
217            .lifetime(0.4, 0.8 + h * 0.2)
218            .max_particles(256)
219            .tag(ParticleTag::FIRE)
220            .build();
221
222        let inner_glow = EmitterBuilder::new()
223            .shape(EmitterShape::Disc { radius: w * 0.25, inner_radius: 0.0, arc_degrees: 360.0 })
224            .mode(SpawnMode::Continuous)
225            .curve(SpawnCurve::Constant(20.0 * i))
226            .velocity(VelocityMode::Directional {
227                direction: Vec3::Y,
228                speed_min: h * 0.3,
229                speed_max: h * 0.7,
230                spread_radians: 0.2,
231            })
232            .color(ColorOverLifetime { stops: vec![
233                (0.0, self.color_inner),
234                (0.4, Vec4::new(1.0, 0.6, 0.1, 0.8)),
235                (1.0, Vec4::new(0.8, 0.1, 0.0, 0.0)),
236            ]})
237            .size_curve(SizeOverLifetime::grow_shrink(w * 0.4))
238            .size(w * 0.1, w * 0.35)
239            .lifetime(0.3, 0.6)
240            .max_particles(128)
241            .tag(ParticleTag::FIRE)
242            .build();
243
244        let mut out = vec![main_fire, inner_glow];
245
246        if self.embers {
247            let embers = EmitterBuilder::new()
248                .shape(EmitterShape::Disc { radius: w * 0.3, inner_radius: 0.0, arc_degrees: 360.0 })
249                .mode(SpawnMode::Continuous)
250                .curve(SpawnCurve::Constant(5.0 * i))
251                .velocity(VelocityMode::Directional {
252                    direction: Vec3::Y,
253                    speed_min: h * 0.5,
254                    speed_max: h * 1.5,
255                    spread_radians: 0.8,
256                })
257                .color(ColorOverLifetime { stops: vec![
258                    (0.0, Vec4::new(1.0, 0.8, 0.2, 1.0)),
259                    (0.7, Vec4::new(1.0, 0.3, 0.0, 0.5)),
260                    (1.0, Vec4::new(0.1, 0.0, 0.0, 0.0)),
261                ]})
262                .size_curve(SizeOverLifetime::shrink(w * 0.025))
263                .size(w * 0.01, w * 0.025)
264                .lifetime(1.0, 3.0)
265                .max_particles(64)
266                .tag(ParticleTag::SPARK)
267                .build();
268            out.push(embers);
269        }
270
271        if self.base_smoke {
272            let smoke = EmitterBuilder::new()
273                .shape(EmitterShape::Disc { radius: w * 0.4, inner_radius: 0.0, arc_degrees: 360.0 })
274                .mode(SpawnMode::Continuous)
275                .curve(SpawnCurve::Constant(6.0 * i))
276                .velocity(VelocityMode::Directional {
277                    direction: Vec3::Y,
278                    speed_min: h * 0.2,
279                    speed_max: h * 0.6,
280                    spread_radians: 0.5,
281                })
282                .color(ColorOverLifetime::smoke())
283                .size_curve(SizeOverLifetime { stops: vec![(0.0, w * 0.3), (1.0, w * 2.0)] })
284                .size(w * 0.4, w * 1.0)
285                .lifetime(2.0, 5.0)
286                .max_particles(48)
287                .tag(ParticleTag::SMOKE)
288                .build();
289            out.push(smoke);
290        }
291
292        out
293    }
294}
295
296// ─── Smoke ────────────────────────────────────────────────────────────────────
297
298#[derive(Debug, Clone)]
299pub struct SmokeEffect {
300    pub radius:        f32,
301    pub density:       f32,
302    pub rise_speed:    f32,
303    pub spread:        f32,
304    pub color_start:   Vec4,
305    pub color_end:     Vec4,
306    pub wind_offset:   Vec3,
307}
308
309impl Default for SmokeEffect {
310    fn default() -> Self {
311        Self {
312            radius: 0.5, density: 1.0, rise_speed: 1.5, spread: 0.4,
313            color_start: Vec4::new(0.6, 0.6, 0.6, 0.8),
314            color_end:   Vec4::new(0.2, 0.2, 0.2, 0.0),
315            wind_offset: Vec3::ZERO,
316        }
317    }
318}
319
320impl SmokeEffect {
321    pub fn thin_wisp()  -> Self { Self { radius: 0.1, density: 0.3, rise_speed: 0.8,  ..Default::default() } }
322    pub fn chimney()    -> Self { Self { radius: 0.4, density: 1.5, rise_speed: 1.2,  ..Default::default() } }
323    pub fn grenade()    -> Self { Self { radius: 1.5, density: 2.0, rise_speed: 0.5, spread: 0.8, ..Default::default() } }
324    pub fn poison_gas() -> Self { Self { radius: 2.0, density: 3.0, rise_speed: 0.2, spread: 1.2, color_start: Vec4::new(0.2, 0.7, 0.1, 0.9), color_end: Vec4::new(0.1, 0.4, 0.0, 0.0), ..Default::default() } }
325}
326
327impl EffectPreset for SmokeEffect {
328    fn name(&self) -> &'static str { "Smoke" }
329    fn duration(&self) -> Option<f32> { None }
330
331    fn build_emitters(&self) -> Vec<EmitterConfig> {
332        let base = EmitterBuilder::new()
333            .shape(EmitterShape::Disc { radius: self.radius, inner_radius: 0.0, arc_degrees: 360.0 })
334            .mode(SpawnMode::Continuous)
335            .curve(SpawnCurve::Constant(8.0 * self.density))
336            .velocity(VelocityMode::Directional {
337                direction: Vec3::Y + self.wind_offset,
338                speed_min: self.rise_speed * 0.5,
339                speed_max: self.rise_speed * 1.5,
340                spread_radians: self.spread,
341            })
342            .color(ColorOverLifetime { stops: vec![
343                (0.0, Vec4::new(self.color_start.x, self.color_start.y, self.color_start.z, 0.0)),
344                (0.05, self.color_start),
345                (0.7, Vec4::new(
346                    (self.color_start.x + self.color_end.x) * 0.5,
347                    (self.color_start.y + self.color_end.y) * 0.5,
348                    (self.color_start.z + self.color_end.z) * 0.5,
349                    (self.color_start.w + self.color_end.w) * 0.4,
350                )),
351                (1.0, self.color_end),
352            ]})
353            .size_curve(SizeOverLifetime { stops: vec![(0.0, self.radius * 0.3), (0.3, self.radius * 1.2), (1.0, self.radius * 3.0)] })
354            .size(self.radius * 0.5, self.radius * 1.5)
355            .lifetime(3.0 / self.density.max(0.1), 8.0)
356            .max_particles(96)
357            .tag(ParticleTag::SMOKE)
358            .build();
359        vec![base]
360    }
361}
362
363// ─── Sparks ───────────────────────────────────────────────────────────────────
364
365#[derive(Debug, Clone)]
366pub struct SparksEffect {
367    pub count:         u32,
368    pub speed_min:     f32,
369    pub speed_max:     f32,
370    pub color:         Vec4,
371    pub gravity_scale: f32,
372    pub trail_length:  f32,
373    pub continuous:    bool,
374    pub rate:          f32,
375}
376
377impl Default for SparksEffect {
378    fn default() -> Self {
379        Self {
380            count: 40, speed_min: 2.0, speed_max: 8.0,
381            color: Vec4::new(1.0, 0.8, 0.2, 1.0),
382            gravity_scale: 1.0, trail_length: 0.15,
383            continuous: false, rate: 30.0,
384        }
385    }
386}
387
388impl SparksEffect {
389    pub fn metal_grind() -> Self { Self { count: 80, speed_min: 3.0, speed_max: 10.0, color: Vec4::new(1.0, 0.95, 0.5, 1.0), ..Default::default() } }
390    pub fn electric()    -> Self { Self { count: 60, speed_min: 1.0, speed_max: 5.0,  color: Vec4::new(0.6, 0.8, 1.0, 1.0),  ..Default::default() } }
391    pub fn welding()     -> Self { Self { count: 120, speed_min: 1.5, speed_max: 4.0, color: Vec4::new(1.0, 1.0, 0.6, 1.0),  continuous: true, rate: 60.0, ..Default::default() } }
392}
393
394impl EffectPreset for SparksEffect {
395    fn name(&self) -> &'static str { "Sparks" }
396    fn duration(&self) -> Option<f32> { if self.continuous { None } else { Some(1.5) } }
397
398    fn build_emitters(&self) -> Vec<EmitterConfig> {
399        let mode = if self.continuous {
400            SpawnMode::Continuous
401        } else {
402            SpawnMode::Burst { count: self.count }
403        };
404        let curve = SpawnCurve::Constant(self.rate);
405
406        let sparks = EmitterBuilder::new()
407            .shape(EmitterShape::Point)
408            .mode(mode)
409            .curve(curve)
410            .velocity(VelocityMode::Random { speed_min: self.speed_min, speed_max: self.speed_max })
411            .color(ColorOverLifetime { stops: vec![
412                (0.0, self.color),
413                (0.6, Vec4::new(self.color.x, self.color.y * 0.5, 0.0, 0.8)),
414                (1.0, Vec4::new(0.2, 0.1, 0.0, 0.0)),
415            ]})
416            .size_curve(SizeOverLifetime::shrink(self.trail_length))
417            .size(0.01, 0.03)
418            .lifetime(0.2, 0.8)
419            .max_particles(256)
420            .tag(ParticleTag::SPARK)
421            .build();
422        vec![sparks]
423    }
424}
425
426// ─── Blood splatter ───────────────────────────────────────────────────────────
427
428#[derive(Debug, Clone)]
429pub struct BloodSplatterEffect {
430    pub count:         u32,
431    pub speed_min:     f32,
432    pub speed_max:     f32,
433    pub direction:     Vec3,
434    pub spread:        f32,
435    pub droplet_size:  f32,
436    pub impact_normal: Vec3,
437    pub mist:          bool,
438}
439
440impl Default for BloodSplatterEffect {
441    fn default() -> Self {
442        Self {
443            count: 30, speed_min: 1.5, speed_max: 5.0,
444            direction: Vec3::Y, spread: 1.2,
445            droplet_size: 0.04,
446            impact_normal: Vec3::Y,
447            mist: true,
448        }
449    }
450}
451
452impl BloodSplatterEffect {
453    pub fn light_wound() -> Self { Self { count: 10, speed_min: 0.5, speed_max: 2.0, ..Default::default() } }
454    pub fn heavy_hit()   -> Self { Self { count: 50, speed_min: 2.0, speed_max: 7.0, mist: true, ..Default::default() } }
455    pub fn arterial()    -> Self { Self { count: 80, speed_min: 3.0, speed_max: 9.0, direction: Vec3::new(0.5, 0.8, 0.0), spread: 0.4, mist: true, ..Default::default() } }
456}
457
458impl EffectPreset for BloodSplatterEffect {
459    fn name(&self) -> &'static str { "BloodSplatter" }
460    fn duration(&self) -> Option<f32> { Some(1.5) }
461
462    fn build_emitters(&self) -> Vec<EmitterConfig> {
463        let droplets = EmitterBuilder::new()
464            .shape(EmitterShape::Point)
465            .mode(SpawnMode::Burst { count: self.count })
466            .velocity(VelocityMode::Directional {
467                direction: self.direction,
468                speed_min: self.speed_min,
469                speed_max: self.speed_max,
470                spread_radians: self.spread,
471            })
472            .color(ColorOverLifetime { stops: vec![
473                (0.0, Vec4::new(0.7, 0.04, 0.04, 1.0)),
474                (0.5, Vec4::new(0.5, 0.02, 0.02, 0.9)),
475                (1.0, Vec4::new(0.3, 0.01, 0.01, 0.0)),
476            ]})
477            .size_curve(SizeOverLifetime::shrink(self.droplet_size))
478            .size(self.droplet_size * 0.4, self.droplet_size * 1.4)
479            .lifetime(0.3, 0.9)
480            .max_particles(128)
481            .tag(ParticleTag::BLOOD)
482            .build();
483
484        let mut out = vec![droplets];
485
486        if self.mist {
487            let mist = EmitterBuilder::new()
488                .shape(EmitterShape::Sphere { radius: 0.05, inner_radius: 0.0, hemisphere: false })
489                .mode(SpawnMode::Burst { count: self.count / 3 })
490                .velocity(VelocityMode::Random { speed_min: 0.2, speed_max: 1.5 })
491                .color(ColorOverLifetime { stops: vec![
492                    (0.0, Vec4::new(0.6, 0.05, 0.05, 0.6)),
493                    (1.0, Vec4::new(0.3, 0.02, 0.02, 0.0)),
494                ]})
495                .size_curve(SizeOverLifetime { stops: vec![(0.0, 0.0), (0.2, self.droplet_size * 0.8), (1.0, self.droplet_size * 0.2)] })
496                .size(0.01, self.droplet_size * 0.5)
497                .lifetime(0.5, 1.2)
498                .max_particles(64)
499                .tag(ParticleTag::BLOOD)
500                .build();
501            out.push(mist);
502        }
503
504        out
505    }
506}
507
508// ─── Magic aura ───────────────────────────────────────────────────────────────
509
510#[derive(Debug, Clone)]
511pub struct MagicAuraEffect {
512    pub radius:        f32,
513    pub color_inner:   Vec4,
514    pub color_outer:   Vec4,
515    pub orbit_speed:   f32,
516    pub particle_count: u32,
517    pub rune_sparks:   bool,
518    pub element:       MagicElement,
519}
520
521#[derive(Debug, Clone, Copy, PartialEq)]
522pub enum MagicElement {
523    Arcane,
524    Fire,
525    Ice,
526    Lightning,
527    Nature,
528    Shadow,
529    Holy,
530}
531
532impl MagicElement {
533    pub fn colors(self) -> (Vec4, Vec4) {
534        match self {
535            MagicElement::Arcane    => (Vec4::new(0.7, 0.2, 1.0, 1.0), Vec4::new(0.4, 0.0, 0.8, 0.0)),
536            MagicElement::Fire      => (Vec4::new(1.0, 0.5, 0.1, 1.0), Vec4::new(0.8, 0.1, 0.0, 0.0)),
537            MagicElement::Ice       => (Vec4::new(0.5, 0.9, 1.0, 1.0), Vec4::new(0.2, 0.6, 1.0, 0.0)),
538            MagicElement::Lightning => (Vec4::new(0.9, 0.9, 1.0, 1.0), Vec4::new(0.4, 0.4, 1.0, 0.0)),
539            MagicElement::Nature    => (Vec4::new(0.2, 1.0, 0.3, 1.0), Vec4::new(0.0, 0.6, 0.1, 0.0)),
540            MagicElement::Shadow    => (Vec4::new(0.2, 0.0, 0.3, 1.0), Vec4::new(0.05, 0.0, 0.1, 0.0)),
541            MagicElement::Holy      => (Vec4::new(1.0, 0.95, 0.6, 1.0), Vec4::new(1.0, 0.8, 0.2, 0.0)),
542        }
543    }
544}
545
546impl Default for MagicAuraEffect {
547    fn default() -> Self {
548        let (ci, co) = MagicElement::Arcane.colors();
549        Self { radius: 1.0, color_inner: ci, color_outer: co, orbit_speed: 2.0, particle_count: 48, rune_sparks: true, element: MagicElement::Arcane }
550    }
551}
552
553impl MagicAuraEffect {
554    pub fn for_element(element: MagicElement, radius: f32) -> Self {
555        let (ci, co) = element.colors();
556        Self { radius, color_inner: ci, color_outer: co, element, ..Default::default() }
557    }
558}
559
560impl EffectPreset for MagicAuraEffect {
561    fn name(&self) -> &'static str { "MagicAura" }
562    fn duration(&self) -> Option<f32> { None }
563
564    fn build_emitters(&self) -> Vec<EmitterConfig> {
565        let aura = EmitterBuilder::new()
566            .shape(EmitterShape::Torus { major_radius: self.radius, minor_radius: self.radius * 0.08 })
567            .mode(SpawnMode::Continuous)
568            .curve(SpawnCurve::Constant(self.particle_count as f32))
569            .velocity(VelocityMode::Orbital { tangent_speed: self.orbit_speed, upward_speed: 0.1 })
570            .color(ColorOverLifetime { stops: vec![
571                (0.0, self.color_inner),
572                (0.5, Vec4::new(
573                    (self.color_inner.x + self.color_outer.x) * 0.5,
574                    (self.color_inner.y + self.color_outer.y) * 0.5,
575                    (self.color_inner.z + self.color_outer.z) * 0.5,
576                    0.7,
577                )),
578                (1.0, self.color_outer),
579            ]})
580            .size_curve(SizeOverLifetime::grow_shrink(self.radius * 0.06))
581            .size(self.radius * 0.02, self.radius * 0.06)
582            .lifetime(0.4, 0.9)
583            .max_particles(128)
584            .tag(ParticleTag::MAGIC)
585            .build();
586
587        let glow = EmitterBuilder::new()
588            .shape(EmitterShape::Sphere { radius: self.radius * 0.9, inner_radius: self.radius * 0.6, hemisphere: false })
589            .mode(SpawnMode::Continuous)
590            .curve(SpawnCurve::Constant(15.0))
591            .velocity(VelocityMode::Radial { speed_min: 0.1, speed_max: 0.5 })
592            .color(ColorOverLifetime::two_stop(self.color_inner, Vec4::new(self.color_inner.x, self.color_inner.y, self.color_inner.z, 0.0)))
593            .size_curve(SizeOverLifetime::grow_shrink(self.radius * 0.12))
594            .size(self.radius * 0.04, self.radius * 0.1)
595            .lifetime(0.5, 1.2)
596            .max_particles(64)
597            .tag(ParticleTag::MAGIC)
598            .build();
599
600        let mut out = vec![aura, glow];
601
602        if self.rune_sparks {
603            let sparks = EmitterBuilder::new()
604                .shape(EmitterShape::Sphere { radius: self.radius * 1.1, inner_radius: self.radius * 0.95, hemisphere: false })
605                .mode(SpawnMode::Continuous)
606                .curve(SpawnCurve::Constant(8.0))
607                .velocity(VelocityMode::Radial { speed_min: 0.3, speed_max: 1.5 })
608                .color(ColorOverLifetime::two_stop(self.color_inner, Vec4::new(self.color_outer.x, self.color_outer.y, self.color_outer.z, 0.0)))
609                .size_curve(SizeOverLifetime::shrink(self.radius * 0.025))
610                .size(self.radius * 0.01, self.radius * 0.025)
611                .lifetime(0.3, 0.8)
612                .max_particles(32)
613                .tag(ParticleTag::MAGIC)
614                .build();
615            out.push(sparks);
616        }
617
618        out
619    }
620}
621
622// ─── Portal swirl ─────────────────────────────────────────────────────────────
623
624#[derive(Debug, Clone)]
625pub struct PortalSwirlEffect {
626    pub radius:       f32,
627    pub depth:        f32,
628    pub swirl_speed:  f32,
629    pub color_rim:    Vec4,
630    pub color_center: Vec4,
631    pub particle_density: f32,
632    pub inward:       bool,
633}
634
635impl Default for PortalSwirlEffect {
636    fn default() -> Self {
637        Self {
638            radius: 2.0, depth: 0.4, swirl_speed: 3.0,
639            color_rim:    Vec4::new(0.4, 0.6, 1.0, 1.0),
640            color_center: Vec4::new(0.1, 0.1, 0.4, 0.8),
641            particle_density: 1.0,
642            inward: true,
643        }
644    }
645}
646
647impl EffectPreset for PortalSwirlEffect {
648    fn name(&self) -> &'static str { "PortalSwirl" }
649    fn duration(&self) -> Option<f32> { None }
650
651    fn build_emitters(&self) -> Vec<EmitterConfig> {
652        let rim = EmitterBuilder::new()
653            .shape(EmitterShape::Torus { major_radius: self.radius, minor_radius: self.radius * 0.05 })
654            .mode(SpawnMode::Continuous)
655            .curve(SpawnCurve::Constant(60.0 * self.particle_density))
656            .velocity(VelocityMode::Orbital {
657                tangent_speed: self.swirl_speed * if self.inward { 1.0 } else { -1.0 },
658                upward_speed: -self.swirl_speed * 0.5,
659            })
660            .color(ColorOverLifetime::two_stop(self.color_rim, Vec4::new(self.color_center.x, self.color_center.y, self.color_center.z, 0.0)))
661            .size_curve(SizeOverLifetime::shrink(self.radius * 0.04))
662            .size(self.radius * 0.01, self.radius * 0.04)
663            .lifetime(0.5, 1.0)
664            .max_particles(256)
665            .tag(ParticleTag::MAGIC)
666            .build();
667
668        let interior = EmitterBuilder::new()
669            .shape(EmitterShape::Disc { radius: self.radius * 0.9, inner_radius: 0.0, arc_degrees: 360.0 })
670            .mode(SpawnMode::Continuous)
671            .curve(SpawnCurve::Constant(20.0 * self.particle_density))
672            .velocity(VelocityMode::Orbital { tangent_speed: self.swirl_speed * 0.6, upward_speed: -0.3 })
673            .color(ColorOverLifetime { stops: vec![
674                (0.0, self.color_rim),
675                (0.5, self.color_center),
676                (1.0, Vec4::new(self.color_center.x, self.color_center.y, self.color_center.z, 0.0)),
677            ]})
678            .size_curve(SizeOverLifetime::grow_shrink(self.radius * 0.08))
679            .size(self.radius * 0.02, self.radius * 0.06)
680            .lifetime(0.4, 0.9)
681            .max_particles(128)
682            .tag(ParticleTag::MAGIC)
683            .build();
684
685        vec![rim, interior]
686    }
687}
688
689// ─── Lightning arc ────────────────────────────────────────────────────────────
690
691#[derive(Debug, Clone)]
692pub struct LightningArcEffect {
693    pub start:         Vec3,
694    pub end:           Vec3,
695    pub branch_count:  u32,
696    pub color:         Vec4,
697    pub glow_color:    Vec4,
698    pub intensity:     f32,
699    pub strike_count:  u32,
700}
701
702impl Default for LightningArcEffect {
703    fn default() -> Self {
704        Self {
705            start: Vec3::ZERO, end: Vec3::new(0.0, 5.0, 0.0),
706            branch_count: 4,
707            color: Vec4::new(0.85, 0.9, 1.0, 1.0),
708            glow_color: Vec4::new(0.4, 0.5, 1.0, 0.6),
709            intensity: 1.0,
710            strike_count: 3,
711        }
712    }
713}
714
715impl EffectPreset for LightningArcEffect {
716    fn name(&self) -> &'static str { "LightningArc" }
717    fn duration(&self) -> Option<f32> { Some(0.5 * self.strike_count as f32) }
718
719    fn build_emitters(&self) -> Vec<EmitterConfig> {
720        let _mid = (self.start + self.end) * 0.5;
721        let len = (self.end - self.start).length();
722
723        // Ionisation mist along the arc path
724        let mist = EmitterBuilder::new()
725            .shape(EmitterShape::Line { start: self.start, end: self.end, endpoints_only: false })
726            .mode(SpawnMode::BurstOverTime { count: (40 * self.strike_count), duration: self.duration().unwrap_or(1.0) * 0.5, emitted: 0 })
727            .velocity(VelocityMode::Random { speed_min: 0.1, speed_max: 0.5 })
728            .color(ColorOverLifetime::two_stop(
729                Vec4::new(self.glow_color.x, self.glow_color.y, self.glow_color.z, 0.5),
730                Vec4::new(self.glow_color.x, self.glow_color.y, self.glow_color.z, 0.0),
731            ))
732            .size_curve(SizeOverLifetime::grow_shrink(len * 0.08))
733            .size(len * 0.03, len * 0.08)
734            .lifetime(0.15, 0.4)
735            .max_particles(128)
736            .tag(ParticleTag::ENERGY)
737            .build();
738
739        // Spark discharge at endpoints
740        let endpoint_sparks = EmitterBuilder::new()
741            .shape(EmitterShape::Sphere { radius: 0.05, inner_radius: 0.0, hemisphere: false })
742            .mode(SpawnMode::Burst { count: 20 * self.strike_count })
743            .velocity(VelocityMode::Radial { speed_min: 0.5, speed_max: 3.0 })
744            .color(ColorOverLifetime::two_stop(self.color, Vec4::new(self.color.x, self.color.y, self.color.z, 0.0)))
745            .size_curve(SizeOverLifetime::shrink(0.04))
746            .size(0.01, 0.03)
747            .lifetime(0.1, 0.4)
748            .max_particles(64)
749            .tag(ParticleTag::SPARK)
750            .build();
751
752        vec![mist, endpoint_sparks]
753    }
754}
755
756// ─── Water splash ─────────────────────────────────────────────────────────────
757
758#[derive(Debug, Clone)]
759pub struct WaterSplashEffect {
760    pub radius:        f32,
761    pub impact_speed:  f32,
762    pub droplet_count: u32,
763    pub color:         Vec4,
764    pub foam:          bool,
765    pub mist:          bool,
766}
767
768impl Default for WaterSplashEffect {
769    fn default() -> Self {
770        Self {
771            radius: 1.0, impact_speed: 5.0, droplet_count: 40,
772            color: Vec4::new(0.6, 0.8, 1.0, 0.85),
773            foam: true, mist: true,
774        }
775    }
776}
777
778impl WaterSplashEffect {
779    pub fn raindrop()  -> Self { Self { radius: 0.15, impact_speed: 2.0, droplet_count: 8,  foam: false, mist: false, ..Default::default() } }
780    pub fn large_rock()-> Self { Self { radius: 2.5,  impact_speed: 8.0, droplet_count: 80, foam: true,  mist: true,  ..Default::default() } }
781}
782
783impl EffectPreset for WaterSplashEffect {
784    fn name(&self) -> &'static str { "WaterSplash" }
785    fn duration(&self) -> Option<f32> { Some(1.5) }
786
787    fn build_emitters(&self) -> Vec<EmitterConfig> {
788        let droplets = EmitterBuilder::new()
789            .shape(EmitterShape::Disc { radius: self.radius * 0.3, inner_radius: 0.0, arc_degrees: 360.0 })
790            .mode(SpawnMode::Burst { count: self.droplet_count })
791            .velocity(VelocityMode::Directional {
792                direction: Vec3::Y,
793                speed_min: self.impact_speed * 0.4,
794                speed_max: self.impact_speed * 1.2,
795                spread_radians: 1.1,
796            })
797            .color(ColorOverLifetime { stops: vec![
798                (0.0, self.color),
799                (0.6, Vec4::new(self.color.x, self.color.y, self.color.z, self.color.w * 0.5)),
800                (1.0, Vec4::new(self.color.x, self.color.y, self.color.z, 0.0)),
801            ]})
802            .size_curve(SizeOverLifetime::grow_shrink(self.radius * 0.08))
803            .size(self.radius * 0.02, self.radius * 0.08)
804            .lifetime(0.4, 1.0)
805            .max_particles(128)
806            .tag(ParticleTag::WATER)
807            .build();
808
809        let mut out = vec![droplets];
810
811        if self.foam {
812            let foam = EmitterBuilder::new()
813                .shape(EmitterShape::Disc { radius: self.radius * 0.5, inner_radius: 0.0, arc_degrees: 360.0 })
814                .mode(SpawnMode::Burst { count: self.droplet_count / 3 })
815                .velocity(VelocityMode::Directional {
816                    direction: Vec3::Y,
817                    speed_min: 0.1,
818                    speed_max: 0.5,
819                    spread_radians: 1.4,
820                })
821                .color(ColorOverLifetime { stops: vec![
822                    (0.0, Vec4::new(0.9, 0.95, 1.0, 0.0)),
823                    (0.1, Vec4::new(0.9, 0.95, 1.0, 0.8)),
824                    (1.0, Vec4::new(0.8, 0.9, 1.0, 0.0)),
825                ]})
826                .size_curve(SizeOverLifetime { stops: vec![(0.0, 0.0), (0.2, self.radius * 0.3), (1.0, self.radius * 0.8)] })
827                .size(self.radius * 0.1, self.radius * 0.3)
828                .lifetime(0.8, 1.5)
829                .max_particles(48)
830                .tag(ParticleTag::WATER)
831                .build();
832            out.push(foam);
833        }
834
835        if self.mist {
836            let mist = EmitterBuilder::new()
837                .shape(EmitterShape::Disc { radius: self.radius * 0.4, inner_radius: 0.0, arc_degrees: 360.0 })
838                .mode(SpawnMode::Burst { count: self.droplet_count / 4 })
839                .velocity(VelocityMode::Directional {
840                    direction: Vec3::Y,
841                    speed_min: self.impact_speed * 0.1,
842                    speed_max: self.impact_speed * 0.4,
843                    spread_radians: 1.0,
844                })
845                .color(ColorOverLifetime { stops: vec![
846                    (0.0, Vec4::new(0.8, 0.9, 1.0, 0.0)),
847                    (0.05, Vec4::new(0.8, 0.9, 1.0, 0.5)),
848                    (1.0,  Vec4::new(0.8, 0.9, 1.0, 0.0)),
849                ]})
850                .size_curve(SizeOverLifetime { stops: vec![(0.0, 0.0), (0.3, self.radius * 0.5), (1.0, self.radius * 1.5)] })
851                .size(self.radius * 0.2, self.radius * 0.5)
852                .lifetime(0.5, 1.2)
853                .max_particles(32)
854                .tag(ParticleTag::WATER)
855                .build();
856            out.push(mist);
857        }
858
859        out
860    }
861}
862
863// ─── Dust cloud ───────────────────────────────────────────────────────────────
864
865#[derive(Debug, Clone)]
866pub struct DustCloudEffect {
867    pub radius:       f32,
868    pub height:       f32,
869    pub density:      f32,
870    pub color:        Vec4,
871    pub wind:         Vec3,
872    pub continuous:   bool,
873}
874
875impl Default for DustCloudEffect {
876    fn default() -> Self {
877        Self {
878            radius: 1.5, height: 1.0, density: 1.0,
879            color: Vec4::new(0.75, 0.65, 0.5, 0.6),
880            wind: Vec3::ZERO,
881            continuous: false,
882        }
883    }
884}
885
886impl DustCloudEffect {
887    pub fn footstep() -> Self { Self { radius: 0.3, height: 0.2, density: 0.5, ..Default::default() } }
888    pub fn landing()  -> Self { Self { radius: 1.2, height: 0.4, density: 1.5, ..Default::default() } }
889    pub fn sandstorm()-> Self { Self { radius: 8.0, height: 3.0, density: 3.0, continuous: true, wind: Vec3::new(3.0, 0.0, 0.5), ..Default::default() } }
890}
891
892impl EffectPreset for DustCloudEffect {
893    fn name(&self) -> &'static str { "DustCloud" }
894    fn duration(&self) -> Option<f32> { if self.continuous { None } else { Some(2.5) } }
895
896    fn build_emitters(&self) -> Vec<EmitterConfig> {
897        let count = (30.0 * self.density) as u32;
898        let mode  = if self.continuous {
899            SpawnMode::Continuous
900        } else {
901            SpawnMode::BurstOverTime { count, duration: 0.3, emitted: 0 }
902        };
903        let curve = SpawnCurve::Constant(count as f32);
904
905        let main = EmitterBuilder::new()
906            .shape(EmitterShape::Disc { radius: self.radius, inner_radius: 0.0, arc_degrees: 360.0 })
907            .mode(mode)
908            .curve(curve)
909            .velocity(VelocityMode::Directional {
910                direction: Vec3::Y + self.wind * 0.3,
911                speed_min: 0.2,
912                speed_max: self.height * 1.5,
913                spread_radians: 1.3,
914            })
915            .color(ColorOverLifetime { stops: vec![
916                (0.0, Vec4::new(self.color.x, self.color.y, self.color.z, 0.0)),
917                (0.08, self.color),
918                (0.5, Vec4::new(self.color.x * 0.8, self.color.y * 0.8, self.color.z * 0.8, self.color.w * 0.6)),
919                (1.0, Vec4::new(self.color.x, self.color.y, self.color.z, 0.0)),
920            ]})
921            .size_curve(SizeOverLifetime { stops: vec![(0.0, 0.0), (0.2, self.radius * 0.7), (1.0, self.radius * 2.0)] })
922            .size(self.radius * 0.3, self.radius * 0.8)
923            .lifetime(1.5, 3.5)
924            .max_particles(128)
925            .tag(ParticleTag::DUST)
926            .build();
927        vec![main]
928    }
929}
930
931// ─── Effect registry ──────────────────────────────────────────────────────────
932
933/// Named handles to pre-built effects so they can be referenced by string key.
934pub struct EffectRegistry {
935    entries: Vec<(&'static str, Vec<EmitterConfig>)>,
936}
937
938impl EffectRegistry {
939    pub fn new() -> Self {
940        let mut reg = Self { entries: Vec::new() };
941
942        // Register all built-in presets
943        reg.register(ExplosionEffect::medium());
944        reg.register(FireEffect::default());
945        reg.register(SmokeEffect::default());
946        reg.register(SparksEffect::default());
947        reg.register(BloodSplatterEffect::default());
948        reg.register(MagicAuraEffect::default());
949        reg.register(PortalSwirlEffect::default());
950        reg.register(LightningArcEffect::default());
951        reg.register(WaterSplashEffect::default());
952        reg.register(DustCloudEffect::default());
953
954        reg
955    }
956
957    pub fn register<E: EffectPreset>(&mut self, effect: E) {
958        self.entries.push((effect.name(), effect.build_emitters()));
959    }
960
961    pub fn get(&self, name: &str) -> Option<&Vec<EmitterConfig>> {
962        self.entries.iter().find(|(n, _)| *n == name).map(|(_, cfgs)| cfgs)
963    }
964
965    pub fn names(&self) -> impl Iterator<Item = &'static str> + '_ {
966        self.entries.iter().map(|(n, _)| *n)
967    }
968}
969
970impl Default for EffectRegistry {
971    fn default() -> Self { Self::new() }
972}