1use std::f32;
9use std::f32::consts::{PI, TAU};
10
11#[derive(Clone, Copy, Debug, Default, PartialEq)]
14pub struct Vec2 {
15 pub x: f32,
16 pub y: f32,
17}
18
19impl Vec2 {
20 #[inline] pub fn new(x: f32, y: f32) -> Self { Self { x, y } }
21 #[inline] pub fn zero() -> Self { Self::new(0.0, 0.0) }
22 #[inline] pub fn len_sq(self) -> f32 { self.x * self.x + self.y * self.y }
23 #[inline] pub fn len(self) -> f32 { self.len_sq().sqrt() }
24 #[inline] pub fn norm(self) -> Self {
25 let l = self.len();
26 if l < 1e-9 { Self::zero() } else { Self::new(self.x / l, self.y / l) }
27 }
28 #[inline] pub fn dot(self, o: Self) -> f32 { self.x * o.x + self.y * o.y }
29 #[inline] pub fn cross(self, o: Self) -> f32 { self.x * o.y - self.y * o.x }
30 #[inline] pub fn sub(self, o: Self) -> Self { Self::new(self.x - o.x, self.y - o.y) }
31 #[inline] pub fn add(self, o: Self) -> Self { Self::new(self.x + o.x, self.y + o.y) }
32 #[inline] pub fn scale(self, s: f32) -> Self { Self::new(self.x * s, self.y * s) }
33 #[inline] pub fn clamp_len(self, max: f32) -> Self {
34 let l = self.len();
35 if l > max { self.scale(max / l) } else { self }
36 }
37 #[inline] pub fn dist(self, o: Self) -> f32 { self.sub(o).len() }
38 #[inline] pub fn dist_sq(self, o: Self) -> f32 { self.sub(o).len_sq() }
39 #[inline] pub fn lerp(self, o: Self, t: f32) -> Self {
40 Self::new(self.x + (o.x - self.x) * t, self.y + (o.y - self.y) * t)
41 }
42 #[inline] pub fn perp(self) -> Self { Self::new(-self.y, self.x) }
43 #[inline] pub fn rotate(self, angle: f32) -> Self {
44 let (s, c) = angle.sin_cos();
45 Self::new(self.x * c - self.y * s, self.x * s + self.y * c)
46 }
47 #[inline] pub fn angle(self) -> f32 { self.y.atan2(self.x) }
48 #[inline] pub fn from_angle(a: f32) -> Self { Self::new(a.cos(), a.sin()) }
49 #[inline] pub fn reflect(self, normal: Self) -> Self {
50 self.sub(normal.scale(2.0 * self.dot(normal)))
51 }
52}
53
54#[derive(Clone, Debug)]
58pub struct SteeringAgent {
59 pub position: Vec2,
60 pub velocity: Vec2,
61 pub orientation: f32, pub max_speed: f32,
63 pub max_force: f32,
64 pub mass: f32,
65 pub radius: f32,
66}
67
68impl SteeringAgent {
69 pub fn new(position: Vec2, max_speed: f32, max_force: f32) -> Self {
70 Self {
71 position,
72 velocity: Vec2::zero(),
73 orientation: 0.0,
74 max_speed,
75 max_force,
76 mass: 1.0,
77 radius: 0.5,
78 }
79 }
80
81 pub fn apply_force(&mut self, force: Vec2, dt: f32) {
83 let clamped = force.clamp_len(self.max_force);
84 let accel = clamped.scale(1.0 / self.mass);
85 self.velocity = (self.velocity.add(accel.scale(dt))).clamp_len(self.max_speed);
86 self.position = self.position.add(self.velocity.scale(dt));
87 if self.velocity.len_sq() > 1e-9 {
88 self.orientation = self.velocity.angle();
89 }
90 }
91
92 pub fn forward(&self) -> Vec2 { Vec2::from_angle(self.orientation) }
94 pub fn right(&self) -> Vec2 { Vec2::from_angle(self.orientation - PI / 2.0) }
96}
97
98#[derive(Clone, Copy, Debug, Default)]
100pub struct SteeringOutput {
101 pub linear: Vec2,
102 pub angular: f32,
103}
104
105impl SteeringOutput {
106 pub fn new(linear: Vec2) -> Self { Self { linear, angular: 0.0 } }
107 pub fn zero() -> Self { Self::default() }
108
109 pub fn add(self, o: Self) -> Self {
110 Self { linear: self.linear.add(o.linear), angular: self.angular + o.angular }
111 }
112 pub fn scale(self, s: f32) -> Self {
113 Self { linear: self.linear.scale(s), angular: self.angular * s }
114 }
115 pub fn clamp_linear(self, max: f32) -> Self {
116 Self { linear: self.linear.clamp_len(max), angular: self.angular }
117 }
118}
119
120pub struct Seek {
124 pub target: Vec2,
125}
126
127impl Seek {
128 pub fn new(target: Vec2) -> Self { Self { target } }
129
130 pub fn steer(&self, agent: &SteeringAgent) -> SteeringOutput {
131 let desired = self.target.sub(agent.position).norm().scale(agent.max_speed);
132 SteeringOutput::new(desired.sub(agent.velocity))
133 }
134}
135
136pub struct Flee {
140 pub threat: Vec2,
141 pub panic_dist: f32, }
143
144impl Flee {
145 pub fn new(threat: Vec2) -> Self { Self { threat, panic_dist: 0.0 } }
146 pub fn with_panic_distance(mut self, d: f32) -> Self { self.panic_dist = d; self }
147
148 pub fn steer(&self, agent: &SteeringAgent) -> SteeringOutput {
149 let diff = agent.position.sub(self.threat);
150 if self.panic_dist > 0.0 && diff.len() > self.panic_dist {
151 return SteeringOutput::zero();
152 }
153 let desired = diff.norm().scale(agent.max_speed);
154 SteeringOutput::new(desired.sub(agent.velocity))
155 }
156}
157
158pub struct Arrive {
162 pub target: Vec2,
163 pub slowing_radius: f32, pub stopping_dist: f32, }
166
167impl Arrive {
168 pub fn new(target: Vec2, slowing_radius: f32) -> Self {
169 Self { target, slowing_radius, stopping_dist: 0.1 }
170 }
171
172 pub fn steer(&self, agent: &SteeringAgent) -> SteeringOutput {
173 let to_target = self.target.sub(agent.position);
174 let dist = to_target.len();
175 if dist < self.stopping_dist {
176 return SteeringOutput::new(agent.velocity.scale(-1.0));
178 }
179 let target_speed = if dist < self.slowing_radius {
180 agent.max_speed * (dist / self.slowing_radius)
181 } else {
182 agent.max_speed
183 };
184 let desired = to_target.norm().scale(target_speed);
185 SteeringOutput::new(desired.sub(agent.velocity))
186 }
187}
188
189pub struct Pursuit {
193 pub target_pos: Vec2,
194 pub target_vel: Vec2,
195 pub max_predict_time: f32,
196}
197
198impl Pursuit {
199 pub fn new(target_pos: Vec2, target_vel: Vec2) -> Self {
200 Self { target_pos, target_vel, max_predict_time: 2.0 }
201 }
202
203 pub fn steer(&self, agent: &SteeringAgent) -> SteeringOutput {
204 let to_target = self.target_pos.sub(agent.position);
205 let dist = to_target.len();
206 let speed = agent.velocity.len().max(0.01);
207 let predict_time = (dist / speed).min(self.max_predict_time);
208 let predicted = self.target_pos.add(self.target_vel.scale(predict_time));
209 let seek = Seek::new(predicted);
210 seek.steer(agent)
211 }
212}
213
214pub struct Evade {
218 pub threat_pos: Vec2,
219 pub threat_vel: Vec2,
220 pub panic_dist: f32,
221 pub max_predict_time: f32,
222}
223
224impl Evade {
225 pub fn new(threat_pos: Vec2, threat_vel: Vec2, panic_dist: f32) -> Self {
226 Self { threat_pos, threat_vel, panic_dist, max_predict_time: 2.0 }
227 }
228
229 pub fn steer(&self, agent: &SteeringAgent) -> SteeringOutput {
230 let to_threat = self.threat_pos.sub(agent.position);
231 if to_threat.len() > self.panic_dist { return SteeringOutput::zero(); }
232 let dist = to_threat.len();
233 let speed = agent.velocity.len().max(0.01);
234 let pt = (dist / speed).min(self.max_predict_time);
235 let predicted = self.threat_pos.add(self.threat_vel.scale(pt));
236 let flee = Flee::new(predicted);
237 flee.steer(agent)
238 }
239}
240
241pub struct Wander {
245 pub wander_distance: f32, pub wander_radius: f32, pub wander_jitter: f32, pub wander_angle: f32, }
250
251impl Wander {
252 pub fn new() -> Self {
253 Self {
254 wander_distance: 2.0,
255 wander_radius: 1.0,
256 wander_jitter: 0.5,
257 wander_angle: 0.0,
258 }
259 }
260
261 pub fn steer(&mut self, agent: &SteeringAgent, rng_val: f32) -> SteeringOutput {
264 self.wander_angle += rng_val * self.wander_jitter;
265 let circle_center = agent.position.add(agent.forward().scale(self.wander_distance));
267 let wander_pt = circle_center.add(
269 Vec2::from_angle(agent.orientation + self.wander_angle).scale(self.wander_radius)
270 );
271 let seek = Seek::new(wander_pt);
272 seek.steer(agent)
273 }
274}
275
276impl Default for Wander {
277 fn default() -> Self { Self::new() }
278}
279
280#[derive(Clone, Copy, Debug)]
284pub struct CircleObstacle {
285 pub center: Vec2,
286 pub radius: f32,
287}
288
289pub struct ObstacleAvoidance {
291 pub obstacles: Vec<CircleObstacle>,
292 pub detection_length: f32,
293 pub avoidance_force: f32,
294}
295
296impl ObstacleAvoidance {
297 pub fn new(detection_length: f32) -> Self {
298 Self {
299 obstacles: Vec::new(),
300 detection_length,
301 avoidance_force: 10.0,
302 }
303 }
304
305 pub fn add_obstacle(&mut self, obs: CircleObstacle) { self.obstacles.push(obs); }
306
307 pub fn steer(&self, agent: &SteeringAgent) -> SteeringOutput {
308 let forward = agent.forward();
309 let feeler_end = agent.position.add(forward.scale(self.detection_length));
310
311 let mut nearest_dist = f32::MAX;
313 let mut avoidance = Vec2::zero();
314
315 for obs in &self.obstacles {
316 let to_obs = obs.center.sub(agent.position);
318 let ahead = to_obs.dot(forward);
319 if ahead < 0.0 { continue; }
321 let lateral = (to_obs.len_sq() - ahead * ahead).sqrt();
323 let combined_radius = obs.radius + agent.radius + 0.1;
324 if lateral > combined_radius { continue; }
325 let dist = to_obs.len();
327 if dist < nearest_dist {
328 nearest_dist = dist;
329 let right = forward.perp();
331 let side = to_obs.dot(right);
332 avoidance = if side >= 0.0 {
333 right.scale(-self.avoidance_force)
334 } else {
335 right.scale(self.avoidance_force)
336 };
337 }
338 }
339 SteeringOutput::new(avoidance)
340 }
341}
342
343#[derive(Clone, Copy, Debug)]
347pub struct WallSegment {
348 pub a: Vec2,
349 pub b: Vec2,
350}
351
352impl WallSegment {
353 pub fn new(a: Vec2, b: Vec2) -> Self { Self { a, b } }
354 pub fn normal(&self) -> Vec2 { self.b.sub(self.a).perp().norm() }
355 pub fn closest_point(&self, p: Vec2) -> Vec2 {
356 let ab = self.b.sub(self.a);
357 let ap = p.sub(self.a);
358 let t = ap.dot(ab) / (ab.len_sq() + 1e-12);
359 self.a.add(ab.scale(t.clamp(0.0, 1.0)))
360 }
361}
362
363pub struct WallFollowing {
365 pub walls: Vec<WallSegment>,
366 pub follow_distance: f32, pub detection_range: f32,
368 pub side: f32, }
370
371impl WallFollowing {
372 pub fn new(follow_distance: f32) -> Self {
373 Self { walls: Vec::new(), follow_distance, detection_range: 5.0, side: 1.0 }
374 }
375
376 pub fn add_wall(&mut self, wall: WallSegment) { self.walls.push(wall); }
377
378 pub fn steer(&self, agent: &SteeringAgent) -> SteeringOutput {
379 let mut nearest_wall: Option<&WallSegment> = None;
381 let mut nearest_dist = self.detection_range;
382 let mut nearest_cp = Vec2::zero();
383
384 for wall in &self.walls {
385 let cp = wall.closest_point(agent.position);
386 let d = cp.dist(agent.position);
387 if d < nearest_dist {
388 nearest_dist = d;
389 nearest_wall = Some(wall);
390 nearest_cp = cp;
391 }
392 }
393
394 if let Some(wall) = nearest_wall {
395 let normal = wall.normal().scale(self.side);
396 let desired_pos = nearest_cp.add(normal.scale(self.follow_distance));
397 let seek = Seek::new(desired_pos);
399 seek.steer(agent)
400 } else {
401 SteeringOutput::zero()
402 }
403 }
404}
405
406#[derive(Clone, Debug)]
410pub struct FlockingConfig {
411 pub separation_radius: f32,
412 pub alignment_radius: f32,
413 pub cohesion_radius: f32,
414 pub separation_weight: f32,
415 pub alignment_weight: f32,
416 pub cohesion_weight: f32,
417 pub max_neighbors: usize,
418}
419
420impl Default for FlockingConfig {
421 fn default() -> Self {
422 Self {
423 separation_radius: 2.0,
424 alignment_radius: 5.0,
425 cohesion_radius: 5.0,
426 separation_weight: 1.5,
427 alignment_weight: 1.0,
428 cohesion_weight: 1.0,
429 max_neighbors: 16,
430 }
431 }
432}
433
434#[derive(Clone, Debug)]
436pub struct FlockingAgent {
437 pub position: Vec2,
438 pub velocity: Vec2,
439 pub max_speed: f32,
440 pub radius: f32,
441}
442
443pub struct Flock {
445 pub config: FlockingConfig,
446}
447
448impl Flock {
449 pub fn new(config: FlockingConfig) -> Self { Self { config } }
450
451 pub fn steer(&self, agent: &SteeringAgent, flock: &[FlockingAgent]) -> SteeringOutput {
453 let sep = self.separation(agent, flock);
454 let ali = self.alignment(agent, flock);
455 let coh = self.cohesion(agent, flock);
456
457 let combined = sep.scale(self.config.separation_weight)
458 .add(ali.scale(self.config.alignment_weight))
459 .add(coh.scale(self.config.cohesion_weight));
460 SteeringOutput::new(combined)
461 }
462
463 fn separation(&self, agent: &SteeringAgent, flock: &[FlockingAgent]) -> Vec2 {
464 let mut force = Vec2::zero();
465 let mut count = 0usize;
466 for other in flock {
467 let diff = agent.position.sub(other.position);
468 let dist = diff.len();
469 if dist < 1e-9 || dist > self.config.separation_radius { continue; }
470 force = force.add(diff.norm().scale(1.0 / dist));
472 count += 1;
473 if count >= self.config.max_neighbors { break; }
474 }
475 if count > 0 {
476 force.scale(1.0 / count as f32)
477 } else {
478 Vec2::zero()
479 }
480 }
481
482 fn alignment(&self, agent: &SteeringAgent, flock: &[FlockingAgent]) -> Vec2 {
483 let mut avg_vel = Vec2::zero();
484 let mut count = 0usize;
485 for other in flock {
486 let dist = agent.position.dist(other.position);
487 if dist > self.config.alignment_radius { continue; }
488 avg_vel = avg_vel.add(other.velocity);
489 count += 1;
490 if count >= self.config.max_neighbors { break; }
491 }
492 if count > 0 {
493 let avg = avg_vel.scale(1.0 / count as f32);
494 let desired = avg.clamp_len(agent.max_speed);
495 desired.sub(agent.velocity)
496 } else {
497 Vec2::zero()
498 }
499 }
500
501 fn cohesion(&self, agent: &SteeringAgent, flock: &[FlockingAgent]) -> Vec2 {
502 let mut center = Vec2::zero();
503 let mut count = 0usize;
504 for other in flock {
505 let dist = agent.position.dist(other.position);
506 if dist > self.config.cohesion_radius { continue; }
507 center = center.add(other.position);
508 count += 1;
509 if count >= self.config.max_neighbors { break; }
510 }
511 if count > 0 {
512 let avg_center = center.scale(1.0 / count as f32);
513 let seek = Seek::new(avg_center);
514 seek.steer(agent).linear
515 } else {
516 Vec2::zero()
517 }
518 }
519}
520
521#[derive(Clone, Debug)]
525pub struct FormationSlot {
526 pub offset: Vec2, pub role: &'static str,
528}
529
530impl FormationSlot {
531 pub fn new(offset: Vec2, role: &'static str) -> Self { Self { offset, role } }
532}
533
534pub struct FormationMovement {
536 pub slots: Vec<FormationSlot>,
537 pub leader: Vec2,
538 pub heading: f32, }
540
541impl FormationMovement {
542 pub fn wedge(count: usize, spacing: f32) -> Self {
543 let mut slots = Vec::new();
544 slots.push(FormationSlot::new(Vec2::zero(), "leader"));
545 let half = (count.saturating_sub(1)) as f32 / 2.0;
546 for i in 1..count {
547 let row = i;
548 let col = (i as f32) - half;
549 slots.push(FormationSlot::new(
550 Vec2::new(col * spacing, -(row as f32) * spacing),
551 "follower",
552 ));
553 }
554 Self { slots, leader: Vec2::zero(), heading: 0.0 }
555 }
556
557 pub fn line(count: usize, spacing: f32) -> Self {
558 let mut slots = Vec::new();
559 let half = (count - 1) as f32 * spacing / 2.0;
560 for i in 0..count {
561 slots.push(FormationSlot::new(
562 Vec2::new(i as f32 * spacing - half, 0.0),
563 if i == 0 { "leader" } else { "follower" },
564 ));
565 }
566 Self { slots, leader: Vec2::zero(), heading: 0.0 }
567 }
568
569 pub fn column(count: usize, spacing: f32) -> Self {
570 let mut slots = Vec::new();
571 for i in 0..count {
572 slots.push(FormationSlot::new(
573 Vec2::new(0.0, -(i as f32) * spacing),
574 if i == 0 { "leader" } else { "follower" },
575 ));
576 }
577 Self { slots, leader: Vec2::zero(), heading: 0.0 }
578 }
579
580 pub fn circle(count: usize, radius: f32) -> Self {
581 let mut slots = Vec::new();
582 for i in 0..count {
583 let angle = (i as f32 / count as f32) * TAU;
584 slots.push(FormationSlot::new(
585 Vec2::new(angle.cos() * radius, angle.sin() * radius),
586 if i == 0 { "leader" } else { "follower" },
587 ));
588 }
589 Self { slots, leader: Vec2::zero(), heading: 0.0 }
590 }
591
592 pub fn slot_position(&self, idx: usize) -> Vec2 {
594 if idx >= self.slots.len() { return self.leader; }
595 let local_offset = self.slots[idx].offset;
596 let rotated = local_offset.rotate(self.heading);
598 self.leader.add(rotated)
599 }
600
601 pub fn steer_to_slot(
604 &self,
605 agent: &SteeringAgent,
606 slot_idx: usize,
607 slowing_radius: f32,
608 ) -> SteeringOutput {
609 let target = self.slot_position(slot_idx);
610 let arrive = Arrive::new(target, slowing_radius);
611 arrive.steer(agent)
612 }
613
614 pub fn update_leader(&mut self, pos: Vec2, heading: f32) {
616 self.leader = pos;
617 self.heading = heading;
618 }
619}
620
621pub struct PathFollower {
625 pub waypoints: Vec<Vec2>,
626 pub lookahead: f32, pub current_segment: usize,
628 pub path_progress: f32, pub loop_path: bool,
630 pub stopping_radius: f32,
631 pub slowing_radius: f32,
632}
633
634impl PathFollower {
635 pub fn new(waypoints: Vec<Vec2>, lookahead: f32) -> Self {
636 Self {
637 waypoints,
638 lookahead,
639 current_segment: 0,
640 path_progress: 0.0,
641 loop_path: false,
642 stopping_radius: 0.2,
643 slowing_radius: 1.5,
644 }
645 }
646
647 pub fn with_loop(mut self) -> Self { self.loop_path = true; self }
648
649 pub fn is_done(&self, agent: &SteeringAgent) -> bool {
651 if self.waypoints.is_empty() { return true; }
652 if self.loop_path { return false; }
653 let last = *self.waypoints.last().unwrap();
654 agent.position.dist_sq(last) < self.stopping_radius * self.stopping_radius
655 }
656
657 pub fn steer(&mut self, agent: &SteeringAgent) -> SteeringOutput {
659 if self.waypoints.is_empty() { return SteeringOutput::zero(); }
660 if self.waypoints.len() == 1 {
661 return Arrive::new(self.waypoints[0], self.slowing_radius).steer(agent);
662 }
663
664 let target = self.compute_target(agent);
666
667 let at_last = !self.loop_path
668 && self.current_segment + 1 >= self.waypoints.len()
669 && agent.position.dist(target) < self.slowing_radius;
670
671 if at_last {
672 Arrive::new(target, self.slowing_radius).steer(agent)
673 } else {
674 Seek::new(target).steer(agent)
675 }
676 }
677
678 fn compute_target(&mut self, agent: &SteeringAgent) -> Vec2 {
679 let n = self.waypoints.len();
680 while self.current_segment + 1 < n {
682 let next = self.waypoints[self.current_segment + 1];
683 if agent.position.dist(next) < self.lookahead {
684 self.current_segment += 1;
685 } else {
686 break;
687 }
688 }
689 if self.loop_path && self.current_segment + 1 >= n {
690 self.current_segment = 0;
691 }
692
693 let seg_start = self.waypoints[self.current_segment];
694 let seg_end = self.waypoints[(self.current_segment + 1).min(n - 1)];
695
696 let seg_dir = seg_end.sub(seg_start);
698 let seg_len = seg_dir.len();
699 if seg_len < 1e-9 { return seg_end; }
700 let t = agent.position.sub(seg_start).dot(seg_dir) / seg_len;
701 let proj_t = ((t + self.lookahead) / seg_len).clamp(0.0, 1.0);
702 seg_start.lerp(seg_end, proj_t)
703 }
704}
705
706pub struct BehaviorWeight {
710 pub weight: f32,
711 pub output: SteeringOutput,
712}
713
714impl BehaviorWeight {
715 pub fn new(weight: f32, output: SteeringOutput) -> Self { Self { weight, output } }
716}
717
718pub struct BlendedSteering {
720 pub behaviors: Vec<BehaviorWeight>,
721}
722
723impl BlendedSteering {
724 pub fn new() -> Self { Self { behaviors: Vec::new() } }
725
726 pub fn add(&mut self, weight: f32, output: SteeringOutput) {
727 self.behaviors.push(BehaviorWeight::new(weight, output));
728 }
729
730 pub fn weighted_sum(&self) -> SteeringOutput {
732 let total_w: f32 = self.behaviors.iter().map(|b| b.weight.abs()).sum();
733 if total_w < 1e-9 { return SteeringOutput::zero(); }
734 let mut combined = SteeringOutput::zero();
735 for b in &self.behaviors {
736 combined = combined.add(b.output.scale(b.weight / total_w));
737 }
738 combined
739 }
740
741 pub fn priority(&self, epsilon: f32) -> SteeringOutput {
743 for b in &self.behaviors {
744 let out = b.output.scale(b.weight);
745 if out.linear.len() > epsilon || out.angular.abs() > epsilon {
746 return out;
747 }
748 }
749 SteeringOutput::zero()
750 }
751
752 pub fn truncated_sum(&self, max_force: f32) -> SteeringOutput {
754 let mut combined = SteeringOutput::zero();
755 let mut remaining = max_force;
756 for b in &self.behaviors {
757 let out = b.output.scale(b.weight);
758 let fl = out.linear.len();
759 if fl <= 1e-9 { continue; }
760 if fl <= remaining {
761 combined = combined.add(out);
762 remaining -= fl;
763 } else {
764 combined = combined.add(SteeringOutput::new(out.linear.norm().scale(remaining)));
766 remaining = 0.0;
767 break;
768 }
769 }
770 combined
771 }
772
773 pub fn clear(&mut self) { self.behaviors.clear(); }
774}
775
776impl Default for BlendedSteering {
777 fn default() -> Self { Self::new() }
778}
779
780pub struct ContextMap {
784 pub slots: usize, pub interest: Vec<f32>,
786 pub danger: Vec<f32>,
787}
788
789impl ContextMap {
790 pub fn new(slots: usize) -> Self {
791 Self {
792 slots,
793 interest: vec![0.0; slots],
794 danger: vec![0.0; slots],
795 }
796 }
797
798 pub fn slot_direction(&self, slot: usize) -> Vec2 {
799 let angle = (slot as f32 / self.slots as f32) * TAU;
800 Vec2::from_angle(angle)
801 }
802
803 pub fn add_interest(&mut self, direction: Vec2, weight: f32) {
804 for i in 0..self.slots {
805 let d = self.slot_direction(i);
806 let dot = d.dot(direction.norm()).max(0.0);
807 self.interest[i] = self.interest[i].max(dot * weight);
808 }
809 }
810
811 pub fn add_danger(&mut self, direction: Vec2, weight: f32) {
812 for i in 0..self.slots {
813 let d = self.slot_direction(i);
814 let dot = d.dot(direction.norm()).max(0.0);
815 self.danger[i] = self.danger[i].max(dot * weight);
816 }
817 }
818
819 pub fn resolve(&self) -> Vec2 {
821 let mut best_val = f32::NEG_INFINITY;
822 let mut best_dir = Vec2::zero();
823 for i in 0..self.slots {
824 let masked = (self.interest[i] - self.danger[i]).max(0.0);
825 if masked > best_val {
826 best_val = masked;
827 best_dir = self.slot_direction(i);
828 }
829 }
830 best_dir
831 }
832
833 pub fn reset(&mut self) {
834 for v in &mut self.interest { *v = 0.0; }
835 for v in &mut self.danger { *v = 0.0; }
836 }
837}
838
839pub struct NeighborhoodGrid {
843 pub cell_size: f32,
844 pub cells: std::collections::HashMap<(i32,i32), Vec<usize>>,
845}
846
847impl NeighborhoodGrid {
848 pub fn new(cell_size: f32) -> Self {
849 Self { cell_size, cells: std::collections::HashMap::new() }
850 }
851
852 fn cell_key(&self, pos: Vec2) -> (i32, i32) {
853 ((pos.x / self.cell_size).floor() as i32,
854 (pos.y / self.cell_size).floor() as i32)
855 }
856
857 pub fn clear(&mut self) { self.cells.clear(); }
858
859 pub fn insert(&mut self, pos: Vec2, idx: usize) {
860 self.cells.entry(self.cell_key(pos)).or_default().push(idx);
861 }
862
863 pub fn query(&self, pos: Vec2, radius: f32) -> Vec<usize> {
865 let cells = (radius / self.cell_size).ceil() as i32 + 1;
866 let key = self.cell_key(pos);
867 let r2 = radius * radius;
868 let mut result = Vec::new();
869 for dy in -cells..=cells {
870 for dx in -cells..=cells {
871 let k = (key.0 + dx, key.1 + dy);
872 if let Some(indices) = self.cells.get(&k) {
873 result.extend(indices.iter().copied());
874 }
875 }
876 }
877 result
878 }
879}
880
881#[derive(Clone, Copy, Debug, PartialEq, Eq)]
885pub enum SteeringMode {
886 Idle,
887 Seek,
888 Flee,
889 Arrive,
890 Pursuit,
891 Evade,
892 Wander,
893 FollowPath,
894 Formation,
895 Flocking,
896}
897
898pub struct AgentController {
900 pub agent: SteeringAgent,
901 pub mode: SteeringMode,
902 pub wander: Wander,
903 pub blender: BlendedSteering,
904 pub path_follower: Option<PathFollower>,
906 pub target_pos: Vec2,
908 pub target_vel: Vec2,
909}
910
911impl AgentController {
912 pub fn new(position: Vec2, max_speed: f32, max_force: f32) -> Self {
913 Self {
914 agent: SteeringAgent::new(position, max_speed, max_force),
915 mode: SteeringMode::Idle,
916 wander: Wander::new(),
917 blender: BlendedSteering::new(),
918 path_follower: None,
919 target_pos: Vec2::zero(),
920 target_vel: Vec2::zero(),
921 }
922 }
923
924 pub fn set_mode(&mut self, mode: SteeringMode) { self.mode = mode; }
925
926 pub fn set_target(&mut self, pos: Vec2) { self.target_pos = pos; }
927
928 pub fn set_path(&mut self, waypoints: Vec<Vec2>, lookahead: f32) {
929 self.path_follower = Some(PathFollower::new(waypoints, lookahead));
930 self.mode = SteeringMode::FollowPath;
931 }
932
933 pub fn update(&mut self, dt: f32, rng_val: f32) {
935 let output = match self.mode {
936 SteeringMode::Idle => SteeringOutput::zero(),
937 SteeringMode::Seek => Seek::new(self.target_pos).steer(&self.agent),
938 SteeringMode::Flee => Flee::new(self.target_pos).steer(&self.agent),
939 SteeringMode::Arrive => Arrive::new(self.target_pos, 2.0).steer(&self.agent),
940 SteeringMode::Pursuit => {
941 Pursuit::new(self.target_pos, self.target_vel).steer(&self.agent)
942 }
943 SteeringMode::Evade => {
944 Evade::new(self.target_pos, self.target_vel, 10.0).steer(&self.agent)
945 }
946 SteeringMode::Wander => self.wander.steer(&self.agent, rng_val),
947 SteeringMode::FollowPath => {
948 if let Some(ref mut pf) = self.path_follower {
949 pf.steer(&self.agent)
950 } else {
951 SteeringOutput::zero()
952 }
953 }
954 SteeringMode::Formation => self.blender.weighted_sum(),
955 SteeringMode::Flocking => self.blender.weighted_sum(),
956 };
957 self.agent.apply_force(output.linear, dt);
958 }
959}
960
961#[cfg(test)]
964mod tests {
965 use super::*;
966
967 fn agent_at(x: f32, y: f32) -> SteeringAgent {
968 SteeringAgent::new(Vec2::new(x, y), 5.0, 10.0)
969 }
970
971 #[test]
972 fn test_seek_moves_toward_target() {
973 let mut agent = agent_at(0.0, 0.0);
974 let seek = Seek::new(Vec2::new(10.0, 0.0));
975 let out = seek.steer(&agent);
976 assert!(out.linear.x > 0.0, "seek should pull toward positive x");
977 agent.apply_force(out.linear, 0.1);
978 assert!(agent.position.x > 0.0, "agent should have moved right");
979 }
980
981 #[test]
982 fn test_flee_moves_away() {
983 let mut agent = agent_at(0.0, 0.0);
984 let flee = Flee::new(Vec2::new(1.0, 0.0));
985 let out = flee.steer(&agent);
986 assert!(out.linear.x < 0.0, "flee should push away from threat");
987 }
988
989 #[test]
990 fn test_arrive_slows_down() {
991 let mut agent = agent_at(0.0, 0.0);
992 agent.velocity = Vec2::new(5.0, 0.0);
993 let arrive = Arrive::new(Vec2::new(1.0, 0.0), 3.0);
994 let out = arrive.steer(&agent);
995 assert!(out.linear.x < 0.0 || out.linear.len() < agent.max_force);
997 }
998
999 #[test]
1000 fn test_wander_changes_direction() {
1001 let agent = agent_at(0.0, 0.0);
1002 let mut wander = Wander::new();
1003 let out1 = wander.steer(&agent, 1.0);
1004 let out2 = wander.steer(&agent, -1.0);
1005 assert!(out1.linear.x != out2.linear.x || out1.linear.y != out2.linear.y);
1007 }
1008
1009 #[test]
1010 fn test_obstacle_avoidance() {
1011 let mut agent = agent_at(0.0, 0.0);
1012 agent.velocity = Vec2::new(1.0, 0.0); let mut oa = ObstacleAvoidance::new(5.0);
1014 oa.add_obstacle(CircleObstacle { center: Vec2::new(2.0, 0.0), radius: 1.0 });
1015 let out = oa.steer(&agent);
1016 assert!(out.linear.len() > 0.0);
1018 }
1019
1020 #[test]
1021 fn test_flock_separation() {
1022 let agent = agent_at(0.0, 0.0);
1023 let flock_members = vec![
1024 FlockingAgent { position: Vec2::new(0.5, 0.0), velocity: Vec2::zero(), max_speed: 5.0, radius: 0.5 },
1025 FlockingAgent { position: Vec2::new(-0.5, 0.0), velocity: Vec2::zero(), max_speed: 5.0, radius: 0.5 },
1026 ];
1027 let flock = Flock::new(FlockingConfig::default());
1028 let out = flock.steer(&agent, &flock_members);
1029 assert!(out.linear.len() < 10.0);
1031 }
1032
1033 #[test]
1034 fn test_formation_slot_positions() {
1035 let fm = FormationMovement::line(3, 2.0);
1036 let pos0 = fm.slot_position(0);
1037 let pos2 = fm.slot_position(2);
1038 assert!((pos2.x - pos0.x).abs() > 1.0);
1040 }
1041
1042 #[test]
1043 fn test_path_follower_advances() {
1044 let mut agent = agent_at(0.0, 0.0);
1045 let waypoints = vec![
1046 Vec2::new(0.0, 0.0),
1047 Vec2::new(5.0, 0.0),
1048 Vec2::new(10.0, 0.0),
1049 ];
1050 let mut pf = PathFollower::new(waypoints, 1.0);
1051 let out = pf.steer(&agent);
1052 assert!(out.linear.x > 0.0, "should steer toward next waypoint");
1053 }
1054
1055 #[test]
1056 fn test_blended_steering_weighted_sum() {
1057 let mut blender = BlendedSteering::new();
1058 blender.add(1.0, SteeringOutput::new(Vec2::new(2.0, 0.0)));
1059 blender.add(1.0, SteeringOutput::new(Vec2::new(-2.0, 0.0)));
1060 let out = blender.weighted_sum();
1061 assert!(out.linear.x.abs() < 1e-4);
1063 }
1064
1065 #[test]
1066 fn test_blended_steering_priority() {
1067 let mut blender = BlendedSteering::new();
1068 blender.add(1.0, SteeringOutput::new(Vec2::new(0.0, 0.0)));
1069 blender.add(1.0, SteeringOutput::new(Vec2::new(3.0, 0.0)));
1070 let out = blender.priority(0.1);
1071 assert!(out.linear.x > 0.0);
1072 }
1073
1074 #[test]
1075 fn test_context_map_resolve() {
1076 let mut ctx = ContextMap::new(16);
1077 ctx.add_interest(Vec2::new(1.0, 0.0), 1.0);
1078 let dir = ctx.resolve();
1079 assert!(dir.x > 0.5, "should point roughly right");
1080 }
1081
1082 #[test]
1083 fn test_neighborhood_grid() {
1084 let mut grid = NeighborhoodGrid::new(5.0);
1085 grid.insert(Vec2::new(0.0, 0.0), 0);
1086 grid.insert(Vec2::new(3.0, 0.0), 1);
1087 grid.insert(Vec2::new(20.0, 0.0), 2);
1088 let nearby = grid.query(Vec2::new(0.0, 0.0), 5.0);
1089 assert!(nearby.contains(&0));
1090 assert!(nearby.contains(&1));
1091 assert!(!nearby.contains(&2));
1092 }
1093
1094 #[test]
1095 fn test_agent_controller_seek() {
1096 let mut ctrl = AgentController::new(Vec2::zero(), 5.0, 20.0);
1097 ctrl.set_mode(SteeringMode::Seek);
1098 ctrl.set_target(Vec2::new(10.0, 0.0));
1099 ctrl.update(0.016, 0.0);
1100 assert!(ctrl.agent.position.x > 0.0);
1101 }
1102}