1pub mod emitter;
6pub mod effects;
7pub mod forces;
8
9pub use emitter::{
10 Emitter, EmitterConfig, EmitterPool, EmitterBuilder, EmitterShape,
11 SpawnMode, SpawnCurve, VelocityMode, ColorOverLifetime, SizeOverLifetime,
12 LodController, LodLevel, EmitterTransformAnim, TransformKeyframe,
13 Particle, ParticleTag, lcg_f32, lcg_range, lcg_next,
14};
15pub use effects::{
16 EffectPreset, EffectRegistry,
17 ExplosionEffect, FireEffect, SmokeEffect, SparksEffect, BloodSplatterEffect,
18 MagicAuraEffect, MagicElement, PortalSwirlEffect, LightningArcEffect,
19 WaterSplashEffect, DustCloudEffect,
20};
21pub use forces::{
22 ForceField, ForceFieldId, ForceFieldKind, ForceFieldWorld, ForceComposite,
23 ForceBlendMode, ForcePresets, FalloffMode, TagMask,
24 GravityWell, VortexField, TurbulenceField, WindZone,
25 AttractorRepulsor, AttractorMode, DragField, BuoyancyField,
26 ForceDebugSample,
27};
28
29use glam::{Vec2, Vec3, Vec4, Mat4, Quat};
30use std::collections::HashMap;
31
32#[derive(Debug, Clone)]
36pub struct Decal {
37 pub id: u32,
38 pub position: Vec3,
39 pub normal: Vec3, pub rotation: f32, pub size: Vec2, pub uv_offset: Vec2, pub uv_scale: Vec2, pub color: Vec4,
45 pub opacity: f32,
46 pub lifetime: f32, pub fade_out_time: f32, pub age: f32,
49 pub category: DecalCategory,
50 pub layer: u32,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum DecalCategory {
55 BulletHole,
56 BloodSplat,
57 ScorchMark,
58 Explosion,
59 Footprint,
60 Graffiti,
61 Crack,
62 Water,
63 Custom(u32),
64}
65
66impl Decal {
67 pub fn new(id: u32, pos: Vec3, normal: Vec3) -> Self {
68 Self {
69 id, position: pos, normal,
70 rotation: 0.0,
71 size: Vec2::new(0.2, 0.2),
72 uv_offset: Vec2::ZERO,
73 uv_scale: Vec2::ONE,
74 color: Vec4::ONE,
75 opacity: 1.0,
76 lifetime: -1.0,
77 fade_out_time: 2.0,
78 age: 0.0,
79 category: DecalCategory::Custom(0),
80 layer: 0,
81 }
82 }
83
84 pub fn with_lifetime(mut self, secs: f32) -> Self { self.lifetime = secs; self }
85 pub fn with_color(mut self, c: Vec4) -> Self { self.color = c; self }
86 pub fn with_size(mut self, s: Vec2) -> Self { self.size = s; self }
87 pub fn with_rotation(mut self, r: f32) -> Self { self.rotation = r; self }
88
89 pub fn projection_matrix(&self) -> Mat4 {
91 let forward = self.normal;
92 let up = if forward.dot(Vec3::Y).abs() < 0.99 { Vec3::Y } else { Vec3::Z };
93 let right = up.cross(forward).normalize_or_zero();
94 let up2 = forward.cross(right).normalize_or_zero();
95 let rot = Mat4::from_cols(
96 (right * self.size.x).extend(0.0),
97 (up2 * self.size.y).extend(0.0),
98 forward.extend(0.0),
99 self.position.extend(1.0),
100 );
101 rot
102 }
103
104 pub fn is_expired(&self) -> bool {
105 self.lifetime > 0.0 && self.age >= self.lifetime
106 }
107
108 pub fn current_opacity(&self) -> f32 {
109 if self.lifetime <= 0.0 { return self.opacity; }
110 let remaining = (self.lifetime - self.age).max(0.0);
111 if remaining < self.fade_out_time {
112 self.opacity * (remaining / self.fade_out_time.max(0.001))
113 } else {
114 self.opacity
115 }
116 }
117
118 pub fn tick(&mut self, dt: f32) { self.age += dt; }
119}
120
121pub struct DecalPool {
124 decals: Vec<Decal>,
125 next_id: u32,
126 max_decals: usize,
127}
128
129impl DecalPool {
130 pub fn new(max_decals: usize) -> Self {
131 Self { decals: Vec::with_capacity(max_decals), next_id: 1, max_decals }
132 }
133
134 pub fn spawn(&mut self, pos: Vec3, normal: Vec3) -> u32 {
135 let id = self.next_id;
136 self.next_id += 1;
137 if self.decals.len() >= self.max_decals {
138 self.decals.remove(0);
140 }
141 self.decals.push(Decal::new(id, pos, normal));
142 id
143 }
144
145 pub fn spawn_configured(&mut self, mut d: Decal) -> u32 {
146 let id = self.next_id;
147 self.next_id += 1;
148 d.id = id;
149 if self.decals.len() >= self.max_decals {
150 self.decals.remove(0);
151 }
152 self.decals.push(d);
153 id
154 }
155
156 pub fn get_mut(&mut self, id: u32) -> Option<&mut Decal> {
157 self.decals.iter_mut().find(|d| d.id == id)
158 }
159
160 pub fn tick(&mut self, dt: f32) {
161 for d in &mut self.decals { d.tick(dt); }
162 self.decals.retain(|d| !d.is_expired());
163 }
164
165 pub fn visible_decals(&self) -> &[Decal] { &self.decals }
166
167 pub fn clear_category(&mut self, cat: DecalCategory) {
168 self.decals.retain(|d| d.category != cat);
169 }
170
171 pub fn count(&self) -> usize { self.decals.len() }
172}
173
174#[derive(Debug, Clone)]
178pub struct TrailPoint {
179 pub position: Vec3,
180 pub width: f32,
181 pub color: Vec4,
182 pub time: f32,
183}
184
185#[derive(Debug, Clone)]
187pub struct Trail {
188 pub id: u32,
189 pub points: Vec<TrailPoint>,
190 pub max_points: usize,
191 pub lifetime: f32, pub min_distance: f32, pub width_start: f32,
194 pub width_end: f32,
195 pub color_start: Vec4,
196 pub color_end: Vec4,
197 pub time: f32,
198 pub enabled: bool,
199 pub smooth: bool,
200}
201
202impl Trail {
203 pub fn new(id: u32) -> Self {
204 Self {
205 id, points: Vec::new(), max_points: 64,
206 lifetime: 1.5, min_distance: 0.05,
207 width_start: 0.1, width_end: 0.0,
208 color_start: Vec4::ONE,
209 color_end: Vec4::new(1.0, 1.0, 1.0, 0.0),
210 time: 0.0, enabled: true, smooth: true,
211 }
212 }
213
214 pub fn emit(&mut self, pos: Vec3) {
215 if let Some(last) = self.points.last() {
216 if (pos - last.position).length() < self.min_distance { return; }
217 }
218 if self.points.len() >= self.max_points {
219 self.points.remove(0);
220 }
221 self.points.push(TrailPoint {
222 position: pos,
223 width: self.width_start,
224 color: self.color_start,
225 time: 0.0,
226 });
227 }
228
229 pub fn tick(&mut self, dt: f32) {
230 self.time += dt;
231 for p in &mut self.points { p.time += dt; }
232
233 self.points.retain(|p| p.time < self.lifetime);
235 for p in &mut self.points {
236 let t = p.time / self.lifetime.max(0.001);
237 p.width = self.width_start + t * (self.width_end - self.width_start);
238 let r = self.color_start.x + t * (self.color_end.x - self.color_start.x);
240 let g = self.color_start.y + t * (self.color_end.y - self.color_start.y);
241 let b = self.color_start.z + t * (self.color_end.z - self.color_start.z);
242 let a = self.color_start.w + t * (self.color_end.w - self.color_start.w);
243 p.color = Vec4::new(r, g, b, a);
244 }
245 }
246
247 pub fn is_empty(&self) -> bool { self.points.is_empty() }
248
249 pub fn generate_ribbon(&self) -> Vec<(Vec3, Vec2, Vec4)> {
251 if self.points.len() < 2 { return Vec::new(); }
252 let mut verts = Vec::new();
253 let total = self.points.len();
254
255 for i in 0..total {
256 let p = &self.points[i];
257 let fwd = if i + 1 < total {
258 (self.points[i + 1].position - p.position).normalize_or_zero()
259 } else if i > 0 {
260 (p.position - self.points[i - 1].position).normalize_or_zero()
261 } else {
262 Vec3::X
263 };
264
265 let up = Vec3::Y;
266 let right = fwd.cross(up).normalize_or_zero();
267 let half_w = p.width * 0.5;
268 let u = i as f32 / (total - 1) as f32;
269
270 verts.push((p.position - right * half_w, Vec2::new(u, 0.0), p.color));
271 verts.push((p.position + right * half_w, Vec2::new(u, 1.0), p.color));
272 }
273 verts
274 }
275}
276
277pub struct TrailManager {
280 trails: HashMap<u32, Trail>,
281 next_id: u32,
282}
283
284impl TrailManager {
285 pub fn new() -> Self {
286 Self { trails: HashMap::new(), next_id: 1 }
287 }
288
289 pub fn create(&mut self) -> u32 {
290 let id = self.next_id; self.next_id += 1;
291 self.trails.insert(id, Trail::new(id));
292 id
293 }
294
295 pub fn get(&self, id: u32) -> Option<&Trail> { self.trails.get(&id) }
296 pub fn get_mut(&mut self, id: u32) -> Option<&mut Trail> { self.trails.get_mut(&id) }
297
298 pub fn emit(&mut self, id: u32, pos: Vec3) {
299 if let Some(t) = self.trails.get_mut(&id) { t.emit(pos); }
300 }
301
302 pub fn remove(&mut self, id: u32) { self.trails.remove(&id); }
303
304 pub fn tick(&mut self, dt: f32) {
305 for t in self.trails.values_mut() { t.tick(dt); }
306 }
307
308 pub fn all_trails(&self) -> impl Iterator<Item = &Trail> {
309 self.trails.values()
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum ImpactType {
317 Bullet,
318 Explosion,
319 Slash,
320 Magic,
321 Fire,
322 Ice,
323 Lightning,
324 Custom(u32),
325}
326
327#[derive(Debug, Clone)]
329pub struct ImpactEffect {
330 pub id: u32,
331 pub position: Vec3,
332 pub normal: Vec3,
333 pub kind: ImpactType,
334 pub power: f32, pub age: f32,
336 pub duration: f32,
337 pub color: Vec4,
338 pub spawned_decal_id: Option<u32>,
339 pub spawned_trail_id: Option<u32>,
340}
341
342impl ImpactEffect {
343 pub fn new(id: u32, pos: Vec3, normal: Vec3, kind: ImpactType, power: f32) -> Self {
344 let (color, duration) = match kind {
345 ImpactType::Fire => (Vec4::new(1.0, 0.5, 0.1, 1.0), 0.8),
346 ImpactType::Ice => (Vec4::new(0.5, 0.8, 1.0, 1.0), 0.6),
347 ImpactType::Lightning => (Vec4::new(0.9, 0.9, 0.2, 1.0), 0.3),
348 ImpactType::Magic => (Vec4::new(0.8, 0.2, 1.0, 1.0), 0.7),
349 ImpactType::Explosion => (Vec4::new(1.0, 0.6, 0.1, 1.0), 1.0),
350 _ => (Vec4::ONE, 0.4),
351 };
352 Self {
353 id, position: pos, normal, kind, power,
354 age: 0.0, duration, color,
355 spawned_decal_id: None,
356 spawned_trail_id: None,
357 }
358 }
359
360 pub fn is_done(&self) -> bool { self.age >= self.duration }
361 pub fn progress(&self) -> f32 { (self.age / self.duration.max(0.001)).min(1.0) }
362 pub fn tick(&mut self, dt: f32) { self.age += dt; }
363}
364
365#[derive(Debug, Clone)]
369pub enum VfxCommand {
370 SpawnDecal { pos: Vec3, normal: Vec3, category: DecalCategory, size: Vec2, color: Vec4, lifetime: f32 },
371 SpawnImpact { pos: Vec3, normal: Vec3, kind: ImpactType, power: f32 },
372 SpawnTrail { attach_to: u64, color_start: Vec4, color_end: Vec4, width: f32, lifetime: f32 },
373 RemoveTrail { trail_id: u32 },
374 Shockwave { center: Vec3, radius: f32, thickness: f32, speed: f32, color: Vec4 },
375 ScreenFlash { color: Vec4, duration: f32 },
376}
377
378#[derive(Debug, Clone)]
382pub struct Shockwave {
383 pub id: u32,
384 pub center: Vec3,
385 pub radius: f32, pub max_radius: f32,
387 pub thickness: f32,
388 pub speed: f32, pub color: Vec4,
390 pub age: f32,
391 pub duration: f32,
392}
393
394impl Shockwave {
395 pub fn new(id: u32, center: Vec3, max_radius: f32, speed: f32, color: Vec4) -> Self {
396 Self {
397 id, center,
398 radius: 0.0,
399 max_radius,
400 thickness: max_radius * 0.1,
401 speed, color, age: 0.0,
402 duration: max_radius / speed.max(0.001),
403 }
404 }
405
406 pub fn tick(&mut self, dt: f32) {
407 self.age += dt;
408 self.radius = (self.speed * self.age).min(self.max_radius);
409 }
410
411 pub fn alpha(&self) -> f32 {
412 let t = self.age / self.duration.max(0.001);
413 (1.0 - t * t).max(0.0)
414 }
415
416 pub fn is_done(&self) -> bool { self.radius >= self.max_radius }
417}
418
419#[derive(Debug, Clone)]
423pub struct ScreenFlash {
424 pub color: Vec4,
425 pub duration: f32,
426 pub age: f32,
427}
428
429impl ScreenFlash {
430 pub fn new(color: Vec4, duration: f32) -> Self {
431 Self { color, duration, age: 0.0 }
432 }
433
434 pub fn alpha(&self) -> f32 {
435 let t = self.age / self.duration.max(0.001);
436 self.color.w * (1.0 - t).max(0.0)
437 }
438
439 pub fn tick(&mut self, dt: f32) { self.age += dt; }
440 pub fn is_done(&self) -> bool { self.age >= self.duration }
441}
442
443pub struct VfxManager {
447 pub decals: DecalPool,
448 pub trails: TrailManager,
449 pub impacts: Vec<ImpactEffect>,
450 pub shockwaves: Vec<Shockwave>,
451 pub flashes: Vec<ScreenFlash>,
452 next_effect_id: u32,
453 pub command_queue: Vec<VfxCommand>,
454}
455
456impl VfxManager {
457 pub fn new() -> Self {
458 Self {
459 decals: DecalPool::new(512),
460 trails: TrailManager::new(),
461 impacts: Vec::new(),
462 shockwaves: Vec::new(),
463 flashes: Vec::new(),
464 next_effect_id: 1,
465 command_queue: Vec::new(),
466 }
467 }
468
469 fn alloc_id(&mut self) -> u32 {
470 let id = self.next_effect_id; self.next_effect_id += 1; id
471 }
472
473 pub fn queue(&mut self, cmd: VfxCommand) {
474 self.command_queue.push(cmd);
475 }
476
477 pub fn flush_commands(&mut self) {
478 let cmds = std::mem::take(&mut self.command_queue);
479 for cmd in cmds {
480 self.execute(cmd);
481 }
482 }
483
484 pub fn execute(&mut self, cmd: VfxCommand) {
485 match cmd {
486 VfxCommand::SpawnDecal { pos, normal, category, size, color, lifetime } => {
487 let mut d = Decal::new(0, pos, normal);
488 d.category = category;
489 d.size = size;
490 d.color = color;
491 d.lifetime = lifetime;
492 self.decals.spawn_configured(d);
493 }
494 VfxCommand::SpawnImpact { pos, normal, kind, power } => {
495 let id = self.alloc_id();
496 self.impacts.push(ImpactEffect::new(id, pos, normal, kind, power));
497 }
498 VfxCommand::SpawnTrail { attach_to: _, color_start, color_end, width, lifetime } => {
499 let tid = self.trails.create();
500 if let Some(t) = self.trails.get_mut(tid) {
501 t.color_start = color_start;
502 t.color_end = color_end;
503 t.width_start = width;
504 t.lifetime = lifetime;
505 }
506 }
507 VfxCommand::RemoveTrail { trail_id } => {
508 self.trails.remove(trail_id);
509 }
510 VfxCommand::Shockwave { center, radius, thickness, speed, color } => {
511 let id = self.alloc_id();
512 let mut sw = Shockwave::new(id, center, radius, speed, color);
513 sw.thickness = thickness;
514 self.shockwaves.push(sw);
515 }
516 VfxCommand::ScreenFlash { color, duration } => {
517 self.flashes.push(ScreenFlash::new(color, duration));
518 }
519 }
520 }
521
522 pub fn tick(&mut self, dt: f32) {
523 self.flush_commands();
524 self.decals.tick(dt);
525 self.trails.tick(dt);
526 for e in &mut self.impacts { e.tick(dt); }
527 for s in &mut self.shockwaves { s.tick(dt); }
528 for f in &mut self.flashes { f.tick(dt); }
529 self.impacts.retain(|e| !e.is_done());
530 self.shockwaves.retain(|s| !s.is_done());
531 self.flashes.retain(|f| !f.is_done());
532 }
533
534 pub fn screen_flash_color(&self) -> Vec4 {
536 let mut out = Vec4::ZERO;
537 for f in &self.flashes {
538 let a = f.alpha();
539 out += Vec4::new(f.color.x * a, f.color.y * a, f.color.z * a, a);
540 }
541 out
542 }
543}
544
545#[derive(Debug, Clone)]
549pub struct HitFlash {
550 pub intensity: f32,
551 pub decay: f32, }
553
554impl HitFlash {
555 pub fn new() -> Self { Self { intensity: 0.0, decay: 8.0 } }
556
557 pub fn trigger(&mut self, amount: f32) {
558 self.intensity = (self.intensity + amount).min(1.0);
559 }
560
561 pub fn tick(&mut self, dt: f32) {
562 self.intensity = (self.intensity - self.decay * dt).max(0.0);
563 }
564
565 pub fn value(&self) -> f32 { self.intensity }
566}
567
568#[derive(Debug, Clone)]
572pub struct DissolveEffect {
573 pub threshold: f32, pub edge_width: f32,
575 pub edge_color: Vec4,
576 pub speed: f32,
577 pub dissolving: bool,
578 pub reassembling: bool,
579}
580
581impl DissolveEffect {
582 pub fn new() -> Self {
583 Self {
584 threshold: 0.0, edge_width: 0.05,
585 edge_color: Vec4::new(1.0, 0.5, 0.0, 1.0),
586 speed: 1.0, dissolving: false, reassembling: false,
587 }
588 }
589
590 pub fn start_dissolve(&mut self) { self.dissolving = true; self.reassembling = false; }
591 pub fn start_reassemble(&mut self) { self.reassembling = true; self.dissolving = false; }
592
593 pub fn tick(&mut self, dt: f32) {
594 if self.dissolving {
595 self.threshold = (self.threshold + self.speed * dt).min(1.0);
596 if self.threshold >= 1.0 { self.dissolving = false; }
597 } else if self.reassembling {
598 self.threshold = (self.threshold - self.speed * dt).max(0.0);
599 if self.threshold <= 0.0 { self.reassembling = false; }
600 }
601 }
602
603 pub fn is_fully_dissolved(&self) -> bool { self.threshold >= 1.0 }
604 pub fn is_fully_visible(&self) -> bool { self.threshold <= 0.0 }
605}
606
607#[derive(Debug, Clone)]
611pub struct OutlineEffect {
612 pub color: Vec4,
613 pub width: f32, pub enabled: bool,
615 pub pulse: bool,
616 pub pulse_speed: f32,
617 pub pulse_min: f32,
618 pub pulse_max: f32,
619 time: f32,
620}
621
622impl OutlineEffect {
623 pub fn new(color: Vec4, width: f32) -> Self {
624 Self { color, width, enabled: true, pulse: false, pulse_speed: 2.0, pulse_min: 0.5, pulse_max: 1.0, time: 0.0 }
625 }
626
627 pub fn tick(&mut self, dt: f32) { self.time += dt; }
628
629 pub fn current_width(&self) -> f32 {
630 if !self.pulse { return self.width; }
631 let t = (self.time * self.pulse_speed * std::f32::consts::TAU).sin() * 0.5 + 0.5;
632 let w = self.pulse_min + t * (self.pulse_max - self.pulse_min);
633 self.width * w
634 }
635}
636
637#[derive(Debug, Clone)]
641pub struct ElectricArc {
642 pub id: u32,
643 pub start: Vec3,
644 pub end: Vec3,
645 pub segments: u32,
646 pub jitter: f32, pub color: Vec4,
648 pub width: f32,
649 pub lifetime: f32,
650 pub age: f32,
651 pub flicker: bool,
652 pub visible: bool,
653 pub seed: u32,
654}
655
656impl ElectricArc {
657 pub fn new(id: u32, start: Vec3, end: Vec3) -> Self {
658 Self {
659 id, start, end, segments: 12, jitter: 0.3,
660 color: Vec4::new(0.7, 0.8, 1.0, 0.9),
661 width: 0.03, lifetime: 0.2, age: 0.0, flicker: true, visible: true, seed: id,
662 }
663 }
664
665 pub fn tick(&mut self, dt: f32) { self.age += dt; }
666 pub fn is_done(&self) -> bool { self.age >= self.lifetime }
667 pub fn alpha(&self) -> f32 { (1.0 - self.age / self.lifetime.max(0.001)).max(0.0) }
668
669 pub fn generate_points(&self) -> Vec<Vec3> {
671 let mut rng_state = self.seed.wrapping_mul(2654435761).wrapping_add(self.age.to_bits());
672 let next_f = |s: &mut u32| -> f32 {
673 *s = s.wrapping_mul(1664525).wrapping_add(1013904223);
674 (*s as f32 / u32::MAX as f32) * 2.0 - 1.0
675 };
676
677 let n = self.segments as usize;
678 let mut pts = Vec::with_capacity(n + 2);
679 pts.push(self.start);
680
681 for i in 1..=n {
682 let t = i as f32 / (n + 1) as f32;
683 let base = self.start + (self.end - self.start) * t;
684 let perpendicular = {
685 let dir = (self.end - self.start).normalize_or_zero();
686 let up = if dir.dot(Vec3::Y).abs() < 0.9 { Vec3::Y } else { Vec3::Z };
687 let right = dir.cross(up).normalize_or_zero();
688 let up2 = dir.cross(right).normalize_or_zero();
689 right * next_f(&mut rng_state) + up2 * next_f(&mut rng_state)
690 };
691 pts.push(base + perpendicular * self.jitter);
692 }
693
694 pts.push(self.end);
695 pts
696 }
697}
698
699impl VfxManager {
702 pub fn bullet_impact(&mut self, pos: Vec3, normal: Vec3, material: BulletMaterial) {
704 let (color, sparks) = match material {
705 BulletMaterial::Metal => (Vec4::new(1.0, 0.8, 0.3, 1.0), true),
706 BulletMaterial::Stone => (Vec4::new(0.7, 0.6, 0.5, 1.0), false),
707 BulletMaterial::Wood => (Vec4::new(0.6, 0.4, 0.2, 1.0), false),
708 BulletMaterial::Flesh => (Vec4::new(0.8, 0.1, 0.1, 1.0), false),
709 BulletMaterial::Glass => (Vec4::new(0.8, 0.9, 1.0, 0.7), false),
710 BulletMaterial::Energy => (Vec4::new(0.5, 0.3, 1.0, 1.0), true),
711 };
712
713 self.queue(VfxCommand::SpawnDecal {
714 pos, normal,
715 category: DecalCategory::BulletHole,
716 size: Vec2::splat(0.05 + 0.02 * (if sparks { 1.0 } else { 0.0 })),
717 color, lifetime: 30.0,
718 });
719 self.queue(VfxCommand::SpawnImpact { pos, normal, kind: ImpactType::Bullet, power: 0.5 });
720 }
721
722 pub fn explosion(&mut self, center: Vec3, radius: f32, power: f32) {
724 self.queue(VfxCommand::SpawnDecal {
725 pos: center - Vec3::Y * 0.01,
726 normal: Vec3::Y,
727 category: DecalCategory::Explosion,
728 size: Vec2::splat(radius * 0.8),
729 color: Vec4::new(0.3, 0.2, 0.1, 0.8),
730 lifetime: 60.0,
731 });
732 self.queue(VfxCommand::SpawnImpact {
733 pos: center, normal: Vec3::Y,
734 kind: ImpactType::Explosion, power,
735 });
736 self.queue(VfxCommand::Shockwave {
737 center, radius: radius * 1.5, thickness: radius * 0.15,
738 speed: radius * 3.0, color: Vec4::new(1.0, 0.8, 0.5, 0.6),
739 });
740 self.queue(VfxCommand::ScreenFlash {
741 color: Vec4::new(1.0, 0.9, 0.7, power * 0.7),
742 duration: 0.15 + power * 0.1,
743 });
744 }
745
746 pub fn magic_impact(&mut self, pos: Vec3, color: Vec4, radius: f32) {
748 self.queue(VfxCommand::SpawnImpact { pos, normal: Vec3::Y, kind: ImpactType::Magic, power: 0.8 });
749 self.queue(VfxCommand::Shockwave {
750 center: pos, radius, thickness: radius * 0.08,
751 speed: radius * 4.0, color,
752 });
753 }
754}
755
756#[derive(Debug, Clone, Copy)]
758pub enum BulletMaterial {
759 Metal, Stone, Wood, Flesh, Glass, Energy,
760}
761
762#[derive(Debug, Clone)]
766pub struct BurstDescriptor {
767 pub origin: Vec3,
768 pub direction: Vec3,
769 pub spread: f32, pub count: u32,
771 pub speed_min: f32,
772 pub speed_max: f32,
773 pub size_min: f32,
774 pub size_max: f32,
775 pub lifetime_min: f32,
776 pub lifetime_max: f32,
777 pub color: Vec4,
778 pub gravity: Vec3,
779}
780
781impl BurstDescriptor {
782 pub fn explosion_sparks(origin: Vec3) -> Self {
783 Self {
784 origin, direction: Vec3::Y, spread: std::f32::consts::PI,
785 count: 24, speed_min: 1.5, speed_max: 4.0,
786 size_min: 0.02, size_max: 0.06,
787 lifetime_min: 0.3, lifetime_max: 0.9,
788 color: Vec4::new(1.0, 0.6, 0.1, 1.0),
789 gravity: Vec3::new(0.0, -9.8, 0.0),
790 }
791 }
792
793 pub fn magic_burst(origin: Vec3, color: Vec4) -> Self {
794 Self {
795 origin, direction: Vec3::Y, spread: std::f32::consts::PI,
796 count: 32, speed_min: 0.5, speed_max: 2.0,
797 size_min: 0.03, size_max: 0.08,
798 lifetime_min: 0.5, lifetime_max: 1.5,
799 color, gravity: Vec3::ZERO,
800 }
801 }
802}
803
804impl Default for VfxManager {
807 fn default() -> Self { Self::new() }
808}
809
810impl Default for TrailManager {
811 fn default() -> Self { Self::new() }
812}
813
814impl Default for HitFlash {
815 fn default() -> Self { Self::new() }
816}
817
818impl Default for DissolveEffect {
819 fn default() -> Self { Self::new() }
820}