1use 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
42struct 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 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
76pub 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
106pub 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 {
146 return NodeStatus::Success;
147 }
148
149 let dir = delta / dist;
150 let step = (speed * dt).min(dist - arrival_radius);
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 {
155 NodeStatus::Success
156 } else {
157 NodeStatus::Running
158 }
159 }),
160 on_exit: None,
161 entered: false,
162 }
163}
164
165pub 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
210pub 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 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 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
277pub 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
329pub 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
350pub 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
365pub 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
383pub 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
412pub 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
422pub 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
445pub enum CompareOp { Lt, Lte, Gt, Gte, Eq }
451
452pub 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
479pub fn check_health_low(name: &str, health_key: &str, threshold: f64) -> BehaviorNode {
481 check_health(name, health_key, CompareOp::Lt, threshold)
482}
483
484pub fn check_health_ok(name: &str, health_key: &str, threshold: f64) -> BehaviorNode {
486 check_health(name, health_key, CompareOp::Gte, threshold)
487}
488
489pub 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; } let dir = diff / dist;
527
528 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
549fn 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
564pub 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
581pub 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
608pub 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
622pub 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
634pub 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
646pub 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
656pub 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
668pub 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
678pub 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
688pub fn random_selector(name: &str, mut children: Vec<BehaviorNode>) -> BehaviorNode {
699 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 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 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, ®);
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
772pub 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, ®)
813 }),
814 on_exit: None,
815 entered: false,
816 }
817}
818
819pub 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
837pub 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
855pub 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
903pub 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 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 }),
948 on_exit: None,
949 entered: false,
950 }
951}
952
953pub 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
989pub 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
1029pub 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
1069pub 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
1082pub 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
1093pub 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#[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, ®)
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 for _ in 0..6 {
1122 assert_eq!(tick(&mut node, &mut bb), NodeStatus::Running);
1123 }
1124 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 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}