1use glam::Vec2;
7
8#[derive(Debug, Clone, Copy)]
12pub struct AABB {
13 pub center: Vec2,
14 pub size: Vec2,
15}
16
17impl AABB {
18 pub fn new(center: Vec2, size: Vec2) -> Self {
20 Self { center, size }
21 }
22
23 pub fn from_top_left(top_left: Vec2, size: Vec2) -> Self {
25 let center = top_left + size * 0.5;
26 Self { center, size }
27 }
28
29 pub fn min(&self) -> Vec2 {
34 self.center - self.size * 0.5
35 }
36
37 pub fn max(&self) -> Vec2 {
39 self.center + self.size * 0.5
40 }
41
42 pub fn top_left(&self) -> Vec2 {
44 self.min()
45 }
46
47 pub fn check_collision(&self, other: &AABB) -> bool {
51 let self_min = self.min();
52 let self_max = self.max();
53 let other_min = other.min();
54 let other_max = other.max();
55
56 self_min.x < other_max.x
57 && self_max.x > other_min.x
58 && self_min.y < other_max.y
59 && self_max.y > other_min.y
60 }
61
62 pub fn get_overlap(&self, other: &AABB) -> Vec2 {
65 let self_min = self.min();
66 let self_max = self.max();
67 let other_min = other.min();
68 let other_max = other.max();
69
70 let x_overlap = (self_max.x - other_min.x).min(other_max.x - self_min.x);
71 let y_overlap = (self_max.y - other_min.y).min(other_max.y - self_min.y);
72
73 Vec2::new(x_overlap, y_overlap)
74 }
75
76 pub fn resolve_collision(mover: &AABB, obstacle: &AABB) -> Option<Vec2> {
84 let overlap = mover.get_overlap(obstacle);
85 if overlap.x <= 0.0 || overlap.y <= 0.0 {
86 return None;
87 }
88 if overlap.x < overlap.y {
89 let sign = (mover.center.x - obstacle.center.x).signum();
90 let sign = if sign == 0.0 { 1.0 } else { sign };
91 Some(Vec2::new(overlap.x * sign, 0.0))
92 } else {
93 let sign = (mover.center.y - obstacle.center.y).signum();
94 let sign = if sign == 0.0 { -1.0 } else { sign };
95 Some(Vec2::new(0.0, overlap.y * sign))
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub enum CollisionLayer {
103 Pushbox,
104 Hurtbox,
105 Hitbox,
106 Parrybox,
107}
108
109#[derive(Debug, Clone, Copy)]
111pub struct BoxVolume {
112 pub layer: CollisionLayer,
113 pub local_offset: Vec2,
114 pub size: Vec2,
115 pub active: bool,
116}
117
118impl BoxVolume {
119 pub fn new(layer: CollisionLayer, offset: Vec2, size: Vec2) -> Self {
120 Self {
121 layer,
122 local_offset: offset,
123 size,
124 active: true,
125 }
126 }
127
128 pub fn world_aabb(&self, entity_pos: Vec2, facing_right: bool) -> AABB {
130 let flip = if facing_right { 1.0 } else { -1.0 };
131 let center = entity_pos + Vec2::new(self.local_offset.x * flip, self.local_offset.y);
132 AABB::new(center, self.size)
133 }
134}
135
136#[derive(Debug, Clone, Copy)]
137pub struct SweepResult {
138 pub time: f32,
139 pub normal: Vec2,
140}
141
142impl AABB {
143 pub fn swept_collision(&self, displacement: Vec2, obstacle: &AABB) -> Option<SweepResult> {
146 if displacement.x == 0.0 && displacement.y == 0.0 {
147 return None;
148 }
149
150 let expanded_half = (obstacle.size + self.size) * 0.5;
152 let exp_min = obstacle.center - expanded_half;
153 let exp_max = obstacle.center + expanded_half;
154 let origin = self.center;
155
156 let (t_near_x, t_far_x) = if displacement.x.abs() > f32::EPSILON {
157 let t1 = (exp_min.x - origin.x) / displacement.x;
158 let t2 = (exp_max.x - origin.x) / displacement.x;
159 (t1.min(t2), t1.max(t2))
160 } else if origin.x >= exp_min.x && origin.x <= exp_max.x {
161 (f32::NEG_INFINITY, f32::INFINITY)
162 } else {
163 return None;
164 };
165
166 let (t_near_y, t_far_y) = if displacement.y.abs() > f32::EPSILON {
167 let t1 = (exp_min.y - origin.y) / displacement.y;
168 let t2 = (exp_max.y - origin.y) / displacement.y;
169 (t1.min(t2), t1.max(t2))
170 } else if origin.y >= exp_min.y && origin.y <= exp_max.y {
171 (f32::NEG_INFINITY, f32::INFINITY)
172 } else {
173 return None;
174 };
175
176 let t_entry = t_near_x.max(t_near_y);
177 let t_exit = t_far_x.min(t_far_y);
178
179 if t_entry > t_exit || t_entry >= 1.0 || t_exit <= 0.0 {
180 return None;
181 }
182
183 let time = t_entry.clamp(0.0, 1.0);
184
185 let normal = if t_near_x > t_near_y {
186 Vec2::new(-displacement.x.signum(), 0.0)
187 } else {
188 Vec2::new(0.0, -displacement.y.signum())
189 };
190
191 Some(SweepResult { time, normal })
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn swept_detects_head_on_collision() {
201 let mover = AABB::new(Vec2::new(0.0, 0.0), Vec2::new(10.0, 10.0));
202 let wall = AABB::new(Vec2::new(30.0, 0.0), Vec2::new(10.0, 20.0));
203 let displacement = Vec2::new(50.0, 0.0);
204
205 let result = mover.swept_collision(displacement, &wall);
206 assert!(result.is_some());
207 let r = result.unwrap();
208 assert!(r.time > 0.0 && r.time < 1.0);
209 assert_eq!(r.normal, Vec2::new(-1.0, 0.0));
210 }
211
212 #[test]
213 fn swept_misses_when_parallel() {
214 let mover = AABB::new(Vec2::new(0.0, 0.0), Vec2::new(10.0, 10.0));
215 let wall = AABB::new(Vec2::new(0.0, 50.0), Vec2::new(10.0, 10.0));
216 let displacement = Vec2::new(100.0, 0.0);
217
218 assert!(mover.swept_collision(displacement, &wall).is_none());
219 }
220
221 #[test]
222 fn swept_returns_none_for_zero_displacement() {
223 let mover = AABB::new(Vec2::ZERO, Vec2::new(10.0, 10.0));
224 let wall = AABB::new(Vec2::new(20.0, 0.0), Vec2::new(10.0, 10.0));
225 assert!(mover.swept_collision(Vec2::ZERO, &wall).is_none());
226 }
227
228 #[test]
229 fn swept_detects_downward_landing() {
230 let mover = AABB::new(Vec2::new(0.0, 0.0), Vec2::new(10.0, 10.0));
231 let floor = AABB::new(Vec2::new(0.0, 40.0), Vec2::new(100.0, 10.0));
232 let displacement = Vec2::new(0.0, 60.0);
233
234 let result = mover.swept_collision(displacement, &floor);
235 assert!(result.is_some());
236 let r = result.unwrap();
237 assert!(r.time < 1.0);
238 assert_eq!(r.normal, Vec2::new(0.0, -1.0));
239 }
240
241 #[test]
242 fn resolve_collision_pushes_out() {
243 let mover = AABB::new(Vec2::new(10.0, 0.0), Vec2::new(10.0, 10.0));
244 let wall = AABB::new(Vec2::new(18.0, 0.0), Vec2::new(10.0, 10.0));
245 let mtv = AABB::resolve_collision(&mover, &wall);
246 assert!(mtv.is_some());
247 let mtv = mtv.unwrap();
248 assert!(mtv.x < 0.0, "should push mover left, away from wall");
249 }
250}