1#![allow(clippy::missing_const_for_fn, clippy::suboptimal_flops)]
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Default)]
18pub struct ScreenShake {
19 intensity: f32,
21 duration: f32,
23 offset_x: f32,
25 offset_y: f32,
27 seed: u64,
29}
30
31impl ScreenShake {
32 #[must_use]
34 pub const fn new() -> Self {
35 Self {
36 intensity: 0.0,
37 duration: 0.0,
38 offset_x: 0.0,
39 offset_y: 0.0,
40 seed: 12345,
41 }
42 }
43
44 pub fn trigger(&mut self, intensity: f32, duration: f32) {
51 if intensity > self.intensity {
53 self.intensity = intensity;
54 self.duration = duration;
55 }
56 }
57
58 pub fn update(&mut self, dt: f32) -> (f32, f32) {
68 if self.duration <= 0.0 {
69 self.offset_x = 0.0;
70 self.offset_y = 0.0;
71 return (0.0, 0.0);
72 }
73
74 self.duration -= dt;
75
76 let decay = (self.duration * 10.0).min(1.0);
78 let current_intensity = self.intensity * decay;
79
80 self.seed ^= self.seed << 13;
82 self.seed ^= self.seed >> 7;
83 self.seed ^= self.seed << 17;
84
85 let rand_x = (self.seed as f32 / u64::MAX as f32) * 2.0 - 1.0;
86
87 self.seed ^= self.seed << 13;
88 self.seed ^= self.seed >> 7;
89 self.seed ^= self.seed << 17;
90
91 let rand_y = (self.seed as f32 / u64::MAX as f32) * 2.0 - 1.0;
92
93 self.offset_x = rand_x * current_intensity;
94 self.offset_y = rand_y * current_intensity;
95
96 (self.offset_x, self.offset_y)
97 }
98
99 #[must_use]
101 pub fn is_active(&self) -> bool {
102 self.duration > 0.0
103 }
104
105 #[must_use]
107 pub const fn offset(&self) -> (f32, f32) {
108 (self.offset_x, self.offset_y)
109 }
110
111 pub fn reset(&mut self) {
113 self.intensity = 0.0;
114 self.duration = 0.0;
115 self.offset_x = 0.0;
116 self.offset_y = 0.0;
117 }
118}
119
120#[derive(Debug, Clone, Copy, Default)]
122pub struct TrailPoint {
123 pub x: f32,
125 pub y: f32,
127 pub age: f32,
129 pub active: bool,
131}
132
133#[derive(Debug, Clone)]
135pub struct BallTrail {
136 points: Vec<TrailPoint>,
138 write_index: usize,
140 time_since_last: f32,
142 interval: f32,
144 max_age: f32,
146}
147
148impl Default for BallTrail {
149 fn default() -> Self {
150 Self::new(10, 0.016, 0.15)
151 }
152}
153
154impl BallTrail {
155 #[must_use]
163 pub fn new(max_points: usize, interval: f32, max_age: f32) -> Self {
164 Self {
165 points: vec![TrailPoint::default(); max_points],
166 write_index: 0,
167 time_since_last: 0.0,
168 interval,
169 max_age,
170 }
171 }
172
173 pub fn update(&mut self, x: f32, y: f32, dt: f32) {
181 for point in &mut self.points {
183 if point.active {
184 point.age += dt;
185 if point.age > self.max_age {
186 point.active = false;
187 }
188 }
189 }
190
191 self.time_since_last += dt;
193 if self.time_since_last >= self.interval {
194 self.time_since_last = 0.0;
195
196 self.points[self.write_index] = TrailPoint {
197 x,
198 y,
199 age: 0.0,
200 active: true,
201 };
202
203 self.write_index = (self.write_index + 1) % self.points.len();
204 }
205 }
206
207 #[must_use]
211 pub fn get_points(&self) -> Vec<(f32, f32, f32)> {
212 self.points
213 .iter()
214 .filter(|p| p.active)
215 .map(|p| {
216 let alpha = 1.0 - (p.age / self.max_age).min(1.0);
217 (p.x, p.y, alpha)
218 })
219 .collect()
220 }
221
222 pub fn clear(&mut self) {
224 for point in &mut self.points {
225 point.active = false;
226 }
227 self.time_since_last = 0.0;
228 }
229
230 #[must_use]
232 pub fn active_count(&self) -> usize {
233 self.points.iter().filter(|p| p.active).count()
234 }
235}
236
237#[derive(Debug, Clone, Default)]
239pub struct HitFlash {
240 duration: f32,
242 intensity: f32,
244 right_paddle: bool,
246}
247
248impl HitFlash {
249 #[must_use]
251 pub const fn new() -> Self {
252 Self {
253 duration: 0.0,
254 intensity: 0.0,
255 right_paddle: false,
256 }
257 }
258
259 pub fn trigger(&mut self, right_paddle: bool, intensity: f32, duration: f32) {
267 self.right_paddle = right_paddle;
268 self.intensity = intensity;
269 self.duration = duration;
270 }
271
272 pub fn update(&mut self, dt: f32) -> (bool, bool, f32) {
282 if self.duration <= 0.0 {
283 return (false, false, 0.0);
284 }
285
286 self.duration -= dt;
287
288 let current_intensity = self.intensity * (self.duration * 20.0).min(1.0);
290
291 (true, self.right_paddle, current_intensity)
292 }
293
294 #[must_use]
296 pub fn is_active(&self) -> bool {
297 self.duration > 0.0
298 }
299
300 pub fn reset(&mut self) {
302 self.duration = 0.0;
303 self.intensity = 0.0;
304 }
305
306 #[must_use]
312 pub fn flash_state(&self) -> (bool, bool, f32) {
313 if self.duration <= 0.0 {
314 return (false, false, 0.0);
315 }
316 let current_intensity = self.intensity * (self.duration * 20.0).min(1.0);
317 if self.right_paddle {
318 (false, true, current_intensity)
319 } else {
320 (true, false, current_intensity)
321 }
322 }
323}
324
325#[derive(Debug, Clone, Copy, Default)]
327pub struct Particle {
328 pub x: f32,
330 pub y: f32,
332 pub vx: f32,
334 pub vy: f32,
336 pub lifetime: f32,
338 pub initial_lifetime: f32,
340 pub size: f32,
342 pub color: u32,
344 pub active: bool,
346}
347
348impl Particle {
349 #[must_use]
351 pub const fn new(
352 x: f32,
353 y: f32,
354 vx: f32,
355 vy: f32,
356 lifetime: f32,
357 size: f32,
358 color: u32,
359 ) -> Self {
360 Self {
361 x,
362 y,
363 vx,
364 vy,
365 lifetime,
366 initial_lifetime: lifetime,
367 size,
368 color,
369 active: true,
370 }
371 }
372
373 pub fn update(&mut self, dt: f32) -> bool {
379 if !self.active {
380 return false;
381 }
382
383 self.lifetime -= dt;
384 if self.lifetime <= 0.0 {
385 self.active = false;
386 return false;
387 }
388
389 self.x += self.vx * dt;
391 self.y += self.vy * dt;
392
393 self.vy += 200.0 * dt;
395
396 let life_ratio = self.lifetime / self.initial_lifetime;
398 self.size *= 0.99 + 0.01 * life_ratio;
399
400 true
401 }
402
403 #[must_use]
405 pub fn alpha(&self) -> f32 {
406 if self.initial_lifetime <= 0.0 {
407 return 0.0;
408 }
409 (self.lifetime / self.initial_lifetime).clamp(0.0, 1.0)
410 }
411
412 #[must_use]
414 pub const fn rgb(&self) -> (f32, f32, f32) {
415 let r = ((self.color >> 16) & 0xFF) as f32 / 255.0;
416 let g = ((self.color >> 8) & 0xFF) as f32 / 255.0;
417 let b = (self.color & 0xFF) as f32 / 255.0;
418 (r, g, b)
419 }
420}
421
422#[derive(Debug, Clone)]
424pub struct ParticleSystem {
425 particles: Vec<Particle>,
427 write_index: usize,
429 seed: u64,
431}
432
433impl Default for ParticleSystem {
434 fn default() -> Self {
435 Self::new(200) }
437}
438
439impl ParticleSystem {
440 #[must_use]
442 pub fn new(pool_size: usize) -> Self {
443 Self {
444 particles: vec![Particle::default(); pool_size],
445 write_index: 0,
446 seed: 42,
447 }
448 }
449
450 fn random(&mut self) -> f32 {
452 self.seed ^= self.seed << 13;
453 self.seed ^= self.seed >> 7;
454 self.seed ^= self.seed << 17;
455 (self.seed as f32) / (u64::MAX as f32)
456 }
457
458 fn random_signed(&mut self) -> f32 {
460 self.random() * 2.0 - 1.0
461 }
462
463 #[allow(clippy::too_many_arguments)]
475 pub fn spawn(
476 &mut self,
477 x: f32,
478 y: f32,
479 count: usize,
480 speed: f32,
481 lifetime: f32,
482 size: f32,
483 color: u32,
484 ) {
485 for _ in 0..count {
486 let angle = self.random() * core::f32::consts::TAU;
488 let speed_var = speed * (0.5 + self.random() * 0.5);
489 let vx = angle.cos() * speed_var;
490 let vy = angle.sin() * speed_var;
491
492 let life_var = lifetime * (0.7 + self.random() * 0.3);
494 let size_var = size * (0.5 + self.random() * 0.5);
495
496 self.particles[self.write_index] =
497 Particle::new(x, y, vx, vy, life_var, size_var, color);
498 self.write_index = (self.write_index + 1) % self.particles.len();
499 }
500 }
501
502 #[allow(clippy::too_many_arguments)]
517 pub fn spawn_directional(
518 &mut self,
519 x: f32,
520 y: f32,
521 direction_x: f32,
522 direction_y: f32,
523 spread: f32,
524 count: usize,
525 speed: f32,
526 lifetime: f32,
527 size: f32,
528 color: u32,
529 ) {
530 let base_angle = direction_y.atan2(direction_x);
531
532 for _ in 0..count {
533 let angle = base_angle + self.random_signed() * spread;
535 let speed_var = speed * (0.5 + self.random() * 0.5);
536 let vx = angle.cos() * speed_var;
537 let vy = angle.sin() * speed_var;
538
539 let life_var = lifetime * (0.7 + self.random() * 0.3);
541 let size_var = size * (0.5 + self.random() * 0.5);
542
543 self.particles[self.write_index] =
544 Particle::new(x, y, vx, vy, life_var, size_var, color);
545 self.write_index = (self.write_index + 1) % self.particles.len();
546 }
547 }
548
549 pub fn update(&mut self, dt: f32) {
551 for particle in &mut self.particles {
552 let _ = particle.update(dt);
553 }
554 }
555
556 #[must_use]
558 pub fn get_active(&self) -> Vec<&Particle> {
559 self.particles.iter().filter(|p| p.active).collect()
560 }
561
562 #[must_use]
564 pub fn active_count(&self) -> usize {
565 self.particles.iter().filter(|p| p.active).count()
566 }
567
568 pub fn clear(&mut self) {
570 for particle in &mut self.particles {
571 particle.active = false;
572 }
573 }
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
578pub struct ScorePopup {
579 pub x: f32,
581 pub y: f32,
583 pub text: String,
585 pub duration: f32,
587 pub start_y: f32,
589}
590
591impl ScorePopup {
592 #[must_use]
594 pub fn new(x: f32, y: f32, text: &str, duration: f32) -> Self {
595 Self {
596 x,
597 y,
598 text: text.to_string(),
599 duration,
600 start_y: y,
601 }
602 }
603
604 pub fn update(&mut self, dt: f32) -> bool {
610 if self.duration <= 0.0 {
611 return false;
612 }
613
614 self.duration -= dt;
615
616 self.y -= 50.0 * dt;
618
619 true
620 }
621
622 #[must_use]
624 pub fn alpha(&self) -> f32 {
625 (self.duration * 2.0).min(1.0)
626 }
627}
628
629#[derive(Debug, Clone)]
631pub struct JuiceEffects {
632 pub screen_shake: ScreenShake,
634 pub ball_trail: BallTrail,
636 pub hit_flash: HitFlash,
638 pub score_popups: Vec<ScorePopup>,
640 pub particles: ParticleSystem,
642}
643
644impl Default for JuiceEffects {
645 fn default() -> Self {
646 Self::new()
647 }
648}
649
650impl JuiceEffects {
651 #[must_use]
653 pub fn new() -> Self {
654 Self {
655 screen_shake: ScreenShake::new(),
656 ball_trail: BallTrail::default(),
657 hit_flash: HitFlash::new(),
658 score_popups: Vec::new(),
659 particles: ParticleSystem::default(),
660 }
661 }
662
663 pub fn update(&mut self, ball_x: f32, ball_y: f32, dt: f32) {
671 let _ = self.screen_shake.update(dt);
672 self.ball_trail.update(ball_x, ball_y, dt);
673 let _ = self.hit_flash.update(dt);
674 self.particles.update(dt);
675
676 self.score_popups.retain_mut(|popup| popup.update(dt));
678 }
679
680 pub fn on_goal(&mut self, scorer_x: f32, scorer_y: f32, points_text: &str) {
688 self.screen_shake.trigger(8.0, 0.3);
690
691 self.score_popups
693 .push(ScorePopup::new(scorer_x, scorer_y, points_text, 1.0));
694
695 self.particles
697 .spawn(scorer_x, scorer_y, 30, 200.0, 0.8, 4.0, 0x00FF_D700);
698
699 self.ball_trail.clear();
701 }
702
703 pub fn on_paddle_hit(&mut self, right_paddle: bool) {
709 self.screen_shake.trigger(3.0, 0.1);
711
712 self.hit_flash.trigger(right_paddle, 0.8, 0.1);
714 }
715
716 pub fn on_paddle_hit_at(&mut self, ball_x: f32, ball_y: f32, right_paddle: bool) {
724 self.screen_shake.trigger(3.0, 0.1);
726
727 self.hit_flash.trigger(right_paddle, 0.8, 0.1);
729
730 let direction_x = if right_paddle { -1.0 } else { 1.0 };
733 self.particles.spawn_directional(
734 ball_x,
735 ball_y,
736 direction_x,
737 0.0,
738 0.5, 12,
740 150.0,
741 0.4,
742 3.0,
743 0x0000_FFFF, );
745 }
746
747 pub fn on_wall_bounce(&mut self) {
749 self.screen_shake.trigger(1.5, 0.05);
751 }
752
753 pub fn reset(&mut self) {
755 self.screen_shake.reset();
756 self.ball_trail.clear();
757 self.hit_flash.reset();
758 self.score_popups.clear();
759 self.particles.clear();
760 }
761}
762
763#[cfg(test)]
764#[allow(clippy::float_cmp, clippy::unreadable_literal)]
765mod tests {
766 use super::*;
767
768 #[test]
773 fn test_screen_shake_new() {
774 let shake = ScreenShake::new();
775 assert_eq!(shake.intensity, 0.0);
776 assert_eq!(shake.duration, 0.0);
777 assert!(!shake.is_active());
778 }
779
780 #[test]
781 fn test_screen_shake_trigger() {
782 let mut shake = ScreenShake::new();
783 shake.trigger(10.0, 0.5);
784
785 assert_eq!(shake.intensity, 10.0);
786 assert_eq!(shake.duration, 0.5);
787 assert!(shake.is_active());
788 }
789
790 #[test]
791 fn test_screen_shake_stronger_override() {
792 let mut shake = ScreenShake::new();
793 shake.trigger(5.0, 0.5);
794 shake.trigger(10.0, 0.3); assert_eq!(shake.intensity, 10.0);
797 }
798
799 #[test]
800 fn test_screen_shake_weaker_no_override() {
801 let mut shake = ScreenShake::new();
802 shake.trigger(10.0, 0.5);
803 shake.trigger(5.0, 0.3); assert_eq!(shake.intensity, 10.0); }
807
808 #[test]
809 fn test_screen_shake_update_decay() {
810 let mut shake = ScreenShake::new();
811 shake.trigger(10.0, 0.5);
812
813 for _ in 0..100 {
815 let _ = shake.update(0.016);
816 }
817
818 assert!(!shake.is_active());
820 let (x, y) = shake.offset();
821 assert_eq!(x, 0.0);
822 assert_eq!(y, 0.0);
823 }
824
825 #[test]
826 fn test_screen_shake_produces_offset() {
827 let mut shake = ScreenShake::new();
828 shake.trigger(10.0, 0.5);
829
830 let (x, y) = shake.update(0.016);
831
832 assert!(x != 0.0 || y != 0.0);
834 }
835
836 #[test]
837 fn test_screen_shake_reset() {
838 let mut shake = ScreenShake::new();
839 shake.trigger(10.0, 0.5);
840 let _ = shake.update(0.016);
841
842 shake.reset();
843
844 assert!(!shake.is_active());
845 assert_eq!(shake.offset(), (0.0, 0.0));
846 }
847
848 #[test]
853 fn test_ball_trail_new() {
854 let trail = BallTrail::new(10, 0.016, 0.15);
855 assert_eq!(trail.points.len(), 10);
856 assert_eq!(trail.active_count(), 0);
857 }
858
859 #[test]
860 fn test_ball_trail_default() {
861 let trail = BallTrail::default();
862 assert_eq!(trail.points.len(), 10);
863 }
864
865 #[test]
866 fn test_ball_trail_update_adds_points() {
867 let mut trail = BallTrail::new(10, 0.016, 0.15);
868
869 trail.update(100.0, 200.0, 0.016);
871
872 assert_eq!(trail.active_count(), 1);
873 }
874
875 #[test]
876 fn test_ball_trail_points_age() {
877 let mut trail = BallTrail::new(10, 0.016, 0.1);
878 trail.update(100.0, 200.0, 0.016);
879
880 for _ in 0..20 {
882 trail.update(100.0, 200.0, 0.016);
883 }
884
885 let points = trail.get_points();
887 for (_, _, alpha) in &points {
888 assert!(*alpha > 0.0);
889 }
890 }
891
892 #[test]
893 fn test_ball_trail_get_points() {
894 let mut trail = BallTrail::new(10, 0.016, 0.15);
895 trail.update(100.0, 200.0, 0.016);
896
897 let points = trail.get_points();
898 assert_eq!(points.len(), 1);
899
900 let (x, y, alpha) = points[0];
901 assert_eq!(x, 100.0);
902 assert_eq!(y, 200.0);
903 assert!((alpha - 1.0).abs() < 0.1); }
905
906 #[test]
907 fn test_ball_trail_clear() {
908 let mut trail = BallTrail::new(10, 0.016, 0.15);
909 trail.update(100.0, 200.0, 0.016);
910 trail.update(100.0, 200.0, 0.016);
911
912 trail.clear();
913
914 assert_eq!(trail.active_count(), 0);
915 }
916
917 #[test]
918 fn test_ball_trail_ring_buffer() {
919 let mut trail = BallTrail::new(5, 0.001, 1.0); for i in 0..10 {
923 trail.update(i as f32 * 10.0, 0.0, 0.002);
924 }
925
926 assert!(trail.active_count() <= 5);
928 }
929
930 #[test]
935 fn test_hit_flash_new() {
936 let flash = HitFlash::new();
937 assert!(!flash.is_active());
938 }
939
940 #[test]
941 fn test_hit_flash_trigger() {
942 let mut flash = HitFlash::new();
943 flash.trigger(true, 0.8, 0.1);
944
945 assert!(flash.is_active());
946 }
947
948 #[test]
949 fn test_hit_flash_update() {
950 let mut flash = HitFlash::new();
951 flash.trigger(false, 1.0, 0.1);
952
953 let (active, right, intensity) = flash.update(0.016);
954
955 assert!(active);
956 assert!(!right);
957 assert!(intensity > 0.0);
958 }
959
960 #[test]
961 fn test_hit_flash_decays() {
962 let mut flash = HitFlash::new();
963 flash.trigger(true, 1.0, 0.1);
964
965 for _ in 0..20 {
967 let _ = flash.update(0.016);
968 }
969
970 assert!(!flash.is_active());
971 }
972
973 #[test]
974 fn test_hit_flash_reset() {
975 let mut flash = HitFlash::new();
976 flash.trigger(true, 1.0, 0.5);
977
978 flash.reset();
979
980 assert!(!flash.is_active());
981 }
982
983 #[test]
984 fn test_hit_flash_state_inactive() {
985 let flash = HitFlash::new();
986 let (left, right, intensity) = flash.flash_state();
987
988 assert!(!left);
989 assert!(!right);
990 assert_eq!(intensity, 0.0);
991 }
992
993 #[test]
994 fn test_hit_flash_state_left_paddle() {
995 let mut flash = HitFlash::new();
996 flash.trigger(false, 1.0, 0.1);
997
998 let (left, right, intensity) = flash.flash_state();
999
1000 assert!(left);
1001 assert!(!right);
1002 assert!(intensity > 0.0);
1003 }
1004
1005 #[test]
1006 fn test_hit_flash_state_right_paddle() {
1007 let mut flash = HitFlash::new();
1008 flash.trigger(true, 1.0, 0.1);
1009
1010 let (left, right, intensity) = flash.flash_state();
1011
1012 assert!(!left);
1013 assert!(right);
1014 assert!(intensity > 0.0);
1015 }
1016
1017 #[test]
1022 fn test_score_popup_new() {
1023 let popup = ScorePopup::new(400.0, 300.0, "+1", 1.0);
1024
1025 assert_eq!(popup.x, 400.0);
1026 assert_eq!(popup.y, 300.0);
1027 assert_eq!(popup.text, "+1");
1028 assert_eq!(popup.duration, 1.0);
1029 }
1030
1031 #[test]
1032 fn test_score_popup_update() {
1033 let mut popup = ScorePopup::new(400.0, 300.0, "+1", 1.0);
1034 let initial_y = popup.y;
1035
1036 let active = popup.update(0.1);
1037
1038 assert!(active);
1039 assert!(popup.y < initial_y); }
1041
1042 #[test]
1043 fn test_score_popup_expires() {
1044 let mut popup = ScorePopup::new(400.0, 300.0, "+1", 0.1);
1045
1046 for _ in 0..20 {
1048 let _ = popup.update(0.016);
1049 }
1050
1051 let active = popup.update(0.016);
1052 assert!(!active);
1053 }
1054
1055 #[test]
1056 fn test_score_popup_alpha() {
1057 let popup = ScorePopup::new(400.0, 300.0, "+1", 1.0);
1058 assert_eq!(popup.alpha(), 1.0);
1059
1060 let mut popup2 = ScorePopup::new(400.0, 300.0, "+1", 0.3);
1061 let _ = popup2.update(0.2);
1062 assert!(popup2.alpha() < 1.0);
1063 }
1064
1065 #[test]
1070 fn test_juice_effects_new() {
1071 let juice = JuiceEffects::new();
1072 assert!(!juice.screen_shake.is_active());
1073 assert!(!juice.hit_flash.is_active());
1074 assert!(juice.score_popups.is_empty());
1075 }
1076
1077 #[test]
1078 fn test_juice_effects_default() {
1079 let juice = JuiceEffects::default();
1080 assert!(!juice.screen_shake.is_active());
1081 }
1082
1083 #[test]
1084 fn test_juice_effects_on_goal() {
1085 let mut juice = JuiceEffects::new();
1086 juice.on_goal(400.0, 300.0, "+1");
1087
1088 assert!(juice.screen_shake.is_active());
1089 assert_eq!(juice.score_popups.len(), 1);
1090 }
1091
1092 #[test]
1093 fn test_juice_effects_on_paddle_hit() {
1094 let mut juice = JuiceEffects::new();
1095 juice.on_paddle_hit(true);
1096
1097 assert!(juice.screen_shake.is_active());
1098 assert!(juice.hit_flash.is_active());
1099 }
1100
1101 #[test]
1102 fn test_juice_effects_on_wall_bounce() {
1103 let mut juice = JuiceEffects::new();
1104 juice.on_wall_bounce();
1105
1106 assert!(juice.screen_shake.is_active());
1107 }
1108
1109 #[test]
1110 fn test_juice_effects_update() {
1111 let mut juice = JuiceEffects::new();
1112 juice.on_goal(400.0, 300.0, "+1");
1113
1114 juice.update(100.0, 200.0, 0.016);
1116
1117 assert!(juice.ball_trail.active_count() > 0);
1119 }
1120
1121 #[test]
1122 fn test_juice_effects_reset() {
1123 let mut juice = JuiceEffects::new();
1124 juice.on_goal(400.0, 300.0, "+1");
1125 juice.on_paddle_hit(false);
1126 juice.update(100.0, 200.0, 0.016);
1127
1128 juice.reset();
1129
1130 assert!(!juice.screen_shake.is_active());
1131 assert!(!juice.hit_flash.is_active());
1132 assert!(juice.score_popups.is_empty());
1133 assert_eq!(juice.ball_trail.active_count(), 0);
1134 }
1135
1136 #[test]
1137 fn test_juice_effects_popup_cleanup() {
1138 let mut juice = JuiceEffects::new();
1139 juice.on_goal(400.0, 300.0, "+1");
1140
1141 for _ in 0..100 {
1143 juice.update(100.0, 200.0, 0.016);
1144 }
1145
1146 assert!(juice.score_popups.is_empty());
1148 }
1149
1150 #[test]
1155 fn test_particle_new() {
1156 let particle = Particle::new(100.0, 200.0, 50.0, -30.0, 1.0, 5.0, 0xFF0000);
1157
1158 assert_eq!(particle.x, 100.0);
1159 assert_eq!(particle.y, 200.0);
1160 assert_eq!(particle.vx, 50.0);
1161 assert_eq!(particle.vy, -30.0);
1162 assert_eq!(particle.lifetime, 1.0);
1163 assert_eq!(particle.initial_lifetime, 1.0);
1164 assert_eq!(particle.size, 5.0);
1165 assert_eq!(particle.color, 0xFF0000);
1166 assert!(particle.active);
1167 }
1168
1169 #[test]
1170 fn test_particle_update_position() {
1171 let mut particle = Particle::new(100.0, 200.0, 50.0, -30.0, 1.0, 5.0, 0xFF0000);
1172
1173 let alive = particle.update(0.1);
1174
1175 assert!(alive);
1176 assert!((particle.x - 105.0).abs() < 0.01);
1178 assert!(particle.y < 200.0 - 2.0); }
1181
1182 #[test]
1183 fn test_particle_expires() {
1184 let mut particle = Particle::new(100.0, 200.0, 50.0, -30.0, 0.1, 5.0, 0xFF0000);
1185
1186 for _ in 0..20 {
1188 let _ = particle.update(0.016);
1189 }
1190
1191 assert!(!particle.active);
1192 assert!(!particle.update(0.016)); }
1194
1195 #[test]
1196 fn test_particle_alpha() {
1197 let mut particle = Particle::new(100.0, 200.0, 50.0, -30.0, 1.0, 5.0, 0xFF0000);
1198
1199 assert!((particle.alpha() - 1.0).abs() < 0.01);
1201
1202 let _ = particle.update(0.5);
1204 assert!(particle.alpha() > 0.4 && particle.alpha() < 0.6);
1205
1206 particle.lifetime = 0.0;
1208 particle.active = false;
1209 assert_eq!(particle.alpha(), 0.0);
1210 }
1211
1212 #[test]
1213 fn test_particle_alpha_zero_initial_lifetime() {
1214 let mut particle = Particle::new(100.0, 200.0, 50.0, -30.0, 1.0, 5.0, 0xFF0000);
1215 particle.initial_lifetime = 0.0;
1216
1217 assert_eq!(particle.alpha(), 0.0);
1218 }
1219
1220 #[test]
1221 fn test_particle_rgb() {
1222 let particle = Particle::new(100.0, 200.0, 0.0, 0.0, 1.0, 5.0, 0xFF8000); let (r, g, b) = particle.rgb();
1225
1226 assert!((r - 1.0).abs() < 0.01); assert!((g - 0.5).abs() < 0.02); assert!((b - 0.0).abs() < 0.01); }
1230
1231 #[test]
1232 fn test_particle_inactive_doesnt_update() {
1233 let mut particle = Particle::new(100.0, 200.0, 50.0, -30.0, 1.0, 5.0, 0xFF0000);
1234 particle.active = false;
1235
1236 let alive = particle.update(0.1);
1237
1238 assert!(!alive);
1239 assert_eq!(particle.x, 100.0);
1241 }
1242
1243 #[test]
1248 fn test_particle_system_new() {
1249 let system = ParticleSystem::new(100);
1250
1251 assert_eq!(system.particles.len(), 100);
1252 assert_eq!(system.active_count(), 0);
1253 }
1254
1255 #[test]
1256 fn test_particle_system_default() {
1257 let system = ParticleSystem::default();
1258
1259 assert_eq!(system.particles.len(), 200);
1260 assert_eq!(system.active_count(), 0);
1261 }
1262
1263 #[test]
1264 fn test_particle_system_spawn() {
1265 let mut system = ParticleSystem::new(100);
1266
1267 system.spawn(400.0, 300.0, 10, 100.0, 1.0, 5.0, 0xFFFFFF);
1268
1269 assert_eq!(system.active_count(), 10);
1270 }
1271
1272 #[test]
1273 fn test_particle_system_spawn_directional() {
1274 let mut system = ParticleSystem::new(100);
1275
1276 system.spawn_directional(400.0, 300.0, 1.0, 0.0, 0.5, 15, 100.0, 1.0, 5.0, 0x00FF00);
1277
1278 assert_eq!(system.active_count(), 15);
1279 }
1280
1281 #[test]
1282 fn test_particle_system_update() {
1283 let mut system = ParticleSystem::new(100);
1284 system.spawn(400.0, 300.0, 5, 100.0, 1.0, 5.0, 0xFFFFFF);
1285
1286 let initial: Vec<(f32, f32)> = system.get_active().iter().map(|p| (p.x, p.y)).collect();
1288
1289 system.update(0.1);
1290
1291 let updated: Vec<(f32, f32)> = system.get_active().iter().map(|p| (p.x, p.y)).collect();
1293 assert_ne!(initial, updated);
1294 }
1295
1296 #[test]
1297 fn test_particle_system_particles_expire() {
1298 let mut system = ParticleSystem::new(100);
1299 system.spawn(400.0, 300.0, 10, 100.0, 0.1, 5.0, 0xFFFFFF); for _ in 0..20 {
1303 system.update(0.016);
1304 }
1305
1306 assert_eq!(system.active_count(), 0);
1308 }
1309
1310 #[test]
1311 fn test_particle_system_get_active() {
1312 let mut system = ParticleSystem::new(100);
1313 system.spawn(400.0, 300.0, 5, 100.0, 1.0, 5.0, 0xFFFFFF);
1314
1315 let active = system.get_active();
1316
1317 assert_eq!(active.len(), 5);
1318 for particle in active {
1319 assert!(particle.active);
1320 }
1321 }
1322
1323 #[test]
1324 fn test_particle_system_clear() {
1325 let mut system = ParticleSystem::new(100);
1326 system.spawn(400.0, 300.0, 10, 100.0, 1.0, 5.0, 0xFFFFFF);
1327 assert_eq!(system.active_count(), 10);
1328
1329 system.clear();
1330
1331 assert_eq!(system.active_count(), 0);
1332 }
1333
1334 #[test]
1335 fn test_particle_system_ring_buffer() {
1336 let mut system = ParticleSystem::new(10);
1337
1338 system.spawn(400.0, 300.0, 15, 100.0, 1.0, 5.0, 0xFFFFFF);
1340
1341 assert!(system.active_count() <= 10);
1343 }
1344
1345 #[test]
1350 fn test_juice_effects_on_goal_spawns_particles() {
1351 let mut juice = JuiceEffects::new();
1352 juice.on_goal(400.0, 300.0, "+1");
1353
1354 assert!(juice.particles.active_count() > 0);
1356 }
1357
1358 #[test]
1359 fn test_juice_effects_on_paddle_hit_at_spawns_particles() {
1360 let mut juice = JuiceEffects::new();
1361 juice.on_paddle_hit_at(50.0, 300.0, false);
1362
1363 assert!(juice.particles.active_count() > 0);
1365 }
1366
1367 #[test]
1368 fn test_juice_effects_particles_update() {
1369 let mut juice = JuiceEffects::new();
1370 juice.on_goal(400.0, 300.0, "+1");
1371 let initial_count = juice.particles.active_count();
1372
1373 for _ in 0..100 {
1375 juice.update(100.0, 200.0, 0.016);
1376 }
1377
1378 assert!(juice.particles.active_count() < initial_count);
1380 }
1381
1382 #[test]
1383 fn test_juice_effects_reset_clears_particles() {
1384 let mut juice = JuiceEffects::new();
1385 juice.on_goal(400.0, 300.0, "+1");
1386 assert!(juice.particles.active_count() > 0);
1387
1388 juice.reset();
1389
1390 assert_eq!(juice.particles.active_count(), 0);
1391 }
1392}