1use glam::{Vec2, Vec3};
9use std::collections::{HashMap, BTreeMap};
10
11const DEFAULT_GRAVITY: Vec2 = Vec2::new(0.0, -9.81);
14const DEFAULT_DAMPING: f32 = 0.99;
15const ANGULAR_DAMPING: f32 = 0.98;
16const POSITION_SLOP: f32 = 0.005;
17const POSITION_CORRECTION: f32 = 0.4;
18const SOLVER_ITERATIONS: usize = 8;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct ObjectId(pub u32);
25
26#[derive(Debug, Clone)]
30pub enum CollisionShape {
31 Circle { radius: f32 },
33 AABB { half_extents: Vec2 },
35 Capsule { radius: f32, height: f32 },
37}
38
39impl CollisionShape {
40 pub fn local_aabb(&self) -> AABB {
42 match self {
43 CollisionShape::Circle { radius } => AABB {
44 min: Vec2::new(-*radius, -*radius),
45 max: Vec2::new(*radius, *radius),
46 },
47 CollisionShape::AABB { half_extents } => AABB {
48 min: -*half_extents,
49 max: *half_extents,
50 },
51 CollisionShape::Capsule { radius, height } => {
52 let half_h = height * 0.5;
53 AABB {
54 min: Vec2::new(-*radius, -half_h - *radius),
55 max: Vec2::new(*radius, half_h + *radius),
56 }
57 }
58 }
59 }
60
61 pub fn area(&self) -> f32 {
63 match self {
64 CollisionShape::Circle { radius } => std::f32::consts::PI * radius * radius,
65 CollisionShape::AABB { half_extents } => 4.0 * half_extents.x * half_extents.y,
66 CollisionShape::Capsule { radius, height } => {
67 std::f32::consts::PI * radius * radius + 2.0 * radius * height
68 }
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy)]
77pub struct AABB {
78 pub min: Vec2,
79 pub max: Vec2,
80}
81
82impl AABB {
83 pub fn new(min: Vec2, max: Vec2) -> Self {
84 Self { min, max }
85 }
86
87 pub fn from_center_half(center: Vec2, half: Vec2) -> Self {
88 Self {
89 min: center - half,
90 max: center + half,
91 }
92 }
93
94 pub fn overlaps(&self, other: &AABB) -> bool {
95 self.min.x <= other.max.x
96 && self.max.x >= other.min.x
97 && self.min.y <= other.max.y
98 && self.max.y >= other.min.y
99 }
100
101 pub fn contains_point(&self, p: Vec2) -> bool {
102 p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y
103 }
104
105 pub fn center(&self) -> Vec2 {
106 (self.min + self.max) * 0.5
107 }
108
109 pub fn half_extents(&self) -> Vec2 {
110 (self.max - self.min) * 0.5
111 }
112
113 pub fn union(&self, other: &AABB) -> Self {
115 Self {
116 min: self.min.min(other.min),
117 max: self.max.max(other.max),
118 }
119 }
120
121 pub fn ray_intersect(&self, origin: Vec2, dir: Vec2) -> Option<f32> {
123 let inv_d = Vec2::new(
124 if dir.x.abs() > 1e-12 { 1.0 / dir.x } else { f32::MAX },
125 if dir.y.abs() > 1e-12 { 1.0 / dir.y } else { f32::MAX },
126 );
127 let t1 = (self.min.x - origin.x) * inv_d.x;
128 let t2 = (self.max.x - origin.x) * inv_d.x;
129 let t3 = (self.min.y - origin.y) * inv_d.y;
130 let t4 = (self.max.y - origin.y) * inv_d.y;
131 let tmin = t1.min(t2).max(t3.min(t4));
132 let tmax = t1.max(t2).min(t3.max(t4));
133 if tmax < 0.0 || tmin > tmax {
134 None
135 } else {
136 Some(if tmin >= 0.0 { tmin } else { tmax })
137 }
138 }
139
140 pub fn expand(&self, margin: f32) -> Self {
142 Self {
143 min: self.min - Vec2::splat(margin),
144 max: self.max + Vec2::splat(margin),
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
153pub struct PhysicsObject {
154 pub position: Vec3,
155 pub velocity: Vec3,
156 pub angular_velocity: f32,
157 pub mass: f32,
158 pub restitution: f32,
159 pub friction: f32,
160 pub shape: CollisionShape,
161 pub is_static: bool,
162 pub is_trigger: bool,
163 pub collision_layer: u32,
164 inv_mass: f32,
166 force_accum: Vec2,
167 torque_accum: f32,
168 angle: f32,
169}
170
171impl PhysicsObject {
172 pub fn new(position: Vec3, mass: f32, shape: CollisionShape) -> Self {
174 let inv = if mass > 0.0 && !mass.is_infinite() {
175 1.0 / mass
176 } else {
177 0.0
178 };
179 Self {
180 position,
181 velocity: Vec3::ZERO,
182 angular_velocity: 0.0,
183 mass,
184 restitution: 0.3,
185 friction: 0.4,
186 shape,
187 is_static: false,
188 is_trigger: false,
189 collision_layer: 1,
190 inv_mass: inv,
191 force_accum: Vec2::ZERO,
192 torque_accum: 0.0,
193 angle: 0.0,
194 }
195 }
196
197 pub fn new_static(position: Vec3, shape: CollisionShape) -> Self {
199 let mut obj = Self::new(position, 0.0, shape);
200 obj.is_static = true;
201 obj.inv_mass = 0.0;
202 obj
203 }
204
205 pub fn new_trigger(position: Vec3, shape: CollisionShape) -> Self {
207 let mut obj = Self::new_static(position, shape);
208 obj.is_trigger = true;
209 obj
210 }
211
212 pub fn pos2d(&self) -> Vec2 {
214 Vec2::new(self.position.x, self.position.y)
215 }
216
217 pub fn vel2d(&self) -> Vec2 {
219 Vec2::new(self.velocity.x, self.velocity.y)
220 }
221
222 pub fn inv_mass(&self) -> f32 {
224 self.inv_mass
225 }
226
227 pub fn world_aabb(&self) -> AABB {
229 let local = self.shape.local_aabb();
230 let pos = self.pos2d();
231 AABB {
232 min: local.min + pos,
233 max: local.max + pos,
234 }
235 }
236
237 pub fn apply_force(&mut self, force: Vec2) {
239 self.force_accum += force;
240 }
241
242 pub fn apply_torque(&mut self, torque: f32) {
244 self.torque_accum += torque;
245 }
246
247 pub fn apply_impulse(&mut self, impulse: Vec2) {
249 if self.is_static {
250 return;
251 }
252 self.velocity.x += impulse.x * self.inv_mass;
253 self.velocity.y += impulse.y * self.inv_mass;
254 }
255
256 pub fn apply_impulse_at(&mut self, impulse: Vec2, contact_offset: Vec2) {
258 if self.is_static {
259 return;
260 }
261 self.velocity.x += impulse.x * self.inv_mass;
262 self.velocity.y += impulse.y * self.inv_mass;
263 self.angular_velocity += cross2d(contact_offset, impulse) * self.inv_mass;
264 }
265
266 fn integrate(&mut self, dt: f32, gravity: Vec2, damping: f32) {
268 if self.is_static {
269 return;
270 }
271 self.velocity.x += (gravity.x + self.force_accum.x * self.inv_mass) * dt;
273 self.velocity.y += (gravity.y + self.force_accum.y * self.inv_mass) * dt;
274 self.angular_velocity += self.torque_accum * self.inv_mass * dt;
276 self.velocity.x *= damping;
278 self.velocity.y *= damping;
279 self.angular_velocity *= ANGULAR_DAMPING;
280 self.position.x += self.velocity.x * dt;
282 self.position.y += self.velocity.y * dt;
283 self.angle += self.angular_velocity * dt;
284 self.force_accum = Vec2::ZERO;
286 self.torque_accum = 0.0;
287 }
288}
289
290#[inline]
294fn cross2d(a: Vec2, b: Vec2) -> f32 {
295 a.x * b.y - a.y * b.x
296}
297
298#[inline]
300fn clampf(val: f32, lo: f32, hi: f32) -> f32 {
301 val.max(lo).min(hi)
302}
303
304fn closest_point_on_segment(a: Vec2, b: Vec2, p: Vec2) -> Vec2 {
306 let ab = b - a;
307 let len_sq = ab.length_squared();
308 if len_sq < 1e-12 {
309 return a;
310 }
311 let t = clampf((p - a).dot(ab) / len_sq, 0.0, 1.0);
312 a + ab * t
313}
314
315#[derive(Debug, Clone)]
319pub struct Contact {
320 pub point: Vec2,
322 pub normal: Vec2,
324 pub penetration: f32,
326}
327
328#[derive(Debug, Clone)]
332pub struct RayHit {
333 pub object_id: ObjectId,
334 pub point: Vec2,
335 pub normal: Vec2,
336 pub t: f32,
337}
338
339fn collide_circle_circle(
343 pos_a: Vec2,
344 radius_a: f32,
345 pos_b: Vec2,
346 radius_b: f32,
347) -> Option<Contact> {
348 let delta = pos_b - pos_a;
349 let dist_sq = delta.length_squared();
350 let sum_r = radius_a + radius_b;
351 if dist_sq >= sum_r * sum_r {
352 return None;
353 }
354 let dist = dist_sq.sqrt();
355 let normal = if dist > 1e-6 {
356 delta / dist
357 } else {
358 Vec2::new(1.0, 0.0)
359 };
360 let penetration = sum_r - dist;
361 let point = pos_a + normal * (radius_a - penetration * 0.5);
362 Some(Contact {
363 point,
364 normal,
365 penetration,
366 })
367}
368
369fn collide_circle_aabb(
371 circle_pos: Vec2,
372 radius: f32,
373 aabb_pos: Vec2,
374 half_ext: Vec2,
375) -> Option<Contact> {
376 let center = aabb_pos + half_ext;
377 let local = circle_pos - center;
378 let clamped = Vec2::new(
379 clampf(local.x, -half_ext.x, half_ext.x),
380 clampf(local.y, -half_ext.y, half_ext.y),
381 );
382 let closest = center + clamped;
383 let delta = circle_pos - closest;
384 let dist_sq = delta.length_squared();
385 if dist_sq >= radius * radius {
386 return None;
387 }
388 let dist = dist_sq.sqrt();
389 let normal = if dist > 1e-6 {
390 delta / dist
391 } else {
392 let dx = half_ext.x - local.x.abs();
394 let dy = half_ext.y - local.y.abs();
395 if dx < dy {
396 Vec2::new(if local.x >= 0.0 { 1.0 } else { -1.0 }, 0.0)
397 } else {
398 Vec2::new(0.0, if local.y >= 0.0 { 1.0 } else { -1.0 })
399 }
400 };
401 let penetration = radius - dist;
402 let point = closest;
403 Some(Contact {
404 point,
405 normal,
406 penetration,
407 })
408}
409
410fn collide_aabb_aabb(
412 pos_a: Vec2,
413 half_a: Vec2,
414 pos_b: Vec2,
415 half_b: Vec2,
416) -> Option<Contact> {
417 let delta = pos_b - pos_a;
418 let overlap_x = half_a.x + half_b.x - delta.x.abs();
419 if overlap_x <= 0.0 {
420 return None;
421 }
422 let overlap_y = half_a.y + half_b.y - delta.y.abs();
423 if overlap_y <= 0.0 {
424 return None;
425 }
426 let (normal, penetration) = if overlap_x < overlap_y {
427 (
428 Vec2::new(if delta.x >= 0.0 { 1.0 } else { -1.0 }, 0.0),
429 overlap_x,
430 )
431 } else {
432 (
433 Vec2::new(0.0, if delta.y >= 0.0 { 1.0 } else { -1.0 }),
434 overlap_y,
435 )
436 };
437 let point = pos_a + delta * 0.5;
438 Some(Contact {
439 point,
440 normal,
441 penetration,
442 })
443}
444
445fn collide_shapes(
447 pos_a: Vec2,
448 shape_a: &CollisionShape,
449 pos_b: Vec2,
450 shape_b: &CollisionShape,
451) -> Option<Contact> {
452 match (shape_a, shape_b) {
453 (CollisionShape::Circle { radius: ra }, CollisionShape::Circle { radius: rb }) => {
454 collide_circle_circle(pos_a, *ra, pos_b, *rb)
455 }
456 (CollisionShape::Circle { radius }, CollisionShape::AABB { half_extents }) => {
457 collide_circle_aabb(pos_a, *radius, pos_b, *half_extents)
458 }
459 (CollisionShape::AABB { half_extents }, CollisionShape::Circle { radius }) => {
460 collide_circle_aabb(pos_b, *radius, pos_a, *half_extents).map(|mut c| {
461 c.normal = -c.normal;
462 c
463 })
464 }
465 (CollisionShape::AABB { half_extents: ha }, CollisionShape::AABB { half_extents: hb }) => {
466 collide_aabb_aabb(pos_a, *ha, pos_b, *hb)
467 }
468 (CollisionShape::Capsule { radius, height }, other) => {
470 let half_h = height * 0.5;
471 let top = pos_a + Vec2::new(0.0, half_h);
472 let bot = pos_a - Vec2::new(0.0, half_h);
473 let circ = CollisionShape::Circle { radius: *radius };
474 let c1 = collide_shapes(top, &circ, pos_b, other);
475 let c2 = collide_shapes(bot, &circ, pos_b, other);
476 let rect = CollisionShape::AABB {
478 half_extents: Vec2::new(*radius, half_h),
479 };
480 let c3 = collide_shapes(pos_a, &rect, pos_b, other);
481 [c1, c2, c3]
483 .into_iter()
484 .flatten()
485 .max_by(|a, b| a.penetration.partial_cmp(&b.penetration).unwrap())
486 }
487 (other, CollisionShape::Capsule { radius, height }) => {
488 let half_h = height * 0.5;
489 let top = pos_b + Vec2::new(0.0, half_h);
490 let bot = pos_b - Vec2::new(0.0, half_h);
491 let circ = CollisionShape::Circle { radius: *radius };
492 let c1 = collide_shapes(pos_a, other, top, &circ);
493 let c2 = collide_shapes(pos_a, other, bot, &circ);
494 let rect = CollisionShape::AABB {
495 half_extents: Vec2::new(*radius, half_h),
496 };
497 let c3 = collide_shapes(pos_a, other, pos_b, &rect);
498 [c1, c2, c3]
499 .into_iter()
500 .flatten()
501 .max_by(|a, b| a.penetration.partial_cmp(&b.penetration).unwrap())
502 }
503 }
504}
505
506fn raycast_shape(
508 origin: Vec2,
509 dir: Vec2,
510 max_dist: f32,
511 pos: Vec2,
512 shape: &CollisionShape,
513) -> Option<(f32, Vec2)> {
514 match shape {
515 CollisionShape::Circle { radius } => {
516 let oc = origin - pos;
517 let a = dir.dot(dir);
518 let b = 2.0 * oc.dot(dir);
519 let c = oc.dot(oc) - radius * radius;
520 let disc = b * b - 4.0 * a * c;
521 if disc < 0.0 {
522 return None;
523 }
524 let sqrt_disc = disc.sqrt();
525 let t = (-b - sqrt_disc) / (2.0 * a);
526 if t < 0.0 || t > max_dist {
527 return None;
528 }
529 let hit = origin + dir * t;
530 let normal = (hit - pos).normalize_or_zero();
531 Some((t, normal))
532 }
533 CollisionShape::AABB { half_extents } => {
534 let aabb = AABB::from_center_half(pos, *half_extents);
535 aabb.ray_intersect(origin, dir).and_then(|t| {
536 if t > max_dist {
537 return None;
538 }
539 let hit = origin + dir * t;
540 let local = hit - pos;
541 let nx = if (local.x.abs() - half_extents.x).abs() < 0.01 {
543 if local.x > 0.0 { 1.0 } else { -1.0 }
544 } else {
545 0.0
546 };
547 let ny = if (nx as f32).abs() < 0.5 {
548 if local.y > 0.0 { 1.0 } else { -1.0 }
549 } else {
550 0.0
551 };
552 Some((t, Vec2::new(nx, ny)))
553 })
554 }
555 CollisionShape::Capsule { radius, height } => {
556 let half_h = height * 0.5;
557 let top = pos + Vec2::new(0.0, half_h);
559 let bot = pos - Vec2::new(0.0, half_h);
560 let circ = CollisionShape::Circle { radius: *radius };
561 let rect = CollisionShape::AABB {
562 half_extents: Vec2::new(*radius, half_h),
563 };
564 let r1 = raycast_shape(origin, dir, max_dist, top, &circ);
565 let r2 = raycast_shape(origin, dir, max_dist, bot, &circ);
566 let r3 = raycast_shape(origin, dir, max_dist, pos, &rect);
567 [r1, r2, r3]
568 .into_iter()
569 .flatten()
570 .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
571 }
572 }
573}
574
575#[derive(Debug, Clone)]
579pub struct CollisionPair {
580 pub id_a: ObjectId,
581 pub id_b: ObjectId,
582 pub contact: Contact,
583}
584
585#[derive(Debug, Clone)]
589pub struct TriggerEvent {
590 pub trigger_id: ObjectId,
591 pub other_id: ObjectId,
592 pub is_enter: bool,
593}
594
595#[derive(Debug, Clone)]
599pub struct DamageEvent {
600 pub source_description: String,
601 pub target_object_id: ObjectId,
602 pub damage: f32,
603 pub knockback: Vec2,
604}
605
606pub struct PhysicsWorld {
610 objects: HashMap<ObjectId, PhysicsObject>,
611 next_id: u32,
612 pub gravity: Vec2,
613 pub damping: f32,
614 broad_cache: Vec<(ObjectId, f32, f32)>, active_triggers: HashMap<(ObjectId, ObjectId), bool>,
618}
619
620impl PhysicsWorld {
621 pub fn new() -> Self {
622 Self {
623 objects: HashMap::new(),
624 next_id: 0,
625 gravity: DEFAULT_GRAVITY,
626 damping: DEFAULT_DAMPING,
627 broad_cache: Vec::new(),
628 active_triggers: HashMap::new(),
629 }
630 }
631
632 pub fn add_object(&mut self, obj: PhysicsObject) -> ObjectId {
634 let id = ObjectId(self.next_id);
635 self.next_id += 1;
636 self.objects.insert(id, obj);
637 id
638 }
639
640 pub fn remove_object(&mut self, id: ObjectId) -> Option<PhysicsObject> {
642 self.active_triggers.retain(|k, _| k.0 != id && k.1 != id);
644 self.objects.remove(&id)
645 }
646
647 pub fn get_object(&self, id: ObjectId) -> Option<&PhysicsObject> {
649 self.objects.get(&id)
650 }
651
652 pub fn get_object_mut(&mut self, id: ObjectId) -> Option<&mut PhysicsObject> {
654 self.objects.get_mut(&id)
655 }
656
657 pub fn object_count(&self) -> usize {
659 self.objects.len()
660 }
661
662 pub fn object_ids(&self) -> Vec<ObjectId> {
664 self.objects.keys().copied().collect()
665 }
666
667 pub fn step(&mut self, dt: f32) -> (Vec<CollisionPair>, Vec<TriggerEvent>) {
671 let gravity = self.gravity;
673 let damping = self.damping;
674 for obj in self.objects.values_mut() {
675 obj.integrate(dt, gravity, damping);
676 }
677
678 self.broad_cache.clear();
680 for (&id, obj) in &self.objects {
681 let aabb = obj.world_aabb();
682 self.broad_cache.push((id, aabb.min.x, aabb.max.x));
683 }
684 self.broad_cache.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
685
686 let mut broad_pairs: Vec<(ObjectId, ObjectId)> = Vec::new();
687 for i in 0..self.broad_cache.len() {
688 let (id_a, _min_a, max_a) = self.broad_cache[i];
689 for j in (i + 1)..self.broad_cache.len() {
690 let (id_b, min_b, _max_b) = self.broad_cache[j];
691 if min_b > max_a {
692 break; }
694 let aabb_a = self.objects[&id_a].world_aabb();
696 let aabb_b = self.objects[&id_b].world_aabb();
697 if aabb_a.overlaps(&aabb_b) {
698 broad_pairs.push((id_a, id_b));
699 }
700 }
701 }
702
703 let mut collision_pairs = Vec::new();
705 let mut trigger_events = Vec::new();
706 let mut new_triggers: HashMap<(ObjectId, ObjectId), bool> = HashMap::new();
707
708 for (id_a, id_b) in &broad_pairs {
709 let obj_a = &self.objects[id_a];
710 let obj_b = &self.objects[id_b];
711
712 if obj_a.collision_layer & obj_b.collision_layer == 0 {
714 continue;
715 }
716
717 let pos_a = obj_a.pos2d();
718 let pos_b = obj_b.pos2d();
719
720 if let Some(contact) = collide_shapes(pos_a, &obj_a.shape, pos_b, &obj_b.shape) {
721 if obj_a.is_trigger || obj_b.is_trigger {
723 let key = if *id_a < *id_b {
724 (*id_a, *id_b)
725 } else {
726 (*id_b, *id_a)
727 };
728 new_triggers.insert(key, true);
729 if !self.active_triggers.contains_key(&key) {
730 trigger_events.push(TriggerEvent {
731 trigger_id: if obj_a.is_trigger { *id_a } else { *id_b },
732 other_id: if obj_a.is_trigger { *id_b } else { *id_a },
733 is_enter: true,
734 });
735 }
736 continue;
737 }
738
739 collision_pairs.push(CollisionPair {
740 id_a: *id_a,
741 id_b: *id_b,
742 contact: contact.clone(),
743 });
744 }
745 }
746
747 for (key, _) in &self.active_triggers {
749 if !new_triggers.contains_key(key) {
750 let obj_a = self.objects.get(&key.0);
751 let obj_b = self.objects.get(&key.1);
752 if let (Some(a), Some(b)) = (obj_a, obj_b) {
753 trigger_events.push(TriggerEvent {
754 trigger_id: if a.is_trigger { key.0 } else { key.1 },
755 other_id: if a.is_trigger { key.1 } else { key.0 },
756 is_enter: false,
757 });
758 }
759 }
760 }
761 self.active_triggers = new_triggers;
762
763 for _iter in 0..SOLVER_ITERATIONS {
765 for pair in &collision_pairs {
766 self.resolve_collision(pair);
767 }
768 }
769
770 for pair in &collision_pairs {
772 self.correct_positions(pair);
773 }
774
775 (collision_pairs, trigger_events)
776 }
777
778 fn resolve_collision(&mut self, pair: &CollisionPair) {
780 let inv_a;
781 let inv_b;
782 let vel_a;
783 let vel_b;
784 let rest;
785 let fric;
786 {
787 let a = &self.objects[&pair.id_a];
788 let b = &self.objects[&pair.id_b];
789 inv_a = a.inv_mass;
790 inv_b = b.inv_mass;
791 vel_a = a.vel2d();
792 vel_b = b.vel2d();
793 rest = (a.restitution + b.restitution) * 0.5;
794 fric = (a.friction + b.friction) * 0.5;
795 }
796
797 if inv_a + inv_b < 1e-12 {
798 return; }
800
801 let n = pair.contact.normal;
802 let rel_vel = vel_b - vel_a;
803 let vel_along_normal = rel_vel.dot(n);
804
805 if vel_along_normal > 0.0 {
807 return;
808 }
809
810 let j = -(1.0 + rest) * vel_along_normal / (inv_a + inv_b);
812 let impulse = n * j;
813
814 if let Some(a) = self.objects.get_mut(&pair.id_a) {
816 a.apply_impulse(-impulse);
817 }
818 if let Some(b) = self.objects.get_mut(&pair.id_b) {
819 b.apply_impulse(impulse);
820 }
821
822 let vel_a2;
824 let vel_b2;
825 {
826 let a = &self.objects[&pair.id_a];
827 let b = &self.objects[&pair.id_b];
828 vel_a2 = a.vel2d();
829 vel_b2 = b.vel2d();
830 }
831 let rel_vel2 = vel_b2 - vel_a2;
832 let tangent = rel_vel2 - n * rel_vel2.dot(n);
833 let tangent_len = tangent.length();
834 if tangent_len > 1e-6 {
835 let tangent_norm = tangent / tangent_len;
836 let jt = -rel_vel2.dot(tangent_norm) / (inv_a + inv_b);
837 let friction_impulse = if jt.abs() < j * fric {
838 tangent_norm * jt
839 } else {
840 tangent_norm * (-j * fric)
841 };
842 if let Some(a) = self.objects.get_mut(&pair.id_a) {
843 a.apply_impulse(-friction_impulse);
844 }
845 if let Some(b) = self.objects.get_mut(&pair.id_b) {
846 b.apply_impulse(friction_impulse);
847 }
848 }
849 }
850
851 fn correct_positions(&mut self, pair: &CollisionPair) {
853 let inv_a;
854 let inv_b;
855 {
856 let a = &self.objects[&pair.id_a];
857 let b = &self.objects[&pair.id_b];
858 inv_a = a.inv_mass;
859 inv_b = b.inv_mass;
860 }
861 let total_inv = inv_a + inv_b;
862 if total_inv < 1e-12 {
863 return;
864 }
865 let pen = pair.contact.penetration;
866 if pen <= POSITION_SLOP {
867 return;
868 }
869 let correction = pair.contact.normal * (POSITION_CORRECTION * (pen - POSITION_SLOP) / total_inv);
870 if let Some(a) = self.objects.get_mut(&pair.id_a) {
871 a.position.x -= correction.x * inv_a;
872 a.position.y -= correction.y * inv_a;
873 }
874 if let Some(b) = self.objects.get_mut(&pair.id_b) {
875 b.position.x += correction.x * inv_b;
876 b.position.y += correction.y * inv_b;
877 }
878 }
879
880 pub fn raycast(&self, origin: Vec2, dir: Vec2, max_dist: f32) -> Option<RayHit> {
882 let dir_norm = if dir.length_squared() > 1e-12 {
883 dir.normalize()
884 } else {
885 return None;
886 };
887
888 let mut best: Option<RayHit> = None;
889 for (&id, obj) in &self.objects {
890 if obj.is_trigger {
891 continue;
892 }
893 if let Some((t, normal)) = raycast_shape(origin, dir_norm, max_dist, obj.pos2d(), &obj.shape) {
894 if best.as_ref().map_or(true, |b| t < b.t) {
895 best = Some(RayHit {
896 object_id: id,
897 point: origin + dir_norm * t,
898 normal,
899 t,
900 });
901 }
902 }
903 }
904 best
905 }
906
907 pub fn overlap_test(&self, shape: &CollisionShape, pos: Vec2) -> Vec<ObjectId> {
909 let mut results = Vec::new();
910 for (&id, obj) in &self.objects {
911 let obj_pos = obj.pos2d();
912 if collide_shapes(pos, shape, obj_pos, &obj.shape).is_some() {
913 results.push(id);
914 }
915 }
916 results
917 }
918
919 pub fn apply_radial_force(&mut self, center: Vec2, radius: f32, strength: f32) {
921 for obj in self.objects.values_mut() {
922 if obj.is_static {
923 continue;
924 }
925 let delta = obj.pos2d() - center;
926 let dist = delta.length();
927 if dist < radius && dist > 1e-6 {
928 let falloff = 1.0 - dist / radius;
929 let force = delta.normalize() * strength * falloff;
930 obj.apply_force(force);
931 }
932 }
933 }
934
935 pub fn apply_vortex_force(&mut self, center: Vec2, radius: f32, pull_strength: f32, spin_strength: f32) {
937 for obj in self.objects.values_mut() {
938 if obj.is_static {
939 continue;
940 }
941 let delta = obj.pos2d() - center;
942 let dist = delta.length();
943 if dist < radius && dist > 1e-6 {
944 let falloff = 1.0 - dist / radius;
945 let dir = delta.normalize();
946 let tangent = Vec2::new(-dir.y, dir.x);
947 let force = -dir * pull_strength * falloff + tangent * spin_strength * falloff;
948 obj.apply_force(force);
949 }
950 }
951 }
952}
953
954#[derive(Debug, Clone, Copy, PartialEq, Eq)]
958pub enum RoomType {
959 Normal,
960 Trap,
961 Treasure,
962 Boss,
963 ChaosRift,
964 Shop,
965}
966
967#[derive(Debug, Clone)]
969pub struct RoomExit {
970 pub position: Vec2,
971 pub direction: Vec2,
972 pub target_room: Option<u32>,
973}
974
975#[derive(Debug, Clone)]
977pub struct ArenaRoom {
978 pub room_type: RoomType,
979 pub bounds: AABB,
980 pub physics_objects: Vec<ObjectId>,
981 pub spawn_points: Vec<Vec2>,
982 pub exits: Vec<RoomExit>,
983 pub room_id: u32,
984}
985
986impl ArenaRoom {
987 pub fn new(room_id: u32, room_type: RoomType, bounds: AABB) -> Self {
988 Self {
989 room_type,
990 bounds,
991 physics_objects: Vec::new(),
992 spawn_points: Vec::new(),
993 exits: Vec::new(),
994 room_id,
995 }
996 }
997
998 pub fn add_spawn_point(&mut self, point: Vec2) {
1000 self.spawn_points.push(point);
1001 }
1002
1003 pub fn add_exit(&mut self, exit: RoomExit) {
1005 self.exits.push(exit);
1006 }
1007
1008 pub fn register_object(&mut self, id: ObjectId) {
1010 self.physics_objects.push(id);
1011 }
1012
1013 pub fn contains_point(&self, p: Vec2) -> bool {
1015 self.bounds.contains_point(p)
1016 }
1017
1018 pub fn center(&self) -> Vec2 {
1020 self.bounds.center()
1021 }
1022
1023 pub fn dimensions(&self) -> Vec2 {
1025 self.bounds.max - self.bounds.min
1026 }
1027}
1028
1029#[derive(Debug, Clone)]
1033pub struct SwingingPendulum {
1034 pub pivot: Vec2,
1035 pub rope_length: f32,
1036 pub bob_mass: f32,
1037 pub angular_position: f32,
1038 pub angular_velocity: f32,
1039 pub damage: f32,
1040 pub bob_radius: f32,
1041 pub physics_object_id: Option<ObjectId>,
1042}
1043
1044impl SwingingPendulum {
1045 pub fn new(pivot: Vec2, rope_length: f32, bob_mass: f32, initial_angle: f32, damage: f32) -> Self {
1046 Self {
1047 pivot,
1048 rope_length,
1049 bob_mass,
1050 angular_position: initial_angle,
1051 angular_velocity: 0.0,
1052 damage,
1053 bob_radius: 0.5,
1054 physics_object_id: None,
1055 }
1056 }
1057
1058 pub fn bob_position(&self) -> Vec2 {
1060 self.pivot
1061 + Vec2::new(
1062 self.angular_position.sin() * self.rope_length,
1063 -self.angular_position.cos() * self.rope_length,
1064 )
1065 }
1066
1067 pub fn update(&mut self, dt: f32, gravity: f32) {
1069 let alpha = -(gravity / self.rope_length) * self.angular_position.sin();
1071 self.angular_velocity += alpha * dt;
1072 self.angular_velocity *= 0.999;
1074 self.angular_position += self.angular_velocity * dt;
1075 }
1076
1077 pub fn sync_physics(&self, world: &mut PhysicsWorld) {
1079 if let Some(id) = self.physics_object_id {
1080 if let Some(obj) = world.get_object_mut(id) {
1081 let pos = self.bob_position();
1082 obj.position.x = pos.x;
1083 obj.position.y = pos.y;
1084 }
1085 }
1086 }
1087
1088 pub fn spawn(&mut self, world: &mut PhysicsWorld) -> ObjectId {
1090 let pos = self.bob_position();
1091 let mut obj = PhysicsObject::new(
1092 Vec3::new(pos.x, pos.y, 0.0),
1093 self.bob_mass,
1094 CollisionShape::Circle {
1095 radius: self.bob_radius,
1096 },
1097 );
1098 obj.is_static = true; let id = world.add_object(obj);
1100 self.physics_object_id = Some(id);
1101 id
1102 }
1103}
1104
1105#[derive(Debug, Clone)]
1107pub struct FallingRock {
1108 pub object_id: ObjectId,
1109 pub has_shattered: bool,
1110 pub debris_ids: Vec<ObjectId>,
1111}
1112
1113#[derive(Debug, Clone)]
1115pub struct FallingRocks {
1116 pub spawn_positions: Vec<Vec2>,
1117 pub trigger_zone: AABB,
1118 pub rock_mass: f32,
1119 pub rock_radius: f32,
1120 pub damage: f32,
1121 pub is_triggered: bool,
1122 pub active_rocks: Vec<FallingRock>,
1123 pub debris_lifetime: f32,
1124 pub debris_timer: f32,
1125 pub shatter_threshold_velocity: f32,
1126}
1127
1128impl FallingRocks {
1129 pub fn new(spawn_positions: Vec<Vec2>, trigger_zone: AABB, damage: f32) -> Self {
1130 Self {
1131 spawn_positions,
1132 trigger_zone,
1133 rock_mass: 5.0,
1134 rock_radius: 0.4,
1135 damage,
1136 is_triggered: false,
1137 active_rocks: Vec::new(),
1138 debris_lifetime: 3.0,
1139 debris_timer: 0.0,
1140 shatter_threshold_velocity: 5.0,
1141 }
1142 }
1143
1144 pub fn trigger(&mut self, world: &mut PhysicsWorld) {
1146 if self.is_triggered {
1147 return;
1148 }
1149 self.is_triggered = true;
1150 for &pos in &self.spawn_positions {
1151 let obj = PhysicsObject::new(
1152 Vec3::new(pos.x, pos.y, 0.0),
1153 self.rock_mass,
1154 CollisionShape::Circle {
1155 radius: self.rock_radius,
1156 },
1157 );
1158 let id = world.add_object(obj);
1159 self.active_rocks.push(FallingRock {
1160 object_id: id,
1161 has_shattered: false,
1162 debris_ids: Vec::new(),
1163 });
1164 }
1165 }
1166
1167 pub fn update(&mut self, dt: f32, world: &mut PhysicsWorld, floor_y: f32) {
1169 if !self.is_triggered {
1170 return;
1171 }
1172
1173 for rock in &mut self.active_rocks {
1175 if rock.has_shattered {
1176 continue;
1177 }
1178 if let Some(obj) = world.get_object(rock.object_id) {
1179 let vel = obj.vel2d().length();
1180 let at_floor = obj.position.y <= floor_y + self.rock_radius * 2.0;
1181 if at_floor && vel < self.shatter_threshold_velocity {
1182 rock.has_shattered = true;
1183 let pos = obj.pos2d();
1185 let debris_count = 4;
1186 for i in 0..debris_count {
1187 let angle =
1188 (i as f32 / debris_count as f32) * std::f32::consts::TAU;
1189 let dir = Vec2::new(angle.cos(), angle.sin());
1190 let mut debris = PhysicsObject::new(
1191 Vec3::new(pos.x, pos.y, 0.0),
1192 self.rock_mass * 0.15,
1193 CollisionShape::Circle {
1194 radius: self.rock_radius * 0.3,
1195 },
1196 );
1197 debris.velocity.x = dir.x * 3.0;
1198 debris.velocity.y = dir.y * 3.0 + 2.0;
1199 debris.restitution = 0.5;
1200 let did = world.add_object(debris);
1201 rock.debris_ids.push(did);
1202 }
1203 }
1204 }
1205 }
1206
1207 if self.active_rocks.iter().any(|r| !r.debris_ids.is_empty()) {
1209 self.debris_timer += dt;
1210 if self.debris_timer >= self.debris_lifetime {
1211 for rock in &mut self.active_rocks {
1212 for &did in &rock.debris_ids {
1213 world.remove_object(did);
1214 }
1215 rock.debris_ids.clear();
1216 }
1217 self.debris_timer = 0.0;
1218 }
1219 }
1220 }
1221
1222 pub fn check_trigger(&self, point: Vec2) -> bool {
1224 !self.is_triggered && self.trigger_zone.contains_point(point)
1225 }
1226}
1227
1228#[derive(Debug, Clone)]
1230pub struct SpikePit {
1231 pub trigger_zone: AABB,
1232 pub spike_positions: Vec<Vec2>,
1233 pub spike_heights: Vec<f32>,
1234 pub spike_velocities: Vec<f32>,
1235 pub target_height: f32,
1236 pub spring_constant: f32,
1237 pub spring_damping: f32,
1238 pub damage_per_second: f32,
1239 pub is_active: bool,
1240 pub object_ids: Vec<ObjectId>,
1241}
1242
1243impl SpikePit {
1244 pub fn new(trigger_zone: AABB, spike_count: usize, target_height: f32, damage: f32) -> Self {
1245 let width = trigger_zone.max.x - trigger_zone.min.x;
1246 let spacing = width / (spike_count as f32 + 1.0);
1247 let base_y = trigger_zone.min.y;
1248 let positions: Vec<Vec2> = (0..spike_count)
1249 .map(|i| {
1250 Vec2::new(
1251 trigger_zone.min.x + spacing * (i as f32 + 1.0),
1252 base_y,
1253 )
1254 })
1255 .collect();
1256 Self {
1257 trigger_zone,
1258 spike_positions: positions,
1259 spike_heights: vec![0.0; spike_count],
1260 spike_velocities: vec![0.0; spike_count],
1261 target_height,
1262 spring_constant: 200.0,
1263 spring_damping: 8.0,
1264 damage_per_second: damage,
1265 is_active: false,
1266 object_ids: Vec::new(),
1267 }
1268 }
1269
1270 pub fn activate(&mut self) {
1272 self.is_active = true;
1273 }
1274
1275 pub fn update(&mut self, dt: f32, world: &mut PhysicsWorld) {
1277 if !self.is_active {
1278 return;
1279 }
1280 for i in 0..self.spike_heights.len() {
1281 let displacement = self.target_height - self.spike_heights[i];
1282 let spring_force = self.spring_constant * displacement;
1283 let damping_force = -self.spring_damping * self.spike_velocities[i];
1284 let accel = spring_force + damping_force;
1285 self.spike_velocities[i] += accel * dt;
1286 self.spike_heights[i] += self.spike_velocities[i] * dt;
1287 if self.spike_heights[i] < 0.0 {
1289 self.spike_heights[i] = 0.0;
1290 self.spike_velocities[i] = 0.0;
1291 }
1292
1293 if i < self.object_ids.len() {
1295 if let Some(obj) = world.get_object_mut(self.object_ids[i]) {
1296 obj.position.y = self.spike_positions[i].y + self.spike_heights[i] * 0.5;
1297 }
1298 }
1299 }
1300 }
1301
1302 pub fn spawn(&mut self, world: &mut PhysicsWorld) {
1304 self.object_ids.clear();
1305 for pos in &self.spike_positions {
1306 let obj = PhysicsObject::new_static(
1307 Vec3::new(pos.x, pos.y, 0.0),
1308 CollisionShape::AABB {
1309 half_extents: Vec2::new(0.1, 0.01),
1310 },
1311 );
1312 let id = world.add_object(obj);
1313 self.object_ids.push(id);
1314 }
1315 }
1316
1317 pub fn check_trigger(&self, point: Vec2) -> bool {
1319 !self.is_active && self.trigger_zone.contains_point(point)
1320 }
1321
1322 pub fn is_touching_spike(&self, point: Vec2, spike_radius: f32) -> bool {
1324 for (i, pos) in self.spike_positions.iter().enumerate() {
1325 if i >= self.spike_heights.len() {
1326 continue;
1327 }
1328 let h = self.spike_heights[i];
1329 if h < 0.1 {
1330 continue;
1331 }
1332 let spike_top = pos.y + h;
1333 let spike_aabb = AABB {
1334 min: Vec2::new(pos.x - spike_radius, pos.y),
1335 max: Vec2::new(pos.x + spike_radius, spike_top),
1336 };
1337 if spike_aabb.contains_point(point) {
1338 return true;
1339 }
1340 }
1341 false
1342 }
1343}
1344
1345#[derive(Debug, Clone)]
1347pub struct FlameJet {
1348 pub position: Vec2,
1349 pub direction: Vec2,
1350 pub warmup_time: f32,
1351 pub active_time: f32,
1352 pub cooldown_time: f32,
1353 pub cycle_timer: f32,
1354 pub damage_per_second: f32,
1355 pub jet_length: f32,
1356 pub jet_width: f32,
1357 pub particle_spawn_rate: f32,
1358 pub particles: Vec<FlameParticle>,
1359 state: FlameJetState,
1360}
1361
1362#[derive(Debug, Clone, Copy, PartialEq)]
1363enum FlameJetState {
1364 Warmup,
1365 Active,
1366 Cooldown,
1367}
1368
1369#[derive(Debug, Clone)]
1371pub struct FlameParticle {
1372 pub position: Vec2,
1373 pub velocity: Vec2,
1374 pub lifetime: f32,
1375 pub max_lifetime: f32,
1376 pub size: f32,
1377}
1378
1379impl FlameJet {
1380 pub fn new(position: Vec2, direction: Vec2, damage: f32) -> Self {
1381 Self {
1382 position,
1383 direction: direction.normalize_or_zero(),
1384 warmup_time: 0.5,
1385 active_time: 2.0,
1386 cooldown_time: 1.5,
1387 cycle_timer: 0.0,
1388 damage_per_second: damage,
1389 jet_length: 4.0,
1390 jet_width: 1.0,
1391 particle_spawn_rate: 30.0,
1392 particles: Vec::new(),
1393 state: FlameJetState::Cooldown,
1394 }
1395 }
1396
1397 fn cycle_duration(&self) -> f32 {
1399 self.warmup_time + self.active_time + self.cooldown_time
1400 }
1401
1402 pub fn is_firing(&self) -> bool {
1404 self.state == FlameJetState::Active
1405 }
1406
1407 pub fn is_warming_up(&self) -> bool {
1409 self.state == FlameJetState::Warmup
1410 }
1411
1412 pub fn update(&mut self, dt: f32) {
1414 self.cycle_timer += dt;
1415 let cycle = self.cycle_duration();
1416 if self.cycle_timer >= cycle {
1417 self.cycle_timer -= cycle;
1418 }
1419
1420 self.state = if self.cycle_timer < self.warmup_time {
1421 FlameJetState::Warmup
1422 } else if self.cycle_timer < self.warmup_time + self.active_time {
1423 FlameJetState::Active
1424 } else {
1425 FlameJetState::Cooldown
1426 };
1427
1428 if self.is_firing() {
1430 let count = (self.particle_spawn_rate * dt).ceil() as usize;
1431 for _ in 0..count {
1432 let spread = (pseudo_random(self.cycle_timer) - 0.5) * self.jet_width;
1433 let perp = Vec2::new(-self.direction.y, self.direction.x);
1434 let vel = self.direction * 8.0 + perp * spread * 2.0;
1435 self.particles.push(FlameParticle {
1436 position: self.position,
1437 velocity: vel,
1438 lifetime: 0.0,
1439 max_lifetime: 0.5,
1440 size: 0.2,
1441 });
1442 }
1443 }
1444
1445 for p in &mut self.particles {
1447 p.position += p.velocity * dt;
1448 p.velocity.y += 1.5 * dt; p.lifetime += dt;
1450 p.size *= 0.98;
1451 }
1452
1453 self.particles.retain(|p| p.lifetime < p.max_lifetime);
1455 }
1456
1457 pub fn is_in_flame_zone(&self, point: Vec2) -> bool {
1459 if !self.is_firing() {
1460 return false;
1461 }
1462 let to_point = point - self.position;
1463 let along = to_point.dot(self.direction);
1464 if along < 0.0 || along > self.jet_length {
1465 return false;
1466 }
1467 let perp = Vec2::new(-self.direction.y, self.direction.x);
1468 let lateral = to_point.dot(perp).abs();
1469 let width_at_dist = self.jet_width * 0.5 * (1.0 + along / self.jet_length);
1471 lateral < width_at_dist
1472 }
1473}
1474
1475fn pseudo_random(seed: f32) -> f32 {
1477 let x = (seed * 12.9898).sin() * 43758.5453;
1478 x - x.floor()
1479}
1480
1481#[derive(Debug, Clone)]
1483pub struct CrushingWalls {
1484 pub left_wall_pos: f32,
1485 pub right_wall_pos: f32,
1486 pub left_start: f32,
1487 pub right_start: f32,
1488 pub close_speed: f32,
1489 pub wall_height: f32,
1490 pub wall_y: f32,
1491 pub min_gap: f32,
1492 pub damage_per_second: f32,
1493 pub is_active: bool,
1494 pub is_stopped: bool,
1495 pub push_force: f32,
1496 pub left_wall_id: Option<ObjectId>,
1497 pub right_wall_id: Option<ObjectId>,
1498 pub power_source_id: Option<ObjectId>,
1499 pub power_source_health: f32,
1500}
1501
1502impl CrushingWalls {
1503 pub fn new(left_x: f32, right_x: f32, wall_y: f32, wall_height: f32, damage: f32) -> Self {
1504 Self {
1505 left_wall_pos: left_x,
1506 right_wall_pos: right_x,
1507 left_start: left_x,
1508 right_start: right_x,
1509 close_speed: 0.8,
1510 wall_height,
1511 wall_y,
1512 min_gap: 0.5,
1513 damage_per_second: damage,
1514 is_active: false,
1515 is_stopped: false,
1516 push_force: 50.0,
1517 left_wall_id: None,
1518 right_wall_id: None,
1519 power_source_id: None,
1520 power_source_health: 100.0,
1521 }
1522 }
1523
1524 pub fn activate(&mut self) {
1526 self.is_active = true;
1527 }
1528
1529 pub fn damage_power_source(&mut self, amount: f32) -> bool {
1531 self.power_source_health -= amount;
1532 if self.power_source_health <= 0.0 {
1533 self.is_stopped = true;
1534 self.power_source_health = 0.0;
1535 true
1536 } else {
1537 false
1538 }
1539 }
1540
1541 pub fn spawn(&mut self, world: &mut PhysicsWorld) {
1543 let wall_half = Vec2::new(0.5, self.wall_height * 0.5);
1544 let left_obj = PhysicsObject::new_static(
1545 Vec3::new(self.left_wall_pos, self.wall_y, 0.0),
1546 CollisionShape::AABB { half_extents: wall_half },
1547 );
1548 self.left_wall_id = Some(world.add_object(left_obj));
1549
1550 let right_obj = PhysicsObject::new_static(
1551 Vec3::new(self.right_wall_pos, self.wall_y, 0.0),
1552 CollisionShape::AABB { half_extents: wall_half },
1553 );
1554 self.right_wall_id = Some(world.add_object(right_obj));
1555
1556 let mid_x = (self.left_wall_pos + self.right_wall_pos) * 0.5;
1558 let ps_obj = PhysicsObject::new_static(
1559 Vec3::new(mid_x, self.wall_y + self.wall_height * 0.5 + 1.0, 0.0),
1560 CollisionShape::AABB {
1561 half_extents: Vec2::new(0.3, 0.3),
1562 },
1563 );
1564 self.power_source_id = Some(world.add_object(ps_obj));
1565 }
1566
1567 pub fn update(&mut self, dt: f32, world: &mut PhysicsWorld) {
1569 if !self.is_active || self.is_stopped {
1570 return;
1571 }
1572 let gap = self.right_wall_pos - self.left_wall_pos;
1573 if gap > self.min_gap {
1574 self.left_wall_pos += self.close_speed * dt;
1575 self.right_wall_pos -= self.close_speed * dt;
1576 }
1577 if let Some(id) = self.left_wall_id {
1579 if let Some(obj) = world.get_object_mut(id) {
1580 obj.position.x = self.left_wall_pos;
1581 }
1582 }
1583 if let Some(id) = self.right_wall_id {
1584 if let Some(obj) = world.get_object_mut(id) {
1585 obj.position.x = self.right_wall_pos;
1586 }
1587 }
1588 }
1589
1590 pub fn is_being_crushed(&self, point: Vec2) -> bool {
1592 if !self.is_active || self.is_stopped {
1593 return false;
1594 }
1595 let in_y = point.y >= self.wall_y - self.wall_height * 0.5
1596 && point.y <= self.wall_y + self.wall_height * 0.5;
1597 let in_x = point.x >= self.left_wall_pos && point.x <= self.right_wall_pos;
1598 let gap = self.right_wall_pos - self.left_wall_pos;
1599 in_x && in_y && gap < 2.0 }
1601
1602 pub fn push_direction(&self, point: Vec2) -> Vec2 {
1604 let mid = (self.left_wall_pos + self.right_wall_pos) * 0.5;
1605 if point.x < mid {
1606 Vec2::new(self.push_force, 0.0)
1607 } else {
1608 Vec2::new(-self.push_force, 0.0)
1609 }
1610 }
1611
1612 pub fn reset(&mut self) {
1614 self.left_wall_pos = self.left_start;
1615 self.right_wall_pos = self.right_start;
1616 self.is_active = false;
1617 self.is_stopped = false;
1618 self.power_source_health = 100.0;
1619 }
1620}
1621
1622#[derive(Debug, Clone)]
1624pub struct ArrowTrap {
1625 pub position: Vec2,
1626 pub direction: Vec2,
1627 pub fire_interval: f32,
1628 pub arrow_speed: f32,
1629 pub arrow_mass: f32,
1630 pub damage: f32,
1631 pub timer: f32,
1632 pub active_arrows: Vec<ObjectId>,
1633 pub max_arrows: usize,
1634 pub arrow_lifetime: f32,
1635 pub arrow_timers: Vec<f32>,
1636}
1637
1638impl ArrowTrap {
1639 pub fn new(position: Vec2, direction: Vec2, fire_interval: f32, damage: f32) -> Self {
1640 Self {
1641 position,
1642 direction: direction.normalize_or_zero(),
1643 fire_interval,
1644 arrow_speed: 12.0,
1645 arrow_mass: 0.2,
1646 damage,
1647 timer: 0.0,
1648 active_arrows: Vec::new(),
1649 max_arrows: 10,
1650 arrow_lifetime: 5.0,
1651 arrow_timers: Vec::new(),
1652 }
1653 }
1654
1655 pub fn update(&mut self, dt: f32, world: &mut PhysicsWorld) {
1657 self.timer += dt;
1658
1659 if self.timer >= self.fire_interval {
1661 self.timer -= self.fire_interval;
1662 if self.active_arrows.len() < self.max_arrows {
1663 let mut arrow = PhysicsObject::new(
1664 Vec3::new(self.position.x, self.position.y, 0.0),
1665 self.arrow_mass,
1666 CollisionShape::Capsule {
1667 radius: 0.05,
1668 height: 0.4,
1669 },
1670 );
1671 arrow.velocity.x = self.direction.x * self.arrow_speed;
1672 arrow.velocity.y = self.direction.y * self.arrow_speed;
1673 arrow.restitution = 0.1;
1674 arrow.collision_layer = 0xFFFF_FFFF;
1675 let id = world.add_object(arrow);
1676 self.active_arrows.push(id);
1677 self.arrow_timers.push(0.0);
1678 }
1679 }
1680
1681 let mut to_remove = Vec::new();
1683 for i in 0..self.arrow_timers.len() {
1684 self.arrow_timers[i] += dt;
1685 if self.arrow_timers[i] >= self.arrow_lifetime {
1686 to_remove.push(i);
1687 }
1688 }
1689
1690 for &i in to_remove.iter().rev() {
1692 if i < self.active_arrows.len() {
1693 world.remove_object(self.active_arrows[i]);
1694 self.active_arrows.remove(i);
1695 self.arrow_timers.remove(i);
1696 }
1697 }
1698 }
1699}
1700
1701#[derive(Debug, Clone)]
1703pub struct TrapSystem {
1704 pub pendulums: Vec<SwingingPendulum>,
1705 pub falling_rocks: Vec<FallingRocks>,
1706 pub spike_pits: Vec<SpikePit>,
1707 pub flame_jets: Vec<FlameJet>,
1708 pub crushing_walls: Vec<CrushingWalls>,
1709 pub arrow_traps: Vec<ArrowTrap>,
1710}
1711
1712impl TrapSystem {
1713 pub fn new() -> Self {
1714 Self {
1715 pendulums: Vec::new(),
1716 falling_rocks: Vec::new(),
1717 spike_pits: Vec::new(),
1718 flame_jets: Vec::new(),
1719 crushing_walls: Vec::new(),
1720 arrow_traps: Vec::new(),
1721 }
1722 }
1723
1724 pub fn update(&mut self, dt: f32, world: &mut PhysicsWorld, floor_y: f32) {
1726 let gravity = world.gravity.y.abs();
1727
1728 for pendulum in &mut self.pendulums {
1729 pendulum.update(dt, gravity);
1730 pendulum.sync_physics(world);
1731 }
1732
1733 for rocks in &mut self.falling_rocks {
1734 rocks.update(dt, world, floor_y);
1735 }
1736
1737 for spikes in &mut self.spike_pits {
1738 spikes.update(dt, world);
1739 }
1740
1741 for jet in &mut self.flame_jets {
1742 jet.update(dt);
1743 }
1744
1745 for walls in &mut self.crushing_walls {
1746 walls.update(dt, world);
1747 }
1748
1749 for arrows in &mut self.arrow_traps {
1750 arrows.update(dt, world);
1751 }
1752 }
1753
1754 pub fn check_damage(&self, entity_pos: Vec2, entity_id: ObjectId) -> Vec<DamageEvent> {
1756 let mut events = Vec::new();
1757
1758 for pendulum in &self.pendulums {
1759 let bob_pos = pendulum.bob_position();
1760 let dist = (entity_pos - bob_pos).length();
1761 if dist < pendulum.bob_radius + 0.5 {
1762 let knockback = (entity_pos - bob_pos).normalize_or_zero() * 5.0;
1763 events.push(DamageEvent {
1764 source_description: "Swinging Pendulum".to_string(),
1765 target_object_id: entity_id,
1766 damage: pendulum.damage,
1767 knockback,
1768 });
1769 }
1770 }
1771
1772 for jet in &self.flame_jets {
1773 if jet.is_in_flame_zone(entity_pos) {
1774 events.push(DamageEvent {
1775 source_description: "Flame Jet".to_string(),
1776 target_object_id: entity_id,
1777 damage: jet.damage_per_second,
1778 knockback: jet.direction * 2.0,
1779 });
1780 }
1781 }
1782
1783 for spikes in &self.spike_pits {
1784 if spikes.is_touching_spike(entity_pos, 0.15) {
1785 events.push(DamageEvent {
1786 source_description: "Spike Pit".to_string(),
1787 target_object_id: entity_id,
1788 damage: spikes.damage_per_second,
1789 knockback: Vec2::new(0.0, 3.0),
1790 });
1791 }
1792 }
1793
1794 for walls in &self.crushing_walls {
1795 if walls.is_being_crushed(entity_pos) {
1796 let push = walls.push_direction(entity_pos);
1797 events.push(DamageEvent {
1798 source_description: "Crushing Walls".to_string(),
1799 target_object_id: entity_id,
1800 damage: walls.damage_per_second,
1801 knockback: push,
1802 });
1803 }
1804 }
1805
1806 events
1807 }
1808
1809 pub fn check_triggers(&mut self, entity_pos: Vec2, world: &mut PhysicsWorld) {
1811 for rocks in &mut self.falling_rocks {
1812 if rocks.check_trigger(entity_pos) {
1813 rocks.trigger(world);
1814 }
1815 }
1816
1817 for spikes in &mut self.spike_pits {
1818 if spikes.check_trigger(entity_pos) {
1819 spikes.activate();
1820 }
1821 }
1822 }
1823}
1824
1825#[derive(Debug, Clone)]
1829pub struct TreasureChest {
1830 pub position: Vec2,
1831 pub lid_angle: f32,
1832 pub lid_angular_velocity: f32,
1833 pub lid_spring_constant: f32,
1834 pub lid_damping: f32,
1835 pub is_open: bool,
1836 pub target_angle: f32,
1837 pub loot_items: Vec<ObjectId>,
1838 pub body_object_id: Option<ObjectId>,
1839 pub loot_scatter_speed: f32,
1840}
1841
1842impl TreasureChest {
1843 pub fn new(position: Vec2) -> Self {
1844 Self {
1845 position,
1846 lid_angle: 0.0,
1847 lid_angular_velocity: 0.0,
1848 lid_spring_constant: 80.0,
1849 lid_damping: 6.0,
1850 is_open: false,
1851 target_angle: std::f32::consts::FRAC_PI_2 * 1.5, loot_items: Vec::new(),
1853 body_object_id: None,
1854 loot_scatter_speed: 4.0,
1855 }
1856 }
1857
1858 pub fn spawn(&mut self, world: &mut PhysicsWorld) -> ObjectId {
1860 let obj = PhysicsObject::new_static(
1861 Vec3::new(self.position.x, self.position.y, 0.0),
1862 CollisionShape::AABB {
1863 half_extents: Vec2::new(0.5, 0.3),
1864 },
1865 );
1866 let id = world.add_object(obj);
1867 self.body_object_id = Some(id);
1868 id
1869 }
1870
1871 pub fn open(&mut self, world: &mut PhysicsWorld, loot_count: usize) {
1873 if self.is_open {
1874 return;
1875 }
1876 self.is_open = true;
1877
1878 for i in 0..loot_count {
1880 let angle = (i as f32 / loot_count as f32) * std::f32::consts::PI + 0.1;
1881 let dir = Vec2::new(angle.cos(), angle.sin());
1882 let mut item = PhysicsObject::new(
1883 Vec3::new(self.position.x, self.position.y + 0.5, 0.0),
1884 0.3,
1885 CollisionShape::Circle { radius: 0.15 },
1886 );
1887 item.velocity.x = dir.x * self.loot_scatter_speed;
1888 item.velocity.y = dir.y * self.loot_scatter_speed + 2.0;
1889 item.friction = 0.8; item.restitution = 0.3;
1891 let id = world.add_object(item);
1892 self.loot_items.push(id);
1893 }
1894 }
1895
1896 pub fn update(&mut self, dt: f32) {
1898 if !self.is_open {
1899 return;
1900 }
1901 let displacement = self.target_angle - self.lid_angle;
1902 let spring_torque = self.lid_spring_constant * displacement;
1903 let damping_torque = -self.lid_damping * self.lid_angular_velocity;
1904 let accel = spring_torque + damping_torque;
1905 self.lid_angular_velocity += accel * dt;
1906 self.lid_angle += self.lid_angular_velocity * dt;
1907 self.lid_angle = clampf(self.lid_angle, 0.0, self.target_angle + 0.1);
1909 }
1910}
1911
1912#[derive(Debug, Clone)]
1914pub struct PedestalItem {
1915 pub base_position: Vec2,
1916 pub bob_amplitude: f32,
1917 pub bob_frequency: f32,
1918 pub bob_timer: f32,
1919 pub halo_radius: f32,
1920 pub halo_particle_count: usize,
1921 pub collision_object_id: Option<ObjectId>,
1922 pub item_name: String,
1923}
1924
1925impl PedestalItem {
1926 pub fn new(position: Vec2, item_name: String) -> Self {
1927 Self {
1928 base_position: position,
1929 bob_amplitude: 0.3,
1930 bob_frequency: 2.0,
1931 bob_timer: 0.0,
1932 halo_radius: 0.6,
1933 halo_particle_count: 8,
1934 collision_object_id: None,
1935 item_name,
1936 }
1937 }
1938
1939 pub fn spawn(&mut self, world: &mut PhysicsWorld) -> ObjectId {
1941 let obj = PhysicsObject::new_static(
1942 Vec3::new(self.base_position.x, self.base_position.y, 0.0),
1943 CollisionShape::Circle { radius: 0.3 },
1944 );
1945 let id = world.add_object(obj);
1946 self.collision_object_id = Some(id);
1947 id
1948 }
1949
1950 pub fn display_position(&self) -> Vec2 {
1952 let offset = (self.bob_timer * self.bob_frequency * std::f32::consts::TAU).sin()
1953 * self.bob_amplitude;
1954 Vec2::new(self.base_position.x, self.base_position.y + offset)
1955 }
1956
1957 pub fn halo_positions(&self) -> Vec<Vec2> {
1959 let center = self.display_position();
1960 (0..self.halo_particle_count)
1961 .map(|i| {
1962 let angle = (i as f32 / self.halo_particle_count as f32) * std::f32::consts::TAU
1963 + self.bob_timer * 1.5;
1964 center + Vec2::new(angle.cos(), angle.sin()) * self.halo_radius
1965 })
1966 .collect()
1967 }
1968
1969 pub fn update(&mut self, dt: f32) {
1971 self.bob_timer += dt;
1972 }
1973}
1974
1975#[derive(Debug, Clone)]
1977pub struct TreasureRoom {
1978 pub chests: Vec<TreasureChest>,
1979 pub pedestals: Vec<PedestalItem>,
1980}
1981
1982impl TreasureRoom {
1983 pub fn new() -> Self {
1984 Self {
1985 chests: Vec::new(),
1986 pedestals: Vec::new(),
1987 }
1988 }
1989
1990 pub fn update(&mut self, dt: f32) {
1991 for chest in &mut self.chests {
1992 chest.update(dt);
1993 }
1994 for pedestal in &mut self.pedestals {
1995 pedestal.update(dt);
1996 }
1997 }
1998}
1999
2000#[derive(Debug, Clone)]
2004pub struct RiftObject {
2005 pub object_id: ObjectId,
2006 pub lifetime: f32,
2007 pub max_lifetime: f32,
2008}
2009
2010#[derive(Debug, Clone)]
2012pub struct ChaosRiftRoom {
2013 pub rift_center: Vec2,
2014 pub rift_radius: f32,
2015 pub vortex_pull_strength: f32,
2016 pub vortex_spin_strength: f32,
2017 pub spawn_rate: f32,
2018 pub spawn_rate_escalation: f32,
2019 pub max_spawn_rate: f32,
2020 pub spawn_timer: f32,
2021 pub elapsed: f32,
2022 pub active_objects: Vec<RiftObject>,
2023 pub max_objects: usize,
2024 pub despawn_burst_count: usize,
2025 pub seed: f32,
2026}
2027
2028impl ChaosRiftRoom {
2029 pub fn new(center: Vec2, radius: f32) -> Self {
2030 Self {
2031 rift_center: center,
2032 rift_radius: radius,
2033 vortex_pull_strength: 15.0,
2034 vortex_spin_strength: 8.0,
2035 spawn_rate: 1.0,
2036 spawn_rate_escalation: 0.1,
2037 max_spawn_rate: 10.0,
2038 spawn_timer: 0.0,
2039 elapsed: 0.0,
2040 active_objects: Vec::new(),
2041 max_objects: 50,
2042 despawn_burst_count: 6,
2043 seed: 42.0,
2044 }
2045 }
2046
2047 pub fn update(&mut self, dt: f32, world: &mut PhysicsWorld) {
2049 self.elapsed += dt;
2050
2051 let current_rate = (self.spawn_rate + self.spawn_rate_escalation * self.elapsed)
2053 .min(self.max_spawn_rate);
2054 let spawn_interval = 1.0 / current_rate;
2055
2056 self.spawn_timer += dt;
2057
2058 while self.spawn_timer >= spawn_interval && self.active_objects.len() < self.max_objects {
2060 self.spawn_timer -= spawn_interval;
2061 self.seed += 1.0;
2062
2063 let angle = pseudo_random(self.seed * 0.7) * std::f32::consts::TAU;
2064 let speed = 2.0 + pseudo_random(self.seed * 1.3) * 5.0;
2065 let dir = Vec2::new(angle.cos(), angle.sin());
2066
2067 let shape_rand = pseudo_random(self.seed * 2.1);
2069 let shape = if shape_rand < 0.33 {
2070 let r = 0.1 + pseudo_random(self.seed * 3.7) * 0.5;
2071 CollisionShape::Circle { radius: r }
2072 } else if shape_rand < 0.66 {
2073 let w = 0.1 + pseudo_random(self.seed * 4.3) * 0.4;
2074 let h = 0.1 + pseudo_random(self.seed * 5.1) * 0.4;
2075 CollisionShape::AABB {
2076 half_extents: Vec2::new(w, h),
2077 }
2078 } else {
2079 let r = 0.08 + pseudo_random(self.seed * 6.2) * 0.2;
2080 let h = 0.2 + pseudo_random(self.seed * 7.4) * 0.5;
2081 CollisionShape::Capsule { radius: r, height: h }
2082 };
2083
2084 let mass = 0.5 + pseudo_random(self.seed * 8.9) * 3.0;
2085 let mut obj = PhysicsObject::new(
2086 Vec3::new(self.rift_center.x + dir.x * self.rift_radius, self.rift_center.y + dir.y * self.rift_radius, 0.0),
2087 mass,
2088 shape,
2089 );
2090 obj.velocity.x = dir.x * speed;
2091 obj.velocity.y = dir.y * speed;
2092 obj.restitution = 0.6;
2093
2094 let id = world.add_object(obj);
2095 self.active_objects.push(RiftObject {
2096 object_id: id,
2097 lifetime: 0.0,
2098 max_lifetime: 8.0 + pseudo_random(self.seed * 9.1) * 4.0,
2099 });
2100 }
2101
2102 world.apply_vortex_force(
2104 self.rift_center,
2105 self.rift_radius * 3.0,
2106 self.vortex_pull_strength,
2107 self.vortex_spin_strength,
2108 );
2109
2110 let mut to_despawn = Vec::new();
2112 for (i, rift_obj) in self.active_objects.iter_mut().enumerate() {
2113 rift_obj.lifetime += dt;
2114 if let Some(obj) = world.get_object(rift_obj.object_id) {
2115 let dist = (obj.pos2d() - self.rift_center).length();
2116 if dist < self.rift_radius * 0.3 || rift_obj.lifetime >= rift_obj.max_lifetime {
2117 to_despawn.push(i);
2118 }
2119 } else {
2120 to_despawn.push(i);
2121 }
2122 }
2123
2124 for &i in to_despawn.iter().rev() {
2126 if i < self.active_objects.len() {
2127 let rift_obj = self.active_objects.remove(i);
2128 world.remove_object(rift_obj.object_id);
2129 }
2130 }
2131 }
2132
2133 pub fn current_spawn_rate(&self) -> f32 {
2135 (self.spawn_rate + self.spawn_rate_escalation * self.elapsed).min(self.max_spawn_rate)
2136 }
2137
2138 pub fn active_count(&self) -> usize {
2140 self.active_objects.len()
2141 }
2142}
2143
2144pub type CollisionCallback = fn(ObjectId, ObjectId, &Contact);
2148
2149pub struct ArenaPhysicsManager {
2151 pub world: PhysicsWorld,
2152 pub rooms: Vec<ArenaRoom>,
2153 pub trap_systems: HashMap<u32, TrapSystem>,
2154 pub treasure_rooms: HashMap<u32, TreasureRoom>,
2155 pub chaos_rifts: HashMap<u32, ChaosRiftRoom>,
2156 pub damage_events: Vec<DamageEvent>,
2157 pub collision_callbacks: Vec<CollisionCallback>,
2158 pub floor_y: f32,
2159}
2160
2161impl ArenaPhysicsManager {
2162 pub fn new() -> Self {
2163 Self {
2164 world: PhysicsWorld::new(),
2165 rooms: Vec::new(),
2166 trap_systems: HashMap::new(),
2167 treasure_rooms: HashMap::new(),
2168 chaos_rifts: HashMap::new(),
2169 damage_events: Vec::new(),
2170 collision_callbacks: Vec::new(),
2171 floor_y: 0.0,
2172 }
2173 }
2174
2175 pub fn set_floor(&mut self, y: f32) {
2177 self.floor_y = y;
2178 }
2179
2180 pub fn add_room(&mut self, room: ArenaRoom) -> u32 {
2182 let id = room.room_id;
2183 self.rooms.push(room);
2184 id
2185 }
2186
2187 pub fn register_trap_system(&mut self, room_id: u32, system: TrapSystem) {
2189 self.trap_systems.insert(room_id, system);
2190 }
2191
2192 pub fn register_treasure_room(&mut self, room_id: u32, treasure: TreasureRoom) {
2194 self.treasure_rooms.insert(room_id, treasure);
2195 }
2196
2197 pub fn register_chaos_rift(&mut self, room_id: u32, rift: ChaosRiftRoom) {
2199 self.chaos_rifts.insert(room_id, rift);
2200 }
2201
2202 pub fn on_collision(&mut self, callback: CollisionCallback) {
2204 self.collision_callbacks.push(callback);
2205 }
2206
2207 pub fn step(&mut self, dt: f32) -> Vec<DamageEvent> {
2209 self.damage_events.clear();
2210
2211 let (collisions, _triggers) = self.world.step(dt);
2213
2214 for pair in &collisions {
2216 for cb in &self.collision_callbacks {
2217 cb(pair.id_a, pair.id_b, &pair.contact);
2218 }
2219 }
2220
2221 let floor_y = self.floor_y;
2223 for (_, system) in &mut self.trap_systems {
2224 system.update(dt, &mut self.world, floor_y);
2225 }
2226
2227 for (_, treasure) in &mut self.treasure_rooms {
2229 treasure.update(dt);
2230 }
2231
2232 for (_, rift) in &mut self.chaos_rifts {
2234 rift.update(dt, &mut self.world);
2235 }
2236
2237 self.damage_events.clone()
2238 }
2239
2240 pub fn check_entity_damage(&self, entity_pos: Vec2, entity_id: ObjectId) -> Vec<DamageEvent> {
2242 let mut all_damage = Vec::new();
2243 for (_, system) in &self.trap_systems {
2244 let events = system.check_damage(entity_pos, entity_id);
2245 all_damage.extend(events);
2246 }
2247 all_damage
2248 }
2249
2250 pub fn notify_entity_position(&mut self, entity_pos: Vec2) {
2252 for (_, system) in &mut self.trap_systems {
2253 system.check_triggers(entity_pos, &mut self.world);
2254 }
2255 }
2256
2257 pub fn raycast(&self, origin: Vec2, dir: Vec2, max_dist: f32) -> Option<RayHit> {
2259 self.world.raycast(origin, dir, max_dist)
2260 }
2261
2262 pub fn overlap_test(&self, shape: &CollisionShape, pos: Vec2) -> Vec<ObjectId> {
2264 self.world.overlap_test(shape, pos)
2265 }
2266
2267 pub fn room_at(&self, pos: Vec2) -> Option<&ArenaRoom> {
2269 self.rooms.iter().find(|r| r.contains_point(pos))
2270 }
2271
2272 pub fn total_objects(&self) -> usize {
2274 self.world.object_count()
2275 }
2276}
2277
2278#[cfg(test)]
2281mod tests {
2282 use super::*;
2283
2284 #[test]
2287 fn test_circle_circle_collision() {
2288 let c = collide_circle_circle(Vec2::ZERO, 1.0, Vec2::new(1.5, 0.0), 1.0);
2289 assert!(c.is_some());
2290 let c = c.unwrap();
2291 assert!((c.penetration - 0.5).abs() < 0.01);
2292 assert!(c.normal.x > 0.9);
2293 }
2294
2295 #[test]
2296 fn test_circle_circle_no_collision() {
2297 let c = collide_circle_circle(Vec2::ZERO, 1.0, Vec2::new(3.0, 0.0), 1.0);
2298 assert!(c.is_none());
2299 }
2300
2301 #[test]
2302 fn test_aabb_aabb_collision() {
2303 let c = collide_aabb_aabb(
2304 Vec2::ZERO,
2305 Vec2::new(1.0, 1.0),
2306 Vec2::new(1.5, 0.0),
2307 Vec2::new(1.0, 1.0),
2308 );
2309 assert!(c.is_some());
2310 let c = c.unwrap();
2311 assert!(c.penetration > 0.0);
2312 }
2313
2314 #[test]
2315 fn test_aabb_aabb_no_collision() {
2316 let c = collide_aabb_aabb(
2317 Vec2::ZERO,
2318 Vec2::new(1.0, 1.0),
2319 Vec2::new(5.0, 0.0),
2320 Vec2::new(1.0, 1.0),
2321 );
2322 assert!(c.is_none());
2323 }
2324
2325 #[test]
2326 fn test_circle_aabb_collision() {
2327 let c = collide_circle_aabb(Vec2::new(1.8, 0.0), 0.5, Vec2::ZERO, Vec2::new(1.0, 1.0));
2328 assert!(c.is_some());
2329 }
2330
2331 #[test]
2332 fn test_circle_aabb_no_collision() {
2333 let c = collide_circle_aabb(Vec2::new(3.0, 0.0), 0.5, Vec2::ZERO, Vec2::new(1.0, 1.0));
2334 assert!(c.is_none());
2335 }
2336
2337 #[test]
2340 fn test_add_remove_objects() {
2341 let mut world = PhysicsWorld::new();
2342 let id = world.add_object(PhysicsObject::new(
2343 Vec3::ZERO,
2344 1.0,
2345 CollisionShape::Circle { radius: 1.0 },
2346 ));
2347 assert_eq!(world.object_count(), 1);
2348 world.remove_object(id);
2349 assert_eq!(world.object_count(), 0);
2350 }
2351
2352 #[test]
2353 fn test_gravity_integration() {
2354 let mut world = PhysicsWorld::new();
2355 let id = world.add_object(PhysicsObject::new(
2356 Vec3::new(0.0, 10.0, 0.0),
2357 1.0,
2358 CollisionShape::Circle { radius: 0.5 },
2359 ));
2360 for _ in 0..10 {
2362 world.step(0.1);
2363 }
2364 let obj = world.get_object(id).unwrap();
2365 assert!(obj.position.y < 10.0);
2367 }
2368
2369 #[test]
2370 fn test_impulse_resolution() {
2371 let mut world = PhysicsWorld::new();
2372 world.gravity = Vec2::ZERO; let id_a = world.add_object({
2376 let mut obj = PhysicsObject::new(
2377 Vec3::new(-1.0, 0.0, 0.0),
2378 1.0,
2379 CollisionShape::Circle { radius: 1.0 },
2380 );
2381 obj.velocity.x = 5.0;
2382 obj
2383 });
2384 let id_b = world.add_object({
2385 let mut obj = PhysicsObject::new(
2386 Vec3::new(1.0, 0.0, 0.0),
2387 1.0,
2388 CollisionShape::Circle { radius: 1.0 },
2389 );
2390 obj.velocity.x = -5.0;
2391 obj
2392 });
2393
2394 world.step(0.016);
2395
2396 let a = world.get_object(id_a).unwrap();
2397 let b = world.get_object(id_b).unwrap();
2398 assert!(a.velocity.x <= 0.0);
2400 assert!(b.velocity.x >= 0.0);
2401 }
2402
2403 #[test]
2404 fn test_static_object_immovable() {
2405 let mut world = PhysicsWorld::new();
2406 let id = world.add_object(PhysicsObject::new_static(
2407 Vec3::new(0.0, 0.0, 0.0),
2408 CollisionShape::AABB {
2409 half_extents: Vec2::new(5.0, 0.5),
2410 },
2411 ));
2412 world.step(1.0);
2413 let obj = world.get_object(id).unwrap();
2414 assert!((obj.position.y - 0.0).abs() < 0.001);
2415 }
2416
2417 #[test]
2418 fn test_raycast() {
2419 let mut world = PhysicsWorld::new();
2420 world.gravity = Vec2::ZERO;
2421 world.add_object(PhysicsObject::new_static(
2422 Vec3::new(5.0, 0.0, 0.0),
2423 CollisionShape::Circle { radius: 1.0 },
2424 ));
2425 let hit = world.raycast(Vec2::ZERO, Vec2::new(1.0, 0.0), 100.0);
2426 assert!(hit.is_some());
2427 let hit = hit.unwrap();
2428 assert!((hit.point.x - 4.0).abs() < 0.1);
2429 }
2430
2431 #[test]
2432 fn test_overlap_test() {
2433 let mut world = PhysicsWorld::new();
2434 world.gravity = Vec2::ZERO;
2435 let id = world.add_object(PhysicsObject::new_static(
2436 Vec3::new(0.0, 0.0, 0.0),
2437 CollisionShape::Circle { radius: 2.0 },
2438 ));
2439 let results = world.overlap_test(&CollisionShape::Circle { radius: 1.0 }, Vec2::new(1.0, 0.0));
2440 assert!(results.contains(&id));
2441 }
2442
2443 #[test]
2444 fn test_trigger_events() {
2445 let mut world = PhysicsWorld::new();
2446 world.gravity = Vec2::ZERO;
2447
2448 world.add_object(PhysicsObject::new_trigger(
2450 Vec3::new(0.0, 0.0, 0.0),
2451 CollisionShape::Circle { radius: 2.0 },
2452 ));
2453
2454 let mut entering = PhysicsObject::new(
2456 Vec3::new(0.5, 0.0, 0.0),
2457 1.0,
2458 CollisionShape::Circle { radius: 0.5 },
2459 );
2460 entering.collision_layer = 1;
2461 world.add_object(entering);
2462
2463 let (_, triggers) = world.step(0.016);
2464 assert!(!triggers.is_empty());
2465 assert!(triggers[0].is_enter);
2466 }
2467
2468 #[test]
2471 fn test_pendulum_oscillation() {
2472 let mut pendulum = SwingingPendulum::new(Vec2::new(0.0, 5.0), 3.0, 2.0, 0.5, 10.0);
2473 let initial_angle = pendulum.angular_position;
2474 for _ in 0..100 {
2476 pendulum.update(0.016, 9.81);
2477 }
2478 assert!((pendulum.angular_position - initial_angle).abs() > 0.01);
2480 }
2481
2482 #[test]
2483 fn test_spike_pit_spring_physics() {
2484 let trigger_zone = AABB::new(Vec2::new(-2.0, -1.0), Vec2::new(2.0, 0.0));
2485 let mut spikes = SpikePit::new(trigger_zone, 4, 1.5, 20.0);
2486 let mut world = PhysicsWorld::new();
2487 spikes.spawn(&mut world);
2488 spikes.activate();
2489
2490 for _ in 0..200 {
2492 spikes.update(0.016, &mut world);
2493 }
2494
2495 for h in &spikes.spike_heights {
2497 assert!((*h - 1.5).abs() < 0.3, "Spike height {} not near target 1.5", h);
2498 }
2499 }
2500
2501 #[test]
2502 fn test_flame_jet_cycle() {
2503 let mut jet = FlameJet::new(Vec2::ZERO, Vec2::new(1.0, 0.0), 15.0);
2504 let mut was_firing = false;
2506 let mut was_warming = false;
2507 for _ in 0..300 {
2508 jet.update(0.016);
2509 if jet.is_firing() {
2510 was_firing = true;
2511 }
2512 if jet.is_warming_up() {
2513 was_warming = true;
2514 }
2515 }
2516 assert!(was_firing, "Flame jet should have fired");
2517 assert!(was_warming, "Flame jet should have warmed up");
2518 }
2519
2520 #[test]
2521 fn test_crushing_walls_close() {
2522 let mut walls = CrushingWalls::new(-5.0, 5.0, 0.0, 4.0, 30.0);
2523 let mut world = PhysicsWorld::new();
2524 walls.spawn(&mut world);
2525 walls.activate();
2526
2527 let initial_gap = walls.right_wall_pos - walls.left_wall_pos;
2528 for _ in 0..100 {
2529 walls.update(0.016, &mut world);
2530 }
2531 let final_gap = walls.right_wall_pos - walls.left_wall_pos;
2532 assert!(final_gap < initial_gap, "Walls should have closed in");
2533 }
2534
2535 #[test]
2536 fn test_crushing_walls_stop_on_power_source_destroy() {
2537 let mut walls = CrushingWalls::new(-5.0, 5.0, 0.0, 4.0, 30.0);
2538 let mut world = PhysicsWorld::new();
2539 walls.spawn(&mut world);
2540 walls.activate();
2541
2542 for _ in 0..50 {
2544 walls.update(0.016, &mut world);
2545 }
2546 let gap_before = walls.right_wall_pos - walls.left_wall_pos;
2547
2548 walls.damage_power_source(200.0);
2550 assert!(walls.is_stopped);
2551
2552 for _ in 0..50 {
2554 walls.update(0.016, &mut world);
2555 }
2556 let gap_after = walls.right_wall_pos - walls.left_wall_pos;
2557 assert!((gap_before - gap_after).abs() < 0.001);
2558 }
2559
2560 #[test]
2561 fn test_arrow_trap_fires() {
2562 let mut trap = ArrowTrap::new(Vec2::ZERO, Vec2::new(1.0, 0.0), 0.5, 10.0);
2563 let mut world = PhysicsWorld::new();
2564 world.gravity = Vec2::ZERO;
2565
2566 for _ in 0..125 {
2568 trap.update(0.016, &mut world);
2569 }
2570 assert!(
2571 !trap.active_arrows.is_empty(),
2572 "Arrow trap should have fired at least one arrow"
2573 );
2574 }
2575
2576 #[test]
2577 fn test_treasure_chest_open() {
2578 let mut chest = TreasureChest::new(Vec2::ZERO);
2579 let mut world = PhysicsWorld::new();
2580 world.gravity = Vec2::ZERO;
2581 chest.spawn(&mut world);
2582 chest.open(&mut world, 5);
2583 assert!(chest.is_open);
2584 assert_eq!(chest.loot_items.len(), 5);
2585 for _ in 0..100 {
2587 chest.update(0.016);
2588 }
2589 assert!(chest.lid_angle > 0.5, "Lid should have opened");
2590 }
2591
2592 #[test]
2593 fn test_chaos_rift_spawns_objects() {
2594 let mut rift = ChaosRiftRoom::new(Vec2::ZERO, 3.0);
2595 let mut world = PhysicsWorld::new();
2596 world.gravity = Vec2::ZERO;
2597
2598 for _ in 0..200 {
2600 rift.update(0.016, &mut world);
2601 }
2602 assert!(
2603 rift.active_count() > 0,
2604 "Chaos rift should have spawned objects"
2605 );
2606 }
2607
2608 #[test]
2609 fn test_chaos_rift_escalation() {
2610 let mut rift = ChaosRiftRoom::new(Vec2::ZERO, 3.0);
2611 let rate_initial = rift.current_spawn_rate();
2612 rift.elapsed = 30.0;
2613 let rate_later = rift.current_spawn_rate();
2614 assert!(rate_later > rate_initial, "Spawn rate should escalate");
2615 }
2616
2617 #[test]
2618 fn test_arena_manager_step() {
2619 let mut mgr = ArenaPhysicsManager::new();
2620 mgr.world.gravity = Vec2::ZERO;
2621
2622 let room = ArenaRoom::new(0, RoomType::Trap, AABB::new(Vec2::new(-10.0, -10.0), Vec2::new(10.0, 10.0)));
2623 mgr.add_room(room);
2624
2625 let mut traps = TrapSystem::new();
2626 traps.pendulums.push(SwingingPendulum::new(
2627 Vec2::new(0.0, 5.0),
2628 3.0,
2629 2.0,
2630 0.5,
2631 10.0,
2632 ));
2633 mgr.register_trap_system(0, traps);
2634
2635 for _ in 0..60 {
2637 mgr.step(0.016);
2638 }
2639 }
2640
2641 #[test]
2642 fn test_aabb_helpers() {
2643 let a = AABB::new(Vec2::ZERO, Vec2::new(2.0, 2.0));
2644 assert!(a.contains_point(Vec2::new(1.0, 1.0)));
2645 assert!(!a.contains_point(Vec2::new(3.0, 1.0)));
2646 assert_eq!(a.center(), Vec2::new(1.0, 1.0));
2647 assert_eq!(a.half_extents(), Vec2::new(1.0, 1.0));
2648 }
2649
2650 #[test]
2651 fn test_pedestal_item_bob() {
2652 let mut pedestal = PedestalItem::new(Vec2::new(0.0, 3.0), "Magic Sword".to_string());
2653 pedestal.update(0.5);
2654 let pos = pedestal.display_position();
2655 assert!((pos.y - 3.0).abs() <= pedestal.bob_amplitude + 0.01);
2657 }
2658
2659 #[test]
2660 fn test_radial_force() {
2661 let mut world = PhysicsWorld::new();
2662 world.gravity = Vec2::ZERO;
2663 let id = world.add_object(PhysicsObject::new(
2664 Vec3::new(2.0, 0.0, 0.0),
2665 1.0,
2666 CollisionShape::Circle { radius: 0.5 },
2667 ));
2668 world.apply_radial_force(Vec2::ZERO, 5.0, 100.0);
2669 world.step(0.016);
2670 let obj = world.get_object(id).unwrap();
2671 assert!(obj.velocity.x > 0.0);
2673 }
2674}