wow_m2/particles/
particle.rs

1//! Individual particle representation
2
3/// A single particle in the system
4#[derive(Debug, Clone, Default)]
5pub struct Particle {
6    /// Current age in seconds
7    pub age: f32,
8    /// Total lifespan in seconds
9    pub lifespan: f32,
10    /// Current color (RGBA, 0.0-1.0)
11    pub color: [f32; 4],
12    /// Current scale (width, height)
13    pub scale: [f32; 2],
14    /// Texture coordinates for head cell
15    pub tex_coord_head: [f32; 2],
16    /// Texture coordinates for tail cell (for ribbon particles)
17    pub tex_coord_tail: [f32; 2],
18    /// World-space position
19    pub position: [f32; 3],
20    /// Velocity vector
21    pub velocity: [f32; 3],
22}
23
24impl Particle {
25    /// Create a new particle with initial position, velocity, and lifespan
26    pub fn new(position: [f32; 3], velocity: [f32; 3], lifespan: f32) -> Self {
27        Self {
28            age: 0.0,
29            lifespan,
30            color: [1.0, 1.0, 1.0, 1.0],
31            scale: [1.0, 1.0],
32            tex_coord_head: [0.0, 0.0],
33            tex_coord_tail: [0.0, 0.0],
34            position,
35            velocity,
36        }
37    }
38
39    /// Check if the particle is still alive
40    #[inline]
41    pub fn is_alive(&self) -> bool {
42        self.age < self.lifespan
43    }
44
45    /// Get the age as a percentage of lifespan (0.0 to 1.0)
46    #[inline]
47    pub fn age_percent(&self) -> f32 {
48        if self.lifespan > 0.0 {
49            (self.age / self.lifespan).clamp(0.0, 1.0)
50        } else {
51            1.0
52        }
53    }
54
55    /// Update particle physics
56    ///
57    /// # Arguments
58    /// * `dt` - Delta time in seconds
59    /// * `force` - Combined force vector (wind - gravity)
60    /// * `drag` - Drag coefficient (0.0 = no drag, 1.0 = full drag)
61    pub fn update_physics(&mut self, dt: f32, force: [f32; 3], drag: f32) {
62        // Apply forces to velocity
63        self.velocity[0] += force[0] * dt;
64        self.velocity[1] += force[1] * dt;
65        self.velocity[2] += force[2] * dt;
66
67        // Apply drag
68        if drag > 0.0 {
69            let drag_factor = (1.0 - drag).powf(dt);
70            self.velocity[0] *= drag_factor;
71            self.velocity[1] *= drag_factor;
72            self.velocity[2] *= drag_factor;
73        }
74
75        // Update position
76        self.position[0] += self.velocity[0] * dt;
77        self.position[1] += self.velocity[1] * dt;
78        self.position[2] += self.velocity[2] * dt;
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_particle_new() {
88        let p = Particle::new([1.0, 2.0, 3.0], [0.1, 0.2, 0.3], 5.0);
89        assert_eq!(p.position, [1.0, 2.0, 3.0]);
90        assert_eq!(p.velocity, [0.1, 0.2, 0.3]);
91        assert_eq!(p.lifespan, 5.0);
92        assert_eq!(p.age, 0.0);
93        assert!(p.is_alive());
94    }
95
96    #[test]
97    fn test_particle_age_percent() {
98        let mut p = Particle::new([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 10.0);
99        assert_eq!(p.age_percent(), 0.0);
100
101        p.age = 5.0;
102        assert_eq!(p.age_percent(), 0.5);
103
104        p.age = 10.0;
105        assert_eq!(p.age_percent(), 1.0);
106
107        p.age = 15.0; // Past lifespan
108        assert_eq!(p.age_percent(), 1.0); // Clamped
109    }
110
111    #[test]
112    fn test_particle_is_alive() {
113        let mut p = Particle::new([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], 5.0);
114        assert!(p.is_alive());
115
116        p.age = 4.9;
117        assert!(p.is_alive());
118
119        p.age = 5.0;
120        assert!(!p.is_alive());
121
122        p.age = 6.0;
123        assert!(!p.is_alive());
124    }
125
126    #[test]
127    fn test_particle_physics() {
128        let mut p = Particle::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 10.0);
129
130        // Apply gravity (downward force)
131        p.update_physics(1.0, [0.0, 0.0, -9.8], 0.0);
132
133        // Velocity should be affected
134        assert!((p.velocity[2] - (-9.8)).abs() < 0.001);
135
136        // Position should be updated
137        assert!((p.position[0] - 1.0).abs() < 0.001);
138    }
139
140    #[test]
141    fn test_particle_drag() {
142        let mut p = Particle::new([0.0, 0.0, 0.0], [10.0, 0.0, 0.0], 10.0);
143
144        // Apply drag
145        p.update_physics(1.0, [0.0, 0.0, 0.0], 0.5);
146
147        // Velocity should be reduced
148        assert!(p.velocity[0] < 10.0);
149        assert!(p.velocity[0] > 0.0);
150    }
151}