1pub mod field_viz;
6
7use crate::render::postfx::{
22 bloom::BloomParams,
23 grain::GrainParams,
24 scanlines::ScanlineParams,
25 chromatic::ChromaticParams,
26 distortion::DistortionParams,
27 motion_blur::MotionBlurParams,
28 color_grade::ColorGradeParams,
29};
30use crate::math::springs::SpringDamper;
31
32#[derive(Debug, Clone)]
38pub enum EffectEvent {
39 CameraShake { trauma: f32 },
41 Explosion { power: f32, is_boss: bool },
43 BossEnter,
45 PlayerDeath,
47 LevelUp,
49 ChaosRift { entropy: f32 },
51 Heal { amount_fraction: f32 },
53 ColorFlash { r: f32, g: f32, b: f32, intensity: f32, duration: f32 },
55 Portal,
57 TimeSlow { factor: f32 },
59 TimeResume,
61 LightningStrike,
63 DisplayGlitch { intensity: f32, duration: f32 },
65 Reset,
67}
68
69#[derive(Debug, Clone)]
73struct EffectLayer {
74 kind: LayerKind,
75 age: f32,
76 duration: f32,
77 strength: f32,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
81enum LayerKind {
82 GrainFlash,
83 BloomSpike,
84 ChromaticBurst,
85 DistortionBurst,
86 ColorFlash { r: f32, g: f32, b: f32 },
87 VignetteCrush,
88 HueRainbow,
89 GlitchBurst,
90 WhiteFlash,
91}
92
93impl EffectLayer {
94 fn is_expired(&self) -> bool { self.age >= self.duration }
95 fn progress(&self) -> f32 { (self.age / self.duration).clamp(0.0, 1.0) }
96 fn intensity(&self) -> f32 {
97 let t = self.progress();
98 self.strength * (1.0 - t) * (1.0 - t)
100 }
101}
102
103pub struct EffectsController {
110 pub bloom: BloomParams,
112 pub grain: GrainParams,
113 pub scanlines: ScanlineParams,
114 pub chromatic: ChromaticParams,
115 pub distortion: DistortionParams,
116 pub motion_blur: MotionBlurParams,
117 pub color_grade: ColorGradeParams,
118
119 trauma: f32, entropy: f32, time_slow: f32, bloom_spring: SpringDamper,
126 grain_spring: SpringDamper,
127 chromatic_spring: SpringDamper,
128 vignette_spring: SpringDamper,
129 saturation_spring: SpringDamper,
130 brightness_spring: SpringDamper,
131
132 layers: Vec<EffectLayer>,
134
135 pub is_dead: bool,
137 pub boss_mode: bool,
138 pub chaos_rift_active: bool,
139 pub time_slow_active: bool,
140}
141
142impl EffectsController {
143 pub fn new() -> Self {
144 Self {
145 bloom: BloomParams::default(),
146 grain: GrainParams::default(),
147 scanlines: ScanlineParams::default(),
148 chromatic: ChromaticParams::none(),
149 distortion: DistortionParams::none(),
150 motion_blur: MotionBlurParams::default(),
151 color_grade: ColorGradeParams::default(),
152
153 trauma: 0.0,
154 entropy: 0.0,
155 time_slow: 1.0,
156
157 bloom_spring: SpringDamper::critical(1.0, 6.0),
158 grain_spring: SpringDamper::critical(0.0, 8.0),
159 chromatic_spring: SpringDamper::critical(0.0, 8.0),
160 vignette_spring: SpringDamper::critical(0.15, 5.0),
161 saturation_spring: SpringDamper::critical(1.0, 4.0),
162 brightness_spring: SpringDamper::critical(0.0, 6.0),
163
164 layers: Vec::new(),
165 is_dead: false,
166 boss_mode: false,
167 chaos_rift_active: false,
168 time_slow_active: false,
169 }
170 }
171
172 pub fn send(&mut self, event: EffectEvent) {
176 match event {
177 EffectEvent::CameraShake { trauma } => {
178 self.trauma = (self.trauma + trauma).clamp(0.0, 1.0);
179 }
180
181 EffectEvent::Explosion { power, is_boss } => {
182 let p = power.clamp(0.0, 1.0);
183 self.push_layer(EffectLayer {
184 kind: LayerKind::GrainFlash, age: 0.0,
185 duration: 0.3 + p * 0.4, strength: p * 0.8
186 });
187 self.push_layer(EffectLayer {
188 kind: LayerKind::BloomSpike, age: 0.0,
189 duration: 0.5 + p * 0.5, strength: p * 2.0
190 });
191 self.push_layer(EffectLayer {
192 kind: LayerKind::DistortionBurst, age: 0.0,
193 duration: 0.4 + p * 0.3, strength: p
194 });
195 if is_boss {
196 self.push_layer(EffectLayer {
197 kind: LayerKind::WhiteFlash, age: 0.0,
198 duration: 0.15, strength: 1.0
199 });
200 self.push_layer(EffectLayer {
201 kind: LayerKind::ChromaticBurst, age: 0.0,
202 duration: 0.6, strength: 1.0
203 });
204 }
205 self.trauma = (self.trauma + p * 0.5).min(1.0);
206 }
207
208 EffectEvent::BossEnter => {
209 self.boss_mode = true;
210 self.push_layer(EffectLayer {
211 kind: LayerKind::VignetteCrush, age: 0.0,
212 duration: 3.0, strength: 1.0
213 });
214 self.push_layer(EffectLayer {
215 kind: LayerKind::ChromaticBurst, age: 0.0,
216 duration: 1.5, strength: 0.8
217 });
218 self.saturation_spring.set_target(0.0);
219 }
220
221 EffectEvent::PlayerDeath => {
222 self.is_dead = true;
223 self.saturation_spring.set_target(0.0);
224 self.brightness_spring.set_target(-0.8);
225 self.vignette_spring.set_target(1.0);
226 }
227
228 EffectEvent::LevelUp => {
229 self.push_layer(EffectLayer {
230 kind: LayerKind::HueRainbow, age: 0.0,
231 duration: 2.0, strength: 1.0
232 });
233 self.push_layer(EffectLayer {
234 kind: LayerKind::BloomSpike, age: 0.0,
235 duration: 0.8, strength: 3.0
236 });
237 self.bloom_spring.set_target(3.0);
238 }
239
240 EffectEvent::ChaosRift { entropy } => {
241 self.entropy = entropy.clamp(0.0, 1.0);
242 self.chaos_rift_active = true;
243 }
244
245 EffectEvent::Heal { amount_fraction } => {
246 let g = amount_fraction.clamp(0.0, 1.0);
247 self.push_layer(EffectLayer {
248 kind: LayerKind::ColorFlash { r: 0.2, g: 1.0, b: 0.3 },
249 age: 0.0, duration: 0.5, strength: g * 0.6
250 });
251 self.push_layer(EffectLayer {
252 kind: LayerKind::BloomSpike, age: 0.0,
253 duration: 0.4, strength: g * 1.5
254 });
255 }
256
257 EffectEvent::ColorFlash { r, g, b, intensity, duration } => {
258 self.push_layer(EffectLayer {
259 kind: LayerKind::ColorFlash { r, g, b },
260 age: 0.0, duration, strength: intensity
261 });
262 }
263
264 EffectEvent::Portal => {
265 self.push_layer(EffectLayer {
266 kind: LayerKind::DistortionBurst, age: 0.0,
267 duration: 0.8, strength: 0.6
268 });
269 self.push_layer(EffectLayer {
270 kind: LayerKind::ChromaticBurst, age: 0.0,
271 duration: 0.5, strength: 0.5
272 });
273 }
274
275 EffectEvent::TimeSlow { factor } => {
276 self.time_slow_active = true;
277 self.time_slow = factor.clamp(0.05, 1.0);
278 self.saturation_spring.set_target(0.4);
279 self.motion_blur.scale = 0.6;
280 self.motion_blur.temporal = 0.4;
281 }
282
283 EffectEvent::TimeResume => {
284 self.time_slow_active = false;
285 self.time_slow = 1.0;
286 self.saturation_spring.set_target(1.0);
287 self.motion_blur = MotionBlurParams::default();
288 }
289
290 EffectEvent::LightningStrike => {
291 self.push_layer(EffectLayer {
292 kind: LayerKind::WhiteFlash, age: 0.0,
293 duration: 0.1, strength: 1.0
294 });
295 self.push_layer(EffectLayer {
296 kind: LayerKind::ChromaticBurst, age: 0.0,
297 duration: 0.3, strength: 1.0
298 });
299 self.trauma = (self.trauma + 0.4).min(1.0);
300 }
301
302 EffectEvent::DisplayGlitch { intensity, duration } => {
303 let i = intensity.clamp(0.0, 1.0);
304 self.push_layer(EffectLayer {
305 kind: LayerKind::GlitchBurst, age: 0.0,
306 duration, strength: i
307 });
308 self.push_layer(EffectLayer {
309 kind: LayerKind::ChromaticBurst, age: 0.0,
310 duration: duration * 0.7, strength: i * 0.8
311 });
312 }
313
314 EffectEvent::Reset => {
315 self.reset();
316 }
317 }
318 }
319
320 fn push_layer(&mut self, layer: EffectLayer) {
321 self.layers.push(layer);
322 }
323
324 pub fn tick(&mut self, dt: f32) {
330 self.trauma = (self.trauma - dt * 1.5).max(0.0);
332 let t2 = self.trauma * self.trauma;
333
334 if !self.chaos_rift_active {
336 self.entropy = (self.entropy - dt * 0.5).max(0.0);
337 }
338 self.chaos_rift_active = false; let mut grain_add = 0.0_f32;
342 let mut bloom_add = 0.0_f32;
343 let mut chromatic_add = 0.0_f32;
344 let mut distortion_add = 0.0_f32;
345 let mut vignette_add = 0.0_f32;
346 let mut brightness_add = 0.0_f32;
347 let mut hue_shift = 0.0_f32;
348
349 for layer in &mut self.layers {
350 layer.age += dt;
351 let intensity = layer.intensity();
352 match layer.kind {
353 LayerKind::GrainFlash => grain_add += intensity * 0.8,
354 LayerKind::BloomSpike => bloom_add += intensity * 2.0,
355 LayerKind::ChromaticBurst => chromatic_add += intensity * 0.025,
356 LayerKind::DistortionBurst => distortion_add += intensity * 0.05,
357 LayerKind::VignetteCrush => vignette_add += intensity * 0.7,
358 LayerKind::HueRainbow => hue_shift = layer.progress() * 360.0,
359 LayerKind::GlitchBurst => {
360 grain_add += intensity * 0.4;
361 chromatic_add += intensity * 0.02;
362 }
363 LayerKind::WhiteFlash => brightness_add += intensity * 1.5,
364 LayerKind::ColorFlash { .. } => {
365 brightness_add += intensity * 0.3;
366 }
367 }
368 }
369 self.layers.retain(|l| !l.is_expired());
370
371 chromatic_add += t2 * 0.015;
373 grain_add += t2 * 0.4;
374 bloom_add += t2 * 0.5;
375
376 let e = self.entropy;
378 distortion_add += e * 0.08;
379 chromatic_add += e * 0.02;
380 grain_add += e * 0.3;
381
382 self.bloom_spring.set_target(1.0 + bloom_add);
384 self.grain_spring.set_target(grain_add);
385 self.chromatic_spring.set_target(chromatic_add);
386 if !self.is_dead && !self.boss_mode {
387 self.saturation_spring.set_target(1.0 - e * 0.3);
388 self.vignette_spring.set_target(0.15 + vignette_add);
389 self.brightness_spring.set_target(brightness_add);
390 }
391
392 self.bloom_spring.tick(dt);
394 self.grain_spring.tick(dt);
395 self.chromatic_spring.tick(dt);
396 self.vignette_spring.tick(dt);
397 self.saturation_spring.tick(dt);
398 self.brightness_spring.tick(dt);
399
400 let bloom_target = self.bloom_spring.position.max(0.0);
402 self.bloom.threshold = (0.8 - (bloom_target - 1.0) * 0.2).clamp(0.0, 1.0);
403 self.bloom.intensity = bloom_target;
404
405 self.grain.intensity = self.grain_spring.position.max(0.0);
406 self.grain.enabled = self.grain.intensity > 0.001;
407
408 let chrom = self.chromatic_spring.position.max(0.0);
409 if chrom > 0.001 {
410 self.chromatic = ChromaticParams {
411 enabled: true,
412 red_offset: 0.002 + chrom,
413 blue_offset: 0.003 + chrom * 1.2,
414 green_offset: chrom * 0.1,
415 radial_scale: true,
416 tangential: t2 * 0.3 + e * 0.2,
417 spectrum_spread: (chrom * 10.0).min(0.8),
418 barrel_distortion: e * 0.04,
419 };
420 } else {
421 self.chromatic = ChromaticParams::none();
422 }
423
424 let dist = distortion_add;
425 if dist > 0.001 || e > 0.01 {
426 self.distortion.enabled = true;
427 self.distortion.scale = (dist + e * 0.5).min(3.0);
428 self.distortion.max_offset = (dist * 0.5 + e * 0.06).min(0.15);
429 self.distortion.chromatic_split = (dist * 2.0 + e * 0.4).min(1.0);
430 } else {
431 self.distortion.enabled = false;
432 }
433
434 self.color_grade.saturation = self.saturation_spring.position.clamp(0.0, 2.0);
435 self.color_grade.brightness = self.brightness_spring.position.clamp(-1.0, 2.0);
436 self.color_grade.vignette = self.vignette_spring.position.clamp(0.0, 1.0);
437 self.color_grade.hue_shift = hue_shift;
438
439 if self.boss_mode {
441 self.color_grade.contrast = 1.3;
442 } else {
443 self.color_grade.contrast = 1.0;
444 }
445
446 if self.time_slow_active {
448 self.color_grade.saturation = self.color_grade.saturation * 0.5;
449 }
450 }
451
452 pub fn reset(&mut self) {
456 self.trauma = 0.0;
457 self.entropy = 0.0;
458 self.time_slow = 1.0;
459 self.is_dead = false;
460 self.boss_mode = false;
461 self.chaos_rift_active = false;
462 self.time_slow_active = false;
463 self.layers.clear();
464
465 self.bloom = BloomParams::default();
466 self.grain = GrainParams::default();
467 self.scanlines = ScanlineParams::default();
468 self.chromatic = ChromaticParams::none();
469 self.distortion = DistortionParams::none();
470 self.motion_blur = MotionBlurParams::default();
471 self.color_grade = ColorGradeParams::default();
472
473 self.bloom_spring.teleport(1.0);
474 self.grain_spring.teleport(0.0);
475 self.chromatic_spring.teleport(0.0);
476 self.vignette_spring.teleport(0.15);
477 self.saturation_spring.teleport(1.0);
478 self.brightness_spring.teleport(0.0);
479 }
480
481 pub fn trauma(&self) -> f32 { self.trauma }
484 pub fn entropy(&self) -> f32 { self.entropy }
485 pub fn time_slow_factor(&self) -> f32 { self.time_slow }
486 pub fn active_layer_count(&self) -> usize { self.layers.len() }
487
488 pub fn has_active_effects(&self) -> bool {
490 !self.layers.is_empty() || self.trauma > 0.01 || self.entropy > 0.01
491 }
492
493 pub fn debug_summary(&self) -> String {
495 format!(
496 "trauma={:.2} entropy={:.2} layers={} bloom={:.2} grain={:.2} chrom={:.3} dist={} sat={:.2}",
497 self.trauma, self.entropy, self.layers.len(),
498 self.bloom.intensity, self.grain.intensity,
499 self.chromatic.red_offset,
500 self.distortion.enabled,
501 self.color_grade.saturation,
502 )
503 }
504}
505
506impl Default for EffectsController {
507 fn default() -> Self { Self::new() }
508}
509
510pub struct EffectPresets;
514
515impl EffectPresets {
516 pub fn boss_opening() -> Vec<EffectEvent> {
518 vec![
519 EffectEvent::BossEnter,
520 EffectEvent::CameraShake { trauma: 0.7 },
521 EffectEvent::DisplayGlitch { intensity: 0.5, duration: 0.4 },
522 ]
523 }
524
525 pub fn heavy_hit(damage_fraction: f32) -> Vec<EffectEvent> {
527 vec![
528 EffectEvent::CameraShake { trauma: damage_fraction * 0.8 },
529 EffectEvent::Explosion { power: damage_fraction * 0.5, is_boss: false },
530 EffectEvent::ColorFlash {
531 r: 1.0, g: 0.1, b: 0.1,
532 intensity: damage_fraction * 0.6,
533 duration: 0.3,
534 },
535 ]
536 }
537
538 pub fn aoe_explosion(power: f32) -> Vec<EffectEvent> {
540 vec![
541 EffectEvent::Explosion { power, is_boss: power > 0.8 },
542 EffectEvent::CameraShake { trauma: power * 0.6 },
543 ]
544 }
545
546 pub fn rift_opening(entropy: f32) -> Vec<EffectEvent> {
548 vec![
549 EffectEvent::ChaosRift { entropy },
550 EffectEvent::DisplayGlitch { intensity: entropy * 0.6, duration: 0.5 },
551 EffectEvent::Portal,
552 ]
553 }
554}
555
556#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn controller_smoke() {
564 let mut ctrl = EffectsController::new();
565 ctrl.send(EffectEvent::CameraShake { trauma: 0.5 });
566 ctrl.tick(0.016);
567 assert!(ctrl.trauma > 0.0, "trauma should be set");
568 }
569
570 #[test]
571 fn explosion_triggers_layers() {
572 let mut ctrl = EffectsController::new();
573 ctrl.send(EffectEvent::Explosion { power: 0.8, is_boss: false });
574 assert!(ctrl.active_layer_count() > 0, "explosion should create layers");
576 }
577
578 #[test]
579 fn layers_expire() {
580 let mut ctrl = EffectsController::new();
581 ctrl.send(EffectEvent::Explosion { power: 0.5, is_boss: false });
582 for _ in 0..120 {
584 ctrl.tick(0.016);
585 }
586 assert_eq!(ctrl.active_layer_count(), 0, "all layers should expire");
587 }
588
589 #[test]
590 fn reset_clears_everything() {
591 let mut ctrl = EffectsController::new();
592 ctrl.send(EffectEvent::Explosion { power: 1.0, is_boss: true });
593 ctrl.send(EffectEvent::BossEnter);
594 ctrl.send(EffectEvent::PlayerDeath);
595 ctrl.tick(0.016);
596 ctrl.send(EffectEvent::Reset);
597 ctrl.tick(0.0);
598 assert!(!ctrl.is_dead);
599 assert!(!ctrl.boss_mode);
600 assert_eq!(ctrl.active_layer_count(), 0);
601 }
602
603 #[test]
604 fn chaos_rift_activates_distortion() {
605 let mut ctrl = EffectsController::new();
606 ctrl.chaos_rift_active = true;
607 ctrl.entropy = 0.8;
608 ctrl.tick(0.016);
609 assert!(ctrl.distortion.enabled, "high entropy should enable distortion");
610 }
611
612 #[test]
613 fn trauma_decays() {
614 let mut ctrl = EffectsController::new();
615 ctrl.send(EffectEvent::CameraShake { trauma: 1.0 });
616 for _ in 0..60 {
617 ctrl.tick(0.016);
618 }
619 assert!(ctrl.trauma < 0.5, "trauma should decay over time: {}", ctrl.trauma);
620 }
621
622 #[test]
623 fn preset_boss_opening_generates_events() {
624 let events = EffectPresets::boss_opening();
625 assert!(!events.is_empty(), "boss opening should produce events");
626 let mut ctrl = EffectsController::new();
627 for e in events { ctrl.send(e); }
628 ctrl.tick(0.016);
629 assert!(ctrl.boss_mode, "boss mode should be set");
630 }
631
632 #[test]
633 fn time_slow_desaturates() {
634 let mut ctrl = EffectsController::new();
635 ctrl.send(EffectEvent::TimeSlow { factor: 0.2 });
636 for _ in 0..30 {
637 ctrl.tick(0.016);
638 }
639 assert!(
640 ctrl.color_grade.saturation < 0.8,
641 "time slow should reduce saturation: {}", ctrl.color_grade.saturation
642 );
643 }
644}