1use glam::{Vec2, Vec3};
9use crate::combat::Element;
10
11const MAX_TRAIL_SEGMENTS: usize = 32;
16const MAX_DAMAGE_NUMBERS: usize = 50;
17const GRAVITY: f32 = 9.81;
18const IMPACT_COMPRESS_DURATION: f32 = 0.2;
19const PARRY_TIME_SCALE: f32 = 0.3;
20const PARRY_SLOW_DURATION: f32 = 0.5;
21const DEFAULT_DEBRIS_COUNT_MIN: usize = 5;
22const DEFAULT_DEBRIS_COUNT_MAX: usize = 10;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum WeaponType {
31 Sword,
32 Axe,
33 Mace,
34 Staff,
35 Dagger,
36 Spear,
37 Bow,
38 Fist,
39 Scythe,
40 Whip,
41}
42
43impl WeaponType {
44 pub fn all() -> &'static [WeaponType] {
46 &[
47 WeaponType::Sword,
48 WeaponType::Axe,
49 WeaponType::Mace,
50 WeaponType::Staff,
51 WeaponType::Dagger,
52 WeaponType::Spear,
53 WeaponType::Bow,
54 WeaponType::Fist,
55 WeaponType::Scythe,
56 WeaponType::Whip,
57 ]
58 }
59
60 pub fn name(self) -> &'static str {
62 match self {
63 WeaponType::Sword => "Sword",
64 WeaponType::Axe => "Axe",
65 WeaponType::Mace => "Mace",
66 WeaponType::Staff => "Staff",
67 WeaponType::Dagger => "Dagger",
68 WeaponType::Spear => "Spear",
69 WeaponType::Bow => "Bow",
70 WeaponType::Fist => "Fist",
71 WeaponType::Scythe => "Scythe",
72 WeaponType::Whip => "Whip",
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
84pub struct WeaponProfile {
85 pub mass: f32,
87 pub length: f32,
89 pub swing_speed: f32,
91 pub impact_force: f32,
93 pub trail_width: f32,
95 pub trail_segments: usize,
97 pub element: Option<Element>,
99}
100
101impl WeaponProfile {
102 pub fn new(
104 mass: f32,
105 length: f32,
106 swing_speed: f32,
107 impact_force: f32,
108 trail_width: f32,
109 trail_segments: usize,
110 element: Option<Element>,
111 ) -> Self {
112 Self { mass, length, swing_speed, impact_force, trail_width, trail_segments, element }
113 }
114}
115
116pub struct WeaponProfiles;
122
123impl WeaponProfiles {
124 pub fn get(weapon: WeaponType) -> WeaponProfile {
126 match weapon {
127 WeaponType::Sword => WeaponProfile {
128 mass: 1.5,
129 length: 1.0,
130 swing_speed: 1.0,
131 impact_force: 80.0,
132 trail_width: 0.12,
133 trail_segments: 24,
134 element: None,
135 },
136 WeaponType::Axe => WeaponProfile {
137 mass: 3.5,
138 length: 0.9,
139 swing_speed: 0.65,
140 impact_force: 160.0,
141 trail_width: 0.18,
142 trail_segments: 18,
143 element: None,
144 },
145 WeaponType::Mace => WeaponProfile {
146 mass: 4.0,
147 length: 0.8,
148 swing_speed: 0.55,
149 impact_force: 200.0,
150 trail_width: 0.15,
151 trail_segments: 16,
152 element: None,
153 },
154 WeaponType::Staff => WeaponProfile {
155 mass: 1.2,
156 length: 1.6,
157 swing_speed: 0.85,
158 impact_force: 40.0,
159 trail_width: 0.20,
160 trail_segments: 28,
161 element: Some(Element::Entropy),
162 },
163 WeaponType::Dagger => WeaponProfile {
164 mass: 0.5,
165 length: 0.35,
166 swing_speed: 1.6,
167 impact_force: 35.0,
168 trail_width: 0.06,
169 trail_segments: 20,
170 element: None,
171 },
172 WeaponType::Spear => WeaponProfile {
173 mass: 2.0,
174 length: 2.0,
175 swing_speed: 0.75,
176 impact_force: 120.0,
177 trail_width: 0.08,
178 trail_segments: 22,
179 element: None,
180 },
181 WeaponType::Bow => WeaponProfile {
182 mass: 0.8,
183 length: 1.3,
184 swing_speed: 0.40,
185 impact_force: 90.0,
186 trail_width: 0.04,
187 trail_segments: 30,
188 element: None,
189 },
190 WeaponType::Fist => WeaponProfile {
191 mass: 0.3,
192 length: 0.25,
193 swing_speed: 2.0,
194 impact_force: 50.0,
195 trail_width: 0.10,
196 trail_segments: 14,
197 element: None,
198 },
199 WeaponType::Scythe => WeaponProfile {
200 mass: 3.0,
201 length: 1.8,
202 swing_speed: 0.70,
203 impact_force: 140.0,
204 trail_width: 0.22,
205 trail_segments: 26,
206 element: Some(Element::Shadow),
207 },
208 WeaponType::Whip => WeaponProfile {
209 mass: 0.6,
210 length: 3.0,
211 swing_speed: 1.2,
212 impact_force: 30.0,
213 trail_width: 0.05,
214 trail_segments: 32,
215 element: None,
216 },
217 }
218 }
219
220 pub fn all() -> Vec<(WeaponType, WeaponProfile)> {
222 WeaponType::all().iter().map(|&w| (w, Self::get(w))).collect()
223 }
224}
225
226#[derive(Debug, Clone)]
234pub struct SwingArc {
235 pub start_angle: f32,
237 pub end_angle: f32,
239 pub duration: f32,
241 pub elapsed: f32,
243 pub origin: Vec3,
245 pub radius: f32,
247}
248
249impl SwingArc {
250 pub fn new(
252 start_angle: f32,
253 end_angle: f32,
254 duration: f32,
255 origin: Vec3,
256 radius: f32,
257 ) -> Self {
258 Self {
259 start_angle,
260 end_angle,
261 duration,
262 elapsed: 0.0,
263 origin,
264 radius,
265 }
266 }
267
268 pub fn progress(&self) -> f32 {
270 if self.duration <= 0.0 { return 1.0; }
271 (self.elapsed / self.duration).clamp(0.0, 1.0)
272 }
273
274 pub fn finished(&self) -> bool {
276 self.elapsed >= self.duration
277 }
278
279 fn angle_at(&self, t: f32) -> f32 {
281 self.start_angle + (self.end_angle - self.start_angle) * t
282 }
283
284 pub fn sample(&self, t: f32) -> Vec3 {
287 let t_clamped = t.clamp(0.0, 1.0);
288 let angle = self.angle_at(t_clamped);
289 let x = angle.cos() * self.radius;
290 let z = angle.sin() * self.radius;
291 self.origin + Vec3::new(x, 0.0, z)
292 }
293
294 pub fn velocity_at(&self, t: f32) -> Vec3 {
297 let t_clamped = t.clamp(0.0, 1.0);
298 let angle = self.angle_at(t_clamped);
299 let angular_vel = if self.duration > 0.0 {
300 (self.end_angle - self.start_angle) / self.duration
301 } else {
302 0.0
303 };
304 let speed = self.radius * angular_vel;
305 Vec3::new(-angle.sin() * speed, 0.0, angle.cos() * speed)
307 }
308
309 pub fn tick(&mut self, dt: f32) -> bool {
311 self.elapsed += dt;
312 !self.finished()
313 }
314}
315
316#[derive(Debug, Clone)]
322pub struct WeaponTrailSegment {
323 pub position: Vec3,
325 pub velocity: Vec3,
327 pub width: f32,
329 pub color: [f32; 4],
331 pub emission: f32,
333 pub age: f32,
335}
336
337impl WeaponTrailSegment {
338 pub fn new(position: Vec3, velocity: Vec3, width: f32, color: [f32; 4], emission: f32) -> Self {
339 Self { position, velocity, width, color, emission, age: 0.0 }
340 }
341
342 pub fn update(&mut self, dt: f32) {
344 self.age += dt;
345 self.position += self.velocity * dt;
346 self.velocity *= (1.0 - 3.0 * dt).max(0.0);
348 }
349
350 pub fn alpha(&self, max_age: f32) -> f32 {
352 if max_age <= 0.0 { return 0.0; }
353 (1.0 - (self.age / max_age)).clamp(0.0, 1.0)
354 }
355}
356
357#[derive(Debug, Clone, Copy)]
363pub struct TrailVertex {
364 pub position: Vec3,
365 pub color: [f32; 4],
366 pub emission: f32,
367 pub uv: Vec2,
368}
369
370#[derive(Debug, Clone)]
377pub struct WeaponTrail {
378 segments: Vec<WeaponTrailSegment>,
380 head: usize,
382 count: usize,
384 spawn_timer: f32,
386 spawn_interval: f32,
388 pub profile: WeaponProfile,
390 active_arc: Option<SwingArc>,
392 impact_state: Option<ImpactCompressState>,
394 max_segment_age: f32,
396 combo_intensity: f32,
398}
399
400#[derive(Debug, Clone)]
402struct ImpactCompressState {
403 contact_point: Vec3,
404 elapsed: f32,
405 duration: f32,
406 phase: ImpactPhase,
407}
408
409#[derive(Debug, Clone, Copy, PartialEq)]
410enum ImpactPhase {
411 Compress,
413 SpringBack,
415}
416
417impl WeaponTrail {
418 pub fn new(profile: WeaponProfile) -> Self {
420 let seg_count = profile.trail_segments.min(MAX_TRAIL_SEGMENTS);
421 let spawn_interval = if seg_count > 0 { 1.0 / (seg_count as f32 * 2.0) } else { 0.05 };
422 let mut segments = Vec::with_capacity(MAX_TRAIL_SEGMENTS);
423 for _ in 0..MAX_TRAIL_SEGMENTS {
424 segments.push(WeaponTrailSegment::new(
425 Vec3::ZERO, Vec3::ZERO, 0.0, [0.0; 4], 0.0,
426 ));
427 }
428 Self {
429 segments,
430 head: 0,
431 count: 0,
432 spawn_timer: 0.0,
433 spawn_interval,
434 profile,
435 active_arc: None,
436 impact_state: None,
437 max_segment_age: 0.6,
438 combo_intensity: 1.0,
439 }
440 }
441
442 pub fn set_combo_intensity(&mut self, intensity: f32) {
445 self.combo_intensity = intensity.max(1.0);
446 }
447
448 pub fn begin_swing(&mut self, arc: SwingArc) {
450 self.active_arc = Some(arc);
451 self.spawn_timer = 0.0;
452 }
453
454 pub fn update(&mut self, dt: f32) {
456 let trail_color = self.element_trail_color();
458 let trail_width = self.profile.trail_width * self.combo_intensity;
459 let base_emission = 0.5 * self.combo_intensity;
460
461 let mut new_segments: Vec<WeaponTrailSegment> = Vec::new();
463 let mut arc_finished = false;
464 if let Some(ref mut arc) = self.active_arc {
465 arc.tick(dt);
466 self.spawn_timer += dt;
467 while self.spawn_timer >= self.spawn_interval {
468 self.spawn_timer -= self.spawn_interval;
469 let t = arc.progress();
470 let pos = arc.sample(t);
471 let vel = arc.velocity_at(t);
472 new_segments.push(WeaponTrailSegment::new(
473 pos, vel * 0.1, trail_width, trail_color, base_emission,
474 ));
475 }
476 arc_finished = arc.finished();
477 }
478 for seg in new_segments {
479 self.push_segment(seg);
480 }
481 if arc_finished {
482 self.active_arc = None;
483 }
484
485 for i in 0..MAX_TRAIL_SEGMENTS {
487 if self.segment_alive(i) {
488 self.segments[i].update(dt);
489 }
490 }
491
492 if let Some(ref mut state) = self.impact_state.clone() {
494 state.elapsed += dt;
495 match state.phase {
496 ImpactPhase::Compress => {
497 let compress_strength = 8.0 * dt;
499 for i in 0..MAX_TRAIL_SEGMENTS {
500 if self.segment_alive(i) {
501 let diff = state.contact_point - self.segments[i].position;
502 let dist = diff.length();
503 if dist > 0.01 && dist < 2.0 {
504 let pull = diff.normalize() * compress_strength * (1.0 / (dist + 0.5));
505 self.segments[i].velocity += pull;
506 }
507 }
508 }
509 if state.elapsed >= IMPACT_COMPRESS_DURATION {
510 state.phase = ImpactPhase::SpringBack;
511 state.elapsed = 0.0;
512 }
513 }
514 ImpactPhase::SpringBack => {
515 let spring_strength = 12.0 * dt;
517 for i in 0..MAX_TRAIL_SEGMENTS {
518 if self.segment_alive(i) {
519 let diff = self.segments[i].position - state.contact_point;
520 let dist = diff.length();
521 if dist > 0.01 && dist < 3.0 {
522 let push = diff.normalize() * spring_strength * (1.0 / (dist + 0.5));
523 self.segments[i].velocity += push;
524 }
525 }
526 }
527 if state.elapsed >= IMPACT_COMPRESS_DURATION {
528 self.impact_state = None;
529 return;
530 }
531 }
532 }
533 self.impact_state = Some(state.clone());
534 }
535 }
536
537 pub fn on_impact(&mut self, contact_point: Vec3) {
541 for i in 0..MAX_TRAIL_SEGMENTS {
543 if self.segment_alive(i) {
544 let dist = (self.segments[i].position - contact_point).length();
545 if dist < 1.5 {
546 self.segments[i].emission += 2.0 * (1.0 - dist / 1.5);
547 }
548 }
549 }
550 self.impact_state = Some(ImpactCompressState {
551 contact_point,
552 elapsed: 0.0,
553 duration: IMPACT_COMPRESS_DURATION * 2.0,
554 phase: ImpactPhase::Compress,
555 });
556 }
557
558 pub fn get_render_data(&self) -> Vec<TrailVertex> {
560 TrailRibbon::build(self)
561 }
562
563 fn push_segment(&mut self, seg: WeaponTrailSegment) {
566 self.segments[self.head] = seg;
567 self.head = (self.head + 1) % MAX_TRAIL_SEGMENTS;
568 if self.count < MAX_TRAIL_SEGMENTS {
569 self.count += 1;
570 }
571 }
572
573 fn segment_alive(&self, index: usize) -> bool {
574 if index >= MAX_TRAIL_SEGMENTS { return false; }
575 self.segments[index].age < self.max_segment_age && self.count > 0
577 }
578
579 fn ordered_segments(&self) -> Vec<&WeaponTrailSegment> {
580 if self.count == 0 { return Vec::new(); }
581 let mut out = Vec::with_capacity(self.count);
582 let start = if self.count < MAX_TRAIL_SEGMENTS {
583 0
584 } else {
585 self.head
586 };
587 for i in 0..self.count {
588 let idx = (start + i) % MAX_TRAIL_SEGMENTS;
589 if self.segments[idx].age < self.max_segment_age {
590 out.push(&self.segments[idx]);
591 }
592 }
593 out
594 }
595
596 fn element_trail_color(&self) -> [f32; 4] {
597 match self.profile.element {
598 Some(Element::Fire) => [1.0, 0.5, 0.1, 1.0],
599 Some(Element::Ice) => [0.5, 0.85, 1.0, 1.0],
600 Some(Element::Lightning) => [1.0, 0.95, 0.3, 1.0],
601 Some(Element::Void) => [0.3, 0.0, 0.5, 1.0],
602 Some(Element::Entropy) => [0.7, 0.2, 0.9, 1.0],
603 Some(Element::Gravity) => [0.3, 0.3, 0.7, 1.0],
604 Some(Element::Radiant) => [1.0, 1.0, 0.8, 1.0],
605 Some(Element::Shadow) => [0.15, 0.05, 0.25, 1.0],
606 Some(Element::Temporal) => [0.4, 0.9, 0.7, 1.0],
607 Some(Element::Physical) | None => [0.9, 0.9, 0.95, 1.0],
608 }
609 }
610}
611
612pub struct TrailRibbon;
621
622impl TrailRibbon {
623 pub fn build(trail: &WeaponTrail) -> Vec<TrailVertex> {
625 let segments = trail.ordered_segments();
626 let seg_count = segments.len();
627 if seg_count < 2 {
628 return Vec::new();
629 }
630
631 let mut vertices = Vec::with_capacity(seg_count * 2);
632
633 for i in 0..seg_count {
634 let seg = &segments[i];
635 let alpha = seg.alpha(trail.max_segment_age);
636 let mut color = seg.color;
637 color[3] *= alpha;
638
639 let forward = if i + 1 < seg_count {
641 (segments[i + 1].position - seg.position).normalize_or_zero()
642 } else if i > 0 {
643 (seg.position - segments[i - 1].position).normalize_or_zero()
644 } else {
645 Vec3::X
646 };
647
648 let up = Vec3::Y;
649 let perp = forward.cross(up).normalize_or_zero();
650 let half_w = seg.width * 0.5;
651
652 let uv_v = if seg_count > 1 { i as f32 / (seg_count - 1) as f32 } else { 0.0 };
653
654 vertices.push(TrailVertex {
655 position: seg.position + perp * half_w,
656 color,
657 emission: seg.emission,
658 uv: Vec2::new(0.0, uv_v),
659 });
660 vertices.push(TrailVertex {
661 position: seg.position - perp * half_w,
662 color,
663 emission: seg.emission,
664 uv: Vec2::new(1.0, uv_v),
665 });
666 }
667
668 vertices
669 }
670
671 pub fn build_indices(vertex_count: usize) -> Vec<u32> {
674 if vertex_count < 4 { return Vec::new(); }
675 let quad_count = vertex_count / 2 - 1;
676 let mut indices = Vec::with_capacity(quad_count * 6);
677 for q in 0..quad_count {
678 let base = (q * 2) as u32;
679 indices.push(base);
681 indices.push(base + 1);
682 indices.push(base + 2);
683 indices.push(base + 1);
685 indices.push(base + 3);
686 indices.push(base + 2);
687 }
688 indices
689 }
690}
691
692#[derive(Debug, Clone)]
699pub struct ElementEffect {
700 pub name: &'static str,
702 pub particle_count: usize,
704 pub color: [f32; 4],
706 pub emission: f32,
708 pub radius: f32,
710 pub duration: f32,
712 pub chains: bool,
714 pub chain_count: usize,
716 pub chain_range: f32,
718}
719
720impl ElementEffect {
721 pub fn for_element(element: Element) -> Self {
723 match element {
724 Element::Fire => Self {
725 name: "Ember Burst",
726 particle_count: 30,
727 color: [1.0, 0.4, 0.1, 1.0],
728 emission: 3.0,
729 radius: 1.5,
730 duration: 0.8,
731 chains: false,
732 chain_count: 0,
733 chain_range: 0.0,
734 },
735 Element::Ice => Self {
736 name: "Crystal Shatter",
737 particle_count: 20,
738 color: [0.5, 0.85, 1.0, 1.0],
739 emission: 2.0,
740 radius: 1.2,
741 duration: 1.0,
742 chains: false,
743 chain_count: 0,
744 chain_range: 0.0,
745 },
746 Element::Lightning => Self {
747 name: "Arc Chain",
748 particle_count: 15,
749 color: [1.0, 0.95, 0.2, 1.0],
750 emission: 5.0,
751 radius: 0.5,
752 duration: 0.3,
753 chains: true,
754 chain_count: 3,
755 chain_range: 5.0,
756 },
757 Element::Void => Self {
758 name: "Void Collapse",
759 particle_count: 25,
760 color: [0.2, 0.0, 0.4, 1.0],
761 emission: 2.5,
762 radius: 2.0,
763 duration: 1.2,
764 chains: false,
765 chain_count: 0,
766 chain_range: 0.0,
767 },
768 Element::Entropy => Self {
769 name: "Chaos Splatter",
770 particle_count: 40,
771 color: [0.6, 0.1, 0.8, 1.0],
772 emission: 4.0,
773 radius: 2.5,
774 duration: 1.5,
775 chains: false,
776 chain_count: 0,
777 chain_range: 0.0,
778 },
779 Element::Gravity => Self {
780 name: "Gravity Pulse",
781 particle_count: 18,
782 color: [0.3, 0.3, 0.6, 1.0],
783 emission: 2.0,
784 radius: 3.0,
785 duration: 0.6,
786 chains: false,
787 chain_count: 0,
788 chain_range: 0.0,
789 },
790 Element::Radiant => Self {
791 name: "Radiant Burst",
792 particle_count: 35,
793 color: [1.0, 1.0, 0.7, 1.0],
794 emission: 6.0,
795 radius: 2.0,
796 duration: 0.5,
797 chains: false,
798 chain_count: 0,
799 chain_range: 0.0,
800 },
801 Element::Shadow => Self {
802 name: "Shadow Tendrils",
803 particle_count: 22,
804 color: [0.1, 0.05, 0.2, 1.0],
805 emission: 1.5,
806 radius: 2.5,
807 duration: 1.8,
808 chains: true,
809 chain_count: 2,
810 chain_range: 3.0,
811 },
812 Element::Temporal => Self {
813 name: "Time Fracture",
814 particle_count: 16,
815 color: [0.4, 0.9, 0.7, 1.0],
816 emission: 3.5,
817 radius: 1.8,
818 duration: 2.0,
819 chains: false,
820 chain_count: 0,
821 chain_range: 0.0,
822 },
823 Element::Physical => Self {
824 name: "Impact Spark",
825 particle_count: 12,
826 color: [0.85, 0.8, 0.75, 1.0],
827 emission: 1.0,
828 radius: 0.8,
829 duration: 0.4,
830 chains: false,
831 chain_count: 0,
832 chain_range: 0.0,
833 },
834 }
835 }
836}
837
838#[derive(Debug, Clone)]
844pub struct CameraShake {
845 pub duration: f32,
847 pub intensity: f32,
849 pub max_intensity: f32,
851 pub offset: Vec3,
853 pub frequency: f32,
855 pub elapsed: f32,
857}
858
859impl CameraShake {
860 pub fn from_impact(mass: f32, velocity_magnitude: f32) -> Self {
862 let intensity = (mass * velocity_magnitude * 0.01).clamp(0.01, 2.0);
863 let duration = (intensity * 0.3).clamp(0.1, 0.8);
864 Self {
865 duration,
866 intensity,
867 max_intensity: intensity,
868 offset: Vec3::ZERO,
869 frequency: 25.0,
870 elapsed: 0.0,
871 }
872 }
873
874 pub fn update(&mut self, dt: f32) -> Vec3 {
876 if self.duration <= 0.0 {
877 self.offset = Vec3::ZERO;
878 return Vec3::ZERO;
879 }
880 self.elapsed += dt;
881 self.duration -= dt;
882 let decay = (self.duration / (self.max_intensity * 0.3).max(0.01)).clamp(0.0, 1.0);
883 let phase = self.elapsed * self.frequency;
884 self.offset = Vec3::new(
885 phase.sin() * self.intensity * decay,
886 (phase * 1.3).cos() * self.intensity * decay * 0.7,
887 (phase * 0.7).sin() * self.intensity * decay * 0.3,
888 );
889 self.offset
890 }
891
892 pub fn finished(&self) -> bool {
894 self.duration <= 0.0
895 }
896}
897
898#[derive(Debug, Clone)]
904pub struct DebrisGlyph {
905 pub glyph: char,
907 pub position: Vec3,
909 pub velocity: Vec3,
911 pub spin: f32,
913 pub rotation: f32,
915 pub scale: f32,
917 pub color: [f32; 4],
919 pub lifetime: f32,
921 pub max_lifetime: f32,
923}
924
925impl DebrisGlyph {
926 pub fn spawn(contact: Vec3, direction: Vec3, glyph: char, color: [f32; 4]) -> Self {
928 let speed = 3.0 + pseudo_random_from_pos(contact) * 4.0;
929 let spread = Vec3::new(
930 pseudo_random_component(contact.x),
931 pseudo_random_component(contact.y).abs() * 0.5 + 0.5,
932 pseudo_random_component(contact.z),
933 );
934 let vel = (direction.normalize_or_zero() + spread).normalize_or_zero() * speed;
935 let lifetime = 0.5 + pseudo_random_from_pos(contact) * 0.8;
936 Self {
937 glyph,
938 position: contact,
939 velocity: vel,
940 spin: (pseudo_random_component(contact.x + contact.z) * 10.0),
941 rotation: 0.0,
942 scale: 0.8 + pseudo_random_from_pos(contact) * 0.4,
943 color,
944 lifetime,
945 max_lifetime: lifetime,
946 }
947 }
948
949 pub fn update(&mut self, dt: f32) {
951 self.lifetime -= dt;
952 self.position += self.velocity * dt;
953 self.velocity.y -= GRAVITY * dt;
954 self.velocity *= (1.0 - 1.5 * dt).max(0.0);
955 self.rotation += self.spin * dt;
956 let age_ratio = 1.0 - (self.lifetime / self.max_lifetime).clamp(0.0, 1.0);
957 self.scale *= (1.0 - age_ratio * 0.3).max(0.1);
958 self.color[3] = (self.lifetime / self.max_lifetime).clamp(0.0, 1.0);
959 }
960
961 pub fn dead(&self) -> bool {
963 self.lifetime <= 0.0
964 }
965}
966
967pub fn spawn_debris(
969 contact: Vec3,
970 direction: Vec3,
971 color: [f32; 4],
972 count: usize,
973) -> Vec<DebrisGlyph> {
974 let glyphs = ['*', '+', '#', '~', '^', '%', '!', '?', '@', '&'];
975 let actual_count = count.clamp(DEFAULT_DEBRIS_COUNT_MIN, DEFAULT_DEBRIS_COUNT_MAX);
976 (0..actual_count)
977 .map(|i| {
978 let offset = Vec3::new(i as f32 * 0.1, i as f32 * 0.05, -(i as f32) * 0.08);
979 let g = glyphs[i % glyphs.len()];
980 DebrisGlyph::spawn(contact + offset * 0.1, direction, g, color)
981 })
982 .collect()
983}
984
985#[derive(Debug, Clone)]
992pub struct ShockwaveRing {
993 pub center: Vec2,
995 pub radius: f32,
997 pub speed: f32,
999 pub thickness: f32,
1001 pub distortion: f32,
1003 pub lifetime: f32,
1005 pub max_lifetime: f32,
1007}
1008
1009impl ShockwaveRing {
1010 pub fn new(center: Vec2, intensity: f32) -> Self {
1012 Self {
1013 center,
1014 radius: 0.0,
1015 speed: 0.8 + intensity * 0.4,
1016 thickness: 0.02 + intensity * 0.01,
1017 distortion: 0.03 * intensity,
1018 lifetime: 0.4 + intensity * 0.2,
1019 max_lifetime: 0.4 + intensity * 0.2,
1020 }
1021 }
1022
1023 pub fn update(&mut self, dt: f32) {
1025 self.lifetime -= dt;
1026 self.radius += self.speed * dt;
1027 let decay = (self.lifetime / self.max_lifetime).clamp(0.0, 1.0);
1028 self.distortion *= decay;
1029 self.thickness *= decay;
1030 }
1031
1032 pub fn finished(&self) -> bool {
1034 self.lifetime <= 0.0
1035 }
1036}
1037
1038#[derive(Debug, Clone)]
1044pub struct DamageNumber {
1045 pub value: i32,
1047 pub position: Vec3,
1049 pub velocity: Vec3,
1051 pub scale: f32,
1053 pub color: [f32; 4],
1055 pub lifetime: f32,
1057 pub max_lifetime: f32,
1059 pub crit: bool,
1061 pub element: Option<Element>,
1063}
1064
1065impl DamageNumber {
1066 pub fn new(value: i32, position: Vec3, crit: bool, element: Option<Element>) -> Self {
1068 let base_scale = if crit { 1.8 } else { 1.0 };
1069 let lifetime = if crit { 1.5 } else { 1.0 };
1070 let rand_x = pseudo_random_component(position.x) * 1.5;
1071 let rand_z = pseudo_random_component(position.z) * 1.5;
1072 let upward_speed = if crit { 5.0 } else { 3.0 };
1073
1074 let color = match element {
1075 Some(el) => {
1076 let c = el.color();
1077 [c.x, c.y, c.z, 1.0]
1078 }
1079 None => {
1080 if crit {
1081 [1.0, 0.9, 0.1, 1.0] } else {
1083 [1.0, 1.0, 1.0, 1.0] }
1085 }
1086 };
1087
1088 Self {
1089 value,
1090 position,
1091 velocity: Vec3::new(rand_x, upward_speed, rand_z),
1092 scale: base_scale,
1093 color,
1094 lifetime,
1095 max_lifetime: lifetime,
1096 crit,
1097 element,
1098 }
1099 }
1100
1101 pub fn update(&mut self, dt: f32) {
1103 self.lifetime -= dt;
1104 self.position += self.velocity * dt;
1105 self.velocity.y -= GRAVITY * 0.4 * dt;
1107 self.velocity.x *= (1.0 - 2.0 * dt).max(0.0);
1109 self.velocity.z *= (1.0 - 2.0 * dt).max(0.0);
1110 let age_ratio = (self.lifetime / self.max_lifetime).clamp(0.0, 1.0);
1112 self.color[3] = age_ratio;
1113 if self.crit {
1115 let life_pct = 1.0 - age_ratio;
1116 if life_pct < 0.15 {
1117 self.scale = 1.8 + (life_pct / 0.15) * 0.5;
1119 } else {
1120 self.scale = 2.3 * age_ratio;
1121 }
1122 } else {
1123 self.scale = 1.0 * age_ratio.max(0.3);
1124 }
1125 }
1126
1127 pub fn dead(&self) -> bool {
1129 self.lifetime <= 0.0
1130 }
1131}
1132
1133#[derive(Debug, Clone)]
1139pub struct DamageNumberManager {
1140 numbers: Vec<Option<DamageNumber>>,
1142 capacity: usize,
1144}
1145
1146impl DamageNumberManager {
1147 pub fn new(capacity: usize) -> Self {
1149 Self {
1150 numbers: vec![None; capacity],
1151 capacity,
1152 }
1153 }
1154
1155 pub fn spawn(&mut self, value: i32, position: Vec3, crit: bool, element: Option<Element>) {
1158 let dmg = DamageNumber::new(value, position, crit, element);
1159 for slot in self.numbers.iter_mut() {
1161 if slot.is_none() {
1162 *slot = Some(dmg);
1163 return;
1164 }
1165 }
1166 let mut min_life = f32::MAX;
1168 let mut min_idx = 0;
1169 for (i, slot) in self.numbers.iter().enumerate() {
1170 if let Some(ref n) = slot {
1171 if n.lifetime < min_life {
1172 min_life = n.lifetime;
1173 min_idx = i;
1174 }
1175 }
1176 }
1177 self.numbers[min_idx] = Some(dmg);
1178 }
1179
1180 pub fn update(&mut self, dt: f32) {
1182 for slot in self.numbers.iter_mut() {
1183 if let Some(ref mut n) = slot {
1184 n.update(dt);
1185 if n.dead() {
1186 *slot = None;
1187 }
1188 }
1189 }
1190 }
1191
1192 pub fn active(&self) -> Vec<&DamageNumber> {
1194 self.numbers.iter().filter_map(|s| s.as_ref()).collect()
1195 }
1196
1197 pub fn active_count(&self) -> usize {
1199 self.numbers.iter().filter(|s| s.is_some()).count()
1200 }
1201}
1202
1203impl Default for DamageNumberManager {
1204 fn default() -> Self {
1205 Self::new(MAX_DAMAGE_NUMBERS)
1206 }
1207}
1208
1209#[derive(Debug, Clone)]
1216pub struct ImpactEffect {
1217 pub camera_shake: CameraShake,
1219 pub debris: Vec<DebrisGlyph>,
1221 pub shockwave: ShockwaveRing,
1223 pub damage_number: DamageNumber,
1225 pub element_effect: Option<ElementEffect>,
1227 pub contact_point: Vec3,
1229 pub consumed: bool,
1231}
1232
1233impl ImpactEffect {
1234 pub fn generate(
1236 weapon: &WeaponProfile,
1237 contact_point: Vec3,
1238 velocity_magnitude: f32,
1239 damage: i32,
1240 crit: bool,
1241 screen_pos: Vec2,
1242 hit_direction: Vec3,
1243 ) -> Self {
1244 let mass = weapon.mass;
1245
1246 let camera_shake = CameraShake::from_impact(mass, velocity_magnitude);
1248
1249 let debris_count = (5.0 + mass * 1.5).min(10.0) as usize;
1251 let debris_color = match weapon.element {
1252 Some(el) => {
1253 let c = el.color();
1254 [c.x, c.y, c.z, 1.0]
1255 }
1256 None => [0.85, 0.8, 0.75, 1.0],
1257 };
1258 let debris = spawn_debris(contact_point, hit_direction, debris_color, debris_count);
1259
1260 let shock_intensity = (mass * velocity_magnitude * 0.005).clamp(0.5, 2.0);
1262 let shockwave = ShockwaveRing::new(screen_pos, shock_intensity);
1263
1264 let damage_number = DamageNumber::new(damage, contact_point + Vec3::Y * 0.5, crit, weapon.element);
1266
1267 let element_effect = weapon.element.map(ElementEffect::for_element);
1269
1270 Self {
1271 camera_shake,
1272 debris,
1273 shockwave,
1274 damage_number,
1275 element_effect,
1276 contact_point,
1277 consumed: false,
1278 }
1279 }
1280
1281 pub fn update(&mut self, dt: f32) {
1283 self.camera_shake.update(dt);
1284 for d in self.debris.iter_mut() {
1285 d.update(dt);
1286 }
1287 self.debris.retain(|d| !d.dead());
1288 self.shockwave.update(dt);
1289 self.damage_number.update(dt);
1290
1291 if self.camera_shake.finished()
1293 && self.debris.is_empty()
1294 && self.shockwave.finished()
1295 && self.damage_number.dead()
1296 {
1297 self.consumed = true;
1298 }
1299 }
1300}
1301
1302#[derive(Debug, Clone)]
1308pub struct ComboTrailIntegration {
1309 pub combo_count: u32,
1311 pub width_multiplier: f32,
1313 pub emission_multiplier: f32,
1315 pub intensity_multiplier: f32,
1317 pub milestone_pending: bool,
1319 pub milestone_tier: u32,
1321}
1322
1323impl ComboTrailIntegration {
1324 pub fn new() -> Self {
1325 Self {
1326 combo_count: 0,
1327 width_multiplier: 1.0,
1328 emission_multiplier: 1.0,
1329 intensity_multiplier: 1.0,
1330 milestone_pending: false,
1331 milestone_tier: 0,
1332 }
1333 }
1334
1335 pub fn set_combo(&mut self, count: u32) {
1337 let prev = self.combo_count;
1338 self.combo_count = count;
1339
1340 let factor = 1.0 + (count as f32).ln().max(0.0) * 0.3;
1342 self.width_multiplier = factor.min(3.0);
1343 self.emission_multiplier = factor.min(4.0);
1344 self.intensity_multiplier = factor.min(5.0);
1345
1346 self.milestone_pending = false;
1348 for &milestone in &[10u32, 25, 50, 100] {
1349 if prev < milestone && count >= milestone {
1350 self.milestone_pending = true;
1351 self.milestone_tier = milestone;
1352 }
1353 }
1354 }
1355
1356 pub fn take_milestone(&mut self) -> Option<u32> {
1358 if self.milestone_pending {
1359 self.milestone_pending = false;
1360 Some(self.milestone_tier)
1361 } else {
1362 None
1363 }
1364 }
1365
1366 pub fn apply_to_trail(&self, trail: &mut WeaponTrail) {
1368 trail.set_combo_intensity(self.intensity_multiplier);
1369 }
1370}
1371
1372impl Default for ComboTrailIntegration {
1373 fn default() -> Self {
1374 Self::new()
1375 }
1376}
1377
1378#[derive(Debug, Clone)]
1384pub struct ComboMilestoneEffect {
1385 pub tier: u32,
1387 pub particle_count: usize,
1389 pub color: [f32; 4],
1391 pub emission: f32,
1393 pub shockwave_radius: f32,
1395 pub flash_intensity: f32,
1397 pub duration: f32,
1399 pub remaining: f32,
1401}
1402
1403impl ComboMilestoneEffect {
1404 pub fn for_tier(tier: u32) -> Self {
1406 let scale = match tier {
1407 10 => 1.0,
1408 25 => 1.8,
1409 50 => 3.0,
1410 100 => 5.0,
1411 _ => 1.0,
1412 };
1413 let duration = 0.5 + scale * 0.2;
1414 Self {
1415 tier,
1416 particle_count: (20.0 * scale) as usize,
1417 color: match tier {
1418 10 => [1.0, 0.8, 0.2, 1.0], 25 => [0.2, 0.8, 1.0, 1.0], 50 => [1.0, 0.3, 0.8, 1.0], 100 => [1.0, 1.0, 1.0, 1.0], _ => [1.0, 1.0, 1.0, 1.0],
1423 },
1424 emission: 3.0 * scale,
1425 shockwave_radius: 0.5 * scale,
1426 flash_intensity: 0.3 * scale,
1427 duration,
1428 remaining: duration,
1429 }
1430 }
1431
1432 pub fn update(&mut self, dt: f32) {
1434 self.remaining -= dt;
1435 }
1436
1437 pub fn finished(&self) -> bool {
1439 self.remaining <= 0.0
1440 }
1441}
1442
1443#[derive(Debug, Clone)]
1449pub struct BlockEffect {
1450 pub contact_point: Vec3,
1452 pub sparks: Vec<SparkParticle>,
1454 pub pushback_direction: Vec3,
1456 pub pushback_force: f32,
1458 pub trail_bounce: bool,
1460 pub duration: f32,
1462 pub max_duration: f32,
1464}
1465
1466#[derive(Debug, Clone)]
1468pub struct SparkParticle {
1469 pub position: Vec3,
1470 pub velocity: Vec3,
1471 pub color: [f32; 4],
1472 pub lifetime: f32,
1473 pub max_lifetime: f32,
1474 pub size: f32,
1475}
1476
1477impl SparkParticle {
1478 pub fn spawn(origin: Vec3, index: usize) -> Self {
1479 let angle = (index as f32) * 0.7;
1480 let speed = 4.0 + (index as f32) * 0.5;
1481 Self {
1482 position: origin,
1483 velocity: Vec3::new(
1484 angle.cos() * speed,
1485 2.0 + (index as f32 % 3.0) * 1.5,
1486 angle.sin() * speed,
1487 ),
1488 color: [1.0, 0.9, 0.3, 1.0],
1489 lifetime: 0.3 + (index as f32) * 0.02,
1490 max_lifetime: 0.3 + (index as f32) * 0.02,
1491 size: 0.05 + (index as f32) * 0.005,
1492 }
1493 }
1494
1495 pub fn update(&mut self, dt: f32) {
1496 self.lifetime -= dt;
1497 self.position += self.velocity * dt;
1498 self.velocity.y -= GRAVITY * dt;
1499 self.velocity *= (1.0 - 4.0 * dt).max(0.0);
1500 self.color[3] = (self.lifetime / self.max_lifetime).clamp(0.0, 1.0);
1501 self.size *= (1.0 - 2.0 * dt).max(0.01);
1502 }
1503
1504 pub fn dead(&self) -> bool {
1505 self.lifetime <= 0.0
1506 }
1507}
1508
1509impl BlockEffect {
1510 pub fn generate(
1512 contact_point: Vec3,
1513 attacker_direction: Vec3,
1514 weapon_mass: f32,
1515 velocity_magnitude: f32,
1516 ) -> Self {
1517 let spark_count = (8.0 + weapon_mass * 2.0).min(20.0) as usize;
1518 let sparks: Vec<SparkParticle> = (0..spark_count)
1519 .map(|i| SparkParticle::spawn(contact_point, i))
1520 .collect();
1521 let pushback = -attacker_direction.normalize_or_zero();
1522 let force = (weapon_mass * velocity_magnitude * 0.05).clamp(0.5, 5.0);
1523 let dur = 0.3 + force * 0.05;
1524 Self {
1525 contact_point,
1526 sparks,
1527 pushback_direction: pushback,
1528 pushback_force: force,
1529 trail_bounce: true,
1530 duration: dur,
1531 max_duration: dur,
1532 }
1533 }
1534
1535 pub fn update(&mut self, dt: f32) {
1537 self.duration -= dt;
1538 for s in self.sparks.iter_mut() {
1539 s.update(dt);
1540 }
1541 self.sparks.retain(|s| !s.dead());
1542 let decay = (self.duration / self.max_duration).clamp(0.0, 1.0);
1544 self.pushback_force *= decay;
1545 }
1546
1547 pub fn finished(&self) -> bool {
1549 self.duration <= 0.0 && self.sparks.is_empty()
1550 }
1551}
1552
1553#[derive(Debug, Clone)]
1559pub struct ParryEffect {
1560 pub contact_point: Vec3,
1562 pub time_scale: f32,
1564 pub slow_duration: f32,
1566 pub elapsed: f32,
1568 pub flash_intensity: f32,
1570 pub attacker_stunned: bool,
1572 pub stun_duration: f32,
1574 pub burst_particles: Vec<ParryBurstParticle>,
1576 pub consumed: bool,
1578}
1579
1580#[derive(Debug, Clone)]
1582pub struct ParryBurstParticle {
1583 pub position: Vec3,
1584 pub velocity: Vec3,
1585 pub color: [f32; 4],
1586 pub lifetime: f32,
1587 pub max_lifetime: f32,
1588 pub size: f32,
1589 pub emission: f32,
1590}
1591
1592impl ParryBurstParticle {
1593 pub fn spawn(origin: Vec3, index: usize) -> Self {
1594 let angle = (index as f32) * 0.5;
1595 let elevation = ((index as f32) * 0.37).sin() * 0.8;
1596 let speed = 6.0 + (index as f32) * 0.3;
1597 let lifetime = 0.5 + (index as f32) * 0.03;
1598 Self {
1599 position: origin,
1600 velocity: Vec3::new(
1601 angle.cos() * speed,
1602 elevation * speed,
1603 angle.sin() * speed,
1604 ),
1605 color: [1.0, 1.0, 0.9, 1.0],
1606 lifetime,
1607 max_lifetime: lifetime,
1608 size: 0.08,
1609 emission: 5.0,
1610 }
1611 }
1612
1613 pub fn update(&mut self, dt: f32) {
1614 self.lifetime -= dt;
1615 self.position += self.velocity * dt;
1616 self.velocity *= (1.0 - 3.0 * dt).max(0.0);
1617 let age_ratio = (self.lifetime / self.max_lifetime).clamp(0.0, 1.0);
1618 self.color[3] = age_ratio;
1619 self.emission *= age_ratio;
1620 self.size *= (1.0 - 1.5 * dt).max(0.01);
1621 }
1622
1623 pub fn dead(&self) -> bool {
1624 self.lifetime <= 0.0
1625 }
1626}
1627
1628impl ParryEffect {
1629 pub fn generate(contact_point: Vec3) -> Self {
1631 let particle_count = 30;
1632 let burst: Vec<ParryBurstParticle> = (0..particle_count)
1633 .map(|i| ParryBurstParticle::spawn(contact_point, i))
1634 .collect();
1635 Self {
1636 contact_point,
1637 time_scale: PARRY_TIME_SCALE,
1638 slow_duration: PARRY_SLOW_DURATION,
1639 elapsed: 0.0,
1640 flash_intensity: 3.0,
1641 attacker_stunned: true,
1642 stun_duration: 1.0,
1643 burst_particles: burst,
1644 consumed: false,
1645 }
1646 }
1647
1648 pub fn update(&mut self, dt: f32) {
1651 self.elapsed += dt;
1652
1653 self.flash_intensity = (self.flash_intensity - dt * 10.0).max(0.0);
1655
1656 if self.elapsed >= self.slow_duration {
1658 let ramp = ((self.elapsed - self.slow_duration) / 0.3).clamp(0.0, 1.0);
1659 self.time_scale = PARRY_TIME_SCALE + (1.0 - PARRY_TIME_SCALE) * ramp;
1660 }
1661
1662 for p in self.burst_particles.iter_mut() {
1664 p.update(dt);
1665 }
1666 self.burst_particles.retain(|p| !p.dead());
1667
1668 if self.attacker_stunned {
1670 self.stun_duration -= dt;
1671 if self.stun_duration <= 0.0 {
1672 self.attacker_stunned = false;
1673 }
1674 }
1675
1676 if self.time_scale >= 0.99
1678 && self.flash_intensity <= 0.0
1679 && self.burst_particles.is_empty()
1680 && !self.attacker_stunned
1681 {
1682 self.consumed = true;
1683 }
1684 }
1685
1686 pub fn current_time_scale(&self) -> f32 {
1688 self.time_scale
1689 }
1690
1691 pub fn finished(&self) -> bool {
1693 self.consumed
1694 }
1695}
1696
1697#[derive(Debug, Clone)]
1704pub struct WeaponPhysicsSystem {
1705 pub trail: WeaponTrail,
1707 pub impacts: Vec<ImpactEffect>,
1709 pub damage_numbers: DamageNumberManager,
1711 pub combo_integration: ComboTrailIntegration,
1713 pub milestone_effects: Vec<ComboMilestoneEffect>,
1715 pub block_effects: Vec<BlockEffect>,
1717 pub parry_effect: Option<ParryEffect>,
1719 pub time_scale: f32,
1721}
1722
1723impl WeaponPhysicsSystem {
1724 pub fn new(weapon_type: WeaponType) -> Self {
1726 let profile = WeaponProfiles::get(weapon_type);
1727 Self {
1728 trail: WeaponTrail::new(profile),
1729 impacts: Vec::new(),
1730 damage_numbers: DamageNumberManager::default(),
1731 combo_integration: ComboTrailIntegration::new(),
1732 milestone_effects: Vec::new(),
1733 block_effects: Vec::new(),
1734 parry_effect: None,
1735 time_scale: 1.0,
1736 }
1737 }
1738
1739 pub fn switch_weapon(&mut self, weapon_type: WeaponType) {
1741 let profile = WeaponProfiles::get(weapon_type);
1742 self.trail = WeaponTrail::new(profile);
1743 }
1744
1745 pub fn begin_swing(&mut self, arc: SwingArc) {
1747 self.trail.begin_swing(arc);
1748 }
1749
1750 pub fn on_hit(
1752 &mut self,
1753 contact_point: Vec3,
1754 velocity_magnitude: f32,
1755 damage: i32,
1756 crit: bool,
1757 screen_pos: Vec2,
1758 hit_direction: Vec3,
1759 ) {
1760 self.trail.on_impact(contact_point);
1762
1763 let impact = ImpactEffect::generate(
1765 &self.trail.profile,
1766 contact_point,
1767 velocity_magnitude,
1768 damage,
1769 crit,
1770 screen_pos,
1771 hit_direction,
1772 );
1773 self.impacts.push(impact);
1774
1775 self.damage_numbers.spawn(damage, contact_point + Vec3::Y * 0.5, crit, self.trail.profile.element);
1777 }
1778
1779 pub fn on_block(
1781 &mut self,
1782 contact_point: Vec3,
1783 attacker_direction: Vec3,
1784 velocity_magnitude: f32,
1785 ) {
1786 let block = BlockEffect::generate(
1787 contact_point,
1788 attacker_direction,
1789 self.trail.profile.mass,
1790 velocity_magnitude,
1791 );
1792 self.block_effects.push(block);
1793 }
1794
1795 pub fn on_parry(&mut self, contact_point: Vec3) {
1797 let parry = ParryEffect::generate(contact_point);
1798 self.parry_effect = Some(parry);
1799 }
1800
1801 pub fn update_combo(&mut self, combo_count: u32) {
1803 self.combo_integration.set_combo(combo_count);
1804 self.combo_integration.apply_to_trail(&mut self.trail);
1805 if let Some(tier) = self.combo_integration.take_milestone() {
1806 self.milestone_effects.push(ComboMilestoneEffect::for_tier(tier));
1807 }
1808 }
1809
1810 pub fn update(&mut self, dt: f32) {
1812 self.time_scale = if let Some(ref parry) = self.parry_effect {
1814 parry.current_time_scale()
1815 } else {
1816 1.0
1817 };
1818 let game_dt = dt * self.time_scale;
1819
1820 self.trail.update(game_dt);
1822
1823 for impact in self.impacts.iter_mut() {
1825 impact.update(game_dt);
1826 }
1827 self.impacts.retain(|i| !i.consumed);
1828
1829 self.damage_numbers.update(game_dt);
1831
1832 for m in self.milestone_effects.iter_mut() {
1834 m.update(game_dt);
1835 }
1836 self.milestone_effects.retain(|m| !m.finished());
1837
1838 for b in self.block_effects.iter_mut() {
1840 b.update(game_dt);
1841 }
1842 self.block_effects.retain(|b| !b.finished());
1843
1844 if let Some(ref mut parry) = self.parry_effect {
1846 parry.update(dt);
1847 if parry.finished() {
1848 self.parry_effect = None;
1849 }
1850 }
1851 }
1852
1853 pub fn trail_vertices(&self) -> Vec<TrailVertex> {
1855 self.trail.get_render_data()
1856 }
1857}
1858
1859fn pseudo_random_from_pos(p: Vec3) -> f32 {
1865 let seed = (p.x * 12.9898 + p.y * 78.233 + p.z * 45.164).sin() * 43758.5453;
1866 seed.fract().abs()
1867}
1868
1869fn pseudo_random_component(v: f32) -> f32 {
1871 let seed = (v * 127.1 + 311.7).sin() * 43758.5453;
1872 seed.fract() * 2.0 - 1.0
1873}
1874
1875#[cfg(test)]
1880mod tests {
1881 use super::*;
1882 use std::f32::consts::PI;
1883
1884 #[test]
1887 fn swing_arc_sample_start_and_end() {
1888 let arc = SwingArc::new(0.0, PI, 1.0, Vec3::ZERO, 2.0);
1889 let start = arc.sample(0.0);
1890 let end = arc.sample(1.0);
1891 assert!((start.x - 2.0).abs() < 0.001);
1893 assert!(start.z.abs() < 0.001);
1894 assert!((end.x + 2.0).abs() < 0.001);
1896 assert!(end.z.abs() < 0.01);
1897 }
1898
1899 #[test]
1900 fn swing_arc_sample_with_origin() {
1901 let origin = Vec3::new(5.0, 0.0, 3.0);
1902 let arc = SwingArc::new(0.0, PI, 1.0, origin, 1.0);
1903 let mid = arc.sample(0.5);
1904 assert!((mid.x - 5.0).abs() < 0.01);
1906 assert!((mid.z - 4.0).abs() < 0.01);
1907 }
1908
1909 #[test]
1910 fn swing_arc_velocity_direction() {
1911 let arc = SwingArc::new(0.0, PI, 1.0, Vec3::ZERO, 2.0);
1912 let vel = arc.velocity_at(0.0);
1913 assert!(vel.x.abs() < 0.01);
1916 assert!((vel.z - 2.0 * PI).abs() < 0.1);
1917 }
1918
1919 #[test]
1920 fn swing_arc_progress_clamp() {
1921 let mut arc = SwingArc::new(0.0, PI, 1.0, Vec3::ZERO, 1.0);
1922 assert!((arc.progress() - 0.0).abs() < 0.001);
1923 arc.elapsed = 0.5;
1924 assert!((arc.progress() - 0.5).abs() < 0.001);
1925 arc.elapsed = 2.0;
1926 assert!((arc.progress() - 1.0).abs() < 0.001);
1927 }
1928
1929 #[test]
1930 fn swing_arc_tick_finishes() {
1931 let mut arc = SwingArc::new(0.0, PI, 0.5, Vec3::ZERO, 1.0);
1932 assert!(arc.tick(0.3));
1933 assert!(!arc.finished());
1934 assert!(!arc.tick(0.3));
1935 assert!(arc.finished());
1936 }
1937
1938 #[test]
1941 fn trail_segment_ages() {
1942 let mut seg = WeaponTrailSegment::new(
1943 Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), 0.1, [1.0; 4], 1.0,
1944 );
1945 assert!((seg.age - 0.0).abs() < 0.001);
1946 seg.update(0.1);
1947 assert!((seg.age - 0.1).abs() < 0.001);
1948 assert!(seg.position.x > 0.0);
1950 }
1951
1952 #[test]
1953 fn trail_segment_alpha_fades() {
1954 let mut seg = WeaponTrailSegment::new(
1955 Vec3::ZERO, Vec3::ZERO, 0.1, [1.0; 4], 1.0,
1956 );
1957 assert!((seg.alpha(1.0) - 1.0).abs() < 0.001);
1958 seg.age = 0.5;
1959 assert!((seg.alpha(1.0) - 0.5).abs() < 0.001);
1960 seg.age = 1.0;
1961 assert!((seg.alpha(1.0) - 0.0).abs() < 0.001);
1962 }
1963
1964 #[test]
1965 fn trail_segment_velocity_dampens() {
1966 let mut seg = WeaponTrailSegment::new(
1967 Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0), 0.1, [1.0; 4], 1.0,
1968 );
1969 let initial_speed = seg.velocity.length();
1970 seg.update(0.1);
1971 assert!(seg.velocity.length() < initial_speed);
1972 }
1973
1974 #[test]
1977 fn damage_number_moves_upward_initially() {
1978 let mut dmg = DamageNumber::new(100, Vec3::ZERO, false, None);
1979 let initial_y = dmg.position.y;
1980 dmg.update(0.05);
1981 assert!(dmg.position.y > initial_y);
1982 }
1983
1984 #[test]
1985 fn damage_number_arcs_back_down() {
1986 let mut dmg = DamageNumber::new(100, Vec3::ZERO, false, None);
1987 for _ in 0..10 {
1989 dmg.update(0.05);
1990 }
1991 let peak_y = dmg.position.y;
1992 for _ in 0..30 {
1994 dmg.update(0.05);
1995 }
1996 assert!(dmg.position.y < peak_y);
1997 }
1998
1999 #[test]
2000 fn damage_number_crit_is_larger() {
2001 let normal = DamageNumber::new(100, Vec3::ZERO, false, None);
2002 let crit = DamageNumber::new(100, Vec3::ZERO, true, None);
2003 assert!(crit.scale > normal.scale);
2004 }
2005
2006 #[test]
2007 fn damage_number_fades_alpha() {
2008 let mut dmg = DamageNumber::new(50, Vec3::ZERO, false, None);
2009 assert!((dmg.color[3] - 1.0).abs() < 0.01);
2010 for _ in 0..20 {
2011 dmg.update(0.05);
2012 }
2013 assert!(dmg.color[3] < 1.0);
2014 }
2015
2016 #[test]
2017 fn damage_number_dies_after_lifetime() {
2018 let mut dmg = DamageNumber::new(50, Vec3::ZERO, false, None);
2019 assert!(!dmg.dead());
2020 for _ in 0..100 {
2021 dmg.update(0.05);
2022 }
2023 assert!(dmg.dead());
2024 }
2025
2026 #[test]
2029 fn damage_manager_spawns_and_updates() {
2030 let mut mgr = DamageNumberManager::new(5);
2031 mgr.spawn(100, Vec3::ZERO, false, None);
2032 mgr.spawn(200, Vec3::ONE, true, Some(Element::Fire));
2033 assert_eq!(mgr.active_count(), 2);
2034 for _ in 0..100 {
2036 mgr.update(0.05);
2037 }
2038 assert_eq!(mgr.active_count(), 0);
2039 }
2040
2041 #[test]
2042 fn damage_manager_replaces_oldest_when_full() {
2043 let mut mgr = DamageNumberManager::new(3);
2044 mgr.spawn(1, Vec3::ZERO, false, None);
2045 mgr.spawn(2, Vec3::ZERO, false, None);
2046 mgr.spawn(3, Vec3::ZERO, false, None);
2047 assert_eq!(mgr.active_count(), 3);
2048 mgr.update(0.5); mgr.spawn(4, Vec3::ZERO, false, None);
2051 assert_eq!(mgr.active_count(), 3);
2052 }
2053
2054 #[test]
2057 fn trail_spawns_segments_during_swing() {
2058 let profile = WeaponProfiles::get(WeaponType::Sword);
2059 let mut trail = WeaponTrail::new(profile);
2060 let arc = SwingArc::new(0.0, PI, 0.5, Vec3::ZERO, 1.0);
2061 trail.begin_swing(arc);
2062 for _ in 0..20 {
2064 trail.update(0.025);
2065 }
2066 let verts = trail.get_render_data();
2067 assert!(!verts.is_empty());
2069 }
2070
2071 #[test]
2072 fn trail_impact_modifies_segments() {
2073 let profile = WeaponProfiles::get(WeaponType::Axe);
2074 let mut trail = WeaponTrail::new(profile);
2075 let arc = SwingArc::new(0.0, PI, 0.5, Vec3::ZERO, 1.0);
2076 trail.begin_swing(arc);
2077 for _ in 0..10 {
2078 trail.update(0.025);
2079 }
2080 trail.on_impact(Vec3::new(0.5, 0.0, 0.5));
2081 for _ in 0..10 {
2083 trail.update(0.025);
2084 }
2085 }
2086
2087 #[test]
2090 fn trail_ribbon_vertex_pairs() {
2091 let profile = WeaponProfiles::get(WeaponType::Sword);
2092 let mut trail = WeaponTrail::new(profile);
2093 let arc = SwingArc::new(0.0, PI, 0.3, Vec3::ZERO, 1.0);
2094 trail.begin_swing(arc);
2095 for _ in 0..15 {
2096 trail.update(0.02);
2097 }
2098 let verts = trail.get_render_data();
2099 assert_eq!(verts.len() % 2, 0);
2101 }
2102
2103 #[test]
2104 fn trail_ribbon_indices_valid() {
2105 let indices = TrailRibbon::build_indices(8);
2106 assert!(!indices.is_empty());
2107 assert_eq!(indices.len(), 18);
2109 for &idx in &indices {
2110 assert!(idx < 8);
2111 }
2112 }
2113
2114 #[test]
2117 fn camera_shake_decays() {
2118 let mut shake = CameraShake::from_impact(3.0, 10.0);
2119 assert!(!shake.finished());
2120 let initial_intensity = shake.intensity;
2121 for _ in 0..50 {
2122 shake.update(0.02);
2123 }
2124 assert!(shake.finished() || shake.offset.length() < initial_intensity);
2125 }
2126
2127 #[test]
2128 fn camera_shake_zero_mass() {
2129 let shake = CameraShake::from_impact(0.0, 0.0);
2130 assert!(shake.intensity <= 0.01);
2131 }
2132
2133 #[test]
2136 fn combo_integration_milestones() {
2137 let mut combo = ComboTrailIntegration::new();
2138 combo.set_combo(9);
2139 assert!(!combo.milestone_pending);
2140 combo.set_combo(10);
2141 assert!(combo.milestone_pending);
2142 assert_eq!(combo.milestone_tier, 10);
2143 let tier = combo.take_milestone();
2144 assert_eq!(tier, Some(10));
2145 assert!(!combo.milestone_pending);
2146 }
2147
2148 #[test]
2149 fn combo_integration_scaling() {
2150 let mut combo = ComboTrailIntegration::new();
2151 combo.set_combo(1);
2152 let w1 = combo.width_multiplier;
2153 combo.set_combo(50);
2154 let w50 = combo.width_multiplier;
2155 assert!(w50 > w1);
2156 }
2157
2158 #[test]
2161 fn impact_effect_generates_all_components() {
2162 let profile = WeaponProfiles::get(WeaponType::Mace);
2163 let impact = ImpactEffect::generate(
2164 &profile,
2165 Vec3::new(1.0, 0.0, 1.0),
2166 15.0,
2167 250,
2168 true,
2169 Vec2::new(0.5, 0.5),
2170 Vec3::new(1.0, 0.0, 0.0),
2171 );
2172 assert!(!impact.consumed);
2173 assert!(!impact.debris.is_empty());
2174 assert!(impact.damage_number.crit);
2175 assert_eq!(impact.damage_number.value, 250);
2176 }
2177
2178 #[test]
2179 fn impact_effect_eventually_consumed() {
2180 let profile = WeaponProfiles::get(WeaponType::Dagger);
2181 let mut impact = ImpactEffect::generate(
2182 &profile,
2183 Vec3::ZERO,
2184 5.0,
2185 30,
2186 false,
2187 Vec2::new(0.5, 0.5),
2188 Vec3::X,
2189 );
2190 for _ in 0..200 {
2191 impact.update(0.05);
2192 }
2193 assert!(impact.consumed);
2194 }
2195
2196 #[test]
2199 fn block_effect_pushback_direction() {
2200 let block = BlockEffect::generate(
2201 Vec3::ZERO,
2202 Vec3::new(1.0, 0.0, 0.0),
2203 2.0,
2204 10.0,
2205 );
2206 assert!(block.pushback_direction.x < 0.0);
2208 }
2209
2210 #[test]
2211 fn block_effect_finishes() {
2212 let mut block = BlockEffect::generate(
2213 Vec3::ZERO,
2214 Vec3::X,
2215 1.0,
2216 5.0,
2217 );
2218 for _ in 0..100 {
2219 block.update(0.05);
2220 }
2221 assert!(block.finished());
2222 }
2223
2224 #[test]
2227 fn parry_effect_slows_time() {
2228 let parry = ParryEffect::generate(Vec3::ZERO);
2229 assert!((parry.time_scale - PARRY_TIME_SCALE).abs() < 0.01);
2230 assert!(parry.attacker_stunned);
2231 }
2232
2233 #[test]
2234 fn parry_effect_time_returns_to_normal() {
2235 let mut parry = ParryEffect::generate(Vec3::ZERO);
2236 for _ in 0..200 {
2237 parry.update(0.02);
2238 }
2239 assert!(parry.time_scale > 0.95);
2240 }
2241
2242 #[test]
2243 fn parry_effect_finishes() {
2244 let mut parry = ParryEffect::generate(Vec3::ZERO);
2245 for _ in 0..300 {
2246 parry.update(0.02);
2247 }
2248 assert!(parry.finished());
2249 }
2250
2251 #[test]
2254 fn system_swing_and_hit() {
2255 let mut sys = WeaponPhysicsSystem::new(WeaponType::Sword);
2256 let arc = SwingArc::new(0.0, PI, 0.3, Vec3::ZERO, 1.0);
2257 sys.begin_swing(arc);
2258 for _ in 0..10 {
2259 sys.update(0.02);
2260 }
2261 sys.on_hit(
2262 Vec3::new(1.0, 0.0, 0.0),
2263 8.0, 100, false,
2264 Vec2::new(0.5, 0.5),
2265 Vec3::X,
2266 );
2267 assert_eq!(sys.impacts.len(), 1);
2268 assert_eq!(sys.damage_numbers.active_count(), 1);
2269 }
2270
2271 #[test]
2272 fn system_combo_milestones() {
2273 let mut sys = WeaponPhysicsSystem::new(WeaponType::Fist);
2274 sys.update_combo(9);
2275 assert!(sys.milestone_effects.is_empty());
2276 sys.update_combo(10);
2277 assert_eq!(sys.milestone_effects.len(), 1);
2278 assert_eq!(sys.milestone_effects[0].tier, 10);
2279 }
2280
2281 #[test]
2282 fn system_parry_slows_game() {
2283 let mut sys = WeaponPhysicsSystem::new(WeaponType::Sword);
2284 sys.on_parry(Vec3::ZERO);
2285 sys.update(0.01);
2286 assert!(sys.time_scale < 1.0);
2287 }
2288
2289 #[test]
2292 fn all_weapon_profiles_valid() {
2293 for &wt in WeaponType::all() {
2294 let p = WeaponProfiles::get(wt);
2295 assert!(p.mass > 0.0, "{:?} mass must be positive", wt);
2296 assert!(p.length > 0.0, "{:?} length must be positive", wt);
2297 assert!(p.swing_speed > 0.0, "{:?} swing_speed must be positive", wt);
2298 assert!(p.impact_force > 0.0, "{:?} impact_force must be positive", wt);
2299 assert!(p.trail_width > 0.0, "{:?} trail_width must be positive", wt);
2300 assert!(p.trail_segments > 0, "{:?} trail_segments must be > 0", wt);
2301 }
2302 }
2303
2304 #[test]
2305 fn sword_is_faster_than_axe() {
2306 let sword = WeaponProfiles::get(WeaponType::Sword);
2307 let axe = WeaponProfiles::get(WeaponType::Axe);
2308 assert!(sword.swing_speed > axe.swing_speed);
2309 assert!(sword.mass < axe.mass);
2310 }
2311
2312 #[test]
2313 fn dagger_is_fastest() {
2314 let dagger = WeaponProfiles::get(WeaponType::Dagger);
2315 for &wt in WeaponType::all() {
2316 if wt == WeaponType::Dagger || wt == WeaponType::Fist {
2317 continue;
2318 }
2319 let p = WeaponProfiles::get(wt);
2320 assert!(
2321 dagger.swing_speed >= p.swing_speed,
2322 "Dagger should be faster than {:?}", wt
2323 );
2324 }
2325 }
2326
2327 #[test]
2328 fn element_effects_for_all_elements() {
2329 let elements = [
2330 Element::Physical, Element::Fire, Element::Ice, Element::Lightning,
2331 Element::Void, Element::Entropy, Element::Gravity, Element::Radiant,
2332 Element::Shadow, Element::Temporal,
2333 ];
2334 for el in &elements {
2335 let eff = ElementEffect::for_element(*el);
2336 assert!(eff.particle_count > 0);
2337 assert!(eff.duration > 0.0);
2338 }
2339 }
2340
2341 #[test]
2342 fn shockwave_ring_expands() {
2343 let mut ring = ShockwaveRing::new(Vec2::new(0.5, 0.5), 1.0);
2344 let r0 = ring.radius;
2345 ring.update(0.1);
2346 assert!(ring.radius > r0);
2347 }
2348
2349 #[test]
2350 fn shockwave_ring_finishes() {
2351 let mut ring = ShockwaveRing::new(Vec2::new(0.5, 0.5), 1.0);
2352 for _ in 0..100 {
2353 ring.update(0.05);
2354 }
2355 assert!(ring.finished());
2356 }
2357}