Skip to main content

proof_engine/behavior/
nodes.rs

1//! Built-in behavior-tree node library.
2//!
3//! Every function in this module returns a [`BehaviorNode::Leaf`] (or a small
4//! composite that acts like a logical leaf) that encapsulates one well-defined
5//! game-AI action or condition.  They are designed to be composed with
6//! [`crate::behavior::tree::TreeBuilder`].
7//!
8//! # Node catalogue
9//!
10//! **Actions** (mutate world state / run over time)
11//! - [`wait`]             — pause for a fixed duration
12//! - [`move_to`]          — move agent toward a target position
13//! - [`look_at`]          — rotate agent to face a target
14//! - [`play_animation`]   — trigger and wait for an animation clip
15//! - [`set_blackboard`]   — write a value onto the blackboard
16//!
17//! **Conditions** (instant pass/fail checks)
18//! - [`check_distance`]   — is agent within/outside a range?
19//! - [`check_health`]     — compare health against a threshold
20//! - [`check_line_of_sight`] — can the agent see a target?
21//! - [`check_blackboard_bool`] — read a bool flag
22//! - [`check_blackboard_float`] — compare a float value
23//!
24//! **Decorator wrappers** (higher-order node constructors)
25//! - [`invert_node`]      — flip Success↔Failure
26//! - [`repeat_node`]      — run child N times
27//! - [`timeout_node`]     — fail child if it takes too long
28//!
29//! **Composite helpers**
30//! - [`random_selector`]  — shuffle children then select
31//! - [`weighted_selector`] — pick child by probability weight
32
33use std::collections::HashMap;
34use glam::{Vec2, Vec3};
35
36use super::tree::{
37    BehaviorNode, BehaviorTree, Blackboard, BlackboardValue,
38    DecoratorKind, DecoratorState, NodeStatus, ParallelPolicy,
39    SubtreeRegistry,
40};
41
42// ── Internal Utilities ────────────────────────────────────────────────────────
43
44/// Simple deterministic pseudo-random number generator (xorshift64).
45/// Seeded from the blackboard `"__rng_seed"` key, or 12345 if absent.
46struct Rng { state: u64 }
47
48impl Rng {
49    fn new(seed: u64) -> Self { Self { state: seed.max(1) } }
50    fn next_u64(&mut self) -> u64 {
51        self.state ^= self.state << 13;
52        self.state ^= self.state >> 7;
53        self.state ^= self.state << 17;
54        self.state
55    }
56    fn next_f32(&mut self) -> f32 { (self.next_u64() as f32) / (u64::MAX as f32) }
57    fn range_f32(&mut self, lo: f32, hi: f32) -> f32 { lo + self.next_f32() * (hi - lo) }
58    fn range_usize(&mut self, lo: usize, hi: usize) -> usize {
59        lo + (self.next_u64() as usize % (hi - lo))
60    }
61    /// Fisher-Yates shuffle.
62    fn shuffle<T>(&mut self, data: &mut [T]) {
63        let n = data.len();
64        for i in (1..n).rev() {
65            let j = self.range_usize(0, i + 1);
66            data.swap(i, j);
67        }
68    }
69}
70
71fn rng_from_bb(bb: &Blackboard) -> Rng {
72    let seed = bb.get_int("__rng_seed").unwrap_or(12345) as u64;
73    Rng::new(seed)
74}
75
76// ── Wait ──────────────────────────────────────────────────────────────────────
77
78/// Pause execution for `duration_secs` seconds, then succeed.
79///
80/// The node stores its accumulated elapsed time on the blackboard under
81/// `__wait_{name}_elapsed` to survive tree resets gracefully.
82pub fn wait(name: &str, duration_secs: f32) -> BehaviorNode {
83    let elapsed_key = format!("__wait_{name}_elapsed");
84    let enter_key   = elapsed_key.clone();
85    let tick_key    = elapsed_key.clone();
86
87    BehaviorNode::Leaf {
88        name:    name.to_string(),
89        on_enter: Some(Box::new(move |bb: &mut Blackboard| {
90            bb.set(enter_key.as_str(), 0.0f64);
91        })),
92        on_tick: Box::new(move |bb: &mut Blackboard, dt: f32| {
93            let elapsed = bb.get_float(tick_key.as_str()).unwrap_or(0.0) + dt as f64;
94            bb.set(tick_key.as_str(), elapsed);
95            if elapsed >= duration_secs as f64 {
96                NodeStatus::Success
97            } else {
98                NodeStatus::Running
99            }
100        }),
101        on_exit: None,
102        entered: false,
103    }
104}
105
106// ── MoveTo ────────────────────────────────────────────────────────────────────
107
108/// Move an agent toward a 3-D target stored on the blackboard.
109///
110/// Reads:
111/// - `"{agent_pos_key}"` — current agent position (`Vec3`)
112/// - `"{target_key}"`    — target position (`Vec3`)
113///
114/// Writes:
115/// - `"{agent_pos_key}"` — updated position after movement this tick
116///
117/// Succeeds when the agent is within `arrival_radius` of the target.
118/// Returns `Failure` if either position key is absent.
119pub fn move_to(
120    name:          &str,
121    agent_pos_key: &str,
122    target_key:    &str,
123    speed:         f32,
124    arrival_radius: f32,
125) -> BehaviorNode {
126    let pos_key    = agent_pos_key.to_string();
127    let tgt_key    = target_key.to_string();
128
129    BehaviorNode::Leaf {
130        name: name.to_string(),
131        on_enter: None,
132        on_tick: Box::new(move |bb: &mut Blackboard, dt: f32| {
133            let pos = match bb.get_vec3(&pos_key) {
134                Some(p) => p,
135                None    => return NodeStatus::Failure,
136            };
137            let target = match bb.get_vec3(&tgt_key) {
138                Some(t) => t,
139                None    => return NodeStatus::Failure,
140            };
141
142            let delta = target - pos;
143            let dist  = delta.length();
144
145            if dist <= arrival_radius + 1e-4 {
146                return NodeStatus::Success;
147            }
148
149            let dir   = delta / dist;
150            let step  = (speed * dt).min(dist);
151            let new_pos = pos + dir * step;
152            bb.set(pos_key.as_str(), new_pos);
153
154            if (new_pos - target).length() <= arrival_radius + 1e-4 {
155                NodeStatus::Success
156            } else {
157                NodeStatus::Running
158            }
159        }),
160        on_exit: None,
161        entered: false,
162    }
163}
164
165/// Variant that reads the target from a `Vec2` position and moves in 2-D.
166pub fn move_to_2d(
167    name:           &str,
168    agent_pos_key:  &str,
169    target_key:     &str,
170    speed:          f32,
171    arrival_radius: f32,
172) -> BehaviorNode {
173    let pos_key = agent_pos_key.to_string();
174    let tgt_key = target_key.to_string();
175
176    BehaviorNode::Leaf {
177        name: name.to_string(),
178        on_enter: None,
179        on_tick: Box::new(move |bb: &mut Blackboard, dt: f32| {
180            let pos = match bb.get_vec2(&pos_key) {
181                Some(p) => p,
182                None    => return NodeStatus::Failure,
183            };
184            let target = match bb.get_vec2(&tgt_key) {
185                Some(t) => t,
186                None    => return NodeStatus::Failure,
187            };
188
189            let delta = target - pos;
190            let dist  = delta.length();
191            if dist <= arrival_radius {
192                return NodeStatus::Success;
193            }
194            let dir    = delta / dist;
195            let step   = (speed * dt).min(dist - arrival_radius);
196            let new_pos = pos + dir * step;
197            bb.set(pos_key.as_str(), new_pos);
198
199            if (new_pos - target).length() <= arrival_radius {
200                NodeStatus::Success
201            } else {
202                NodeStatus::Running
203            }
204        }),
205        on_exit: None,
206        entered: false,
207    }
208}
209
210// ── LookAt ────────────────────────────────────────────────────────────────────
211
212/// Smoothly rotate the agent's yaw toward a target.
213///
214/// Reads:
215/// - `"{yaw_key}"`    — current yaw angle in radians (`f64`)
216/// - `"{target_key}"` — target position (`Vec3`) *or* target yaw directly
217///                      (`f64`, taken as absolute yaw)
218/// - `"{pos_key}"`    — agent position (`Vec3`, only needed if target is Vec3)
219///
220/// Writes:
221/// - `"{yaw_key}"` — updated yaw
222///
223/// Succeeds when angular delta ≤ `tolerance_rad`.
224pub fn look_at(
225    name:          &str,
226    yaw_key:       &str,
227    pos_key:       &str,
228    target_key:    &str,
229    turn_speed:    f32,
230    tolerance_rad: f32,
231) -> BehaviorNode {
232    let yaw_k = yaw_key.to_string();
233    let pos_k = pos_key.to_string();
234    let tgt_k = target_key.to_string();
235
236    BehaviorNode::Leaf {
237        name: name.to_string(),
238        on_enter: None,
239        on_tick: Box::new(move |bb: &mut Blackboard, dt: f32| {
240            let current_yaw = bb.get_float(&yaw_k).unwrap_or(0.0) as f32;
241
242            // Determine desired yaw from target key (Vec3 or f64).
243            let desired_yaw = if let Some(target_pos) = bb.get_vec3(&tgt_k) {
244                let agent_pos = bb.get_vec3(&pos_k).unwrap_or(Vec3::ZERO);
245                let d = target_pos - agent_pos;
246                d.z.atan2(d.x)
247            } else if let Some(yaw) = bb.get_float(&tgt_k) {
248                yaw as f32
249            } else {
250                return NodeStatus::Failure;
251            };
252
253            // Shortest-arc rotation.
254            let mut delta = desired_yaw - current_yaw;
255            while delta >  std::f32::consts::PI { delta -= std::f32::consts::TAU; }
256            while delta < -std::f32::consts::PI { delta += std::f32::consts::TAU; }
257
258            if delta.abs() <= tolerance_rad {
259                return NodeStatus::Success;
260            }
261
262            let step = (turn_speed * dt).min(delta.abs()) * delta.signum();
263            let new_yaw = current_yaw + step;
264            bb.set(yaw_k.as_str(), new_yaw as f64);
265
266            if (new_yaw - desired_yaw).abs() <= tolerance_rad {
267                NodeStatus::Success
268            } else {
269                NodeStatus::Running
270            }
271        }),
272        on_exit: None,
273        entered: false,
274    }
275}
276
277// ── PlayAnimation ─────────────────────────────────────────────────────────────
278
279/// Trigger a named animation clip and block until it finishes.
280///
281/// Writes `"{anim_request_key}"` on enter with the clip name.
282/// Reads `"{anim_done_key}"` each tick; returns Success when it becomes true.
283///
284/// If the done key is not set within `timeout_secs`, returns Failure.
285pub fn play_animation(
286    name:             &str,
287    clip_name:        &str,
288    anim_request_key: &str,
289    anim_done_key:    &str,
290    timeout_secs:     f32,
291) -> BehaviorNode {
292    let req_key    = anim_request_key.to_string();
293    let done_key   = anim_done_key.to_string();
294    let clip       = clip_name.to_string();
295    let elapsed_key = format!("__anim_{name}_elapsed");
296
297    let enter_req  = req_key.clone();
298    let enter_clip = clip.clone();
299    let enter_ek   = elapsed_key.clone();
300    let tick_dk    = done_key.clone();
301    let tick_ek    = elapsed_key.clone();
302
303    BehaviorNode::Leaf {
304        name: name.to_string(),
305        on_enter: Some(Box::new(move |bb: &mut Blackboard| {
306            bb.set(enter_req.as_str(), enter_clip.as_str());
307            bb.set(enter_ek.as_str(), 0.0f64);
308        })),
309        on_tick: Box::new(move |bb: &mut Blackboard, dt: f32| {
310            let elapsed = bb.get_float(&tick_ek).unwrap_or(0.0) + dt as f64;
311            bb.set(tick_ek.as_str(), elapsed);
312
313            if elapsed >= timeout_secs as f64 {
314                return NodeStatus::Failure;
315            }
316            if bb.get_bool(&tick_dk).unwrap_or(false) {
317                NodeStatus::Success
318            } else {
319                NodeStatus::Running
320            }
321        }),
322        on_exit: Some(Box::new(move |bb: &mut Blackboard, _status: NodeStatus| {
323            bb.remove(done_key.as_str());
324        })),
325        entered: false,
326    }
327}
328
329// ── SetBlackboard ─────────────────────────────────────────────────────────────
330
331/// Instantly write a value onto the blackboard and succeed.
332pub fn set_blackboard<V>(name: &str, key: &str, value: V) -> BehaviorNode
333where
334    V: Into<BlackboardValue> + Clone + Send + 'static,
335{
336    let k = key.to_string();
337    let v = value.into();
338    BehaviorNode::Leaf {
339        name:     name.to_string(),
340        on_enter: None,
341        on_tick:  Box::new(move |bb: &mut Blackboard, _dt: f32| {
342            bb.set(k.as_str(), v.clone());
343            NodeStatus::Success
344        }),
345        on_exit:  None,
346        entered:  false,
347    }
348}
349
350/// Remove a key from the blackboard and succeed.
351pub fn clear_blackboard(name: &str, key: &str) -> BehaviorNode {
352    let k = key.to_string();
353    BehaviorNode::Leaf {
354        name: name.to_string(),
355        on_enter: None,
356        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
357            bb.remove(k.as_str());
358            NodeStatus::Success
359        }),
360        on_exit: None,
361        entered: false,
362    }
363}
364
365/// Copy a blackboard value from one key to another and succeed.
366pub fn copy_blackboard(name: &str, src_key: &str, dst_key: &str) -> BehaviorNode {
367    let src = src_key.to_string();
368    let dst = dst_key.to_string();
369    BehaviorNode::Leaf {
370        name: name.to_string(),
371        on_enter: None,
372        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
373            match bb.get(&src).cloned() {
374                Some(val) => { bb.set(dst.as_str(), val); NodeStatus::Success }
375                None      => NodeStatus::Failure,
376            }
377        }),
378        on_exit: None,
379        entered: false,
380    }
381}
382
383// ── CheckDistance ─────────────────────────────────────────────────────────────
384
385/// Condition: is the distance between two Vec3 positions within `[min, max]`?
386///
387/// Reads `"{pos_a_key}"` and `"{pos_b_key}"` from the blackboard.
388/// Returns Success if `min <= dist <= max`, Failure otherwise.
389pub fn check_distance(
390    name:      &str,
391    pos_a_key: &str,
392    pos_b_key: &str,
393    min_dist:  f32,
394    max_dist:  f32,
395) -> BehaviorNode {
396    let a = pos_a_key.to_string();
397    let b = pos_b_key.to_string();
398    BehaviorNode::Leaf {
399        name: name.to_string(),
400        on_enter: None,
401        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
402            let pa = match bb.get_vec3(&a) { Some(v) => v, None => return NodeStatus::Failure };
403            let pb = match bb.get_vec3(&b) { Some(v) => v, None => return NodeStatus::Failure };
404            let d = (pa - pb).length();
405            if d >= min_dist && d <= max_dist { NodeStatus::Success } else { NodeStatus::Failure }
406        }),
407        on_exit: None,
408        entered: false,
409    }
410}
411
412/// Condition: is the distance within `range` (i.e. `0 <= dist <= range`)?
413pub fn check_in_range(
414    name:     &str,
415    pos_a_key: &str,
416    pos_b_key: &str,
417    range:    f32,
418) -> BehaviorNode {
419    check_distance(name, pos_a_key, pos_b_key, 0.0, range)
420}
421
422/// Condition: is the distance strictly outside `range`?
423pub fn check_out_of_range(
424    name:     &str,
425    pos_a_key: &str,
426    pos_b_key: &str,
427    range:    f32,
428) -> BehaviorNode {
429    let a = pos_a_key.to_string();
430    let b = pos_b_key.to_string();
431    BehaviorNode::Leaf {
432        name: name.to_string(),
433        on_enter: None,
434        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
435            let pa = match bb.get_vec3(&a) { Some(v) => v, None => return NodeStatus::Failure };
436            let pb = match bb.get_vec3(&b) { Some(v) => v, None => return NodeStatus::Failure };
437            let d = (pa - pb).length();
438            if d > range { NodeStatus::Success } else { NodeStatus::Failure }
439        }),
440        on_exit: None,
441        entered: false,
442    }
443}
444
445// ── CheckHealth ───────────────────────────────────────────────────────────────
446
447/// Condition: compare a health value on the blackboard against a threshold.
448///
449/// Reads `"{health_key}"` (float).  Applies `operator` comparison.
450pub enum CompareOp { Lt, Lte, Gt, Gte, Eq }
451
452/// Condition: `health_key OP threshold` → Success / Failure.
453pub fn check_health(
454    name:       &str,
455    health_key: &str,
456    op:         CompareOp,
457    threshold:  f64,
458) -> BehaviorNode {
459    let k = health_key.to_string();
460    BehaviorNode::Leaf {
461        name: name.to_string(),
462        on_enter: None,
463        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
464            let h = match bb.get_float(&k) { Some(v) => v, None => return NodeStatus::Failure };
465            let ok = match op {
466                CompareOp::Lt  => h <  threshold,
467                CompareOp::Lte => h <= threshold,
468                CompareOp::Gt  => h >  threshold,
469                CompareOp::Gte => h >= threshold,
470                CompareOp::Eq  => (h - threshold).abs() < 1e-6,
471            };
472            if ok { NodeStatus::Success } else { NodeStatus::Failure }
473        }),
474        on_exit: None,
475        entered: false,
476    }
477}
478
479/// Shorthand: succeed if health < `threshold` (agent is wounded).
480pub fn check_health_low(name: &str, health_key: &str, threshold: f64) -> BehaviorNode {
481    check_health(name, health_key, CompareOp::Lt, threshold)
482}
483
484/// Shorthand: succeed if health >= `threshold` (agent is healthy).
485pub fn check_health_ok(name: &str, health_key: &str, threshold: f64) -> BehaviorNode {
486    check_health(name, health_key, CompareOp::Gte, threshold)
487}
488
489// ── CheckLineOfSight ──────────────────────────────────────────────────────────
490
491/// Condition: can agent see target?
492///
493/// A simplified LOS check: reads the agent position and target position from
494/// the blackboard, then tests a list of obstacle positions stored under
495/// `"{obstacles_key}"` (a `BlackboardValue::List` of `Vec3` entries).
496///
497/// For each obstacle sphere (radius `obstacle_radius`), the check performs a
498/// ray-sphere intersection.  If any obstacle blocks the ray, returns Failure.
499///
500/// For games with a custom LOS system, replace this function with one that
501/// calls into your spatial query API — the signature is identical.
502pub fn check_line_of_sight(
503    name:           &str,
504    agent_pos_key:  &str,
505    target_pos_key: &str,
506    obstacles_key:  &str,
507    obstacle_radius: f32,
508    max_range:      f32,
509) -> BehaviorNode {
510    let ap = agent_pos_key.to_string();
511    let tp = target_pos_key.to_string();
512    let ok = obstacles_key.to_string();
513
514    BehaviorNode::Leaf {
515        name: name.to_string(),
516        on_enter: None,
517        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
518            let a = match bb.get_vec3(&ap) { Some(v) => v, None => return NodeStatus::Failure };
519            let t = match bb.get_vec3(&tp) { Some(v) => v, None => return NodeStatus::Failure };
520
521            let diff = t - a;
522            let dist = diff.length();
523            if dist > max_range { return NodeStatus::Failure; }
524            if dist < 1e-5      { return NodeStatus::Success;  } // same position
525
526            let dir = diff / dist;
527
528            // Gather obstacle positions.
529            let blocked = match bb.get(&ok) {
530                Some(BlackboardValue::List(list)) => {
531                    list.iter().any(|entry| {
532                        if let BlackboardValue::Vec3(obs) = entry {
533                            ray_sphere_intersect(a, dir, *obs, obstacle_radius, dist)
534                        } else {
535                            false
536                        }
537                    })
538                }
539                _ => false,
540            };
541
542            if blocked { NodeStatus::Failure } else { NodeStatus::Success }
543        }),
544        on_exit: None,
545        entered: false,
546    }
547}
548
549/// Ray-sphere intersection test.
550/// Returns true if a sphere at `center` with `radius` blocks a ray from
551/// `origin` in direction `dir` within `max_t` distance.
552fn ray_sphere_intersect(origin: Vec3, dir: Vec3, center: Vec3, radius: f32, max_t: f32) -> bool {
553    let oc = origin - center;
554    let b  = 2.0 * oc.dot(dir);
555    let c  = oc.dot(oc) - radius * radius;
556    let disc = b * b - 4.0 * c;
557    if disc < 0.0 { return false; }
558    let sqrt_d = disc.sqrt();
559    let t0 = (-b - sqrt_d) * 0.5;
560    let t1 = (-b + sqrt_d) * 0.5;
561    (t0 > 0.0 && t0 < max_t) || (t1 > 0.0 && t1 < max_t)
562}
563
564// ── CheckBlackboard ───────────────────────────────────────────────────────────
565
566/// Condition: return Success if `key` holds a truthy bool.
567pub fn check_blackboard_bool(name: &str, key: &str, expected: bool) -> BehaviorNode {
568    let k = key.to_string();
569    BehaviorNode::Leaf {
570        name: name.to_string(),
571        on_enter: None,
572        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
573            let ok = bb.get_bool(&k).unwrap_or(!expected) == expected;
574            if ok { NodeStatus::Success } else { NodeStatus::Failure }
575        }),
576        on_exit: None,
577        entered: false,
578    }
579}
580
581/// Condition: compare a float blackboard value.
582pub fn check_blackboard_float(
583    name:      &str,
584    key:       &str,
585    op:        CompareOp,
586    threshold: f64,
587) -> BehaviorNode {
588    let k = key.to_string();
589    BehaviorNode::Leaf {
590        name: name.to_string(),
591        on_enter: None,
592        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
593            let v = match bb.get_float(&k) { Some(v) => v, None => return NodeStatus::Failure };
594            let ok = match op {
595                CompareOp::Lt  => v <  threshold,
596                CompareOp::Lte => v <= threshold,
597                CompareOp::Gt  => v >  threshold,
598                CompareOp::Gte => v >= threshold,
599                CompareOp::Eq  => (v - threshold).abs() < 1e-9,
600            };
601            if ok { NodeStatus::Success } else { NodeStatus::Failure }
602        }),
603        on_exit: None,
604        entered: false,
605    }
606}
607
608/// Condition: succeed if key is present in the blackboard.
609pub fn check_blackboard_exists(name: &str, key: &str) -> BehaviorNode {
610    let k = key.to_string();
611    BehaviorNode::Leaf {
612        name: name.to_string(),
613        on_enter: None,
614        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
615            if bb.contains(&k) { NodeStatus::Success } else { NodeStatus::Failure }
616        }),
617        on_exit: None,
618        entered: false,
619    }
620}
621
622// ── InvertDecorator ───────────────────────────────────────────────────────────
623
624/// Wrap `child` in an Invert decorator: Success↔Failure, Running unchanged.
625pub fn invert_node(name: &str, child: BehaviorNode) -> BehaviorNode {
626    BehaviorNode::Decorator {
627        name:  name.to_string(),
628        kind:  DecoratorKind::Invert,
629        child: Box::new(child),
630        state: DecoratorState::default(),
631    }
632}
633
634// ── RepeatDecorator ───────────────────────────────────────────────────────────
635
636/// Repeat `child` exactly `count` times; succeed only if all iterations succeed.
637pub fn repeat_node(name: &str, count: u32, child: BehaviorNode) -> BehaviorNode {
638    BehaviorNode::Decorator {
639        name:  name.to_string(),
640        kind:  DecoratorKind::Repeat { count },
641        child: Box::new(child),
642        state: DecoratorState::default(),
643    }
644}
645
646/// Repeat `child` forever (always returns Running).
647pub fn repeat_forever(name: &str, child: BehaviorNode) -> BehaviorNode {
648    BehaviorNode::Decorator {
649        name:  name.to_string(),
650        kind:  DecoratorKind::RepeatForever,
651        child: Box::new(child),
652        state: DecoratorState::default(),
653    }
654}
655
656// ── TimeoutDecorator ──────────────────────────────────────────────────────────
657
658/// Return Failure if `child` is still Running after `timeout_secs` seconds.
659pub fn timeout_node(name: &str, timeout_secs: f32, child: BehaviorNode) -> BehaviorNode {
660    BehaviorNode::Decorator {
661        name:  name.to_string(),
662        kind:  DecoratorKind::Timeout { timeout_secs },
663        child: Box::new(child),
664        state: DecoratorState::default(),
665    }
666}
667
668/// Only tick `child` if at least `cooldown_secs` have elapsed since last tick.
669pub fn cooldown_node(name: &str, cooldown_secs: f32, child: BehaviorNode) -> BehaviorNode {
670    BehaviorNode::Decorator {
671        name:  name.to_string(),
672        kind:  DecoratorKind::Cooldown { cooldown_secs },
673        child: Box::new(child),
674        state: DecoratorState::default(),
675    }
676}
677
678/// Only tick child when a blackboard bool key equals `expected`.
679pub fn blackboard_guard(name: &str, key: &str, expected: bool, child: BehaviorNode) -> BehaviorNode {
680    BehaviorNode::Decorator {
681        name:  name.to_string(),
682        kind:  DecoratorKind::BlackboardGuard { key: key.to_string(), expected },
683        child: Box::new(child),
684        state: DecoratorState::default(),
685    }
686}
687
688// ── RandomSelector ────────────────────────────────────────────────────────────
689
690/// A Selector that shuffles its children randomly on each activation, then
691/// tries them in the shuffled order.  Because the shuffle is computed
692/// per-activation (inside the tick closure), the node re-shuffles every time
693/// the selector restarts.
694///
695/// The children are pre-built `BehaviorNode` instances passed in.  Internally
696/// this wraps them in a custom Leaf that drives them manually, maintaining its
697/// own cursor and shuffled order.
698pub fn random_selector(name: &str, mut children: Vec<BehaviorNode>) -> BehaviorNode {
699    // We need mutable state (cursor, shuffled order) that lives across ticks.
700    // We box it all into the closure's captured environment.
701    let order_key   = format!("__rselector_{name}_order");
702    let cursor_key  = format!("__rselector_{name}_cursor");
703    let entered_key = format!("__rselector_{name}_active");
704    let n = children.len();
705
706    // children is moved into the closure.
707    BehaviorNode::Leaf {
708        name: name.to_string(),
709        on_enter: Some(Box::new({
710            let ok  = order_key.clone();
711            let ck  = cursor_key.clone();
712            let ek  = entered_key.clone();
713            move |bb: &mut Blackboard| {
714                // Build initial [0,1,...,n-1] order list.
715                let mut rng = rng_from_bb(bb);
716                let mut indices: Vec<i64> = (0..n as i64).collect();
717                rng.shuffle(&mut indices);
718                let list: Vec<BlackboardValue> = indices.iter()
719                    .map(|&i| BlackboardValue::Int(i))
720                    .collect();
721                bb.set(ok.as_str(), BlackboardValue::List(list));
722                bb.set(ck.as_str(), 0i64);
723                bb.set(ek.as_str(), true);
724            }
725        })),
726        on_tick: Box::new({
727            let order_key_tick = order_key.clone();
728            let cursor_key_tick = cursor_key.clone();
729            move |bb: &mut Blackboard, dt: f32| {
730                let reg = SubtreeRegistry::new();
731                loop {
732                    let cursor = bb.get_int(&cursor_key_tick).unwrap_or(0) as usize;
733                    if cursor >= n {
734                        return NodeStatus::Failure;
735                    }
736                    let child_idx = match bb.get(&order_key_tick) {
737                        Some(BlackboardValue::List(list)) => {
738                            list.get(cursor)
739                                .and_then(|v| v.as_int())
740                                .unwrap_or(cursor as i64) as usize
741                        }
742                        _ => cursor,
743                    };
744
745                    if child_idx >= n { return NodeStatus::Failure; }
746
747                    let status = children[child_idx].tick(dt, bb, &reg);
748                    match status {
749                        NodeStatus::Success => return NodeStatus::Success,
750                        NodeStatus::Failure => {
751                            bb.set(cursor_key_tick.as_str(), (cursor + 1) as i64);
752                        }
753                        NodeStatus::Running => return NodeStatus::Running,
754                    }
755                }
756            }
757        }),
758        on_exit: Some(Box::new({
759            let ok = order_key;
760            let ck = cursor_key;
761            let ek = entered_key;
762            move |bb: &mut Blackboard, _: NodeStatus| {
763                bb.remove(ok.as_str());
764                bb.remove(ck.as_str());
765                bb.remove(ek.as_str());
766            }
767        })),
768        entered: false,
769    }
770}
771
772// ── WeightedSelector ─────────────────────────────────────────────────────────
773
774/// A Selector that picks exactly one child based on probability weights and
775/// ticks only that child.  Each activation samples a fresh child.
776///
777/// `weights` must be the same length as `children`.  Weights need not sum to
778/// 1.0 — they are normalized internally.
779pub fn weighted_selector(
780    name:     &str,
781    mut children: Vec<BehaviorNode>,
782    weights:  Vec<f32>,
783) -> BehaviorNode {
784    assert_eq!(children.len(), weights.len(),
785               "weighted_selector: children and weights must be the same length");
786
787    let n          = children.len();
788    let chosen_key = format!("__wsel_{name}_chosen");
789
790    BehaviorNode::Leaf {
791        name: name.to_string(),
792        on_enter: Some(Box::new({
793            let ck      = chosen_key.clone();
794            let ws      = weights.clone();
795            move |bb: &mut Blackboard| {
796                let mut rng  = rng_from_bb(bb);
797                let total: f32 = ws.iter().sum();
798                let sample = rng.next_f32() * total;
799                let mut acc = 0.0f32;
800                let mut chosen = 0usize;
801                for (i, &w) in ws.iter().enumerate() {
802                    acc += w;
803                    if sample <= acc { chosen = i; break; }
804                }
805                bb.set(ck.as_str(), chosen as i64);
806            }
807        })),
808        on_tick: Box::new(move |bb: &mut Blackboard, dt: f32| {
809            let idx = bb.get_int(&chosen_key).unwrap_or(0) as usize;
810            if idx >= n { return NodeStatus::Failure; }
811            let reg = SubtreeRegistry::new();
812            children[idx].tick(dt, bb, &reg)
813        }),
814        on_exit: None,
815        entered: false,
816    }
817}
818
819// ── DebugLog ──────────────────────────────────────────────────────────────────
820
821/// A leaf that logs a message and always succeeds.  Useful for tracing tree
822/// execution during development.
823pub fn debug_log(name: &str, message: &str) -> BehaviorNode {
824    let msg = message.to_string();
825    BehaviorNode::Leaf {
826        name: name.to_string(),
827        on_enter: None,
828        on_tick: Box::new(move |_bb: &mut Blackboard, _dt: f32| {
829            log::debug!("[BT] {msg}");
830            NodeStatus::Success
831        }),
832        on_exit: None,
833        entered: false,
834    }
835}
836
837/// A leaf that logs the current value of a blackboard key and succeeds.
838pub fn debug_log_blackboard(name: &str, key: &str) -> BehaviorNode {
839    let k = key.to_string();
840    BehaviorNode::Leaf {
841        name: name.to_string(),
842        on_enter: None,
843        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
844            match bb.get(&k) {
845                Some(v) => log::debug!("[BT] {k} = {v:?}"),
846                None    => log::debug!("[BT] {k} = <absent>"),
847            }
848            NodeStatus::Success
849        }),
850        on_exit: None,
851        entered: false,
852    }
853}
854
855// ── Flee ─────────────────────────────────────────────────────────────────────
856
857/// Move the agent away from a threat position stored on the blackboard.
858///
859/// Reads:
860/// - `"{agent_pos_key}"` — current agent position (`Vec3`)
861/// - `"{threat_key}"`    — threat position (`Vec3`)
862///
863/// Writes:
864/// - `"{agent_pos_key}"` — updated position
865///
866/// Succeeds when the agent is more than `safe_distance` away.
867pub fn flee(
868    name:          &str,
869    agent_pos_key: &str,
870    threat_key:    &str,
871    speed:         f32,
872    safe_distance: f32,
873) -> BehaviorNode {
874    let pk = agent_pos_key.to_string();
875    let tk = threat_key.to_string();
876
877    BehaviorNode::Leaf {
878        name: name.to_string(),
879        on_enter: None,
880        on_tick: Box::new(move |bb: &mut Blackboard, dt: f32| {
881            let pos    = match bb.get_vec3(&pk) { Some(v) => v, None => return NodeStatus::Failure };
882            let threat = match bb.get_vec3(&tk) { Some(v) => v, None => return NodeStatus::Failure };
883
884            let delta = pos - threat;
885            let dist  = delta.length();
886            if dist >= safe_distance { return NodeStatus::Success; }
887
888            let dir = if dist > 1e-5 { delta / dist } else { Vec3::X };
889            let new_pos = pos + dir * speed * dt;
890            bb.set(pk.as_str(), new_pos);
891
892            if (new_pos - threat).length() >= safe_distance {
893                NodeStatus::Success
894            } else {
895                NodeStatus::Running
896            }
897        }),
898        on_exit: None,
899        entered: false,
900    }
901}
902
903// ── Patrol ────────────────────────────────────────────────────────────────────
904
905/// Move through a fixed list of waypoints in a loop.
906///
907/// Stores current waypoint index in `"{waypoint_idx_key}"`.
908/// Writes the next target to `"{target_pos_key}"` and then delegates movement
909/// to the blackboard consumer (i.e. pair this with `move_to`).
910///
911/// On each tick:
912/// 1. Load current waypoint index from `"{waypoint_idx_key}"` (default 0).
913/// 2. Write that waypoint's Vec3 into `"{target_pos_key}"`.
914/// 3. If agent is within `arrival_radius`, advance the index and return Running.
915/// 4. Otherwise return Running (movement handled externally).
916pub fn patrol_set_target(
917    name:            &str,
918    waypoints:       Vec<Vec3>,
919    waypoint_idx_key: &str,
920    agent_pos_key:   &str,
921    target_pos_key:  &str,
922    arrival_radius:  f32,
923) -> BehaviorNode {
924    let wik = waypoint_idx_key.to_string();
925    let apk = agent_pos_key.to_string();
926    let tpk = target_pos_key.to_string();
927    let n   = waypoints.len();
928
929    BehaviorNode::Leaf {
930        name: name.to_string(),
931        on_enter: None,
932        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
933            if n == 0 { return NodeStatus::Failure; }
934
935            let idx = (bb.get_int(&wik).unwrap_or(0) as usize) % n;
936            let target = waypoints[idx];
937            bb.set(tpk.as_str(), target);
938
939            // Check if arrived at this waypoint.
940            if let Some(pos) = bb.get_vec3(&apk) {
941                if (pos - target).length() <= arrival_radius {
942                    bb.set(wik.as_str(), ((idx + 1) % n) as i64);
943                }
944            }
945
946            NodeStatus::Running // Patrol never "finishes"
947        }),
948        on_exit: None,
949        entered: false,
950    }
951}
952
953// ── FaceDirection ─────────────────────────────────────────────────────────────
954
955/// Instantly snap the agent's yaw to face a target position.
956///
957/// Reads:
958/// - `"{agent_pos_key}"` — Vec3
959/// - `"{target_pos_key}"` — Vec3
960///
961/// Writes:
962/// - `"{yaw_key}"` — f64 yaw angle in radians
963pub fn face_direction(
964    name:           &str,
965    agent_pos_key:  &str,
966    target_pos_key: &str,
967    yaw_key:        &str,
968) -> BehaviorNode {
969    let ap = agent_pos_key.to_string();
970    let tp = target_pos_key.to_string();
971    let yk = yaw_key.to_string();
972
973    BehaviorNode::Leaf {
974        name: name.to_string(),
975        on_enter: None,
976        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
977            let a = match bb.get_vec3(&ap) { Some(v) => v, None => return NodeStatus::Failure };
978            let t = match bb.get_vec3(&tp) { Some(v) => v, None => return NodeStatus::Failure };
979            let d = t - a;
980            let yaw = d.z.atan2(d.x) as f64;
981            bb.set(yk.as_str(), yaw);
982            NodeStatus::Success
983        }),
984        on_exit: None,
985        entered: false,
986    }
987}
988
989// ── FireAtTarget ──────────────────────────────────────────────────────────────
990
991/// Action: attempt to fire a projectile at a target.
992///
993/// Reads:
994/// - `"{can_fire_key}"` — bool: is the weapon ready?
995/// - `"{ammo_key}"`     — int: remaining ammo
996///
997/// Writes:
998/// - `"{fire_request_key}"` — bool true (game system consumes this)
999/// - `"{ammo_key}"`         — decremented by 1
1000///
1001/// Succeeds once the fire request is written.  Fails if can_fire is false or
1002/// ammo is zero.
1003pub fn fire_at_target(
1004    name:            &str,
1005    can_fire_key:    &str,
1006    ammo_key:        &str,
1007    fire_request_key: &str,
1008) -> BehaviorNode {
1009    let cfk = can_fire_key.to_string();
1010    let amk = ammo_key.to_string();
1011    let frk = fire_request_key.to_string();
1012
1013    BehaviorNode::Leaf {
1014        name: name.to_string(),
1015        on_enter: None,
1016        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
1017            let can_fire = bb.get_bool(&cfk).unwrap_or(false);
1018            let ammo     = bb.get_int(&amk).unwrap_or(0);
1019            if !can_fire || ammo <= 0 { return NodeStatus::Failure; }
1020            bb.set(frk.as_str(), true);
1021            bb.set(amk.as_str(), ammo - 1);
1022            NodeStatus::Success
1023        }),
1024        on_exit: None,
1025        entered: false,
1026    }
1027}
1028
1029// ── Melee Attack ──────────────────────────────────────────────────────────────
1030
1031/// Action: perform a melee attack if the target is within `melee_range`.
1032///
1033/// Reads:
1034/// - `"{agent_pos_key}"`  — Vec3
1035/// - `"{target_pos_key}"` — Vec3
1036/// - `"{can_attack_key}"` — bool
1037///
1038/// Writes:
1039/// - `"{attack_request_key}"` — bool true
1040pub fn melee_attack(
1041    name:              &str,
1042    agent_pos_key:     &str,
1043    target_pos_key:    &str,
1044    can_attack_key:    &str,
1045    attack_request_key: &str,
1046    melee_range:       f32,
1047) -> BehaviorNode {
1048    let ap  = agent_pos_key.to_string();
1049    let tp  = target_pos_key.to_string();
1050    let cak = can_attack_key.to_string();
1051    let ark = attack_request_key.to_string();
1052
1053    BehaviorNode::Leaf {
1054        name: name.to_string(),
1055        on_enter: None,
1056        on_tick: Box::new(move |bb: &mut Blackboard, _dt: f32| {
1057            if !bb.get_bool(&cak).unwrap_or(false) { return NodeStatus::Failure; }
1058            let a = match bb.get_vec3(&ap) { Some(v) => v, None => return NodeStatus::Failure };
1059            let t = match bb.get_vec3(&tp) { Some(v) => v, None => return NodeStatus::Failure };
1060            if (a - t).length() > melee_range { return NodeStatus::Failure; }
1061            bb.set(ark.as_str(), true);
1062            NodeStatus::Success
1063        }),
1064        on_exit: None,
1065        entered: false,
1066    }
1067}
1068
1069// ── Idle ─────────────────────────────────────────────────────────────────────
1070
1071/// Always-running idle node.  Returns Running indefinitely.
1072pub fn idle(name: &str) -> BehaviorNode {
1073    BehaviorNode::Leaf {
1074        name: name.to_string(),
1075        on_enter: None,
1076        on_tick: Box::new(|_bb: &mut Blackboard, _dt: f32| NodeStatus::Running),
1077        on_exit: None,
1078        entered: false,
1079    }
1080}
1081
1082/// A leaf that always succeeds immediately.
1083pub fn succeed_always(name: &str) -> BehaviorNode {
1084    BehaviorNode::Leaf {
1085        name: name.to_string(),
1086        on_enter: None,
1087        on_tick: Box::new(|_bb: &mut Blackboard, _dt: f32| NodeStatus::Success),
1088        on_exit: None,
1089        entered: false,
1090    }
1091}
1092
1093/// A leaf that always fails immediately.
1094pub fn fail_always(name: &str) -> BehaviorNode {
1095    BehaviorNode::Leaf {
1096        name: name.to_string(),
1097        on_enter: None,
1098        on_tick: Box::new(|_bb: &mut Blackboard, _dt: f32| NodeStatus::Failure),
1099        on_exit: None,
1100        entered: false,
1101    }
1102}
1103
1104// ── Tests ─────────────────────────────────────────────────────────────────────
1105
1106#[cfg(test)]
1107mod tests {
1108    use super::*;
1109    use crate::behavior::tree::{Blackboard, SubtreeRegistry};
1110
1111    fn tick(node: &mut BehaviorNode, bb: &mut Blackboard) -> NodeStatus {
1112        let reg = SubtreeRegistry::new();
1113        node.tick(0.016, bb, &reg)
1114    }
1115
1116    #[test]
1117    fn wait_ticks_until_done() {
1118        let mut bb  = Blackboard::new();
1119        let mut node = wait("w", 0.1);
1120        // Need more than 6 ticks of 0.016s to exceed 0.1s.
1121        for _ in 0..6 {
1122            assert_eq!(tick(&mut node, &mut bb), NodeStatus::Running);
1123        }
1124        // At tick 7 (0.016*7 = 0.112s > 0.1) should succeed.
1125        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Success);
1126    }
1127
1128    #[test]
1129    fn move_to_reaches_target() {
1130        let mut bb = Blackboard::new();
1131        bb.set("pos", Vec3::ZERO);
1132        bb.set("target", Vec3::new(1.0, 0.0, 0.0));
1133        let mut node = move_to("m", "pos", "target", 10.0, 0.1);
1134        // With speed 10 and dt 0.016, should cover 1m in < 10 ticks.
1135        let mut reached = false;
1136        for _ in 0..20 {
1137            let s = tick(&mut node, &mut bb);
1138            if s == NodeStatus::Success { reached = true; break; }
1139        }
1140        assert!(reached);
1141    }
1142
1143    #[test]
1144    fn check_distance_pass() {
1145        let mut bb = Blackboard::new();
1146        bb.set("a", Vec3::ZERO);
1147        bb.set("b", Vec3::new(3.0, 0.0, 0.0));
1148        let mut node = check_distance("cd", "a", "b", 0.0, 5.0);
1149        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Success);
1150    }
1151
1152    #[test]
1153    fn check_distance_fail() {
1154        let mut bb = Blackboard::new();
1155        bb.set("a", Vec3::ZERO);
1156        bb.set("b", Vec3::new(10.0, 0.0, 0.0));
1157        let mut node = check_distance("cd", "a", "b", 0.0, 5.0);
1158        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Failure);
1159    }
1160
1161    #[test]
1162    fn check_health_low_pass() {
1163        let mut bb = Blackboard::new();
1164        bb.set("hp", 20.0f64);
1165        let mut node = check_health_low("h", "hp", 50.0);
1166        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Success);
1167    }
1168
1169    #[test]
1170    fn check_health_low_fail() {
1171        let mut bb = Blackboard::new();
1172        bb.set("hp", 80.0f64);
1173        let mut node = check_health_low("h", "hp", 50.0);
1174        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Failure);
1175    }
1176
1177    #[test]
1178    fn set_blackboard_writes_value() {
1179        let mut bb = Blackboard::new();
1180        let mut node = set_blackboard("s", "flag", true);
1181        tick(&mut node, &mut bb);
1182        assert_eq!(bb.get_bool("flag"), Some(true));
1183    }
1184
1185    #[test]
1186    fn invert_node_works() {
1187        let mut bb = Blackboard::new();
1188        let mut node = invert_node("inv", succeed_always("ok"));
1189        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Failure);
1190    }
1191
1192    #[test]
1193    fn check_bb_bool_true() {
1194        let mut bb = Blackboard::new();
1195        bb.set("ready", true);
1196        let mut node = check_blackboard_bool("c", "ready", true);
1197        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Success);
1198    }
1199
1200    #[test]
1201    fn los_unobstructed() {
1202        let mut bb = Blackboard::new();
1203        bb.set("agent", Vec3::ZERO);
1204        bb.set("target", Vec3::new(5.0, 0.0, 0.0));
1205        bb.set("obstacles", BlackboardValue::List(vec![]));
1206        let mut node = check_line_of_sight("los", "agent", "target", "obstacles", 0.5, 20.0);
1207        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Success);
1208    }
1209
1210    #[test]
1211    fn los_obstructed() {
1212        let mut bb = Blackboard::new();
1213        bb.set("agent", Vec3::ZERO);
1214        bb.set("target", Vec3::new(5.0, 0.0, 0.0));
1215        let obs = vec![BlackboardValue::Vec3(Vec3::new(2.5, 0.0, 0.0))];
1216        bb.set("obstacles", BlackboardValue::List(obs));
1217        let mut node = check_line_of_sight("los", "agent", "target", "obstacles", 0.5, 20.0);
1218        assert_eq!(tick(&mut node, &mut bb), NodeStatus::Failure);
1219    }
1220}