1use glam::{Vec3, Vec4};
5use super::emitter::{
6 EmitterConfig, EmitterShape, SpawnMode, SpawnCurve, VelocityMode,
7 ColorOverLifetime, SizeOverLifetime, ParticleTag, EmitterBuilder,
8 LodController, LodLevel,
9};
10
11pub trait EffectPreset {
15 fn name(&self) -> &'static str;
17 fn build_emitters(&self) -> Vec<EmitterConfig>;
19 fn duration(&self) -> Option<f32>;
21}
22
23#[derive(Debug, Clone)]
27pub struct ExplosionEffect {
28 pub radius: f32,
30 pub power: f32,
32 pub color_tint: Vec4,
34 pub smoke_trail: bool,
36 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 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 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 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 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#[derive(Debug, Clone)]
167pub struct FireEffect {
168 pub width: f32,
169 pub height: f32,
170 pub intensity: f32, 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#[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#[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#[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#[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#[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#[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 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 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#[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#[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
931pub 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 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}