Skip to main content

proof_engine/pathfinding/
steering.rs

1// src/pathfinding/steering.rs
2// Steering behaviors for autonomous agents:
3//   Seek, Flee, Arrive, Pursuit, Evade, Wander, ObstacleAvoidance,
4//   WallFollowing, Flocking (separation/alignment/cohesion),
5//   FormationMovement, PathFollowing with lookahead,
6//   behavior blending with weights.
7
8use std::f32::consts::{PI, TAU};
9
10// ── 2-D vector ────────────────────────────────────────────────────────────────
11
12#[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// ── Steering agent state ──────────────────────────────────────────────────────
54
55/// A moving agent with position, velocity, and physical limits.
56#[derive(Clone, Debug)]
57pub struct SteeringAgent {
58    pub position:      Vec2,
59    pub velocity:      Vec2,
60    pub orientation:   f32,   // radians
61    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    /// Apply steering force and integrate position by `dt`.
81    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    /// Forward direction based on orientation.
92    pub fn forward(&self) -> Vec2 { Vec2::from_angle(self.orientation) }
93    /// Right direction (perpendicular to forward, CCW).
94    pub fn right(&self) -> Vec2 { Vec2::from_angle(self.orientation - PI / 2.0) }
95}
96
97/// Output from a steering behavior (linear force, optional torque).
98#[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
119// ── Seek ──────────────────────────────────────────────────────────────────────
120
121/// Steers toward a target position at full speed.
122pub 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
135// ── Flee ──────────────────────────────────────────────────────────────────────
136
137/// Steers away from a threat position.
138pub struct Flee {
139    pub threat:      Vec2,
140    pub panic_dist:  f32,   // only flee within this radius; 0 = always flee
141}
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
157// ── Arrive ────────────────────────────────────────────────────────────────────
158
159/// Like Seek but decelerates smoothly inside the slowing radius.
160pub struct Arrive {
161    pub target:         Vec2,
162    pub slowing_radius: f32,   // begin decelerating within this distance
163    pub stopping_dist:  f32,   // come to a full stop at this distance
164}
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            // Cancel current velocity
176            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
188// ── Pursuit ───────────────────────────────────────────────────────────────────
189
190/// Predicts where the target will be and steers toward that position.
191pub 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
213// ── Evade ─────────────────────────────────────────────────────────────────────
214
215/// Predicts where the threat will be and steers away.
216pub 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
240// ── Wander ────────────────────────────────────────────────────────────────────
241
242/// Random-looking wandering using a wander circle in front of the agent.
243pub struct Wander {
244    pub wander_distance: f32,   // distance of wander circle from agent
245    pub wander_radius:   f32,   // radius of wander circle
246    pub wander_jitter:   f32,   // max angle change per frame
247    pub wander_angle:    f32,   // current angle on circle (mutable state)
248}
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    /// Update wander angle with pseudo-random jitter and compute steering.
261    /// `rng_val` should be a value in [-1.0, 1.0] (caller supplies randomness).
262    pub fn steer(&mut self, agent: &SteeringAgent, rng_val: f32) -> SteeringOutput {
263        self.wander_angle += rng_val * self.wander_jitter;
264        // Wander circle center ahead of the agent
265        let circle_center = agent.position.add(agent.forward().scale(self.wander_distance));
266        // Point on circle at wander_angle relative to orientation
267        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// ── Obstacle avoidance ────────────────────────────────────────────────────────
280
281/// A circular obstacle in the world.
282#[derive(Clone, Copy, Debug)]
283pub struct CircleObstacle {
284    pub center: Vec2,
285    pub radius: f32,
286}
287
288/// Steers around circular obstacles using a forward feeler.
289pub 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        // Find closest intersecting obstacle
311        let mut nearest_dist = f32::MAX;
312        let mut avoidance = Vec2::zero();
313
314        for obs in &self.obstacles {
315            // Transform obstacle center to agent-local space
316            let to_obs = obs.center.sub(agent.position);
317            let ahead  = to_obs.dot(forward);
318            // Only consider obstacles ahead
319            if ahead < 0.0 { continue; }
320            // Lateral distance
321            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            // Hit: steer laterally away
325            let dist = to_obs.len();
326            if dist < nearest_dist {
327                nearest_dist = dist;
328                // Avoidance direction: perpendicular to forward, away from obstacle
329                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// ── Wall following ────────────────────────────────────────────────────────────
343
344/// A wall segment (line) for wall-following behavior.
345#[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
362/// Steers along a wall at a desired offset distance.
363pub struct WallFollowing {
364    pub walls:          Vec<WallSegment>,
365    pub follow_distance: f32,   // desired distance from wall
366    pub detection_range: f32,
367    pub side:            f32,   // +1 = keep wall on right, -1 = keep on left
368}
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        // Find the closest wall
379        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            // Seek the desired position along the wall
397            let seek = Seek::new(desired_pos);
398            seek.steer(agent)
399        } else {
400            SteeringOutput::zero()
401        }
402    }
403}
404
405// ── Flocking ──────────────────────────────────────────────────────────────────
406
407/// Configuration for Reynolds flocking behaviors.
408#[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/// A flocking agent (separate from SteeringAgent to hold flock-specific data).
434#[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
442/// Computes flocking steering forces.
443pub struct Flock {
444    pub config: FlockingConfig,
445}
446
447impl Flock {
448    pub fn new(config: FlockingConfig) -> Self { Self { config } }
449
450    /// Compute the combined flocking force for `agent` given its neighbors.
451    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            // Weight by inverse distance
470            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// ── Formation movement ────────────────────────────────────────────────────────
521
522/// One slot in a formation (offset from leader).
523#[derive(Clone, Debug)]
524pub struct FormationSlot {
525    pub offset:     Vec2,   // relative to formation center/leader
526    pub role:       &'static str,
527}
528
529impl FormationSlot {
530    pub fn new(offset: Vec2, role: &'static str) -> Self { Self { offset, role } }
531}
532
533/// Formation definitions: wedge, line, column, circle.
534pub struct FormationMovement {
535    pub slots:   Vec<FormationSlot>,
536    pub leader:  Vec2,
537    pub heading: f32,   // formation facing direction in radians
538}
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    /// Compute the world-space target position for formation slot `idx`.
592    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        // Rotate offset by heading
596        let rotated = local_offset.rotate(self.heading);
597        self.leader.add(rotated)
598    }
599
600    /// Steering force for agent `idx` to maintain its formation slot.
601    /// Agent's slot target = leader pos + rotated offset.
602    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    /// Update formation center (leader position) and heading.
614    pub fn update_leader(&mut self, pos: Vec2, heading: f32) {
615        self.leader  = pos;
616        self.heading = heading;
617    }
618}
619
620// ── Path following ────────────────────────────────────────────────────────────
621
622/// Follows a sequence of waypoints with lookahead.
623pub struct PathFollower {
624    pub waypoints:        Vec<Vec2>,
625    pub lookahead:        f32,    // distance ahead on path to target
626    pub current_segment:  usize,
627    pub path_progress:    f32,    // arc-length progress along path
628    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    /// Check if at end of path.
649    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    /// Compute steering toward lookahead point on the path.
657    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        // Find closest point on path, then project ahead by lookahead
664        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        // Advance segment pointer if agent is close enough to next waypoint
680        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        // Project agent onto segment
696        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
705// ── Behavior blending ─────────────────────────────────────────────────────────
706
707/// A weighted steering behavior entry.
708pub 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
717/// Blended steering: combine multiple behaviors with priority or weighted sum.
718pub 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    /// Weighted sum of all behaviors (normalized by total weight).
730    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    /// Priority blending: use first behavior that exceeds an epsilon force.
741    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    /// Truncated weighted sum: add behaviors in priority order until max force reached.
752    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                // Take partial contribution
764                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
779// ── Context steering (supplemental) ──────────────────────────────────────────
780
781/// Context map for context steering: interest and danger slots around a circle.
782pub struct ContextMap {
783    pub slots:        usize,          // number of directions (e.g., 16)
784    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    /// Mask danger from interest and return best direction.
819    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
838// ── Neighborhood query ────────────────────────────────────────────────────────
839
840/// Efficient neighbor lookup for flocking using a simple spatial bucket.
841pub 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    /// Collect indices of all agents within `radius` of `pos`.
863    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// ── High-level agent controller ───────────────────────────────────────────────
881
882/// Steering state machine mode.
883#[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
897/// An agent controller that manages mode switching and behavior execution.
898pub struct AgentController {
899    pub agent:    SteeringAgent,
900    pub mode:     SteeringMode,
901    pub wander:   Wander,
902    pub blender:  BlendedSteering,
903    // Path following
904    pub path_follower: Option<PathFollower>,
905    // Target for simple behaviors
906    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    /// Compute and apply steering for one time step.
933    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// ── Tests ─────────────────────────────────────────────────────────────────────
961
962#[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        // Inside slowing radius: desired speed is reduced, so braking force
995        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        // Different jitter values should produce different directions
1005        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); // moving right
1012        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        // Should push perpendicular (y direction)
1016        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        // With equal neighbors on both sides, separation forces should partially cancel
1029        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        // Should be spread along x
1038        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        // Should cancel out
1061        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}