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