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