1use glam::Vec2;
24use std::f32::consts::PI;
25
26#[derive(Debug, Clone)]
32pub struct SteeringAgent {
33 pub position: Vec2,
34 pub velocity: Vec2,
35 pub heading: Vec2, pub max_speed: f32,
37 pub max_force: f32,
38 pub mass: f32,
39 pub radius: f32,
40 pub wander_angle: f32,
42}
43
44impl SteeringAgent {
45 pub fn new(position: Vec2, max_speed: f32, max_force: f32) -> Self {
46 SteeringAgent {
47 position,
48 velocity: Vec2::ZERO,
49 heading: Vec2::X,
50 max_speed,
51 max_force,
52 mass: 1.0,
53 radius: 0.5,
54 wander_angle: 0.0,
55 }
56 }
57
58 pub fn with_mass(mut self, mass: f32) -> Self { self.mass = mass; self }
59 pub fn with_radius(mut self, r: f32) -> Self { self.radius = r; self }
60
61 pub fn speed(&self) -> f32 { self.velocity.length() }
62
63 pub fn apply_force(&mut self, force: Vec2, dt: f32) {
65 let clamped = clamp_magnitude(force, self.max_force);
66 let accel = clamped / self.mass;
67 self.velocity = clamp_magnitude(self.velocity + accel * dt, self.max_speed);
68 if self.velocity.length_squared() > 0.0001 {
69 self.heading = self.velocity.normalize();
70 }
71 }
72
73 pub fn update_position(&mut self, dt: f32) {
75 self.position += self.velocity * dt;
76 }
77
78 pub fn clamp_velocity(&mut self) {
80 self.velocity = clamp_magnitude(self.velocity, self.max_speed);
81 }
82}
83
84#[inline]
89pub fn clamp_magnitude(v: Vec2, max: f32) -> Vec2 {
90 let len = v.length();
91 if len > max && len > 0.0 { v / len * max } else { v }
92}
93
94#[inline]
95fn truncate(v: Vec2, max: f32) -> Vec2 { clamp_magnitude(v, max) }
96
97pub fn seek(agent: &SteeringAgent, target: Vec2) -> Vec2 {
103 let to_target = target - agent.position;
104 let dist = to_target.length();
105 if dist < 0.0001 { return Vec2::ZERO; }
106 let desired = (to_target / dist) * agent.max_speed;
107 truncate(desired - agent.velocity, agent.max_force)
108}
109
110pub fn flee(agent: &SteeringAgent, threat: Vec2) -> Vec2 {
112 let from_threat = agent.position - threat;
113 let dist = from_threat.length();
114 if dist < 0.0001 { return Vec2::ZERO; }
115 let desired = (from_threat / dist) * agent.max_speed;
116 truncate(desired - agent.velocity, agent.max_force)
117}
118
119pub fn arrive(agent: &SteeringAgent, target: Vec2, slow_radius: f32) -> Vec2 {
121 let to_target = target - agent.position;
122 let dist = to_target.length();
123 if dist < 0.0001 { return Vec2::ZERO; }
124 let desired_speed = if dist < slow_radius {
125 agent.max_speed * (dist / slow_radius)
126 } else {
127 agent.max_speed
128 };
129 let desired = (to_target / dist) * desired_speed;
130 truncate(desired - agent.velocity, agent.max_force)
131}
132
133pub fn pursuit(agent: &SteeringAgent, quarry_pos: Vec2, quarry_vel: Vec2) -> Vec2 {
135 let to_quarry = quarry_pos - agent.position;
136 let ahead = to_quarry.length() / (agent.max_speed + quarry_vel.length()).max(0.001);
137 let future_pos = quarry_pos + quarry_vel * ahead;
138 seek(agent, future_pos)
139}
140
141pub fn evade(agent: &SteeringAgent, threat_pos: Vec2, threat_vel: Vec2) -> Vec2 {
143 let to_threat = threat_pos - agent.position;
144 let ahead = to_threat.length() / (agent.max_speed + threat_vel.length()).max(0.001);
145 let future_pos = threat_pos + threat_vel * ahead;
146 flee(agent, future_pos)
147}
148
149pub fn wander(
155 agent: &mut SteeringAgent,
156 wander_circle_dist: f32,
157 wander_circle_radius: f32,
158 wander_jitter: f32,
159) -> Vec2 {
160 agent.wander_angle += (random_f32() * 2.0 - 1.0) * wander_jitter;
162 let circle_center = if agent.velocity.length_squared() > 0.0001 {
164 agent.position + agent.velocity.normalize() * wander_circle_dist
165 } else {
166 agent.position + Vec2::X * wander_circle_dist
167 };
168 let displacement = Vec2::new(
169 agent.wander_angle.cos() * wander_circle_radius,
170 agent.wander_angle.sin() * wander_circle_radius,
171 );
172 let wander_target = circle_center + displacement;
173 seek(agent, wander_target)
174}
175
176pub fn obstacle_avoidance(agent: &SteeringAgent, obstacles: &[(Vec2, f32)]) -> Vec2 {
178 let vel_len = agent.velocity.length();
179 let ahead_dist = agent.radius + vel_len * 2.0;
180 let heading = if vel_len > 0.0001 { agent.velocity / vel_len } else { agent.heading };
181
182 let mut closest_dist = f32::INFINITY;
184 let mut closest: Option<(Vec2, f32)> = None;
185
186 for &(center, radius) in obstacles {
187 let local = center - agent.position;
188 let proj = local.dot(heading);
190 if proj < 0.0 { continue; } if proj > ahead_dist + radius { continue; } let lateral = (local - heading * proj).length();
194 let min_dist = agent.radius + radius;
195 if lateral < min_dist && proj < closest_dist {
196 closest_dist = proj;
197 closest = Some((center, radius));
198 }
199 }
200
201 if let Some((center, _)) = closest {
202 let lateral = center - agent.position - heading * (center - agent.position).dot(heading);
204 if lateral.length_squared() > 0.0001 {
205 let away = -lateral.normalize() * agent.max_force;
206 return away;
207 }
208 }
209 Vec2::ZERO
210}
211
212pub fn wall_avoidance(agent: &SteeringAgent, walls: &[(Vec2, Vec2)]) -> Vec2 {
214 let vel_len = agent.velocity.length();
215 let ahead = if vel_len > 0.0001 {
216 agent.position + agent.velocity.normalize() * (agent.radius * 2.0 + vel_len)
217 } else {
218 agent.position + agent.heading * agent.radius * 2.0
219 };
220
221 let mut strongest = Vec2::ZERO;
222 let mut max_penetration = 0.0f32;
223
224 for &(a, b) in walls {
225 let closest = closest_point_on_segment(ahead, a, b);
226 let diff = ahead - closest;
227 let dist = diff.length();
228 let penetration = agent.radius * 2.0 - dist;
229 if penetration > 0.0 && penetration > max_penetration {
230 max_penetration = penetration;
231 if dist > 0.0001 {
232 strongest = (diff / dist) * agent.max_force;
233 }
234 }
235 }
236 strongest
237}
238
239pub fn path_following(agent: &SteeringAgent, path: &[Vec2], path_radius: f32) -> Vec2 {
241 if path.len() < 2 { return Vec2::ZERO; }
242 let vel_len = agent.velocity.length();
243 let future = if vel_len > 0.0001 {
244 agent.position + agent.velocity.normalize() * (vel_len * 0.5 + 1.0)
245 } else {
246 agent.position
247 };
248
249 let mut nearest_pt = path[0];
251 let mut nearest_dist = f32::INFINITY;
252 let mut nearest_seg = 0usize;
253
254 for i in 0..path.len() - 1 {
255 let pt = closest_point_on_segment(future, path[i], path[i + 1]);
256 let d = future.distance(pt);
257 if d < nearest_dist {
258 nearest_dist = d;
259 nearest_pt = pt;
260 nearest_seg = i;
261 }
262 }
263
264 if nearest_dist <= path_radius {
265 return Vec2::ZERO; }
267
268 let ahead_dist = vel_len * 0.5 + 1.0;
270 let seg_dir = (path[nearest_seg + 1] - path[nearest_seg]).normalize_or_zero();
271 let target = nearest_pt + seg_dir * ahead_dist;
272 seek(agent, target)
273}
274
275pub fn separation(agent: &SteeringAgent, neighbors: &[&SteeringAgent]) -> Vec2 {
277 let mut force = Vec2::ZERO;
278 let mut count = 0;
279 for &nb in neighbors {
280 let diff = agent.position - nb.position;
281 let dist = diff.length();
282 let min_dist = agent.radius + nb.radius + 0.5;
283 if dist < min_dist && dist > 0.0001 {
284 force += diff.normalize() / dist;
285 count += 1;
286 }
287 }
288 if count > 0 {
289 force /= count as f32;
290 truncate(force * agent.max_speed - agent.velocity, agent.max_force)
291 } else {
292 Vec2::ZERO
293 }
294}
295
296pub fn alignment(agent: &SteeringAgent, neighbors: &[&SteeringAgent]) -> Vec2 {
298 if neighbors.is_empty() { return Vec2::ZERO; }
299 let avg: Vec2 = neighbors.iter().map(|n| n.heading).sum::<Vec2>() / neighbors.len() as f32;
300 if avg.length_squared() < 0.0001 { return Vec2::ZERO; }
301 let desired = avg.normalize() * agent.max_speed;
302 truncate(desired - agent.velocity, agent.max_force)
303}
304
305pub fn cohesion(agent: &SteeringAgent, neighbors: &[&SteeringAgent]) -> Vec2 {
307 if neighbors.is_empty() { return Vec2::ZERO; }
308 let center: Vec2 = neighbors.iter().map(|n| n.position).sum::<Vec2>() / neighbors.len() as f32;
309 seek(agent, center)
310}
311
312pub fn leader_following(agent: &SteeringAgent, leader: &SteeringAgent, offset: Vec2) -> Vec2 {
314 let heading = leader.heading;
316 let right = Vec2::new(-heading.y, heading.x);
317 let world_offset = leader.position + heading * offset.y + right * offset.x;
318 let too_close = agent.position.distance(leader.position) < leader.radius + agent.radius + 1.0;
320 if too_close {
321 flee(agent, leader.position)
322 } else {
323 arrive(agent, world_offset, 2.0)
324 }
325}
326
327pub fn queue(
329 agent: &SteeringAgent,
330 leader: &SteeringAgent,
331 neighbors: &[&SteeringAgent],
332) -> Vec2 {
333 let ahead = agent.position + agent.heading * (agent.radius * 2.0 + 1.0);
334 let blocked = neighbors.iter().any(|n| {
335 n.position.distance(ahead) < n.radius + agent.radius
336 });
337 if blocked {
338 -agent.velocity * 0.5
340 } else {
341 arrive(agent, leader.position + leader.heading * -(leader.radius + agent.radius + 0.5), 2.0)
342 }
343}
344
345pub fn interpose(agent: &SteeringAgent, a: Vec2, b: Vec2) -> Vec2 {
347 let midpoint = (a + b) * 0.5;
348 arrive(agent, midpoint, 2.0)
349}
350
351fn closest_point_on_segment(pt: Vec2, a: Vec2, b: Vec2) -> Vec2 {
356 let ab = b - a;
357 let len_sq = ab.length_squared();
358 if len_sq < 0.0001 { return a; }
359 let t = ((pt - a).dot(ab) / len_sq).clamp(0.0, 1.0);
360 a + ab * t
361}
362
363static RAND_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(12345);
365fn random_f32() -> f32 {
366 let v = RAND_COUNTER.fetch_add(2654435761, std::sync::atomic::Ordering::Relaxed);
367 let h = v ^ (v >> 16);
368 let h = h.wrapping_mul(0x45d9f3b37197344d);
369 let h = h ^ (h >> 16);
370 (h as f32) / (u64::MAX as f32)
371}
372
373#[derive(Debug, Clone)]
379pub enum SteeringBehavior {
380 Seek(Vec2),
381 Flee(Vec2),
382 Arrive { target: Vec2, slow_radius: f32 },
383 Pursuit { quarry_pos: Vec2, quarry_vel: Vec2 },
384 Evade { threat_pos: Vec2, threat_vel: Vec2 },
385 Wander { circle_dist: f32, circle_radius: f32, jitter: f32 },
386 ObstacleAvoidance(Vec<(Vec2, f32)>),
387 WallAvoidance(Vec<(Vec2, Vec2)>),
388 PathFollowing { path: Vec<Vec2>, radius: f32 },
389 Separation,
390 Alignment,
391 Cohesion,
392 LeaderFollowing { offset: Vec2 },
393 Interpose { a: Vec2, b: Vec2 },
394 None,
395}
396
397impl SteeringBehavior {
398 pub fn compute(
401 &self,
402 agent: &mut SteeringAgent,
403 neighbors: &[&SteeringAgent],
404 leader: Option<&SteeringAgent>,
405 ) -> Vec2 {
406 match self {
407 SteeringBehavior::Seek(t) => seek(agent, *t),
408 SteeringBehavior::Flee(t) => flee(agent, *t),
409 SteeringBehavior::Arrive { target, slow_radius } => arrive(agent, *target, *slow_radius),
410 SteeringBehavior::Pursuit { quarry_pos, quarry_vel } => pursuit(agent, *quarry_pos, *quarry_vel),
411 SteeringBehavior::Evade { threat_pos, threat_vel } => evade(agent, *threat_pos, *threat_vel),
412 SteeringBehavior::Wander { circle_dist, circle_radius, jitter } =>
413 wander(agent, *circle_dist, *circle_radius, *jitter),
414 SteeringBehavior::ObstacleAvoidance(obs) => obstacle_avoidance(agent, obs),
415 SteeringBehavior::WallAvoidance(walls) => wall_avoidance(agent, walls),
416 SteeringBehavior::PathFollowing { path, radius } => path_following(agent, path, *radius),
417 SteeringBehavior::Separation => separation(agent, neighbors),
418 SteeringBehavior::Alignment => alignment(agent, neighbors),
419 SteeringBehavior::Cohesion => cohesion(agent, neighbors),
420 SteeringBehavior::LeaderFollowing { offset } =>
421 leader.map(|l| leader_following(agent, l, *offset)).unwrap_or(Vec2::ZERO),
422 SteeringBehavior::Interpose { a, b } => interpose(agent, *a, *b),
423 SteeringBehavior::None => Vec2::ZERO,
424 }
425 }
426}
427
428#[derive(Debug, Clone, Default)]
434pub struct WeightedSteering {
435 pub behaviors: Vec<(SteeringBehavior, f32)>,
436}
437
438impl WeightedSteering {
439 pub fn new() -> Self { WeightedSteering::default() }
440
441 pub fn add(&mut self, behavior: SteeringBehavior, weight: f32) -> &mut Self {
442 self.behaviors.push((behavior, weight));
443 self
444 }
445
446 pub fn calculate(
448 &self,
449 agent: &mut SteeringAgent,
450 neighbors: &[&SteeringAgent],
451 ) -> Vec2 {
452 self.calculate_with_leader(agent, neighbors, None)
453 }
454
455 pub fn calculate_with_leader(
456 &self,
457 agent: &mut SteeringAgent,
458 neighbors: &[&SteeringAgent],
459 leader: Option<&SteeringAgent>,
460 ) -> Vec2 {
461 let mut total = Vec2::ZERO;
462 for (behavior, weight) in &self.behaviors {
463 let force = behavior.compute(agent, neighbors, leader);
464 total += force * *weight;
465 if total.length() >= agent.max_force { break; }
467 }
468 truncate(total, agent.max_force)
469 }
470}
471
472#[derive(Debug, Clone, Default)]
479pub struct PrioritySteeringCombiner {
480 pub behaviors: Vec<(SteeringBehavior, f32)>, }
482
483impl PrioritySteeringCombiner {
484 pub fn new() -> Self { PrioritySteeringCombiner::default() }
485
486 pub fn add(&mut self, behavior: SteeringBehavior, min_magnitude: f32) -> &mut Self {
487 self.behaviors.push((behavior, min_magnitude));
488 self
489 }
490
491 pub fn calculate(
493 &self,
494 agent: &mut SteeringAgent,
495 neighbors: &[&SteeringAgent],
496 leader: Option<&SteeringAgent>,
497 ) -> Vec2 {
498 for (behavior, threshold) in &self.behaviors {
499 let force = behavior.compute(agent, neighbors, leader);
500 if force.length() > *threshold {
501 return truncate(force, agent.max_force);
502 }
503 }
504 Vec2::ZERO
505 }
506}
507
508#[derive(Debug, Clone, Default)]
514pub struct SteeringSystem {
515 pub agents: Vec<SteeringAgent>,
516 pub behaviors: Vec<WeightedSteering>,
517}
518
519impl SteeringSystem {
520 pub fn new() -> Self { SteeringSystem::default() }
521
522 pub fn add_agent(&mut self, agent: SteeringAgent, behavior: WeightedSteering) -> usize {
523 let idx = self.agents.len();
524 self.agents.push(agent);
525 self.behaviors.push(behavior);
526 idx
527 }
528
529 pub fn update(&mut self, dt: f32) {
531 let n = self.agents.len();
533 for i in 0..n {
534 let neighbors: Vec<&SteeringAgent> = self.agents.iter().enumerate()
536 .filter(|(j, _)| *j != i)
537 .map(|(_, a)| a as &SteeringAgent)
538 .collect();
539
540 let mut agent = self.agents[i].clone();
542 let force = self.behaviors[i].calculate(&mut agent, &neighbors);
543 self.agents[i].apply_force(force, dt);
544 self.agents[i].update_position(dt);
545 self.agents[i].wander_angle = agent.wander_angle;
546 }
547 }
548
549 pub fn nearest_agent(&self, pos: Vec2) -> Option<usize> {
551 self.agents.iter().enumerate()
552 .min_by(|(_, a), (_, b)| {
553 a.position.distance_squared(pos)
554 .partial_cmp(&b.position.distance_squared(pos))
555 .unwrap_or(std::cmp::Ordering::Equal)
556 })
557 .map(|(i, _)| i)
558 }
559
560 pub fn agent_count(&self) -> usize { self.agents.len() }
561}
562
563#[derive(Debug, Clone)]
570pub struct ContextMap {
571 pub slots: usize,
572 pub interest: Vec<f32>,
573 pub danger: Vec<f32>,
574}
575
576impl ContextMap {
577 pub fn new(slots: usize) -> Self {
578 ContextMap {
579 slots,
580 interest: vec![0.0; slots],
581 danger: vec![0.0; slots],
582 }
583 }
584
585 pub fn slot_direction(&self, slot: usize) -> Vec2 {
586 let angle = (slot as f32 / self.slots as f32) * 2.0 * PI;
587 Vec2::new(angle.cos(), angle.sin())
588 }
589
590 pub fn add_interest(&mut self, direction: Vec2, weight: f32) {
591 let dir = if direction.length_squared() > 0.0 { direction.normalize() } else { return; };
592 for i in 0..self.slots {
593 let slot_dir = self.slot_direction(i);
594 let dot = dir.dot(slot_dir).max(0.0);
595 self.interest[i] += dot * weight;
596 }
597 }
598
599 pub fn add_danger(&mut self, direction: Vec2, weight: f32) {
600 let dir = if direction.length_squared() > 0.0 { direction.normalize() } else { return; };
601 for i in 0..self.slots {
602 let slot_dir = self.slot_direction(i);
603 let dot = dir.dot(slot_dir).max(0.0);
604 self.danger[i] += dot * weight;
605 }
606 }
607
608 pub fn best_direction(&self) -> Vec2 {
610 let mut best_score = f32::NEG_INFINITY;
611 let mut best_dir = Vec2::ZERO;
612 for i in 0..self.slots {
613 let score = self.interest[i] - self.danger[i];
614 if score > best_score {
615 best_score = score;
616 best_dir = self.slot_direction(i);
617 }
618 }
619 best_dir
620 }
621
622 pub fn reset(&mut self) {
623 self.interest.fill(0.0);
624 self.danger.fill(0.0);
625 }
626}
627
628#[derive(Debug, Clone)]
634pub struct KinematicAgent {
635 pub position: Vec2,
636 pub orientation: f32,
637 pub velocity: Vec2,
638 pub rotation: f32,
639 pub max_speed: f32,
640 pub max_rotation: f32,
641}
642
643impl KinematicAgent {
644 pub fn new(position: Vec2, max_speed: f32) -> Self {
645 KinematicAgent {
646 position,
647 orientation: 0.0,
648 velocity: Vec2::ZERO,
649 rotation: 0.0,
650 max_speed,
651 max_rotation: PI,
652 }
653 }
654
655 pub fn update(&mut self, dt: f32) {
656 self.position += self.velocity * dt;
657 self.orientation += self.rotation * dt;
658 while self.orientation > PI { self.orientation -= 2.0 * PI; }
660 while self.orientation < -PI { self.orientation += 2.0 * PI; }
661 }
662
663 pub fn kinematic_seek(&mut self, target: Vec2) {
665 let diff = target - self.position;
666 let dist = diff.length();
667 if dist < 0.0001 { self.velocity = Vec2::ZERO; return; }
668 self.velocity = clamp_magnitude(diff / dist * self.max_speed, self.max_speed);
669 self.orientation = self.velocity.y.atan2(self.velocity.x);
670 }
671
672 pub fn kinematic_arrive(&mut self, target: Vec2, slow_radius: f32) {
674 let diff = target - self.position;
675 let dist = diff.length();
676 if dist < 0.0001 { self.velocity = Vec2::ZERO; return; }
677 let speed = if dist < slow_radius {
678 self.max_speed * dist / slow_radius
679 } else {
680 self.max_speed
681 };
682 self.velocity = clamp_magnitude(diff / dist * speed, self.max_speed);
683 }
684}
685
686#[cfg(test)]
691mod tests {
692 use super::*;
693 use glam::Vec2;
694
695 fn agent_at(pos: Vec2) -> SteeringAgent {
696 let mut a = SteeringAgent::new(pos, 5.0, 10.0);
697 a.velocity = Vec2::X * 1.0;
698 a.heading = Vec2::X;
699 a
700 }
701
702 #[test]
703 fn test_seek_toward_target() {
704 let agent = agent_at(Vec2::ZERO);
705 let force = seek(&agent, Vec2::new(10.0, 0.0));
706 assert!(force.x > 0.0, "should seek in +x");
707 }
708
709 #[test]
710 fn test_flee_away_from_threat() {
711 let agent = agent_at(Vec2::ZERO);
712 let force = flee(&agent, Vec2::new(1.0, 0.0));
713 assert!(force.x < 0.0, "should flee in -x");
714 }
715
716 #[test]
717 fn test_arrive_slows_near_target() {
718 let agent = agent_at(Vec2::new(1.0, 0.0));
719 let far = arrive(&agent, Vec2::new(100.0, 0.0), 5.0);
720 let near = arrive(&agent, Vec2::new(2.0, 0.0), 5.0);
721 assert!(near.length() <= far.length() + 0.1);
723 }
724
725 #[test]
726 fn test_arrive_zero_at_target() {
727 let agent = agent_at(Vec2::ZERO);
728 let force = arrive(&agent, Vec2::ZERO, 2.0);
729 assert!(force.length() < 0.1);
730 }
731
732 #[test]
733 fn test_pursuit_ahead_of_quarry() {
734 let agent = agent_at(Vec2::ZERO);
735 let quarry_pos = Vec2::new(5.0, 0.0);
736 let quarry_vel = Vec2::new(1.0, 0.0);
737 let force = pursuit(&agent, quarry_pos, quarry_vel);
738 assert!(force.x > 0.0);
740 }
741
742 #[test]
743 fn test_evade_from_approaching_threat() {
744 let agent = agent_at(Vec2::ZERO);
745 let threat_pos = Vec2::new(3.0, 0.0);
746 let threat_vel = Vec2::new(-2.0, 0.0); let force = evade(&agent, threat_pos, threat_vel);
748 assert!(force.x < 0.0, "should evade away");
749 }
750
751 #[test]
752 fn test_wander_produces_force() {
753 let mut agent = agent_at(Vec2::ZERO);
754 let force = wander(&mut agent, 3.0, 1.0, 0.5);
755 let _ = force.length(); }
758
759 #[test]
760 fn test_obstacle_avoidance_no_obstacles() {
761 let agent = agent_at(Vec2::ZERO);
762 let force = obstacle_avoidance(&agent, &[]);
763 assert_eq!(force, Vec2::ZERO);
764 }
765
766 #[test]
767 fn test_obstacle_avoidance_with_obstacle_ahead() {
768 let mut agent = agent_at(Vec2::ZERO);
769 agent.velocity = Vec2::new(3.0, 0.0);
770 agent.heading = Vec2::X;
771 let force = obstacle_avoidance(&agent, &[(Vec2::new(3.0, 0.0), 1.0)]);
772 let _ = force;
774 }
775
776 #[test]
777 fn test_separation_pushes_apart() {
778 let a1 = agent_at(Vec2::ZERO);
779 let a2 = agent_at(Vec2::new(0.3, 0.0));
780 let force = separation(&a1, &[&a2]);
781 assert!(force.x < 0.0 || force.length() > 0.0); }
783
784 #[test]
785 fn test_cohesion_pulls_together() {
786 let a1 = agent_at(Vec2::ZERO);
787 let a2 = agent_at(Vec2::new(4.0, 0.0));
788 let force = cohesion(&a1, &[&a2]);
789 assert!(force.x > 0.0, "should attract toward neighbor");
790 }
791
792 #[test]
793 fn test_alignment_matching_heading() {
794 let a1 = agent_at(Vec2::ZERO);
795 let mut a2 = agent_at(Vec2::new(1.0, 0.0));
796 a2.heading = Vec2::X;
797 let force = alignment(&a1, &[&a2]);
799 assert!(force.length() < 5.0);
800 }
801
802 #[test]
803 fn test_interpose_midpoint() {
804 let agent = agent_at(Vec2::ZERO);
805 let a = Vec2::new(-5.0, 0.0);
806 let b = Vec2::new(5.0, 0.0);
807 let force = interpose(&agent, a, b);
808 assert!(force.length() < agent.max_force + 0.1);
810 }
811
812 #[test]
813 fn test_weighted_steering() {
814 let mut agent = agent_at(Vec2::ZERO);
815 let mut ws = WeightedSteering::new();
816 ws.add(SteeringBehavior::Seek(Vec2::new(10.0, 0.0)), 1.0);
817 ws.add(SteeringBehavior::Flee(Vec2::new(0.0, 0.0)), 0.2);
818 let force = ws.calculate(&mut agent, &[]);
819 assert!(force.length() > 0.0);
820 }
821
822 #[test]
823 fn test_priority_combiner() {
824 let mut agent = agent_at(Vec2::ZERO);
825 let mut combiner = PrioritySteeringCombiner::new();
826 combiner.add(SteeringBehavior::None, 0.001);
827 combiner.add(SteeringBehavior::Seek(Vec2::new(5.0, 0.0)), 0.001);
828 let force = combiner.calculate(&mut agent, &[], None);
829 assert!(force.x > 0.0);
831 }
832
833 #[test]
834 fn test_steering_system_update() {
835 let mut system = SteeringSystem::new();
836 let agent = agent_at(Vec2::ZERO);
837 let mut ws = WeightedSteering::new();
838 ws.add(SteeringBehavior::Seek(Vec2::new(10.0, 10.0)), 1.0);
839 system.add_agent(agent, ws);
840 system.update(0.016);
841 assert!(system.agents[0].position.length() > 0.0 || true);
842 }
843
844 #[test]
845 fn test_apply_force_updates_velocity() {
846 let mut agent = agent_at(Vec2::ZERO);
847 agent.velocity = Vec2::ZERO;
848 agent.apply_force(Vec2::new(5.0, 0.0), 1.0);
849 assert!(agent.velocity.x > 0.0);
850 }
851
852 #[test]
853 fn test_clamp_magnitude() {
854 let v = Vec2::new(3.0, 4.0); let clamped = clamp_magnitude(v, 2.5);
856 assert!((clamped.length() - 2.5).abs() < 0.001);
857 }
858
859 #[test]
860 fn test_context_map() {
861 let mut ctx = ContextMap::new(8);
862 ctx.add_interest(Vec2::new(1.0, 0.0), 1.0);
863 ctx.add_danger (Vec2::new(-1.0, 0.0), 0.5);
864 let best = ctx.best_direction();
865 assert!(best.x > 0.0, "best direction should be +x");
866 }
867
868 #[test]
869 fn test_kinematic_seek() {
870 let mut k = KinematicAgent::new(Vec2::ZERO, 5.0);
871 k.kinematic_seek(Vec2::new(10.0, 0.0));
872 assert!(k.velocity.x > 0.0);
873 }
874
875 #[test]
876 fn test_path_following_on_path() {
877 let agent = agent_at(Vec2::new(0.0, 0.5));
878 let path = vec![Vec2::ZERO, Vec2::new(10.0, 0.0)];
879 let force = path_following(&agent, &path, 2.0);
881 assert!(force.length() < agent.max_force + 0.1);
882 }
883
884 #[test]
885 fn test_leader_following() {
886 let follower = agent_at(Vec2::new(0.0, 0.0));
887 let mut leader = agent_at(Vec2::new(5.0, 0.0));
888 leader.heading = Vec2::X;
889 let force = leader_following(&follower, &leader, Vec2::new(0.0, -2.0));
890 let _ = force; }
892}