1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6const MAX_BEHAVIOR_TREE_DEPTH: usize = 64;
11const MAX_BLACKBOARD_ENTRIES: usize = 512;
12const GOAP_MAX_PLAN_STEPS: usize = 32;
13const GOAP_MAX_OPEN_NODES: usize = 4096;
14const UTILITY_MAX_CONSIDERATIONS: usize = 16;
15const PERCEPTION_MAX_ENTITIES: usize = 256;
16const FORMATION_MAX_AGENTS: usize = 128;
17const STEERING_MAX_NEIGHBORS: usize = 64;
18const FSM_MAX_STATES: usize = 128;
19const FSM_MAX_TRANSITIONS: usize = 512;
20const EMOTION_DECAY_RATE: f32 = 0.02;
21const EMOTION_INFLUENCE_SCALE: f32 = 0.15;
22const WANDER_CIRCLE_RADIUS: f32 = 1.2;
23const WANDER_CIRCLE_DISTANCE: f32 = 2.0;
24const WANDER_ANGLE_CHANGE: f32 = 0.4;
25const ARRIVE_DECELERATION_RADIUS: f32 = 3.0;
26const SEPARATION_WEIGHT: f32 = 1.5;
27const ALIGNMENT_WEIGHT: f32 = 1.0;
28const COHESION_WEIGHT: f32 = 1.0;
29const LEADER_FOLLOW_DISTANCE: f32 = 2.5;
30const QUEUE_MIN_DIST: f32 = 1.5;
31const PI: f32 = std::f32::consts::PI;
32const TWO_PI: f32 = 2.0 * PI;
33const HALF_PI: f32 = PI / 2.0;
34const SQRT2: f32 = std::f32::consts::SQRT_2;
35const EPSILON: f32 = 1e-6;
36const VISION_NEAR_PLANE: f32 = 0.1;
37const HEARING_MIN_ATTENUATION: f32 = 0.01;
38const SMELL_DIFFUSION_RATE: f32 = 0.005;
39const BT_TICK_RATE_HZ: f32 = 30.0;
40const REINGOLD_NODE_WIDTH: f32 = 120.0;
41const REINGOLD_NODE_HEIGHT: f32 = 60.0;
42const REINGOLD_H_SEPARATION: f32 = 20.0;
43const REINGOLD_V_SEPARATION: f32 = 80.0;
44const PLUTCHIK_EMOTIONS: usize = 8;
45const PLUTCHIK_SECONDARY: usize = 8;
46
47#[derive(Clone, Debug, PartialEq)]
52pub enum BlackboardValue {
53 Bool(bool),
54 Int(i64),
55 Float(f32),
56 Vec2(Vec2),
57 Vec3(Vec3),
58 String(String),
59 EntityId(u64),
60 None,
61}
62
63impl BlackboardValue {
64 pub fn as_bool(&self) -> bool {
65 match self {
66 BlackboardValue::Bool(b) => *b,
67 BlackboardValue::Int(i) => *i != 0,
68 BlackboardValue::Float(f) => *f != 0.0,
69 _ => false,
70 }
71 }
72 pub fn as_float(&self) -> f32 {
73 match self {
74 BlackboardValue::Float(f) => *f,
75 BlackboardValue::Int(i) => *i as f32,
76 BlackboardValue::Bool(b) => if *b { 1.0 } else { 0.0 },
77 _ => 0.0,
78 }
79 }
80 pub fn as_int(&self) -> i64 {
81 match self {
82 BlackboardValue::Int(i) => *i,
83 BlackboardValue::Float(f) => *f as i64,
84 BlackboardValue::Bool(b) => if *b { 1 } else { 0 },
85 _ => 0,
86 }
87 }
88 pub fn as_vec3(&self) -> Vec3 {
89 match self {
90 BlackboardValue::Vec3(v) => *v,
91 BlackboardValue::Vec2(v) => Vec3::new(v.x, v.y, 0.0),
92 _ => Vec3::ZERO,
93 }
94 }
95}
96
97#[derive(Clone, Debug)]
98pub struct Blackboard {
99 pub entries: HashMap<String, BlackboardValue>,
100 pub change_timestamps: HashMap<String, f64>,
101 pub current_time: f64,
102}
103
104impl Blackboard {
105 pub fn new() -> Self {
106 Self {
107 entries: HashMap::with_capacity(64),
108 change_timestamps: HashMap::with_capacity(64),
109 current_time: 0.0,
110 }
111 }
112
113 pub fn set(&mut self, key: &str, value: BlackboardValue) {
114 self.entries.insert(key.to_string(), value);
115 self.change_timestamps.insert(key.to_string(), self.current_time);
116 }
117
118 pub fn get(&self, key: &str) -> &BlackboardValue {
119 self.entries.get(key).unwrap_or(&BlackboardValue::None)
120 }
121
122 pub fn get_bool(&self, key: &str) -> bool {
123 self.get(key).as_bool()
124 }
125
126 pub fn get_float(&self, key: &str) -> f32 {
127 self.get(key).as_float()
128 }
129
130 pub fn get_int(&self, key: &str) -> i64 {
131 self.get(key).as_int()
132 }
133
134 pub fn get_vec3(&self, key: &str) -> Vec3 {
135 self.get(key).as_vec3()
136 }
137
138 pub fn contains(&self, key: &str) -> bool {
139 self.entries.contains_key(key)
140 }
141
142 pub fn remove(&mut self, key: &str) -> Option<BlackboardValue> {
143 self.change_timestamps.remove(key);
144 self.entries.remove(key)
145 }
146
147 pub fn age_of(&self, key: &str) -> f64 {
148 self.change_timestamps.get(key)
149 .map(|t| self.current_time - t)
150 .unwrap_or(f64::MAX)
151 }
152
153 pub fn advance_time(&mut self, dt: f64) {
154 self.current_time += dt;
155 }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub enum BtStatus {
164 Success,
165 Failure,
166 Running,
167 Invalid,
168}
169
170impl BtStatus {
171 pub fn is_terminal(&self) -> bool {
172 matches!(self, BtStatus::Success | BtStatus::Failure)
173 }
174}
175
176#[derive(Clone, Debug)]
181pub enum BtNodeType {
182 Sequence,
184 Selector,
185 ParallelAll, ParallelAny, RandomSelector,
188 RandomSequence,
189 Inverter,
191 Repeater { times: u32 },
192 RepeatForever,
193 RetryUntilSuccess { max_retries: u32 },
194 Timeout { duration: f32 },
195 Cooldown { cooldown: f32 },
196 Succeeder,
197 Failer,
198 UntilFail,
199 UntilSuccess,
200 BlackboardCheck { key: String, op: CompareOp, value: BlackboardValue },
201 BlackboardGuard { key: String },
202 MoveTo { target_key: String, speed: f32, acceptance_radius: f32 },
204 MoveToPosition { position: Vec3, speed: f32, acceptance_radius: f32 },
205 Attack { target_key: String, damage: f32, range: f32 },
206 PlayAnimation { clip: String, layer: u32, blend_time: f32 },
207 SetBlackboard { key: String, value: BlackboardValue },
208 IncrementBlackboard { key: String, amount: f32 },
209 Wait { duration: f32 },
210 WaitBlackboard { key: String },
211 Log { message: String },
212 Idle,
213 FindTarget { radius: f32, faction_key: String, result_key: String },
214 Flee { threat_key: String, speed: f32, distance: f32 },
215 Patrol { waypoints_key: String, speed: f32 },
216 TakeCover { threat_key: String, result_key: String },
217 AlertAllies { radius: f32, message: String },
218 UseItem { item_key: String },
219 PickupItem { item_key: String },
220 DropItem { item_key: String },
221 Interact { target_key: String, interaction_id: String },
222 PlaySound { sound: String, volume: f32 },
223 SpawnEntity { prefab: String, position_key: String },
224 DestroyEntity { target_key: String },
225 SendEvent { event_name: String, payload_key: String },
226 FailAlways,
227 SucceedAlways,
228}
229
230#[derive(Clone, Debug, PartialEq)]
231pub enum CompareOp {
232 Equal,
233 NotEqual,
234 LessThan,
235 LessOrEqual,
236 GreaterThan,
237 GreaterOrEqual,
238 Exists,
239 NotExists,
240}
241
242impl CompareOp {
243 pub fn evaluate(&self, lhs: &BlackboardValue, rhs: &BlackboardValue) -> bool {
244 match self {
245 CompareOp::Exists => !matches!(lhs, BlackboardValue::None),
246 CompareOp::NotExists => matches!(lhs, BlackboardValue::None),
247 CompareOp::Equal => lhs == rhs,
248 CompareOp::NotEqual => lhs != rhs,
249 CompareOp::LessThan => lhs.as_float() < rhs.as_float(),
250 CompareOp::LessOrEqual => lhs.as_float() <= rhs.as_float(),
251 CompareOp::GreaterThan => lhs.as_float() > rhs.as_float(),
252 CompareOp::GreaterOrEqual => lhs.as_float() >= rhs.as_float(),
253 }
254 }
255}
256
257#[derive(Clone, Debug)]
262pub struct BtNode {
263 pub id: u32,
264 pub node_type: BtNodeType,
265 pub children: Vec<u32>,
266 pub parent: Option<u32>,
267 pub status: BtStatus,
268 pub current_child_index: usize,
270 pub repeat_count: u32,
271 pub elapsed_time: f32,
272 pub cooldown_remaining: f32,
273 pub last_run_time: f32,
274 pub position: Vec2,
276 pub size: Vec2,
277 pub is_selected: bool,
278 pub is_collapsed: bool,
279 pub prelim: f32,
281 pub modifier: f32,
282 pub thread: Option<u32>,
283 pub ancestor: Option<u32>,
284 pub number: usize,
285 pub change: f32,
286 pub shift: f32,
287}
288
289impl BtNode {
290 pub fn new(id: u32, node_type: BtNodeType) -> Self {
291 Self {
292 id,
293 node_type,
294 children: Vec::new(),
295 parent: None,
296 status: BtStatus::Invalid,
297 current_child_index: 0,
298 repeat_count: 0,
299 elapsed_time: 0.0,
300 cooldown_remaining: 0.0,
301 last_run_time: 0.0,
302 position: Vec2::ZERO,
303 size: Vec2::new(REINGOLD_NODE_WIDTH, REINGOLD_NODE_HEIGHT),
304 is_selected: false,
305 is_collapsed: false,
306 prelim: 0.0,
307 modifier: 0.0,
308 thread: None,
309 ancestor: None,
310 number: 0,
311 change: 0.0,
312 shift: 0.0,
313 }
314 }
315
316 pub fn display_name(&self) -> &str {
317 match &self.node_type {
318 BtNodeType::Sequence => "Sequence",
319 BtNodeType::Selector => "Selector",
320 BtNodeType::ParallelAll => "Parallel(All)",
321 BtNodeType::ParallelAny => "Parallel(Any)",
322 BtNodeType::RandomSelector => "Random Selector",
323 BtNodeType::RandomSequence => "Random Sequence",
324 BtNodeType::Inverter => "Inverter",
325 BtNodeType::Repeater { .. } => "Repeater",
326 BtNodeType::RepeatForever => "Repeat Forever",
327 BtNodeType::RetryUntilSuccess { .. } => "Retry Until Success",
328 BtNodeType::Timeout { .. } => "Timeout",
329 BtNodeType::Cooldown { .. } => "Cooldown",
330 BtNodeType::Succeeder => "Succeeder",
331 BtNodeType::Failer => "Failer",
332 BtNodeType::UntilFail => "Until Fail",
333 BtNodeType::UntilSuccess => "Until Success",
334 BtNodeType::BlackboardCheck { .. } => "BB Check",
335 BtNodeType::BlackboardGuard { .. } => "BB Guard",
336 BtNodeType::MoveTo { .. } => "Move To",
337 BtNodeType::MoveToPosition { .. } => "Move To Pos",
338 BtNodeType::Attack { .. } => "Attack",
339 BtNodeType::PlayAnimation { .. } => "Play Anim",
340 BtNodeType::SetBlackboard { .. } => "Set BB",
341 BtNodeType::IncrementBlackboard { .. } => "Inc BB",
342 BtNodeType::Wait { .. } => "Wait",
343 BtNodeType::WaitBlackboard { .. } => "Wait BB",
344 BtNodeType::Log { .. } => "Log",
345 BtNodeType::Idle => "Idle",
346 BtNodeType::FindTarget { .. } => "Find Target",
347 BtNodeType::Flee { .. } => "Flee",
348 BtNodeType::Patrol { .. } => "Patrol",
349 BtNodeType::TakeCover { .. } => "Take Cover",
350 BtNodeType::AlertAllies { .. } => "Alert Allies",
351 BtNodeType::UseItem { .. } => "Use Item",
352 BtNodeType::PickupItem { .. } => "Pickup Item",
353 BtNodeType::DropItem { .. } => "Drop Item",
354 BtNodeType::Interact { .. } => "Interact",
355 BtNodeType::PlaySound { .. } => "Play Sound",
356 BtNodeType::SpawnEntity { .. } => "Spawn Entity",
357 BtNodeType::DestroyEntity { .. } => "Destroy Entity",
358 BtNodeType::SendEvent { .. } => "Send Event",
359 BtNodeType::FailAlways => "Fail",
360 BtNodeType::SucceedAlways => "Succeed",
361 }
362 }
363
364 pub fn is_leaf(&self) -> bool {
365 matches!(
366 &self.node_type,
367 BtNodeType::MoveTo { .. }
368 | BtNodeType::MoveToPosition { .. }
369 | BtNodeType::Attack { .. }
370 | BtNodeType::PlayAnimation { .. }
371 | BtNodeType::SetBlackboard { .. }
372 | BtNodeType::IncrementBlackboard { .. }
373 | BtNodeType::Wait { .. }
374 | BtNodeType::WaitBlackboard { .. }
375 | BtNodeType::Log { .. }
376 | BtNodeType::Idle
377 | BtNodeType::FindTarget { .. }
378 | BtNodeType::Flee { .. }
379 | BtNodeType::Patrol { .. }
380 | BtNodeType::TakeCover { .. }
381 | BtNodeType::AlertAllies { .. }
382 | BtNodeType::UseItem { .. }
383 | BtNodeType::PickupItem { .. }
384 | BtNodeType::DropItem { .. }
385 | BtNodeType::Interact { .. }
386 | BtNodeType::PlaySound { .. }
387 | BtNodeType::SpawnEntity { .. }
388 | BtNodeType::DestroyEntity { .. }
389 | BtNodeType::SendEvent { .. }
390 | BtNodeType::FailAlways
391 | BtNodeType::SucceedAlways
392 )
393 }
394
395 pub fn is_composite(&self) -> bool {
396 matches!(
397 &self.node_type,
398 BtNodeType::Sequence
399 | BtNodeType::Selector
400 | BtNodeType::ParallelAll
401 | BtNodeType::ParallelAny
402 | BtNodeType::RandomSelector
403 | BtNodeType::RandomSequence
404 )
405 }
406
407 pub fn is_decorator(&self) -> bool {
408 !self.is_leaf() && !self.is_composite()
409 }
410}
411
412#[derive(Debug)]
417pub struct BtTickContext<'a> {
418 pub blackboard: &'a mut Blackboard,
419 pub delta_time: f32,
420 pub current_time: f32,
421 pub agent_position: Vec3,
422 pub agent_velocity: Vec3,
423 pub agent_id: u64,
424 pub rng_seed: u64,
425 pub debug_log: Vec<String>,
426 pub visited_nodes: Vec<u32>,
427}
428
429impl<'a> BtTickContext<'a> {
430 pub fn new(blackboard: &'a mut Blackboard, dt: f32, current_time: f32, agent_pos: Vec3, agent_id: u64) -> Self {
431 Self {
432 blackboard,
433 delta_time: dt,
434 current_time,
435 agent_position: agent_pos,
436 agent_velocity: Vec3::ZERO,
437 agent_id,
438 rng_seed: 12345 ^ (agent_id * 6364136223846793005),
439 debug_log: Vec::new(),
440 visited_nodes: Vec::new(),
441 }
442 }
443
444 pub fn next_rand_f32(&mut self) -> f32 {
445 self.rng_seed = self.rng_seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
447 let bits = ((self.rng_seed >> 33) as u32) | 0x3F800000;
448 let f = f32::from_bits(bits) - 1.0;
449 f
450 }
451
452 pub fn next_rand_usize(&mut self, n: usize) -> usize {
453 self.rng_seed = self.rng_seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
454 ((self.rng_seed >> 33) as usize) % n.max(1)
455 }
456}
457
458pub struct BehaviorTree {
463 pub nodes: HashMap<u32, BtNode>,
464 pub root_id: Option<u32>,
465 pub next_id: u32,
466 pub name: String,
467 pub tick_count: u64,
468 pub last_status: BtStatus,
469}
470
471impl BehaviorTree {
472 pub fn new(name: &str) -> Self {
473 Self {
474 nodes: HashMap::with_capacity(64),
475 root_id: None,
476 next_id: 1,
477 name: name.to_string(),
478 tick_count: 0,
479 last_status: BtStatus::Invalid,
480 }
481 }
482
483 pub fn add_node(&mut self, node_type: BtNodeType) -> u32 {
484 let id = self.next_id;
485 self.next_id += 1;
486 let node = BtNode::new(id, node_type);
487 self.nodes.insert(id, node);
488 id
489 }
490
491 pub fn set_root(&mut self, id: u32) {
492 self.root_id = Some(id);
493 }
494
495 pub fn add_child(&mut self, parent_id: u32, child_id: u32) {
496 if let Some(parent) = self.nodes.get_mut(&parent_id) {
497 parent.children.push(child_id);
498 }
499 if let Some(child) = self.nodes.get_mut(&child_id) {
500 child.parent = Some(parent_id);
501 }
502 }
503
504 pub fn tick(&mut self, ctx: &mut BtTickContext) -> BtStatus {
505 self.tick_count += 1;
506 let root = match self.root_id {
507 Some(id) => id,
508 None => return BtStatus::Failure,
509 };
510 let status = self.tick_node(root, ctx, 0);
511 self.last_status = status;
512 status
513 }
514
515 fn tick_node(&mut self, node_id: u32, ctx: &mut BtTickContext, depth: usize) -> BtStatus {
516 if depth >= MAX_BEHAVIOR_TREE_DEPTH {
517 return BtStatus::Failure;
518 }
519 ctx.visited_nodes.push(node_id);
520
521 let (node_type, children, mut current_child_index, mut repeat_count, mut elapsed_time, mut cooldown_remaining) = {
523 let node = match self.nodes.get(&node_id) {
524 Some(n) => n,
525 None => return BtStatus::Failure,
526 };
527 (
528 node.node_type.clone(),
529 node.children.clone(),
530 node.current_child_index,
531 node.repeat_count,
532 node.elapsed_time,
533 node.cooldown_remaining,
534 )
535 };
536
537 elapsed_time += ctx.delta_time;
538 cooldown_remaining = (cooldown_remaining - ctx.delta_time).max(0.0);
539
540 let status = match &node_type {
541 BtNodeType::Sequence => {
543 let mut result = BtStatus::Success;
544 let mut new_child_idx = current_child_index;
545 for i in current_child_index..children.len() {
546 let child_id = children[i];
547 let child_status = self.tick_node(child_id, ctx, depth + 1);
548 match child_status {
549 BtStatus::Failure => {
550 result = BtStatus::Failure;
551 new_child_idx = 0;
552 break;
553 }
554 BtStatus::Running => {
555 result = BtStatus::Running;
556 new_child_idx = i;
557 break;
558 }
559 BtStatus::Success => {
560 new_child_idx = i + 1;
561 }
562 BtStatus::Invalid => {
563 result = BtStatus::Failure;
564 new_child_idx = 0;
565 break;
566 }
567 }
568 }
569 if result == BtStatus::Success { new_child_idx = 0; }
570 if let Some(n) = self.nodes.get_mut(&node_id) {
571 n.current_child_index = new_child_idx;
572 n.elapsed_time = elapsed_time;
573 }
574 result
575 }
576
577 BtNodeType::Selector => {
578 let mut result = BtStatus::Failure;
579 let mut new_child_idx = 0usize;
580 for i in 0..children.len() {
581 let child_id = children[i];
582 let child_status = self.tick_node(child_id, ctx, depth + 1);
583 match child_status {
584 BtStatus::Success => {
585 result = BtStatus::Success;
586 new_child_idx = 0;
587 break;
588 }
589 BtStatus::Running => {
590 result = BtStatus::Running;
591 new_child_idx = i;
592 break;
593 }
594 BtStatus::Failure => {}
595 BtStatus::Invalid => {}
596 }
597 }
598 if let Some(n) = self.nodes.get_mut(&node_id) {
599 n.current_child_index = new_child_idx;
600 n.elapsed_time = elapsed_time;
601 }
602 result
603 }
604
605 BtNodeType::ParallelAll => {
606 let mut all_success = true;
607 let mut any_running = false;
608 for &child_id in &children {
609 let child_status = self.tick_node(child_id, ctx, depth + 1);
610 match child_status {
611 BtStatus::Failure => { all_success = false; }
612 BtStatus::Running => { any_running = true; }
613 BtStatus::Success => {}
614 BtStatus::Invalid => { all_success = false; }
615 }
616 }
617 if let Some(n) = self.nodes.get_mut(&node_id) {
618 n.elapsed_time = elapsed_time;
619 }
620 if !all_success { BtStatus::Failure }
621 else if any_running { BtStatus::Running }
622 else { BtStatus::Success }
623 }
624
625 BtNodeType::ParallelAny => {
626 let mut any_success = false;
627 let mut any_running = false;
628 for &child_id in &children {
629 let child_status = self.tick_node(child_id, ctx, depth + 1);
630 match child_status {
631 BtStatus::Success => { any_success = true; }
632 BtStatus::Running => { any_running = true; }
633 _ => {}
634 }
635 }
636 if let Some(n) = self.nodes.get_mut(&node_id) {
637 n.elapsed_time = elapsed_time;
638 }
639 if any_success { BtStatus::Success }
640 else if any_running { BtStatus::Running }
641 else { BtStatus::Failure }
642 }
643
644 BtNodeType::RandomSelector => {
645 if children.is_empty() { return BtStatus::Failure; }
646 let mut indices: Vec<usize> = (0..children.len()).collect();
648 for i in (1..indices.len()).rev() {
649 let j = ctx.next_rand_usize(i + 1);
650 indices.swap(i, j);
651 }
652 let mut result = BtStatus::Failure;
653 for idx in indices {
654 let child_id = children[idx];
655 let s = self.tick_node(child_id, ctx, depth + 1);
656 if s == BtStatus::Success || s == BtStatus::Running {
657 result = s;
658 break;
659 }
660 }
661 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
662 result
663 }
664
665 BtNodeType::RandomSequence => {
666 if children.is_empty() { return BtStatus::Success; }
667 let mut indices: Vec<usize> = (0..children.len()).collect();
668 for i in (1..indices.len()).rev() {
669 let j = ctx.next_rand_usize(i + 1);
670 indices.swap(i, j);
671 }
672 let mut result = BtStatus::Success;
673 for idx in indices {
674 let child_id = children[idx];
675 let s = self.tick_node(child_id, ctx, depth + 1);
676 if s == BtStatus::Failure || s == BtStatus::Running {
677 result = s;
678 break;
679 }
680 }
681 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
682 result
683 }
684
685 BtNodeType::Inverter => {
687 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Failure };
688 let s = self.tick_node(child_id, ctx, depth + 1);
689 let result = match s {
690 BtStatus::Success => BtStatus::Failure,
691 BtStatus::Failure => BtStatus::Success,
692 other => other,
693 };
694 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
695 result
696 }
697
698 BtNodeType::Succeeder => {
699 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Success };
700 self.tick_node(child_id, ctx, depth + 1);
701 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
702 BtStatus::Success
703 }
704
705 BtNodeType::Failer => {
706 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Failure };
707 self.tick_node(child_id, ctx, depth + 1);
708 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
709 BtStatus::Failure
710 }
711
712 BtNodeType::Repeater { times } => {
713 let times = *times;
714 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Success };
715 if repeat_count >= times {
716 if let Some(n) = self.nodes.get_mut(&node_id) { n.repeat_count = 0; n.elapsed_time = elapsed_time; }
717 return BtStatus::Success;
718 }
719 let s = self.tick_node(child_id, ctx, depth + 1);
720 if s.is_terminal() {
721 repeat_count += 1;
722 if repeat_count >= times {
723 if let Some(n) = self.nodes.get_mut(&node_id) { n.repeat_count = 0; n.elapsed_time = elapsed_time; }
724 BtStatus::Success
725 } else {
726 if let Some(n) = self.nodes.get_mut(&node_id) { n.repeat_count = repeat_count; n.elapsed_time = elapsed_time; }
727 BtStatus::Running
728 }
729 } else {
730 if let Some(n) = self.nodes.get_mut(&node_id) { n.repeat_count = repeat_count; n.elapsed_time = elapsed_time; }
731 BtStatus::Running
732 }
733 }
734
735 BtNodeType::RepeatForever => {
736 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Running };
737 self.tick_node(child_id, ctx, depth + 1);
738 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
739 BtStatus::Running
740 }
741
742 BtNodeType::RetryUntilSuccess { max_retries } => {
743 let max = *max_retries;
744 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Failure };
745 let s = self.tick_node(child_id, ctx, depth + 1);
746 match s {
747 BtStatus::Success => {
748 if let Some(n) = self.nodes.get_mut(&node_id) { n.repeat_count = 0; n.elapsed_time = elapsed_time; }
749 BtStatus::Success
750 }
751 BtStatus::Failure => {
752 let new_count = repeat_count + 1;
753 if new_count >= max {
754 if let Some(n) = self.nodes.get_mut(&node_id) { n.repeat_count = 0; n.elapsed_time = elapsed_time; }
755 BtStatus::Failure
756 } else {
757 if let Some(n) = self.nodes.get_mut(&node_id) { n.repeat_count = new_count; n.elapsed_time = elapsed_time; }
758 BtStatus::Running
759 }
760 }
761 other => {
762 if let Some(n) = self.nodes.get_mut(&node_id) { n.repeat_count = repeat_count; n.elapsed_time = elapsed_time; }
763 other
764 }
765 }
766 }
767
768 BtNodeType::Timeout { duration } => {
769 let dur = *duration;
770 if elapsed_time > dur {
771 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
772 return BtStatus::Failure;
773 }
774 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Failure };
775 let s = self.tick_node(child_id, ctx, depth + 1);
776 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
777 s
778 }
779
780 BtNodeType::Cooldown { cooldown } => {
781 let cd = *cooldown;
782 if cooldown_remaining > 0.0 {
783 if let Some(n) = self.nodes.get_mut(&node_id) { n.cooldown_remaining = cooldown_remaining; }
784 return BtStatus::Failure;
785 }
786 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Failure };
787 let s = self.tick_node(child_id, ctx, depth + 1);
788 if s == BtStatus::Success {
789 if let Some(n) = self.nodes.get_mut(&node_id) {
790 n.cooldown_remaining = cd;
791 n.elapsed_time = elapsed_time;
792 }
793 } else {
794 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
795 }
796 s
797 }
798
799 BtNodeType::UntilFail => {
800 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Success };
801 let s = self.tick_node(child_id, ctx, depth + 1);
802 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
803 if s == BtStatus::Failure { BtStatus::Success } else { BtStatus::Running }
804 }
805
806 BtNodeType::UntilSuccess => {
807 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Failure };
808 let s = self.tick_node(child_id, ctx, depth + 1);
809 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
810 if s == BtStatus::Success { BtStatus::Success } else { BtStatus::Running }
811 }
812
813 BtNodeType::BlackboardCheck { key, op, value } => {
814 let key = key.clone();
815 let op = op.clone();
816 let value = value.clone();
817 let bb_val = ctx.blackboard.get(&key).clone();
818 let result = op.evaluate(&bb_val, &value);
819 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
820 if result { BtStatus::Success } else { BtStatus::Failure }
821 }
822
823 BtNodeType::BlackboardGuard { key } => {
824 let key = key.clone();
825 let exists = ctx.blackboard.contains(&key);
826 if !exists {
827 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
828 return BtStatus::Failure;
829 }
830 let child_id = match children.first() { Some(&c) => c, None => return BtStatus::Failure };
831 let s = self.tick_node(child_id, ctx, depth + 1);
832 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
833 s
834 }
835
836 BtNodeType::Wait { duration } => {
838 let dur = *duration;
839 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
840 if elapsed_time >= dur {
841 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
842 BtStatus::Success
843 } else {
844 BtStatus::Running
845 }
846 }
847
848 BtNodeType::WaitBlackboard { key } => {
849 let key = key.clone();
850 let dur = ctx.blackboard.get_float(&key);
851 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
852 if elapsed_time >= dur {
853 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
854 BtStatus::Success
855 } else {
856 BtStatus::Running
857 }
858 }
859
860 BtNodeType::Idle => {
861 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
862 BtStatus::Running
863 }
864
865 BtNodeType::FailAlways => BtStatus::Failure,
866 BtNodeType::SucceedAlways => BtStatus::Success,
867
868 BtNodeType::Log { message } => {
869 ctx.debug_log.push(format!("[BT] {}", message));
870 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
871 BtStatus::Success
872 }
873
874 BtNodeType::SetBlackboard { key, value } => {
875 let key = key.clone();
876 let value = value.clone();
877 ctx.blackboard.set(&key, value);
878 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
879 BtStatus::Success
880 }
881
882 BtNodeType::IncrementBlackboard { key, amount } => {
883 let key = key.clone();
884 let amount = *amount;
885 let current = ctx.blackboard.get_float(&key);
886 ctx.blackboard.set(&key, BlackboardValue::Float(current + amount));
887 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
888 BtStatus::Success
889 }
890
891 BtNodeType::MoveTo { target_key, speed, acceptance_radius } => {
892 let target_key = target_key.clone();
893 let speed = *speed;
894 let acceptance_radius = *acceptance_radius;
895 let target = ctx.blackboard.get_vec3(&target_key);
896 let diff = target - ctx.agent_position;
897 let dist = diff.length();
898 if dist <= acceptance_radius {
899 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
900 BtStatus::Success
901 } else {
902 let move_dist = speed * ctx.delta_time;
903 let dir = diff / dist;
904 let new_pos = ctx.agent_position + dir * move_dist.min(dist);
905 ctx.blackboard.set("agent_position", BlackboardValue::Vec3(new_pos));
906 ctx.agent_position = new_pos;
907 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
908 BtStatus::Running
909 }
910 }
911
912 BtNodeType::MoveToPosition { position, speed, acceptance_radius } => {
913 let target = *position;
914 let speed = *speed;
915 let acceptance_radius = *acceptance_radius;
916 let diff = target - ctx.agent_position;
917 let dist = diff.length();
918 if dist <= acceptance_radius {
919 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
920 BtStatus::Success
921 } else {
922 let dir = diff / dist;
923 let move_dist = speed * ctx.delta_time;
924 ctx.agent_position = ctx.agent_position + dir * move_dist.min(dist);
925 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
926 BtStatus::Running
927 }
928 }
929
930 BtNodeType::Attack { target_key, damage, range } => {
931 let target_key = target_key.clone();
932 let damage = *damage;
933 let range = *range;
934 let target_pos = ctx.blackboard.get_vec3(&target_key);
935 let dist = (target_pos - ctx.agent_position).length();
936 if dist <= range {
937 let key = format!("{}_health", target_key);
939 let current_hp = ctx.blackboard.get_float(&key);
940 ctx.blackboard.set(&key, BlackboardValue::Float(current_hp - damage));
941 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
942 BtStatus::Success
943 } else {
944 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
945 BtStatus::Failure
946 }
947 }
948
949 BtNodeType::PlayAnimation { clip, layer, blend_time } => {
950 ctx.blackboard.set("anim_clip", BlackboardValue::String(clip.clone()));
951 ctx.blackboard.set("anim_layer", BlackboardValue::Int(*layer as i64));
952 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
953 BtStatus::Success
954 }
955
956 BtNodeType::FindTarget { radius, faction_key, result_key } => {
957 let radius = *radius;
958 let result_key = result_key.clone();
959 let faction_key = faction_key.clone();
960 let nearest_key = format!("nearest_enemy_{}", faction_key);
962 let found = ctx.blackboard.contains(&nearest_key);
963 if found {
964 let val = ctx.blackboard.get(&nearest_key).clone();
965 ctx.blackboard.set(&result_key, val);
966 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
967 BtStatus::Success
968 } else {
969 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
970 BtStatus::Failure
971 }
972 }
973
974 BtNodeType::Flee { threat_key, speed, distance } => {
975 let threat_key = threat_key.clone();
976 let speed = *speed;
977 let distance = *distance;
978 let threat_pos = ctx.blackboard.get_vec3(&threat_key);
979 let diff = ctx.agent_position - threat_pos;
980 let dist = diff.length();
981 if dist >= distance {
982 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
983 BtStatus::Success
984 } else {
985 let dir = if dist > EPSILON { diff / dist } else { Vec3::X };
986 ctx.agent_position = ctx.agent_position + dir * speed * ctx.delta_time;
987 ctx.blackboard.set("agent_position", BlackboardValue::Vec3(ctx.agent_position));
988 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
989 BtStatus::Running
990 }
991 }
992
993 BtNodeType::Patrol { waypoints_key, speed } => {
994 let waypoints_key = waypoints_key.clone();
995 let speed = *speed;
996 let wp_index_key = format!("{}_index", waypoints_key);
998 let mut wp_idx = ctx.blackboard.get_int(&wp_index_key) as usize;
999 let wp_pos_key = format!("{}_{}", waypoints_key, wp_idx);
1000 if !ctx.blackboard.contains(&wp_pos_key) {
1001 ctx.blackboard.set(&wp_index_key, BlackboardValue::Int(0));
1002 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
1003 return BtStatus::Running;
1004 }
1005 let target = ctx.blackboard.get_vec3(&wp_pos_key);
1006 let diff = target - ctx.agent_position;
1007 let dist = diff.length();
1008 if dist < 0.5 {
1009 let next_key = format!("{}_{}", waypoints_key, wp_idx + 1);
1010 if ctx.blackboard.contains(&next_key) {
1011 ctx.blackboard.set(&wp_index_key, BlackboardValue::Int((wp_idx + 1) as i64));
1012 } else {
1013 ctx.blackboard.set(&wp_index_key, BlackboardValue::Int(0));
1014 }
1015 } else {
1016 let dir = diff / dist;
1017 ctx.agent_position = ctx.agent_position + dir * speed * ctx.delta_time;
1018 ctx.blackboard.set("agent_position", BlackboardValue::Vec3(ctx.agent_position));
1019 }
1020 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = elapsed_time; }
1021 BtStatus::Running
1022 }
1023
1024 BtNodeType::TakeCover { threat_key, result_key } => {
1025 let threat_key = threat_key.clone();
1027 let result_key = result_key.clone();
1028 let threat_pos = ctx.blackboard.get_vec3(&threat_key);
1029 let to_threat = (threat_pos - ctx.agent_position).normalize_or_zero();
1030 let perp = Vec3::new(-to_threat.z, 0.0, to_threat.x);
1031 let cover_pos = ctx.agent_position + perp * 5.0;
1032 ctx.blackboard.set(&result_key, BlackboardValue::Vec3(cover_pos));
1033 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1034 BtStatus::Success
1035 }
1036
1037 BtNodeType::AlertAllies { radius, message } => {
1038 ctx.blackboard.set("alert_issued", BlackboardValue::Bool(true));
1039 ctx.blackboard.set("alert_message", BlackboardValue::String(message.clone()));
1040 ctx.blackboard.set("alert_radius", BlackboardValue::Float(*radius));
1041 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1042 BtStatus::Success
1043 }
1044
1045 BtNodeType::UseItem { item_key } => {
1046 let item_key = item_key.clone();
1047 if ctx.blackboard.contains(&item_key) {
1048 ctx.blackboard.remove(&item_key);
1049 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1050 BtStatus::Success
1051 } else {
1052 BtStatus::Failure
1053 }
1054 }
1055
1056 BtNodeType::PickupItem { item_key } => {
1057 let item_key = item_key.clone();
1058 let pos_key = format!("{}_pos", item_key);
1059 if !ctx.blackboard.contains(&pos_key) {
1060 return BtStatus::Failure;
1061 }
1062 let item_pos = ctx.blackboard.get_vec3(&pos_key);
1063 let dist = (item_pos - ctx.agent_position).length();
1064 if dist < 1.5 {
1065 ctx.blackboard.set(&item_key, BlackboardValue::Bool(true));
1066 ctx.blackboard.remove(&pos_key);
1067 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1068 BtStatus::Success
1069 } else {
1070 BtStatus::Failure
1071 }
1072 }
1073
1074 BtNodeType::DropItem { item_key } => {
1075 let item_key = item_key.clone();
1076 let drop_pos_key = format!("{}_pos", item_key);
1077 ctx.blackboard.set(&drop_pos_key, BlackboardValue::Vec3(ctx.agent_position));
1078 ctx.blackboard.remove(&item_key);
1079 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1080 BtStatus::Success
1081 }
1082
1083 BtNodeType::Interact { target_key, interaction_id } => {
1084 let result_key = format!("interact_result_{}", interaction_id);
1085 ctx.blackboard.set(&result_key, BlackboardValue::Bool(true));
1086 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1087 BtStatus::Success
1088 }
1089
1090 BtNodeType::PlaySound { sound, volume } => {
1091 ctx.blackboard.set("sound_playing", BlackboardValue::String(sound.clone()));
1092 ctx.blackboard.set("sound_volume", BlackboardValue::Float(*volume));
1093 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1094 BtStatus::Success
1095 }
1096
1097 BtNodeType::SpawnEntity { prefab, position_key } => {
1098 let pos_key = position_key.clone();
1099 let prefab = prefab.clone();
1100 let spawn_pos = ctx.blackboard.get_vec3(&pos_key);
1101 ctx.blackboard.set("last_spawned_prefab", BlackboardValue::String(prefab));
1102 ctx.blackboard.set("last_spawned_pos", BlackboardValue::Vec3(spawn_pos));
1103 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1104 BtStatus::Success
1105 }
1106
1107 BtNodeType::DestroyEntity { target_key } => {
1108 let key = target_key.clone();
1109 ctx.blackboard.set(&format!("{}_destroyed", key), BlackboardValue::Bool(true));
1110 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1111 BtStatus::Success
1112 }
1113
1114 BtNodeType::SendEvent { event_name, payload_key } => {
1115 ctx.blackboard.set("pending_event", BlackboardValue::String(event_name.clone()));
1116 if let Some(n) = self.nodes.get_mut(&node_id) { n.elapsed_time = 0.0; }
1117 BtStatus::Success
1118 }
1119 };
1120
1121 if let Some(n) = self.nodes.get_mut(&node_id) {
1122 n.status = status;
1123 }
1124 status
1125 }
1126
1127 pub fn reset(&mut self) {
1128 for node in self.nodes.values_mut() {
1129 node.status = BtStatus::Invalid;
1130 node.current_child_index = 0;
1131 node.repeat_count = 0;
1132 node.elapsed_time = 0.0;
1133 }
1134 }
1135}
1136
1137pub struct ReingoldTilford {
1142 pub contours: HashMap<u32, f32>,
1143}
1144
1145impl ReingoldTilford {
1146 pub fn new() -> Self {
1147 Self { contours: HashMap::new() }
1148 }
1149
1150 pub fn layout(&mut self, tree: &mut BehaviorTree) {
1151 if let Some(root_id) = tree.root_id {
1152 let depth = 0;
1154 let siblings_count = 1;
1155 self.first_walk(tree, root_id, 0, 0);
1156 let root_prelim = tree.nodes.get(&root_id).map(|n| n.prelim).unwrap_or(0.0);
1158 self.second_walk(tree, root_id, -root_prelim, 0);
1159 }
1160 }
1161
1162 fn first_walk(&mut self, tree: &mut BehaviorTree, node_id: u32, sibling_index: usize, depth: usize) {
1163 if depth >= MAX_BEHAVIOR_TREE_DEPTH { return; }
1164
1165 let children = tree.nodes.get(&node_id).map(|n| n.children.clone()).unwrap_or_default();
1166
1167 if children.is_empty() {
1168 let prelim = if sibling_index == 0 {
1170 0.0
1171 } else {
1172 let parent_id = tree.nodes.get(&node_id).and_then(|n| n.parent);
1174 if let Some(pid) = parent_id {
1175 let siblings = tree.nodes.get(&pid).map(|n| n.children.clone()).unwrap_or_default();
1176 if sibling_index > 0 {
1177 let prev_id = siblings[sibling_index - 1];
1178 let prev_prelim = tree.nodes.get(&prev_id).map(|n| n.prelim).unwrap_or(0.0);
1179 prev_prelim + REINGOLD_NODE_WIDTH + REINGOLD_H_SEPARATION
1180 } else {
1181 0.0
1182 }
1183 } else {
1184 0.0
1185 }
1186 };
1187 if let Some(n) = tree.nodes.get_mut(&node_id) {
1188 n.prelim = prelim;
1189 n.modifier = 0.0;
1190 n.number = sibling_index;
1191 }
1192 } else {
1193 for (i, &child_id) in children.iter().enumerate() {
1195 self.first_walk(tree, child_id, i, depth + 1);
1196 }
1197
1198 let children2 = tree.nodes.get(&node_id).map(|n| n.children.clone()).unwrap_or_default();
1200 self.apportion(tree, node_id);
1201
1202 let first_child = children2[0];
1204 let last_child = children2[children2.len() - 1];
1205 let fc_prelim = tree.nodes.get(&first_child).map(|n| n.prelim).unwrap_or(0.0);
1206 let lc_prelim = tree.nodes.get(&last_child).map(|n| n.prelim).unwrap_or(0.0);
1207 let mid_point = (fc_prelim + lc_prelim) / 2.0;
1208
1209 let parent_id = tree.nodes.get(&node_id).and_then(|n| n.parent);
1210
1211 if sibling_index == 0 {
1212 if let Some(n) = tree.nodes.get_mut(&node_id) {
1213 n.prelim = mid_point;
1214 n.modifier = 0.0;
1215 n.number = sibling_index;
1216 }
1217 } else {
1218 if let Some(pid) = parent_id {
1219 let siblings = tree.nodes.get(&pid).map(|n| n.children.clone()).unwrap_or_default();
1220 if sibling_index > 0 {
1221 let prev_id = siblings[sibling_index - 1];
1222 let prev_prelim = tree.nodes.get(&prev_id).map(|n| n.prelim).unwrap_or(0.0);
1223 let prelim = prev_prelim + REINGOLD_NODE_WIDTH + REINGOLD_H_SEPARATION;
1224 let modifier = prelim - mid_point;
1225 if let Some(n) = tree.nodes.get_mut(&node_id) {
1226 n.prelim = prelim;
1227 n.modifier = modifier;
1228 n.number = sibling_index;
1229 }
1230 }
1231 }
1232 }
1233 }
1234 }
1235
1236 fn apportion(&mut self, tree: &mut BehaviorTree, node_id: u32) {
1237 let children = tree.nodes.get(&node_id).map(|n| n.children.clone()).unwrap_or_default();
1238 if children.len() < 2 { return; }
1239
1240 for i in 1..children.len() {
1241 let child_id = children[i];
1242 let prev_id = children[i - 1];
1243 let child_prelim = tree.nodes.get(&child_id).map(|n| n.prelim).unwrap_or(0.0);
1244 let prev_prelim = tree.nodes.get(&prev_id).map(|n| n.prelim).unwrap_or(0.0);
1245 let gap = child_prelim - prev_prelim - (REINGOLD_NODE_WIDTH + REINGOLD_H_SEPARATION);
1246 if gap < 0.0 {
1247 self.shift_subtree(tree, child_id, -gap);
1249 }
1250 }
1251 }
1252
1253 fn shift_subtree(&mut self, tree: &mut BehaviorTree, node_id: u32, shift: f32) {
1254 if let Some(n) = tree.nodes.get_mut(&node_id) {
1255 n.prelim += shift;
1256 n.modifier += shift;
1257 }
1258 let children = tree.nodes.get(&node_id).map(|n| n.children.clone()).unwrap_or_default();
1259 for child_id in children {
1260 self.shift_subtree(tree, child_id, shift);
1261 }
1262 }
1263
1264 fn second_walk(&mut self, tree: &mut BehaviorTree, node_id: u32, mod_sum: f32, depth: usize) {
1265 if depth >= MAX_BEHAVIOR_TREE_DEPTH { return; }
1266 let (prelim, modifier, children) = {
1267 let n = match tree.nodes.get(&node_id) { Some(n) => n, None => return };
1268 (n.prelim, n.modifier, n.children.clone())
1269 };
1270 let x = prelim + mod_sum;
1271 let y = depth as f32 * (REINGOLD_NODE_HEIGHT + REINGOLD_V_SEPARATION);
1272 if let Some(n) = tree.nodes.get_mut(&node_id) {
1273 n.position = Vec2::new(x, y);
1274 }
1275 for child_id in children {
1276 self.second_walk(tree, child_id, mod_sum + modifier, depth + 1);
1277 }
1278 }
1279}
1280
1281pub type WorldState = u64; #[derive(Clone, Debug)]
1288pub struct GoapAction {
1289 pub id: u32,
1290 pub name: String,
1291 pub preconditions: WorldState, pub preconditions_false: WorldState, pub effects_set: WorldState, pub effects_clear: WorldState, pub cost: f32,
1296 pub duration: f32,
1297 pub cooldown: f32,
1298 pub last_used: f32,
1299}
1300
1301impl GoapAction {
1302 pub fn new(id: u32, name: &str) -> Self {
1303 Self {
1304 id,
1305 name: name.to_string(),
1306 preconditions: 0,
1307 preconditions_false: 0,
1308 effects_set: 0,
1309 effects_clear: 0,
1310 cost: 1.0,
1311 duration: 1.0,
1312 cooldown: 0.0,
1313 last_used: -999.0,
1314 }
1315 }
1316
1317 pub fn can_execute(&self, world: WorldState, current_time: f32) -> bool {
1318 let prec_met = (world & self.preconditions) == self.preconditions;
1319 let false_prec_met = (world & self.preconditions_false) == 0;
1320 let cd_ok = (current_time - self.last_used) >= self.cooldown;
1321 prec_met && false_prec_met && cd_ok
1322 }
1323
1324 pub fn apply(&self, world: WorldState) -> WorldState {
1325 (world | self.effects_set) & !self.effects_clear
1326 }
1327}
1328
1329#[derive(Clone, Debug)]
1334struct GoapNode {
1335 pub world_state: WorldState,
1336 pub g: f32,
1337 pub h: f32,
1338 pub action_index: Option<usize>,
1339 pub parent_index: Option<usize>,
1340}
1341
1342impl GoapNode {
1343 pub fn f(&self) -> f32 { self.g + self.h }
1344}
1345
1346pub struct GoapPlanner {
1347 pub actions: Vec<GoapAction>,
1348 pub world_state_labels: HashMap<u8, String>,
1349}
1350
1351impl GoapPlanner {
1352 pub fn new() -> Self {
1353 Self {
1354 actions: Vec::new(),
1355 world_state_labels: HashMap::new(),
1356 }
1357 }
1358
1359 pub fn add_action(&mut self, action: GoapAction) {
1360 self.actions.push(action);
1361 }
1362
1363 pub fn label_bit(&mut self, bit: u8, label: &str) {
1364 self.world_state_labels.insert(bit, label.to_string());
1365 }
1366
1367 fn heuristic(state: WorldState, goal: WorldState) -> f32 {
1369 let unsatisfied = goal & !state;
1370 unsatisfied.count_ones() as f32
1371 }
1372
1373 pub fn plan(
1374 &self,
1375 start: WorldState,
1376 goal: WorldState,
1377 current_time: f32,
1378 ) -> Option<Vec<usize>> {
1379 let mut open: Vec<GoapNode> = Vec::with_capacity(64);
1381 let mut closed: Vec<GoapNode> = Vec::with_capacity(64);
1382
1383 let h0 = Self::heuristic(start, goal);
1384 open.push(GoapNode {
1385 world_state: start,
1386 g: 0.0,
1387 h: h0,
1388 action_index: None,
1389 parent_index: None,
1390 });
1391
1392 let mut iterations = 0;
1393 while !open.is_empty() && iterations < GOAP_MAX_OPEN_NODES {
1394 iterations += 1;
1395
1396 let mut best_idx = 0;
1398 for i in 1..open.len() {
1399 if open[i].f() < open[best_idx].f() {
1400 best_idx = i;
1401 }
1402 }
1403 let current = open.remove(best_idx);
1404
1405 if (current.world_state & goal) == goal {
1407 let mut plan: Vec<usize> = Vec::new();
1409 let mut node = &closed[closed.len() - 1]; closed.push(current.clone());
1412 let mut idx = closed.len() - 1;
1413 loop {
1414 if let Some(action_idx) = closed[idx].action_index {
1415 plan.push(action_idx);
1416 }
1417 if let Some(parent_idx) = closed[idx].parent_index {
1418 idx = parent_idx;
1419 } else {
1420 break;
1421 }
1422 }
1423 plan.reverse();
1424 return Some(plan);
1425 }
1426
1427 let current_idx = closed.len();
1428 closed.push(current.clone());
1429
1430 if closed.len() > GOAP_MAX_PLAN_STEPS * 10 { break; }
1431
1432 for (action_idx, action) in self.actions.iter().enumerate() {
1434 if !action.can_execute(current.world_state, current_time) { continue; }
1435 let new_state = action.apply(current.world_state);
1436 let in_closed = closed.iter().any(|n| n.world_state == new_state);
1438 if in_closed { continue; }
1439
1440 let new_g = current.g + action.cost;
1441 let new_h = Self::heuristic(new_state, goal);
1442
1443 let existing = open.iter().enumerate().find(|(_, n)| n.world_state == new_state);
1445 if let Some((oi, existing_node)) = existing {
1446 if new_g < existing_node.g {
1447 open[oi].g = new_g;
1448 open[oi].action_index = Some(action_idx);
1449 open[oi].parent_index = Some(current_idx);
1450 }
1451 } else {
1452 open.push(GoapNode {
1453 world_state: new_state,
1454 g: new_g,
1455 h: new_h,
1456 action_index: Some(action_idx),
1457 parent_index: Some(current_idx),
1458 });
1459 }
1460 }
1461 }
1462 None
1463 }
1464
1465 pub fn world_state_description(&self, state: WorldState) -> String {
1466 let mut parts = Vec::new();
1467 for bit in 0..64u8 {
1468 if (state >> bit) & 1 == 1 {
1469 if let Some(label) = self.world_state_labels.get(&bit) {
1470 parts.push(label.clone());
1471 } else {
1472 parts.push(format!("bit{}", bit));
1473 }
1474 }
1475 }
1476 parts.join(", ")
1477 }
1478}
1479
1480#[derive(Clone, Debug)]
1485pub enum ResponseCurve {
1486 Linear { slope: f32, intercept: f32 },
1487 Exponential { base: f32, exponent: f32, scale: f32 },
1488 Logistic { steepness: f32, midpoint: f32 },
1489 Sine { frequency: f32, phase: f32, amplitude: f32, offset: f32 },
1490 Polynomial { coefficients: Vec<f32> },
1491 Inverse { scale: f32 },
1492 Step { threshold: f32, low: f32, high: f32 },
1493 Smoothstep { edge0: f32, edge1: f32 },
1494 Bell { center: f32, width: f32 },
1495 Constant { value: f32 },
1496}
1497
1498impl ResponseCurve {
1499 pub fn evaluate(&self, x: f32) -> f32 {
1500 let x = x.clamp(0.0, 1.0);
1501 match self {
1502 ResponseCurve::Linear { slope, intercept } => {
1503 (slope * x + intercept).clamp(0.0, 1.0)
1504 }
1505 ResponseCurve::Exponential { base, exponent, scale } => {
1506 let v = base.powf(x * exponent) * scale;
1507 v.clamp(0.0, 1.0)
1508 }
1509 ResponseCurve::Logistic { steepness, midpoint } => {
1510 let e = std::f32::consts::E;
1511 let v = 1.0 / (1.0 + e.powf(-steepness * (x - midpoint)));
1512 v.clamp(0.0, 1.0)
1513 }
1514 ResponseCurve::Sine { frequency, phase, amplitude, offset } => {
1515 let v = amplitude * (frequency * x * TWO_PI + phase).sin() + offset;
1516 v.clamp(0.0, 1.0)
1517 }
1518 ResponseCurve::Polynomial { coefficients } => {
1519 let mut result = 0.0f32;
1521 for &c in coefficients.iter().rev() {
1522 result = result * x + c;
1523 }
1524 result.clamp(0.0, 1.0)
1525 }
1526 ResponseCurve::Inverse { scale } => {
1527 if x.abs() < EPSILON { 1.0 }
1528 else { (scale / x).clamp(0.0, 1.0) }
1529 }
1530 ResponseCurve::Step { threshold, low, high } => {
1531 if x >= *threshold { *high } else { *low }
1532 }
1533 ResponseCurve::Smoothstep { edge0, edge1 } => {
1534 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
1535 (t * t * (3.0 - 2.0 * t)).clamp(0.0, 1.0)
1536 }
1537 ResponseCurve::Bell { center, width } => {
1538 let d = (x - center) / (width + EPSILON);
1539 let v = (-d * d * 2.0).exp();
1540 v.clamp(0.0, 1.0)
1541 }
1542 ResponseCurve::Constant { value } => value.clamp(0.0, 1.0),
1543 }
1544 }
1545
1546 pub fn sample_points(&self, n: usize) -> Vec<Vec2> {
1547 (0..n).map(|i| {
1548 let x = i as f32 / (n - 1).max(1) as f32;
1549 Vec2::new(x, self.evaluate(x))
1550 }).collect()
1551 }
1552}
1553
1554#[derive(Clone, Debug)]
1559pub struct Consideration {
1560 pub name: String,
1561 pub input_key: String, pub input_min: f32,
1563 pub input_max: f32,
1564 pub curve: ResponseCurve,
1565 pub weight: f32,
1566}
1567
1568impl Consideration {
1569 pub fn new(name: &str, input_key: &str, curve: ResponseCurve) -> Self {
1570 Self {
1571 name: name.to_string(),
1572 input_key: input_key.to_string(),
1573 input_min: 0.0,
1574 input_max: 1.0,
1575 curve,
1576 weight: 1.0,
1577 }
1578 }
1579
1580 pub fn evaluate(&self, blackboard: &Blackboard) -> f32 {
1581 let raw = blackboard.get_float(&self.input_key);
1582 let range = self.input_max - self.input_min;
1583 let normalized = if range.abs() > EPSILON {
1584 ((raw - self.input_min) / range).clamp(0.0, 1.0)
1585 } else {
1586 0.0
1587 };
1588 self.curve.evaluate(normalized) * self.weight
1589 }
1590}
1591
1592#[derive(Clone, Debug)]
1597pub struct UtilityAction {
1598 pub id: u32,
1599 pub name: String,
1600 pub considerations: Vec<Consideration>,
1601 pub bonus_score: f32,
1602 pub cooldown: f32,
1603 pub last_selected_time: f32,
1604 pub momentum: f32, pub is_active: bool,
1606}
1607
1608impl UtilityAction {
1609 pub fn new(id: u32, name: &str) -> Self {
1610 Self {
1611 id,
1612 name: name.to_string(),
1613 considerations: Vec::new(),
1614 bonus_score: 0.0,
1615 cooldown: 0.0,
1616 last_selected_time: -999.0,
1617 momentum: 0.0,
1618 is_active: false,
1619 }
1620 }
1621
1622 pub fn score(&self, blackboard: &Blackboard, current_time: f32) -> f32 {
1623 if (current_time - self.last_selected_time) < self.cooldown {
1624 return 0.0;
1625 }
1626 if self.considerations.is_empty() {
1627 return self.bonus_score;
1628 }
1629 let n = self.considerations.len() as f32;
1631 let mut product = 1.0f32;
1632 for c in &self.considerations {
1633 let v = c.evaluate(blackboard);
1634 product *= v;
1635 }
1636 let avg = product.powf(1.0 / n);
1638 let modification_factor = 1.0 - (1.0 / n);
1640 let final_score = avg + (avg * modification_factor * (1.0 - avg));
1641 (final_score + self.bonus_score + if self.is_active { self.momentum } else { 0.0 }).clamp(0.0, 1.0)
1642 }
1643}
1644
1645pub struct UtilityDecisionMaker {
1650 pub actions: Vec<UtilityAction>,
1651 pub selected_action_id: Option<u32>,
1652 pub selection_history: VecDeque<(u32, f32)>, pub evaluation_frequency: f32,
1654 pub last_evaluation: f32,
1655 pub score_threshold: f32,
1656}
1657
1658impl UtilityDecisionMaker {
1659 pub fn new() -> Self {
1660 Self {
1661 actions: Vec::new(),
1662 selected_action_id: None,
1663 selection_history: VecDeque::with_capacity(32),
1664 evaluation_frequency: 0.1,
1665 last_evaluation: 0.0,
1666 score_threshold: 0.05,
1667 }
1668 }
1669
1670 pub fn add_action(&mut self, action: UtilityAction) {
1671 self.actions.push(action);
1672 }
1673
1674 pub fn evaluate(&mut self, blackboard: &Blackboard, current_time: f32) -> Option<u32> {
1675 if current_time - self.last_evaluation < self.evaluation_frequency {
1676 return self.selected_action_id;
1677 }
1678 self.last_evaluation = current_time;
1679
1680 let mut best_id = None;
1681 let mut best_score = self.score_threshold;
1682
1683 for action in &self.actions {
1684 let score = action.score(blackboard, current_time);
1685 if score > best_score {
1686 best_score = score;
1687 best_id = Some(action.id);
1688 }
1689 }
1690
1691 for action in &mut self.actions {
1693 action.is_active = Some(action.id) == best_id;
1694 }
1695
1696 if let Some(id) = best_id {
1697 if Some(id) != self.selected_action_id {
1698 if let Some(a) = self.actions.iter_mut().find(|a| a.id == id) {
1699 a.last_selected_time = current_time;
1700 }
1701 self.selection_history.push_back((id, current_time));
1702 if self.selection_history.len() > 32 {
1703 self.selection_history.pop_front();
1704 }
1705 self.selected_action_id = Some(id);
1706 }
1707 }
1708 self.selected_action_id
1709 }
1710}
1711
1712#[derive(Clone, Debug)]
1717pub struct PerceivedEntity {
1718 pub entity_id: u64,
1719 pub position: Vec3,
1720 pub velocity: Vec3,
1721 pub last_seen_time: f32,
1722 pub last_known_position: Vec3,
1723 pub confidence: f32, pub threat_level: f32,
1725 pub is_visible: bool,
1726 pub is_heard: bool,
1727 pub is_smelled: bool,
1728}
1729
1730impl PerceivedEntity {
1731 pub fn new(entity_id: u64, position: Vec3) -> Self {
1732 Self {
1733 entity_id,
1734 position,
1735 velocity: Vec3::ZERO,
1736 last_seen_time: 0.0,
1737 last_known_position: position,
1738 confidence: 1.0,
1739 threat_level: 0.0,
1740 is_visible: false,
1741 is_heard: false,
1742 is_smelled: false,
1743 }
1744 }
1745
1746 pub fn update_position(&mut self, pos: Vec3, vel: Vec3, time: f32) {
1747 self.position = pos;
1748 self.velocity = vel;
1749 self.last_seen_time = time;
1750 self.last_known_position = pos;
1751 self.confidence = 1.0;
1752 }
1753
1754 pub fn decay_confidence(&mut self, dt: f32, decay_rate: f32) {
1755 self.confidence = (self.confidence - decay_rate * dt).max(0.0);
1756 self.last_known_position = self.last_known_position + self.velocity * dt;
1758 self.velocity *= (1.0 - dt * 0.5).max(0.0);
1760 }
1761}
1762
1763#[derive(Clone, Debug)]
1764pub struct VisionConfig {
1765 pub range: f32,
1766 pub half_angle: f32, pub near_range: f32, pub darkness_penalty: f32, pub moving_target_bonus: f32,
1770}
1771
1772impl Default for VisionConfig {
1773 fn default() -> Self {
1774 Self {
1775 range: 20.0,
1776 half_angle: PI / 3.0, near_range: 1.5,
1778 darkness_penalty: 1.0,
1779 moving_target_bonus: 0.2,
1780 }
1781 }
1782}
1783
1784#[derive(Clone, Debug)]
1785pub struct HearingConfig {
1786 pub base_radius: f32,
1787 pub frequency_response: f32, pub noise_floor: f32,
1789}
1790
1791impl Default for HearingConfig {
1792 fn default() -> Self {
1793 Self {
1794 base_radius: 15.0,
1795 frequency_response: 1.0,
1796 noise_floor: 0.1,
1797 }
1798 }
1799}
1800
1801#[derive(Clone, Debug)]
1802pub struct SmellConfig {
1803 pub base_radius: f32,
1804 pub wind_direction: Vec3,
1805 pub wind_speed: f32,
1806 pub min_intensity: f32,
1807}
1808
1809impl Default for SmellConfig {
1810 fn default() -> Self {
1811 Self {
1812 base_radius: 8.0,
1813 wind_direction: Vec3::new(1.0, 0.0, 0.0),
1814 wind_speed: 1.0,
1815 min_intensity: 0.05,
1816 }
1817 }
1818}
1819
1820pub struct PerceptionSystem {
1821 pub vision: VisionConfig,
1822 pub hearing: HearingConfig,
1823 pub smell: SmellConfig,
1824 pub perceived: HashMap<u64, PerceivedEntity>,
1825 pub confidence_decay: f32,
1826 pub forget_threshold: f32,
1827 pub observer_id: u64,
1828}
1829
1830impl PerceptionSystem {
1831 pub fn new(observer_id: u64) -> Self {
1832 Self {
1833 vision: VisionConfig::default(),
1834 hearing: HearingConfig::default(),
1835 smell: SmellConfig::default(),
1836 perceived: HashMap::new(),
1837 confidence_decay: 0.1,
1838 forget_threshold: 0.05,
1839 observer_id,
1840 }
1841 }
1842
1843 pub fn can_see(
1845 &self,
1846 observer_pos: Vec3,
1847 observer_forward: Vec3,
1848 target_pos: Vec3,
1849 target_velocity: Vec3,
1850 obstacles: &[Aabb],
1851 ) -> (bool, f32) {
1852 let to_target = target_pos - observer_pos;
1853 let dist = to_target.length();
1854
1855 if dist < VISION_NEAR_PLANE { return (true, 1.0); }
1856
1857 if dist <= self.vision.near_range { return (true, 1.0); }
1859
1860 if dist > self.vision.range { return (false, 0.0); }
1861
1862 let to_target_norm = to_target / dist;
1864 let fwd = observer_forward.normalize_or_zero();
1865 let dot = fwd.dot(to_target_norm);
1866 let angle = dot.clamp(-1.0, 1.0).acos();
1867
1868 if angle > self.vision.half_angle { return (false, 0.0); }
1869
1870 let range_start = self.vision.range * 0.3;
1872 let dist_factor = if dist < range_start { 1.0 }
1873 else { 1.0 - (dist - range_start) / (self.vision.range - range_start) };
1874
1875 let angle_factor = 1.0 - (angle / self.vision.half_angle);
1877
1878 let vel_factor = 1.0 + (target_velocity.length().min(5.0) / 5.0) * self.vision.moving_target_bonus;
1880
1881 let light_factor = self.vision.darkness_penalty;
1883
1884 let occluded = self.raycast_occluded(observer_pos, target_pos, obstacles);
1886 if occluded { return (false, 0.0); }
1887
1888 let confidence = (dist_factor * angle_factor * vel_factor * light_factor).clamp(0.0, 1.0);
1889 (confidence > 0.1, confidence)
1890 }
1891
1892 fn raycast_occluded(&self, from: Vec3, to: Vec3, obstacles: &[Aabb]) -> bool {
1894 let dir = to - from;
1895 let len = dir.length();
1896 if len < EPSILON { return false; }
1897 let inv_dir = Vec3::new(1.0 / dir.x, 1.0 / dir.y, 1.0 / dir.z);
1898
1899 for obs in obstacles {
1900 if obs.ray_intersects(from, inv_dir, len) {
1901 return true;
1902 }
1903 }
1904 false
1905 }
1906
1907 pub fn can_hear(&self, observer_pos: Vec3, source_pos: Vec3, sound_intensity: f32) -> (bool, f32) {
1909 let dist = (source_pos - observer_pos).length();
1910 if dist < EPSILON { return (true, 1.0); }
1911
1912 let attenuation = (sound_intensity / (1.0 + dist * dist * 0.1)).max(0.0);
1914
1915 if attenuation < self.hearing.noise_floor { return (false, 0.0); }
1916
1917 let max_dist = self.hearing.base_radius * (sound_intensity / 1.0).sqrt();
1918 if dist > max_dist { return (false, 0.0); }
1919
1920 let confidence = (attenuation / sound_intensity).clamp(0.0, 1.0);
1921 (true, confidence)
1922 }
1923
1924 pub fn can_smell(&self, observer_pos: Vec3, source_pos: Vec3, smell_intensity: f32) -> (bool, f32) {
1926 let to_source = source_pos - observer_pos;
1927 let dist = to_source.length();
1928 if dist < EPSILON { return (true, 1.0); }
1929
1930 let wind_dot = self.smell.wind_direction.normalize_or_zero().dot(to_source / dist);
1932 let wind_factor = 1.0 + wind_dot * 0.5;
1934 let effective_radius = self.smell.base_radius * wind_factor * smell_intensity;
1935
1936 if dist > effective_radius { return (false, 0.0); }
1937
1938 let normalized = 1.0 - (dist / effective_radius);
1940 let confidence = normalized * normalized * smell_intensity;
1941 (confidence > self.smell.min_intensity, confidence)
1942 }
1943
1944 pub fn update(
1945 &mut self,
1946 observer_pos: Vec3,
1947 observer_forward: Vec3,
1948 candidates: &[(u64, Vec3, Vec3, f32, f32, f32)], dt: f32,
1950 current_time: f32,
1951 obstacles: &[Aabb],
1952 ) {
1953 let mut to_forget: Vec<u64> = Vec::new();
1955 for (id, p) in self.perceived.iter_mut() {
1956 p.decay_confidence(dt, self.confidence_decay);
1957 if p.confidence < self.forget_threshold {
1958 to_forget.push(*id);
1959 }
1960 }
1961 for id in to_forget {
1962 self.perceived.remove(&id);
1963 }
1964
1965 for &(id, pos, vel, sound, smell, threat) in candidates {
1967 if id == self.observer_id { continue; }
1968
1969 let (vis, vis_conf) = self.can_see(observer_pos, observer_forward, pos, vel, obstacles);
1970 let (hrd, hrd_conf) = self.can_hear(observer_pos, pos, sound);
1971 let (sml, sml_conf) = self.can_smell(observer_pos, pos, smell);
1972
1973 if vis || hrd || sml {
1974 let max_conf = vis_conf.max(hrd_conf).max(sml_conf);
1975 let entry = self.perceived.entry(id).or_insert_with(|| PerceivedEntity::new(id, pos));
1976 entry.is_visible = vis;
1977 entry.is_heard = hrd;
1978 entry.is_smelled = sml;
1979 entry.threat_level = threat;
1980 if vis {
1981 entry.update_position(pos, vel, current_time);
1982 } else {
1983 entry.confidence = entry.confidence.max(max_conf);
1984 }
1985 }
1986 }
1987 }
1988
1989 pub fn most_threatening(&self) -> Option<&PerceivedEntity> {
1990 self.perceived.values()
1991 .filter(|p| p.confidence > 0.2)
1992 .max_by(|a, b| (a.threat_level * a.confidence)
1993 .partial_cmp(&(b.threat_level * b.confidence)).unwrap())
1994 }
1995
1996 pub fn nearest_visible(&self, observer_pos: Vec3) -> Option<&PerceivedEntity> {
1997 self.perceived.values()
1998 .filter(|p| p.is_visible)
1999 .min_by(|a, b| {
2000 let da = (a.position - observer_pos).length_squared();
2001 let db = (b.position - observer_pos).length_squared();
2002 da.partial_cmp(&db).unwrap()
2003 })
2004 }
2005}
2006
2007#[derive(Clone, Debug)]
2012pub struct Aabb {
2013 pub min: Vec3,
2014 pub max: Vec3,
2015}
2016
2017impl Aabb {
2018 pub fn new(center: Vec3, half_extents: Vec3) -> Self {
2019 Self { min: center - half_extents, max: center + half_extents }
2020 }
2021
2022 pub fn contains(&self, p: Vec3) -> bool {
2023 p.x >= self.min.x && p.x <= self.max.x &&
2024 p.y >= self.min.y && p.y <= self.max.y &&
2025 p.z >= self.min.z && p.z <= self.max.z
2026 }
2027
2028 pub fn center(&self) -> Vec3 { (self.min + self.max) * 0.5 }
2029 pub fn half_extents(&self) -> Vec3 { (self.max - self.min) * 0.5 }
2030
2031 pub fn ray_intersects(&self, origin: Vec3, inv_dir: Vec3, max_t: f32) -> bool {
2032 let t1 = (self.min - origin) * inv_dir;
2033 let t2 = (self.max - origin) * inv_dir;
2034 let t_min_v = Vec3::new(t1.x.min(t2.x), t1.y.min(t2.y), t1.z.min(t2.z));
2035 let t_max_v = Vec3::new(t1.x.max(t2.x), t1.y.max(t2.y), t1.z.max(t2.z));
2036 let t_enter = t_min_v.x.max(t_min_v.y).max(t_min_v.z);
2037 let t_exit = t_max_v.x.min(t_max_v.y).min(t_max_v.z);
2038 t_enter <= t_exit && t_exit >= 0.0 && t_enter <= max_t
2039 }
2040}
2041
2042#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2047pub enum FormationType {
2048 Line,
2049 Column,
2050 Wedge,
2051 InvertedWedge,
2052 Circle,
2053 Box,
2054 EchelonLeft,
2055 EchelonRight,
2056 Vee,
2057 Diamond,
2058}
2059
2060pub struct FormationLayout;
2061
2062impl FormationLayout {
2063 pub fn compute_slots(
2065 formation: FormationType,
2066 leader_pos: Vec3,
2067 leader_forward: Vec3,
2068 n_agents: usize,
2069 spacing: f32,
2070 ) -> Vec<Vec3> {
2071 let fwd = leader_forward.normalize_or_zero();
2072 let right = fwd.cross(Vec3::Y).normalize_or_zero();
2073 let mut slots = Vec::with_capacity(n_agents);
2074
2075 match formation {
2076 FormationType::Line => {
2077 let half = (n_agents as f32 - 1.0) * 0.5;
2079 for i in 0..n_agents {
2080 let offset = (i as f32 - half) * spacing;
2081 slots.push(leader_pos + right * offset);
2082 }
2083 }
2084 FormationType::Column => {
2085 for i in 0..n_agents {
2087 slots.push(leader_pos - fwd * (i as f32 * spacing));
2088 }
2089 }
2090 FormationType::Wedge => {
2091 slots.push(leader_pos);
2093 let mut left = true;
2094 for i in 1..n_agents {
2095 let row = (i + 1) / 2;
2096 let side = if left { -1.0 } else { 1.0 };
2097 let pos = leader_pos
2098 - fwd * (row as f32 * spacing)
2099 + right * side * (row as f32 * spacing * 0.7);
2100 slots.push(pos);
2101 left = !left;
2102 }
2103 }
2104 FormationType::InvertedWedge => {
2105 slots.push(leader_pos);
2106 let mut left = true;
2107 for i in 1..n_agents {
2108 let row = (i + 1) / 2;
2109 let side = if left { -1.0 } else { 1.0 };
2110 let pos = leader_pos
2111 + fwd * (row as f32 * spacing)
2112 + right * side * (row as f32 * spacing * 0.7);
2113 slots.push(pos);
2114 left = !left;
2115 }
2116 }
2117 FormationType::Circle => {
2118 let radius = (n_agents as f32 * spacing) / TWO_PI;
2119 for i in 0..n_agents {
2120 let angle = (i as f32 / n_agents as f32) * TWO_PI;
2121 let x = angle.cos();
2122 let z = angle.sin();
2123 let local = right * x + Vec3::new(0.0, 0.0, 1.0).cross(right) * z;
2124 slots.push(leader_pos + local * radius);
2125 }
2126 }
2127 FormationType::Box => {
2128 let side = (n_agents as f32).sqrt().ceil() as usize;
2130 for i in 0..n_agents {
2131 let row = i / side;
2132 let col = i % side;
2133 let half_side = (side as f32 - 1.0) * 0.5;
2134 let pos = leader_pos
2135 - fwd * (row as f32 * spacing)
2136 + right * ((col as f32 - half_side) * spacing);
2137 slots.push(pos);
2138 }
2139 }
2140 FormationType::EchelonLeft => {
2141 for i in 0..n_agents {
2142 let pos = leader_pos
2143 - fwd * (i as f32 * spacing)
2144 - right * (i as f32 * spacing * 0.5);
2145 slots.push(pos);
2146 }
2147 }
2148 FormationType::EchelonRight => {
2149 for i in 0..n_agents {
2150 let pos = leader_pos
2151 - fwd * (i as f32 * spacing)
2152 + right * (i as f32 * spacing * 0.5);
2153 slots.push(pos);
2154 }
2155 }
2156 FormationType::Vee => {
2157 slots.push(leader_pos);
2158 for i in 1..n_agents {
2159 let side = if i % 2 == 0 { 1.0f32 } else { -1.0f32 };
2160 let rank = ((i + 1) / 2) as f32;
2161 let pos = leader_pos
2162 - fwd * rank * spacing
2163 + right * side * rank * spacing;
2164 slots.push(pos);
2165 }
2166 }
2167 FormationType::Diamond => {
2168 if n_agents == 0 { return slots; }
2169 slots.push(leader_pos + fwd * spacing);
2171 if n_agents > 1 { slots.push(leader_pos - right * spacing); }
2173 if n_agents > 2 { slots.push(leader_pos + right * spacing); }
2174 if n_agents > 3 { slots.push(leader_pos - fwd * spacing); }
2176 let half = (n_agents.saturating_sub(4) as f32) * 0.5;
2178 for i in 4..n_agents {
2179 let k = (i - 4) as f32;
2180 let side = if k % 2.0 < 1.0 { -1.0f32 } else { 1.0f32 };
2181 let row = (k * 0.5).floor() + 1.0;
2182 slots.push(leader_pos + right * side * row * spacing * 0.5);
2183 }
2184 }
2185 }
2186
2187 while slots.len() < n_agents {
2189 let last = slots.last().copied().unwrap_or(leader_pos);
2190 slots.push(last - fwd * spacing);
2191 }
2192 slots.truncate(n_agents);
2193 slots
2194 }
2195
2196 pub fn assign_slots(agent_positions: &[Vec3], slots: &[Vec3]) -> Vec<usize> {
2198 let n = agent_positions.len().min(slots.len());
2199 let mut assignment = vec![usize::MAX; n];
2200 let mut used_slots: HashSet<usize> = HashSet::new();
2201
2202 for agent_idx in 0..n {
2203 let ap = agent_positions[agent_idx];
2204 let mut best_slot = 0;
2205 let mut best_dist = f32::MAX;
2206 for slot_idx in 0..slots.len() {
2207 if used_slots.contains(&slot_idx) { continue; }
2208 let d = (slots[slot_idx] - ap).length_squared();
2209 if d < best_dist {
2210 best_dist = d;
2211 best_slot = slot_idx;
2212 }
2213 }
2214 assignment[agent_idx] = best_slot;
2215 used_slots.insert(best_slot);
2216 }
2217 assignment
2218 }
2219}
2220
2221#[derive(Clone, Debug)]
2226pub struct SteeringAgent {
2227 pub id: u64,
2228 pub position: Vec3,
2229 pub velocity: Vec3,
2230 pub heading: Vec3,
2231 pub max_speed: f32,
2232 pub max_force: f32,
2233 pub mass: f32,
2234 pub radius: f32,
2235 pub wander_angle: f32,
2236 pub path_index: usize,
2237}
2238
2239impl SteeringAgent {
2240 pub fn new(id: u64, pos: Vec3, max_speed: f32, max_force: f32) -> Self {
2241 Self {
2242 id,
2243 position: pos,
2244 velocity: Vec3::ZERO,
2245 heading: Vec3::Z,
2246 max_speed,
2247 max_force,
2248 mass: 1.0,
2249 radius: 0.5,
2250 wander_angle: 0.0,
2251 path_index: 0,
2252 }
2253 }
2254
2255 pub fn apply_force(&mut self, force: Vec3, dt: f32) {
2256 let clamped = if force.length() > self.max_force {
2257 force.normalize() * self.max_force
2258 } else { force };
2259 let accel = clamped / self.mass;
2260 self.velocity += accel * dt;
2261 if self.velocity.length() > self.max_speed {
2262 self.velocity = self.velocity.normalize() * self.max_speed;
2263 }
2264 self.position += self.velocity * dt;
2265 if self.velocity.length() > EPSILON {
2266 self.heading = self.velocity.normalize();
2267 }
2268 }
2269
2270 pub fn speed(&self) -> f32 { self.velocity.length() }
2271}
2272
2273pub struct SteeringBehaviors;
2274
2275impl SteeringBehaviors {
2276 pub fn seek(agent: &SteeringAgent, target: Vec3) -> Vec3 {
2278 let desired = (target - agent.position).normalize_or_zero() * agent.max_speed;
2279 desired - agent.velocity
2280 }
2281
2282 pub fn flee(agent: &SteeringAgent, threat: Vec3) -> Vec3 {
2284 let desired = (agent.position - threat).normalize_or_zero() * agent.max_speed;
2285 desired - agent.velocity
2286 }
2287
2288 pub fn arrive(agent: &SteeringAgent, target: Vec3, deceleration: f32) -> Vec3 {
2290 let to_target = target - agent.position;
2291 let dist = to_target.length();
2292 if dist < EPSILON { return Vec3::ZERO; }
2293 let speed = (dist / deceleration).min(agent.max_speed);
2295 let desired = (to_target / dist) * speed;
2296 desired - agent.velocity
2297 }
2298
2299 pub fn pursue(agent: &SteeringAgent, target_pos: Vec3, target_vel: Vec3) -> Vec3 {
2301 let to_target = target_pos - agent.position;
2302 let dist = to_target.length();
2303 let speed = agent.speed();
2304 let target_speed = target_vel.length();
2306 let look_ahead = if speed + target_speed > EPSILON {
2307 dist / (speed + target_speed)
2308 } else { 0.0 };
2309 let future_pos = target_pos + target_vel * look_ahead;
2310 Self::seek(agent, future_pos)
2311 }
2312
2313 pub fn evade(agent: &SteeringAgent, threat_pos: Vec3, threat_vel: Vec3) -> Vec3 {
2315 let to_threat = threat_pos - agent.position;
2316 let dist = to_threat.length();
2317 let look_ahead = dist / (agent.max_speed + threat_vel.length() + EPSILON);
2318 let future_pos = threat_pos + threat_vel * look_ahead;
2319 Self::flee(agent, future_pos)
2320 }
2321
2322 pub fn wander(agent: &mut SteeringAgent, rng_seed: &mut u64, dt: f32) -> Vec3 {
2324 *rng_seed = rng_seed.wrapping_mul(6364136223846793005).wrapping_add(1);
2326 let rand_val = ((*rng_seed >> 33) as i32 as f32) / (i32::MAX as f32);
2327 agent.wander_angle += rand_val * WANDER_ANGLE_CHANGE;
2328
2329 let circle_center = agent.position + agent.heading * WANDER_CIRCLE_DISTANCE;
2331 let displacement = Vec3::new(
2333 agent.wander_angle.cos() * WANDER_CIRCLE_RADIUS,
2334 0.0,
2335 agent.wander_angle.sin() * WANDER_CIRCLE_RADIUS,
2336 );
2337 let wander_target = circle_center + displacement;
2338 Self::seek(agent, wander_target)
2339 }
2340
2341 pub fn obstacle_avoidance(agent: &SteeringAgent, obstacles: &[Aabb]) -> Vec3 {
2343 let look_ahead = agent.max_speed * 1.5;
2344 let ahead = agent.position + agent.heading * look_ahead;
2345 let ahead_half = agent.position + agent.heading * look_ahead * 0.5;
2346
2347 let mut most_threat: Option<(&Aabb, Vec3)> = None;
2348 let mut most_threat_dist = f32::MAX;
2349
2350 for obs in obstacles {
2351 let center = obs.center();
2352 let he = obs.half_extents();
2353 let r = he.x.max(he.z); let to_center = center - agent.position;
2357 let proj = to_center.dot(agent.heading);
2358 if proj < 0.0 { continue; } let closest_on_ray = agent.position + agent.heading * proj.min(look_ahead);
2361 let dist_to_center = (center - closest_on_ray).length();
2362
2363 if dist_to_center < r + agent.radius {
2364 let d = (center - agent.position).length();
2365 if d < most_threat_dist {
2366 most_threat_dist = d;
2367 most_threat = Some((obs, center));
2368 }
2369 }
2370 }
2371
2372 if let Some((obs, center)) = most_threat {
2373 let avoid_dir = (ahead - center).normalize_or_zero();
2375 avoid_dir * agent.max_force
2376 } else {
2377 Vec3::ZERO
2378 }
2379 }
2380
2381 pub fn wall_following(agent: &SteeringAgent, walls: &[(Vec3, Vec3)]) -> Vec3 {
2383 let feeler_len = 2.0;
2385 let feeler = agent.position + agent.heading * feeler_len;
2386
2387 let mut force = Vec3::ZERO;
2388 for &(wall_point, wall_normal) in walls {
2389 let dist = (agent.position - wall_point).dot(wall_normal);
2390 if dist > 0.0 && dist < feeler_len + agent.radius {
2391 let along_wall = Vec3::new(-wall_normal.z, 0.0, wall_normal.x);
2393 let desired = along_wall * agent.max_speed + wall_normal * agent.max_speed * 0.5;
2395 force = desired - agent.velocity;
2396 break;
2397 }
2398 }
2399 force
2400 }
2401
2402 pub fn path_following(agent: &SteeringAgent, waypoints: &[Vec3], path_index: &mut usize) -> Vec3 {
2404 if waypoints.is_empty() { return Vec3::ZERO; }
2405 let current_wp = waypoints[*path_index];
2406 let dist = (current_wp - agent.position).length();
2407 let waypoint_radius = 1.0;
2408 if dist < waypoint_radius && *path_index + 1 < waypoints.len() {
2409 *path_index += 1;
2410 }
2411 Self::arrive(agent, waypoints[*path_index], ARRIVE_DECELERATION_RADIUS)
2412 }
2413
2414 pub fn flow_field_following(
2416 agent: &SteeringAgent,
2417 flow_field: &HashMap<(i32, i32), Vec3>,
2418 cell_size: f32,
2419 ) -> Vec3 {
2420 let cell_x = (agent.position.x / cell_size).floor() as i32;
2421 let cell_z = (agent.position.z / cell_size).floor() as i32;
2422 if let Some(&field_dir) = flow_field.get(&(cell_x, cell_z)) {
2423 let desired = field_dir.normalize_or_zero() * agent.max_speed;
2424 desired - agent.velocity
2425 } else {
2426 Vec3::ZERO
2427 }
2428 }
2429
2430 pub fn alignment(agent: &SteeringAgent, neighbors: &[&SteeringAgent]) -> Vec3 {
2432 if neighbors.is_empty() { return Vec3::ZERO; }
2433 let mut avg_heading = Vec3::ZERO;
2434 let mut count = 0;
2435 for n in neighbors {
2436 if n.id == agent.id { continue; }
2437 avg_heading += n.heading;
2438 count += 1;
2439 }
2440 if count == 0 { return Vec3::ZERO; }
2441 avg_heading /= count as f32;
2442 (avg_heading.normalize_or_zero() * agent.max_speed) - agent.velocity
2443 }
2444
2445 pub fn cohesion(agent: &SteeringAgent, neighbors: &[&SteeringAgent]) -> Vec3 {
2447 if neighbors.is_empty() { return Vec3::ZERO; }
2448 let mut center = Vec3::ZERO;
2449 let mut count = 0;
2450 for n in neighbors {
2451 if n.id == agent.id { continue; }
2452 center += n.position;
2453 count += 1;
2454 }
2455 if count == 0 { return Vec3::ZERO; }
2456 center /= count as f32;
2457 Self::seek(agent, center)
2458 }
2459
2460 pub fn separation(agent: &SteeringAgent, neighbors: &[&SteeringAgent], desired_separation: f32) -> Vec3 {
2462 let mut force = Vec3::ZERO;
2463 let mut count = 0;
2464 for n in neighbors {
2465 if n.id == agent.id { continue; }
2466 let diff = agent.position - n.position;
2467 let dist = diff.length();
2468 if dist < desired_separation && dist > EPSILON {
2469 force += (diff / dist) * (desired_separation - dist) / desired_separation;
2471 count += 1;
2472 }
2473 }
2474 if count > 0 {
2475 force /= count as f32;
2476 force.normalize_or_zero() * agent.max_force
2477 } else {
2478 Vec3::ZERO
2479 }
2480 }
2481
2482 pub fn leader_following(
2484 agent: &SteeringAgent,
2485 leader: &SteeringAgent,
2486 slot_offset: Vec3,
2487 ) -> Vec3 {
2488 let behind_leader = leader.position
2489 - leader.heading * LEADER_FOLLOW_DISTANCE
2490 + leader.heading.cross(Vec3::Y).normalize_or_zero() * slot_offset.x
2491 - leader.heading * slot_offset.z;
2492
2493 let dist_to_slot = (behind_leader - agent.position).length();
2494 let is_on_path = dist_to_slot < 2.0;
2495
2496 let to_agent = agent.position - leader.position;
2498 let dot = to_agent.dot(leader.heading);
2499 if dot > 0.0 && to_agent.length() < LEADER_FOLLOW_DISTANCE {
2500 Self::flee(agent, leader.position + leader.heading * 3.0)
2502 } else {
2503 Self::arrive(agent, behind_leader, ARRIVE_DECELERATION_RADIUS * 0.5)
2504 }
2505 }
2506
2507 pub fn queue_behavior(
2509 agent: &SteeringAgent,
2510 neighbors: &[&SteeringAgent],
2511 target: Vec3,
2512 ) -> Vec3 {
2513 let ahead_in_queue = neighbors.iter()
2515 .filter(|n| n.id != agent.id)
2516 .filter(|n| {
2517 let to_n = n.position - agent.position;
2518 let dist = to_n.length();
2519 dist < QUEUE_MIN_DIST * 3.0 && to_n.dot(agent.heading) > 0.0
2520 })
2521 .min_by(|a, b| {
2522 let da = (a.position - agent.position).length_squared();
2523 let db = (b.position - agent.position).length_squared();
2524 da.partial_cmp(&db).unwrap()
2525 });
2526
2527 if let Some(ahead) = ahead_in_queue {
2528 let dist = (ahead.position - agent.position).length();
2529 if dist < QUEUE_MIN_DIST {
2530 return -agent.velocity;
2532 }
2533 }
2534 Self::arrive(agent, target, ARRIVE_DECELERATION_RADIUS)
2535 }
2536
2537 pub fn collision_avoidance(agent: &SteeringAgent, others: &[&SteeringAgent]) -> Vec3 {
2539 let mut first_threat: Option<(&SteeringAgent, f32)> = None;
2540 let min_time_to_collision = f32::MAX;
2541 let mut min_time = min_time_to_collision;
2542
2543 for other in others {
2544 if other.id == agent.id { continue; }
2545 let rel_pos = other.position - agent.position;
2546 let rel_vel = other.velocity - agent.velocity;
2547 let rel_speed_sq = rel_vel.length_squared();
2549 if rel_speed_sq < EPSILON { continue; }
2550 let t = -rel_pos.dot(rel_vel) / rel_speed_sq;
2551 if t < 0.0 || t > 5.0 { continue; }
2552 let closest_dist = (rel_pos + rel_vel * t).length();
2553 let combined_radius = agent.radius + other.radius;
2554 if closest_dist < combined_radius && t < min_time {
2555 min_time = t;
2556 first_threat = Some((other, t));
2557 }
2558 }
2559
2560 if let Some((threat, t)) = first_threat {
2561 let future_rel_pos = (threat.position + threat.velocity * t) - (agent.position + agent.velocity * t);
2562 let push = (agent.position - threat.position).normalize_or_zero();
2563 push * agent.max_force * (1.0 - (t / 5.0).clamp(0.0, 1.0))
2564 } else {
2565 Vec3::ZERO
2566 }
2567 }
2568
2569 pub fn hide(
2571 agent: &SteeringAgent,
2572 threat: Vec3,
2573 obstacles: &[Aabb],
2574 ) -> Vec3 {
2575 let mut best_hiding_spot = agent.position;
2576 let mut best_dist = f32::MAX;
2577
2578 for obs in obstacles {
2579 let center = obs.center();
2580 let to_center = (center - threat).normalize_or_zero();
2582 let he = obs.half_extents();
2583 let r = he.x.max(he.z);
2584 let hiding_spot = center + to_center * (r + agent.radius + 1.0);
2585 let dist = (hiding_spot - agent.position).length_squared();
2586 if dist < best_dist {
2587 best_dist = dist;
2588 best_hiding_spot = hiding_spot;
2589 }
2590 }
2591 Self::arrive(agent, best_hiding_spot, ARRIVE_DECELERATION_RADIUS)
2592 }
2593
2594 pub fn interpose(
2596 agent: &SteeringAgent,
2597 agent_a: &SteeringAgent,
2598 agent_b: &SteeringAgent,
2599 ) -> Vec3 {
2600 let midpoint = (agent_a.position + agent_b.position) * 0.5;
2602 let time_to_reach = (midpoint - agent.position).length() / (agent.max_speed + EPSILON);
2603 let future_a = agent_a.position + agent_a.velocity * time_to_reach;
2604 let future_b = agent_b.position + agent_b.velocity * time_to_reach;
2605 let future_mid = (future_a + future_b) * 0.5;
2606 Self::arrive(agent, future_mid, ARRIVE_DECELERATION_RADIUS)
2607 }
2608
2609 pub fn compute_weighted(
2611 agent: &mut SteeringAgent,
2612 seek_target: Option<Vec3>,
2613 flee_target: Option<Vec3>,
2614 arrive_target: Option<Vec3>,
2615 pursue_target: Option<(Vec3, Vec3)>,
2616 evade_threat: Option<(Vec3, Vec3)>,
2617 do_wander: bool,
2618 rng_seed: &mut u64,
2619 dt: f32,
2620 obstacles: &[Aabb],
2621 walls: &[(Vec3, Vec3)],
2622 neighbors: &[&SteeringAgent],
2623 waypoints: Option<&[Vec3]>,
2624 flow_field: Option<&HashMap<(i32, i32), Vec3>>,
2625 leader: Option<&SteeringAgent>,
2626 hide_from: Option<Vec3>,
2627 interpose_ab: Option<(&SteeringAgent, &SteeringAgent)>,
2628 ) -> Vec3 {
2629 let mut total = Vec3::ZERO;
2630
2631 macro_rules! add_force {
2632 ($force:expr, $weight:expr, $budget:expr) => {{
2633 let f = $force * $weight;
2634 let len = f.length();
2635 if len > EPSILON {
2636 total += f;
2637 }
2638 }};
2639 }
2640
2641 if let Some(t) = seek_target { add_force!(Self::seek(agent, t), 1.0, agent.max_force); }
2642 if let Some(t) = flee_target { add_force!(Self::flee(agent, t), 1.0, agent.max_force); }
2643 if let Some(t) = arrive_target { add_force!(Self::arrive(agent, t, ARRIVE_DECELERATION_RADIUS), 1.0, agent.max_force); }
2644 if let Some((p, v)) = pursue_target { add_force!(Self::pursue(agent, p, v), 1.0, agent.max_force); }
2645 if let Some((p, v)) = evade_threat { add_force!(Self::evade(agent, p, v), 1.0, agent.max_force); }
2646 if do_wander { add_force!(Self::wander(agent, rng_seed, dt), 0.5, agent.max_force); }
2647 if !obstacles.is_empty() { add_force!(Self::obstacle_avoidance(agent, obstacles), 2.0, agent.max_force); }
2648 if !walls.is_empty() { add_force!(Self::wall_following(agent, walls), 1.0, agent.max_force); }
2649 if !neighbors.is_empty() {
2650 add_force!(Self::alignment(agent, neighbors), ALIGNMENT_WEIGHT, agent.max_force);
2651 add_force!(Self::cohesion(agent, neighbors), COHESION_WEIGHT, agent.max_force);
2652 add_force!(Self::separation(agent, neighbors, agent.radius * 2.5), SEPARATION_WEIGHT, agent.max_force);
2653 add_force!(Self::collision_avoidance(agent, neighbors), 2.0, agent.max_force);
2654 }
2655 if let Some(wps) = waypoints {
2656 let pi = &mut { agent.path_index };
2657 add_force!(Self::path_following(agent, wps, pi), 1.0, agent.max_force);
2658 }
2659 if let Some(ff) = flow_field {
2660 add_force!(Self::flow_field_following(agent, ff, 1.0), 1.0, agent.max_force);
2661 }
2662 if let Some(ldr) = leader {
2663 add_force!(Self::leader_following(agent, ldr, Vec3::ZERO), 1.0, agent.max_force);
2664 }
2665 if let Some(threat) = hide_from {
2666 add_force!(Self::hide(agent, threat, obstacles), 1.0, agent.max_force);
2667 }
2668 if let Some((a, b)) = interpose_ab {
2669 add_force!(Self::interpose(agent, a, b), 1.0, agent.max_force);
2670 }
2671
2672 if total.length() > agent.max_force {
2674 total = total.normalize() * agent.max_force;
2675 }
2676 total
2677 }
2678}
2679
2680#[derive(Clone, Debug)]
2685pub enum FsmConditionOp {
2686 BlackboardBool { key: String, expected: bool },
2687 BlackboardCompare { key: String, op: CompareOp, value: BlackboardValue },
2688 TimeElapsed { duration: f32 },
2689 Always,
2690 Never,
2691 And(Box<FsmConditionOp>, Box<FsmConditionOp>),
2692 Or(Box<FsmConditionOp>, Box<FsmConditionOp>),
2693 Not(Box<FsmConditionOp>),
2694}
2695
2696impl FsmConditionOp {
2697 pub fn evaluate(&self, blackboard: &Blackboard, time_in_state: f32) -> bool {
2698 match self {
2699 FsmConditionOp::Always => true,
2700 FsmConditionOp::Never => false,
2701 FsmConditionOp::BlackboardBool { key, expected } => {
2702 blackboard.get_bool(key) == *expected
2703 }
2704 FsmConditionOp::BlackboardCompare { key, op, value } => {
2705 op.evaluate(blackboard.get(key), value)
2706 }
2707 FsmConditionOp::TimeElapsed { duration } => time_in_state >= *duration,
2708 FsmConditionOp::And(a, b) => {
2709 a.evaluate(blackboard, time_in_state) && b.evaluate(blackboard, time_in_state)
2710 }
2711 FsmConditionOp::Or(a, b) => {
2712 a.evaluate(blackboard, time_in_state) || b.evaluate(blackboard, time_in_state)
2713 }
2714 FsmConditionOp::Not(inner) => !inner.evaluate(blackboard, time_in_state),
2715 }
2716 }
2717}
2718
2719#[derive(Clone, Debug)]
2720pub struct FsmTransition {
2721 pub id: u32,
2722 pub from_state: u32,
2723 pub to_state: u32,
2724 pub condition: FsmConditionOp,
2725 pub priority: i32,
2726 pub actions: Vec<FsmAction>,
2727}
2728
2729#[derive(Clone, Debug)]
2730pub enum FsmAction {
2731 SetBlackboard { key: String, value: BlackboardValue },
2732 IncrementBlackboard { key: String, amount: f32 },
2733 Log { message: String },
2734 PlayAnimation { clip: String },
2735 PlaySound { sound: String },
2736}
2737
2738impl FsmAction {
2739 pub fn execute(&self, blackboard: &mut Blackboard) {
2740 match self {
2741 FsmAction::SetBlackboard { key, value } => {
2742 blackboard.set(key, value.clone());
2743 }
2744 FsmAction::IncrementBlackboard { key, amount } => {
2745 let v = blackboard.get_float(key);
2746 blackboard.set(key, BlackboardValue::Float(v + amount));
2747 }
2748 FsmAction::Log { message } => {
2749 let _ = message;
2751 }
2752 FsmAction::PlayAnimation { clip } => {
2753 blackboard.set("fsm_anim", BlackboardValue::String(clip.clone()));
2754 }
2755 FsmAction::PlaySound { sound } => {
2756 blackboard.set("fsm_sound", BlackboardValue::String(sound.clone()));
2757 }
2758 }
2759 }
2760}
2761
2762#[derive(Clone, Debug)]
2763pub struct FsmState {
2764 pub id: u32,
2765 pub name: String,
2766 pub entry_actions: Vec<FsmAction>,
2767 pub exit_actions: Vec<FsmAction>,
2768 pub tick_actions: Vec<FsmAction>,
2769 pub position: Vec2, pub is_initial: bool,
2771 pub is_final: bool,
2772 pub color: Vec4,
2773 pub sub_fsm: Option<u32>, }
2775
2776impl FsmState {
2777 pub fn new(id: u32, name: &str) -> Self {
2778 Self {
2779 id,
2780 name: name.to_string(),
2781 entry_actions: Vec::new(),
2782 exit_actions: Vec::new(),
2783 tick_actions: Vec::new(),
2784 position: Vec2::ZERO,
2785 is_initial: false,
2786 is_final: false,
2787 color: Vec4::new(0.3, 0.4, 0.7, 1.0),
2788 sub_fsm: None,
2789 }
2790 }
2791}
2792
2793pub struct FsmInstance {
2794 pub id: u32,
2795 pub name: String,
2796 pub states: HashMap<u32, FsmState>,
2797 pub transitions: Vec<FsmTransition>,
2798 pub initial_state: Option<u32>,
2799 pub current_state: Option<u32>,
2800 pub time_in_state: f32,
2801 pub transition_history: VecDeque<(u32, u32, f32)>, pub next_state_id: u32,
2803 pub next_transition_id: u32,
2804}
2805
2806impl FsmInstance {
2807 pub fn new(id: u32, name: &str) -> Self {
2808 Self {
2809 id,
2810 name: name.to_string(),
2811 states: HashMap::new(),
2812 transitions: Vec::new(),
2813 initial_state: None,
2814 current_state: None,
2815 time_in_state: 0.0,
2816 transition_history: VecDeque::with_capacity(32),
2817 next_state_id: 1,
2818 next_transition_id: 1,
2819 }
2820 }
2821
2822 pub fn add_state(&mut self, name: &str) -> u32 {
2823 let id = self.next_state_id;
2824 self.next_state_id += 1;
2825 self.states.insert(id, FsmState::new(id, name));
2826 id
2827 }
2828
2829 pub fn set_initial(&mut self, state_id: u32) {
2830 if let Some(s) = self.states.get_mut(&state_id) { s.is_initial = true; }
2831 self.initial_state = Some(state_id);
2832 }
2833
2834 pub fn add_transition(&mut self, from: u32, to: u32, condition: FsmConditionOp, priority: i32) -> u32 {
2835 let id = self.next_transition_id;
2836 self.next_transition_id += 1;
2837 self.transitions.push(FsmTransition { id, from_state: from, to_state: to, condition, priority, actions: Vec::new() });
2838 id
2839 }
2840
2841 pub fn start(&mut self, blackboard: &mut Blackboard) {
2842 if let Some(init) = self.initial_state {
2843 self.enter_state(init, blackboard, 0.0);
2844 }
2845 }
2846
2847 fn enter_state(&mut self, state_id: u32, blackboard: &mut Blackboard, time: f32) {
2848 if let Some(prev) = self.current_state {
2849 if let Some(state) = self.states.get(&prev) {
2850 let exit_actions: Vec<FsmAction> = state.exit_actions.clone();
2851 for action in &exit_actions { action.execute(blackboard); }
2852 }
2853 }
2854 if let Some(prev) = self.current_state {
2855 self.transition_history.push_back((prev, state_id, time));
2856 if self.transition_history.len() > 32 { self.transition_history.pop_front(); }
2857 }
2858 self.current_state = Some(state_id);
2859 self.time_in_state = 0.0;
2860 if let Some(state) = self.states.get(&state_id) {
2861 let entry_actions: Vec<FsmAction> = state.entry_actions.clone();
2862 for action in &entry_actions { action.execute(blackboard); }
2863 }
2864 }
2865
2866 pub fn tick(&mut self, blackboard: &mut Blackboard, dt: f32, current_time: f32) {
2867 self.time_in_state += dt;
2868 let current = match self.current_state { Some(c) => c, None => return };
2869
2870 if let Some(state) = self.states.get(¤t) {
2872 let tick_actions: Vec<FsmAction> = state.tick_actions.clone();
2873 for action in &tick_actions { action.execute(blackboard); }
2874 }
2875
2876 let mut sorted_transitions: Vec<&FsmTransition> = self.transitions.iter()
2878 .filter(|t| t.from_state == current)
2879 .collect();
2880 sorted_transitions.sort_by(|a, b| b.priority.cmp(&a.priority));
2881
2882 for t in sorted_transitions {
2883 if t.condition.evaluate(blackboard, self.time_in_state) {
2884 let to = t.to_state;
2885 let t_actions: Vec<FsmAction> = t.actions.clone();
2886 for action in &t_actions { action.execute(blackboard); }
2887 self.enter_state(to, blackboard, current_time);
2888 break;
2889 }
2890 }
2891 }
2892
2893 pub fn auto_layout(&mut self) {
2895 let n = self.states.len();
2896 if n == 0 { return; }
2897 let radius = (n as f32 * 80.0) / TWO_PI;
2898 let ids: Vec<u32> = self.states.keys().copied().collect();
2899 for (i, id) in ids.iter().enumerate() {
2900 let angle = (i as f32 / n as f32) * TWO_PI;
2901 let pos = Vec2::new(angle.cos() * radius, angle.sin() * radius);
2902 if let Some(s) = self.states.get_mut(id) { s.position = pos; }
2903 }
2904 }
2905}
2906
2907#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2912pub enum PrimaryEmotion {
2913 Joy,
2914 Trust,
2915 Fear,
2916 Surprise,
2917 Sadness,
2918 Disgust,
2919 Anger,
2920 Anticipation,
2921}
2922
2923#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2924pub enum SecondaryEmotion {
2925 Love, Submission, Awe, Disapproval,Remorse, Contempt, Aggressiveness, Optimism, }
2934
2935impl PrimaryEmotion {
2936 pub const ALL: [PrimaryEmotion; 8] = [
2937 PrimaryEmotion::Joy,
2938 PrimaryEmotion::Trust,
2939 PrimaryEmotion::Fear,
2940 PrimaryEmotion::Surprise,
2941 PrimaryEmotion::Sadness,
2942 PrimaryEmotion::Disgust,
2943 PrimaryEmotion::Anger,
2944 PrimaryEmotion::Anticipation,
2945 ];
2946
2947 pub fn index(&self) -> usize {
2948 match self {
2949 PrimaryEmotion::Joy => 0,
2950 PrimaryEmotion::Trust => 1,
2951 PrimaryEmotion::Fear => 2,
2952 PrimaryEmotion::Surprise => 3,
2953 PrimaryEmotion::Sadness => 4,
2954 PrimaryEmotion::Disgust => 5,
2955 PrimaryEmotion::Anger => 6,
2956 PrimaryEmotion::Anticipation => 7,
2957 }
2958 }
2959
2960 pub fn opposite(&self) -> PrimaryEmotion {
2962 PrimaryEmotion::ALL[(self.index() + 4) % 8]
2963 }
2964
2965 pub fn wheel_position(&self) -> Vec2 {
2967 let angle = (self.index() as f32 / 8.0) * TWO_PI;
2968 Vec2::new(angle.cos(), angle.sin())
2969 }
2970
2971 pub fn blend_with_next(&self) -> SecondaryEmotion {
2973 match self {
2974 PrimaryEmotion::Joy => SecondaryEmotion::Love,
2975 PrimaryEmotion::Trust => SecondaryEmotion::Submission,
2976 PrimaryEmotion::Fear => SecondaryEmotion::Awe,
2977 PrimaryEmotion::Surprise => SecondaryEmotion::Disapproval,
2978 PrimaryEmotion::Sadness => SecondaryEmotion::Remorse,
2979 PrimaryEmotion::Disgust => SecondaryEmotion::Contempt,
2980 PrimaryEmotion::Anger => SecondaryEmotion::Aggressiveness,
2981 PrimaryEmotion::Anticipation => SecondaryEmotion::Optimism,
2982 }
2983 }
2984}
2985
2986#[derive(Clone, Debug)]
2987pub struct EmotionState {
2988 pub intensities: [f32; 8], pub secondary_intensities: [f32; 8],
2990 pub mood_valence: f32, pub mood_arousal: f32, pub decay_rates: [f32; 8],
2993 pub threshold: f32, }
2995
2996impl EmotionState {
2997 pub fn new() -> Self {
2998 Self {
2999 intensities: [0.0; 8],
3000 secondary_intensities: [0.0; 8],
3001 mood_valence: 0.0,
3002 mood_arousal: 0.0,
3003 decay_rates: [EMOTION_DECAY_RATE; 8],
3004 threshold: 0.02,
3005 }
3006 }
3007
3008 pub fn add_emotion(&mut self, emotion: PrimaryEmotion, amount: f32) {
3009 let idx = emotion.index();
3010 self.intensities[idx] = (self.intensities[idx] + amount).clamp(0.0, 1.0);
3011 let opp_idx = emotion.opposite().index();
3013 self.intensities[opp_idx] = (self.intensities[opp_idx] - amount * 0.3).max(0.0);
3014 }
3015
3016 pub fn get_intensity(&self, emotion: PrimaryEmotion) -> f32 {
3017 self.intensities[emotion.index()]
3018 }
3019
3020 pub fn dominant(&self) -> Option<PrimaryEmotion> {
3021 let max_idx = self.intensities.iter().enumerate()
3022 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
3023 .map(|(i, _)| i)?;
3024 if self.intensities[max_idx] < self.threshold { return None; }
3025 Some(PrimaryEmotion::ALL[max_idx])
3026 }
3027
3028 pub fn update(&mut self, dt: f32) {
3029 for i in 0..8 {
3031 self.intensities[i] = (self.intensities[i] - self.decay_rates[i] * dt).max(0.0);
3032 }
3033
3034 for i in 0..8 {
3036 let next = (i + 1) % 8;
3037 self.secondary_intensities[i] = (self.intensities[i] + self.intensities[next]) * 0.5;
3038 }
3039
3040 let positive = self.intensities[0] + self.intensities[1] + self.intensities[7]; let negative = self.intensities[2] + self.intensities[4] + self.intensities[5] + self.intensities[6]; let total = positive + negative;
3044 if total > EPSILON {
3045 self.mood_valence = (positive - negative) / total;
3046 }
3047
3048 let high_arousal = self.intensities[2] + self.intensities[3] + self.intensities[6]; let low_arousal = self.intensities[4]; self.mood_arousal = ((high_arousal - low_arousal * 0.5) / (8.0f32.sqrt())).clamp(0.0, 1.0);
3052 }
3053
3054 pub fn behavior_modifiers(&self) -> EmotionBehaviorModifiers {
3056 EmotionBehaviorModifiers {
3057 speed_multiplier: 1.0 + self.intensities[6] * 0.3 - self.intensities[4] * 0.2 + self.intensities[7] * 0.15, aggression_bias: self.intensities[6] * 0.5 + self.intensities[3] * 0.2, flee_threshold_modifier: self.intensities[2] * 0.4, search_radius_multiplier: 1.0 + self.intensities[7] * 0.3, reaction_time_modifier: -self.intensities[2] * 0.2 + self.intensities[4] * 0.3, accuracy_modifier: 1.0 - self.intensities[2] * 0.15 - self.intensities[3] * 0.1, cooperation_bias: self.intensities[1] * 0.4 - self.intensities[5] * 0.3, curiosity_bias: self.intensities[3] * 0.3 + self.intensities[7] * 0.2,
3070 }
3071 }
3072
3073 pub fn serialize_to_blackboard(&self, blackboard: &mut Blackboard, prefix: &str) {
3074 for (i, &intensity) in self.intensities.iter().enumerate() {
3075 let emotion_name = match i {
3076 0 => "joy", 1 => "trust", 2 => "fear", 3 => "surprise",
3077 4 => "sadness", 5 => "disgust", 6 => "anger", 7 => "anticipation",
3078 _ => "unknown",
3079 };
3080 blackboard.set(
3081 &format!("{}_{}", prefix, emotion_name),
3082 BlackboardValue::Float(intensity),
3083 );
3084 }
3085 blackboard.set(&format!("{}_valence", prefix), BlackboardValue::Float(self.mood_valence));
3086 blackboard.set(&format!("{}_arousal", prefix), BlackboardValue::Float(self.mood_arousal));
3087 }
3088}
3089
3090#[derive(Clone, Debug)]
3091pub struct EmotionBehaviorModifiers {
3092 pub speed_multiplier: f32,
3093 pub aggression_bias: f32,
3094 pub flee_threshold_modifier: f32,
3095 pub search_radius_multiplier: f32,
3096 pub reaction_time_modifier: f32,
3097 pub accuracy_modifier: f32,
3098 pub cooperation_bias: f32,
3099 pub curiosity_bias: f32,
3100}
3101
3102impl Default for EmotionBehaviorModifiers {
3103 fn default() -> Self {
3104 Self {
3105 speed_multiplier: 1.0,
3106 aggression_bias: 0.0,
3107 flee_threshold_modifier: 0.0,
3108 search_radius_multiplier: 1.0,
3109 reaction_time_modifier: 0.0,
3110 accuracy_modifier: 1.0,
3111 cooperation_bias: 0.0,
3112 curiosity_bias: 0.0,
3113 }
3114 }
3115}
3116
3117#[derive(Clone, Debug)]
3119pub struct EmotionalStimulus {
3120 pub emotion: PrimaryEmotion,
3121 pub intensity: f32,
3122 pub source_id: u64,
3123 pub decay_rate_override: Option<f32>,
3124}
3125
3126pub struct EmotionEngine {
3127 pub state: EmotionState,
3128 pub stimuli_queue: VecDeque<EmotionalStimulus>,
3129 pub history: VecDeque<(f32, [f32; 8])>, pub history_capacity: usize,
3131}
3132
3133impl EmotionEngine {
3134 pub fn new() -> Self {
3135 Self {
3136 state: EmotionState::new(),
3137 stimuli_queue: VecDeque::new(),
3138 history: VecDeque::with_capacity(64),
3139 history_capacity: 64,
3140 }
3141 }
3142
3143 pub fn submit_stimulus(&mut self, stimulus: EmotionalStimulus) {
3144 self.stimuli_queue.push_back(stimulus);
3145 }
3146
3147 pub fn update(&mut self, dt: f32, current_time: f32) {
3148 while let Some(stimulus) = self.stimuli_queue.pop_front() {
3150 self.state.add_emotion(stimulus.emotion, stimulus.intensity);
3151 if let Some(rate) = stimulus.decay_rate_override {
3152 let idx = stimulus.emotion.index();
3153 self.state.decay_rates[idx] = rate;
3154 }
3155 }
3156 self.state.update(dt);
3157
3158 self.history.push_back((current_time as f32, self.state.intensities));
3160 if self.history.len() > self.history_capacity {
3161 self.history.pop_front();
3162 }
3163 }
3164
3165 pub fn get_modifier(&self) -> EmotionBehaviorModifiers {
3166 self.state.behavior_modifiers()
3167 }
3168
3169 pub fn apply_contagion(
3171 &mut self,
3172 neighbor_emotions: &[EmotionState],
3173 contagion_rate: f32,
3174 ) {
3175 for neighbor in neighbor_emotions {
3176 for emotion in &PrimaryEmotion::ALL {
3177 let n_intensity = neighbor.get_intensity(*emotion);
3178 if n_intensity > 0.1 {
3179 self.state.add_emotion(*emotion, n_intensity * contagion_rate);
3180 }
3181 }
3182 }
3183 }
3184}
3185
3186#[derive(Clone, Debug)]
3191pub struct NodeGraphCamera {
3192 pub pan: Vec2,
3193 pub zoom: f32,
3194 pub target_pan: Vec2,
3195 pub target_zoom: f32,
3196}
3197
3198impl NodeGraphCamera {
3199 pub fn new() -> Self {
3200 Self { pan: Vec2::ZERO, zoom: 1.0, target_pan: Vec2::ZERO, target_zoom: 1.0 }
3201 }
3202
3203 pub fn world_to_screen(&self, world_pos: Vec2, viewport_size: Vec2) -> Vec2 {
3204 let centered = world_pos * self.zoom + viewport_size * 0.5 + self.pan;
3205 centered
3206 }
3207
3208 pub fn screen_to_world(&self, screen_pos: Vec2, viewport_size: Vec2) -> Vec2 {
3209 (screen_pos - viewport_size * 0.5 - self.pan) / self.zoom
3210 }
3211
3212 pub fn smooth_update(&mut self, dt: f32) {
3213 let speed = 10.0 * dt;
3214 self.pan = self.pan.lerp(self.target_pan, speed.min(1.0));
3215 self.zoom = self.zoom + (self.target_zoom - self.zoom) * speed.min(1.0);
3216 self.zoom = self.zoom.clamp(0.05, 5.0);
3217 }
3218
3219 pub fn zoom_toward(&mut self, screen_point: Vec2, viewport_size: Vec2, delta: f32) {
3220 let world_before = self.screen_to_world(screen_point, viewport_size);
3221 self.target_zoom = (self.target_zoom * (1.0 + delta * 0.1)).clamp(0.05, 5.0);
3222 let world_after = self.screen_to_world(screen_point, viewport_size);
3224 let diff = world_after - world_before;
3225 self.target_pan = self.target_pan + diff * self.target_zoom;
3226 }
3227}
3228
3229#[derive(Clone, Debug)]
3230pub struct ConnectionDraft {
3231 pub from_node: u32,
3232 pub from_port: usize,
3233 pub current_pos: Vec2,
3234 pub is_active: bool,
3235}
3236
3237#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3238pub enum EditorTool {
3239 Select,
3240 Pan,
3241 AddNode,
3242 Connect,
3243 Delete,
3244 Comment,
3245}
3246
3247#[derive(Clone, Debug)]
3248pub struct NodeComment {
3249 pub id: u32,
3250 pub text: String,
3251 pub rect: (Vec2, Vec2), pub color: Vec4,
3253}
3254
3255#[derive(Clone, Debug)]
3256pub struct GraphSelection {
3257 pub selected_nodes: HashSet<u32>,
3258 pub selection_rect: Option<(Vec2, Vec2)>,
3259 pub is_dragging: bool,
3260 pub drag_start: Vec2,
3261 pub drag_offset: HashMap<u32, Vec2>,
3262}
3263
3264impl GraphSelection {
3265 pub fn new() -> Self {
3266 Self {
3267 selected_nodes: HashSet::new(),
3268 selection_rect: None,
3269 is_dragging: false,
3270 drag_start: Vec2::ZERO,
3271 drag_offset: HashMap::new(),
3272 }
3273 }
3274
3275 pub fn select_single(&mut self, id: u32) {
3276 self.selected_nodes.clear();
3277 self.selected_nodes.insert(id);
3278 }
3279
3280 pub fn toggle(&mut self, id: u32) {
3281 if self.selected_nodes.contains(&id) {
3282 self.selected_nodes.remove(&id);
3283 } else {
3284 self.selected_nodes.insert(id);
3285 }
3286 }
3287
3288 pub fn clear(&mut self) {
3289 self.selected_nodes.clear();
3290 self.selection_rect = None;
3291 }
3292
3293 pub fn apply_rect_selection(&mut self, nodes: &HashMap<u32, BtNode>) {
3294 if let Some((min, max)) = self.selection_rect {
3295 let rect_min = Vec2::new(min.x.min(max.x), min.y.min(max.y));
3296 let rect_max = Vec2::new(min.x.max(max.x), min.y.max(max.y));
3297 for (id, node) in nodes {
3298 let center = node.position + node.size * 0.5;
3299 if center.x >= rect_min.x && center.x <= rect_max.x
3300 && center.y >= rect_min.y && center.y <= rect_max.y {
3301 self.selected_nodes.insert(*id);
3302 }
3303 }
3304 }
3305 }
3306}
3307
3308#[derive(Clone, Debug)]
3313pub struct BlackboardInspector {
3314 pub filter_text: String,
3315 pub show_only_changed: bool,
3316 pub sort_by_name: bool,
3317 pub sort_by_time: bool,
3318 pub sort_ascending: bool,
3319 pub highlighted_keys: HashSet<String>,
3320 pub pinned_keys: Vec<String>,
3321 pub edit_key: Option<String>,
3322 pub edit_value: String,
3323 pub history: VecDeque<(String, BlackboardValue, f64)>,
3324 pub history_capacity: usize,
3325}
3326
3327impl BlackboardInspector {
3328 pub fn new() -> Self {
3329 Self {
3330 filter_text: String::new(),
3331 show_only_changed: false,
3332 sort_by_name: true,
3333 sort_by_time: false,
3334 sort_ascending: true,
3335 highlighted_keys: HashSet::new(),
3336 pinned_keys: Vec::new(),
3337 edit_key: None,
3338 edit_value: String::new(),
3339 history: VecDeque::with_capacity(256),
3340 history_capacity: 256,
3341 }
3342 }
3343
3344 pub fn get_filtered_keys<'a>(&'a self, blackboard: &'a Blackboard) -> Vec<&'a str> {
3345 let mut keys: Vec<&str> = blackboard.entries.keys().map(|s| s.as_str()).collect();
3346
3347 if !self.filter_text.is_empty() {
3348 let filter = self.filter_text.to_lowercase();
3349 keys.retain(|k| k.to_lowercase().contains(&filter));
3350 }
3351
3352 if self.sort_by_name {
3353 keys.sort_by(|a, b| {
3354 if self.sort_ascending { a.cmp(b) } else { b.cmp(a) }
3355 });
3356 } else if self.sort_by_time {
3357 keys.sort_by(|a, b| {
3358 let ta = blackboard.change_timestamps.get(*a).copied().unwrap_or(0.0);
3359 let tb = blackboard.change_timestamps.get(*b).copied().unwrap_or(0.0);
3360 if self.sort_ascending {
3361 ta.partial_cmp(&tb).unwrap()
3362 } else {
3363 tb.partial_cmp(&ta).unwrap()
3364 }
3365 });
3366 }
3367
3368 let pinned: Vec<&str> = self.pinned_keys.iter().map(|s| s.as_str()).collect();
3370 let mut result: Vec<&str> = pinned.iter().filter(|&&k| keys.contains(&k)).copied().collect();
3371 result.extend(keys.iter().filter(|&&k| !self.pinned_keys.iter().any(|p| p == k)));
3372 result
3373 }
3374
3375 pub fn record_change(&mut self, key: &str, value: BlackboardValue, time: f64) {
3376 self.history.push_back((key.to_string(), value, time));
3377 if self.history.len() > self.history_capacity {
3378 self.history.pop_front();
3379 }
3380 self.highlighted_keys.insert(key.to_string());
3381 }
3382
3383 pub fn value_to_string(value: &BlackboardValue) -> String {
3384 match value {
3385 BlackboardValue::Bool(b) => format!("{}", b),
3386 BlackboardValue::Int(i) => format!("{}", i),
3387 BlackboardValue::Float(f) => format!("{:.4}", f),
3388 BlackboardValue::Vec2(v) => format!("({:.2}, {:.2})", v.x, v.y),
3389 BlackboardValue::Vec3(v) => format!("({:.2}, {:.2}, {:.2})", v.x, v.y, v.z),
3390 BlackboardValue::String(s) => s.clone(),
3391 BlackboardValue::EntityId(id) => format!("Entity#{}", id),
3392 BlackboardValue::None => "<none>".to_string(),
3393 }
3394 }
3395
3396 pub fn try_parse_value(raw: &str, hint: &BlackboardValue) -> Option<BlackboardValue> {
3397 match hint {
3398 BlackboardValue::Bool(_) => raw.parse::<bool>().ok().map(BlackboardValue::Bool),
3399 BlackboardValue::Int(_) => raw.parse::<i64>().ok().map(BlackboardValue::Int),
3400 BlackboardValue::Float(_) => raw.parse::<f32>().ok().map(BlackboardValue::Float),
3401 BlackboardValue::String(_) => Some(BlackboardValue::String(raw.to_string())),
3402 _ => None,
3403 }
3404 }
3405}
3406
3407#[derive(Clone, Debug)]
3412pub struct DebugVizShape {
3413 pub shape_type: DebugShapeType,
3414 pub color: Vec4,
3415 pub duration: f32, pub elapsed: f32,
3417}
3418
3419#[derive(Clone, Debug)]
3420pub enum DebugShapeType {
3421 Line { from: Vec3, to: Vec3 },
3422 Circle { center: Vec3, radius: f32, normal: Vec3 },
3423 Sphere { center: Vec3, radius: f32 },
3424 Aabb(Aabb),
3425 Arrow { from: Vec3, to: Vec3, head_size: f32 },
3426 Text { position: Vec3, text: String, size: f32 },
3427 Cross { center: Vec3, size: f32 },
3428 Arc { center: Vec3, from_angle: f32, to_angle: f32, radius: f32, normal: Vec3 },
3429}
3430
3431pub struct DebugVisualizationBuffer {
3432 pub shapes: Vec<DebugVizShape>,
3433 pub max_shapes: usize,
3434}
3435
3436impl DebugVisualizationBuffer {
3437 pub fn new(max_shapes: usize) -> Self {
3438 Self { shapes: Vec::with_capacity(max_shapes), max_shapes }
3439 }
3440
3441 pub fn add(&mut self, shape: DebugShapeType, color: Vec4, duration: f32) {
3442 if self.shapes.len() >= self.max_shapes { return; }
3443 self.shapes.push(DebugVizShape { shape_type: shape, color, duration, elapsed: 0.0 });
3444 }
3445
3446 pub fn tick(&mut self, dt: f32) {
3447 self.shapes.retain_mut(|s| {
3448 s.elapsed += dt;
3449 s.duration == 0.0 || s.elapsed < s.duration
3450 });
3451 }
3452
3453 pub fn draw_vision_cone(&mut self, pos: Vec3, forward: Vec3, half_angle: f32, range: f32, color: Vec4) {
3454 let right = forward.cross(Vec3::Y).normalize_or_zero();
3456 let steps = 16;
3457 for i in 0..steps {
3458 let t0 = i as f32 / steps as f32;
3459 let t1 = (i + 1) as f32 / steps as f32;
3460 let a0 = -half_angle + t0 * half_angle * 2.0;
3461 let a1 = -half_angle + t1 * half_angle * 2.0;
3462 let d0 = forward * a0.cos() + right * a0.sin();
3463 let d1 = forward * a1.cos() + right * a1.sin();
3464 self.add(DebugShapeType::Line {
3465 from: pos + d0 * range,
3466 to: pos + d1 * range,
3467 }, color, 0.0);
3468 }
3469 let left_edge = forward * half_angle.cos() - right * half_angle.sin();
3471 self.add(DebugShapeType::Line { from: pos, to: pos + left_edge * range }, color, 0.0);
3472 let right_edge = forward * half_angle.cos() + right * half_angle.sin();
3474 self.add(DebugShapeType::Line { from: pos, to: pos + right_edge * range }, color, 0.0);
3475 }
3476
3477 pub fn draw_hearing_radius(&mut self, pos: Vec3, radius: f32, color: Vec4) {
3478 self.add(DebugShapeType::Circle { center: pos, radius, normal: Vec3::Y }, color, 0.0);
3479 }
3480
3481 pub fn draw_bt_status(&mut self, pos: Vec3, status: BtStatus) {
3482 let color = match status {
3483 BtStatus::Success => Vec4::new(0.0, 1.0, 0.0, 0.8),
3484 BtStatus::Failure => Vec4::new(1.0, 0.0, 0.0, 0.8),
3485 BtStatus::Running => Vec4::new(1.0, 1.0, 0.0, 0.8),
3486 BtStatus::Invalid => Vec4::new(0.5, 0.5, 0.5, 0.5),
3487 };
3488 self.add(DebugShapeType::Sphere { center: pos, radius: 0.3 }, color, 0.0);
3489 }
3490
3491 pub fn draw_formation_slots(&mut self, slots: &[Vec3], assignments: &[usize], color: Vec4) {
3492 for &slot_pos in slots {
3493 self.add(DebugShapeType::Cross { center: slot_pos, size: 0.5 }, color, 0.0);
3494 }
3495 }
3496
3497 pub fn draw_velocity_arrow(&mut self, pos: Vec3, vel: Vec3, color: Vec4) {
3498 if vel.length() > EPSILON {
3499 self.add(DebugShapeType::Arrow {
3500 from: pos,
3501 to: pos + vel,
3502 head_size: vel.length() * 0.2,
3503 }, color, 0.0);
3504 }
3505 }
3506
3507 pub fn draw_emotion_wheel(&mut self, center: Vec3, emotions: &EmotionState, scale: f32) {
3508 for (i, &intensity) in emotions.intensities.iter().enumerate() {
3509 if intensity < 0.01 { continue; }
3510 let angle = (i as f32 / 8.0) * TWO_PI;
3511 let dir = Vec3::new(angle.cos(), 0.0, angle.sin());
3512 let end = center + dir * (intensity * scale);
3513 let hue = i as f32 / 8.0;
3514 let color = hsv_to_rgba(hue, 0.8, 0.9, 0.9);
3515 self.add(DebugShapeType::Arrow { from: center, to: end, head_size: 0.1 }, color, 0.0);
3516 }
3517 }
3518}
3519
3520fn hsv_to_rgba(h: f32, s: f32, v: f32, a: f32) -> Vec4 {
3522 let h6 = h * 6.0;
3523 let hi = h6.floor() as u32 % 6;
3524 let f = h6 - h6.floor();
3525 let p = v * (1.0 - s);
3526 let q = v * (1.0 - s * f);
3527 let t = v * (1.0 - s * (1.0 - f));
3528 let (r, g, b) = match hi {
3529 0 => (v, t, p),
3530 1 => (q, v, p),
3531 2 => (p, v, t),
3532 3 => (p, q, v),
3533 4 => (t, p, v),
3534 _ => (v, p, q),
3535 };
3536 Vec4::new(r, g, b, a)
3537}
3538
3539pub struct BtTemplates;
3544
3545impl BtTemplates {
3546 pub fn combat_patrol_tree() -> BehaviorTree {
3548 let mut tree = BehaviorTree::new("CombatPatrol");
3549
3550 let root = tree.add_node(BtNodeType::Selector);
3551 tree.set_root(root);
3552
3553 let combat_seq = tree.add_node(BtNodeType::Sequence);
3555 let has_target = tree.add_node(BtNodeType::BlackboardCheck {
3556 key: "target".to_string(),
3557 op: CompareOp::Exists,
3558 value: BlackboardValue::None,
3559 });
3560 let in_range_check = tree.add_node(BtNodeType::BlackboardCheck {
3561 key: "target_dist".to_string(),
3562 op: CompareOp::LessThan,
3563 value: BlackboardValue::Float(15.0),
3564 });
3565 let attack_cd = tree.add_node(BtNodeType::Cooldown { cooldown: 1.0 });
3566 let attack = tree.add_node(BtNodeType::Attack {
3567 target_key: "target_pos".to_string(),
3568 damage: 10.0,
3569 range: 2.0,
3570 });
3571 let move_to_target = tree.add_node(BtNodeType::MoveTo {
3572 target_key: "target_pos".to_string(),
3573 speed: 4.0,
3574 acceptance_radius: 2.0,
3575 });
3576
3577 tree.add_child(root, combat_seq);
3578 tree.add_child(combat_seq, has_target);
3579 tree.add_child(combat_seq, in_range_check);
3580
3581 let attack_or_move = tree.add_node(BtNodeType::Selector);
3582 tree.add_child(combat_seq, attack_or_move);
3583 tree.add_child(attack_or_move, attack_cd);
3584 tree.add_child(attack_cd, attack);
3585 tree.add_child(attack_or_move, move_to_target);
3586
3587 let investigate_seq = tree.add_node(BtNodeType::Sequence);
3589 let heard_sound = tree.add_node(BtNodeType::BlackboardCheck {
3590 key: "heard_position".to_string(),
3591 op: CompareOp::Exists,
3592 value: BlackboardValue::None,
3593 });
3594 let move_to_sound = tree.add_node(BtNodeType::MoveTo {
3595 target_key: "heard_position".to_string(),
3596 speed: 3.0,
3597 acceptance_radius: 1.5,
3598 });
3599 let look_around = tree.add_node(BtNodeType::Wait { duration: 2.0 });
3600 let clear_heard = tree.add_node(BtNodeType::SetBlackboard {
3601 key: "heard_position".to_string(),
3602 value: BlackboardValue::None,
3603 });
3604 tree.add_child(root, investigate_seq);
3605 tree.add_child(investigate_seq, heard_sound);
3606 tree.add_child(investigate_seq, move_to_sound);
3607 tree.add_child(investigate_seq, look_around);
3608 tree.add_child(investigate_seq, clear_heard);
3609
3610 let patrol = tree.add_node(BtNodeType::Patrol {
3612 waypoints_key: "patrol_waypoints".to_string(),
3613 speed: 2.0,
3614 });
3615 tree.add_child(root, patrol);
3616
3617 tree
3618 }
3619
3620 pub fn flee_tree() -> BehaviorTree {
3622 let mut tree = BehaviorTree::new("FleeTakeCover");
3623
3624 let root = tree.add_node(BtNodeType::Sequence);
3625 tree.set_root(root);
3626
3627 let threat_check = tree.add_node(BtNodeType::BlackboardCheck {
3628 key: "threat".to_string(),
3629 op: CompareOp::Exists,
3630 value: BlackboardValue::None,
3631 });
3632 let find_cover = tree.add_node(BtNodeType::TakeCover {
3633 threat_key: "threat_pos".to_string(),
3634 result_key: "cover_pos".to_string(),
3635 });
3636 let move_to_cover = tree.add_node(BtNodeType::MoveTo {
3637 target_key: "cover_pos".to_string(),
3638 speed: 6.0,
3639 acceptance_radius: 1.0,
3640 });
3641 let wait_at_cover = tree.add_node(BtNodeType::Wait { duration: 3.0 });
3642 let alert = tree.add_node(BtNodeType::AlertAllies {
3643 radius: 20.0,
3644 message: "Enemy spotted!".to_string(),
3645 });
3646
3647 tree.add_child(root, threat_check);
3648 tree.add_child(root, find_cover);
3649 tree.add_child(root, move_to_cover);
3650 tree.add_child(root, wait_at_cover);
3651 tree.add_child(root, alert);
3652
3653 tree
3654 }
3655
3656 pub fn gather_tree() -> BehaviorTree {
3658 let mut tree = BehaviorTree::new("GatherResources");
3659 let root = tree.add_node(BtNodeType::Selector);
3660 tree.set_root(root);
3661
3662 let check_full = tree.add_node(BtNodeType::BlackboardCheck {
3664 key: "inventory_count".to_string(),
3665 op: CompareOp::GreaterOrEqual,
3666 value: BlackboardValue::Int(10),
3667 });
3668 let return_to_base = tree.add_node(BtNodeType::Sequence);
3669 let move_base = tree.add_node(BtNodeType::MoveTo {
3670 target_key: "base_pos".to_string(),
3671 speed: 3.5,
3672 acceptance_radius: 2.0,
3673 });
3674 let deposit = tree.add_node(BtNodeType::SetBlackboard {
3675 key: "inventory_count".to_string(),
3676 value: BlackboardValue::Int(0),
3677 });
3678 tree.add_child(root, return_to_base);
3679 tree.add_child(return_to_base, check_full);
3680 tree.add_child(return_to_base, move_base);
3681 tree.add_child(return_to_base, deposit);
3682
3683 let gather_seq = tree.add_node(BtNodeType::Sequence);
3685 let find_resource = tree.add_node(BtNodeType::FindTarget {
3686 radius: 20.0,
3687 faction_key: "resource".to_string(),
3688 result_key: "resource_pos".to_string(),
3689 });
3690 let move_to_res = tree.add_node(BtNodeType::MoveTo {
3691 target_key: "resource_pos".to_string(),
3692 speed: 3.5,
3693 acceptance_radius: 1.0,
3694 });
3695 let pickup = tree.add_node(BtNodeType::PickupItem {
3696 item_key: "resource".to_string(),
3697 });
3698 let inc_inv = tree.add_node(BtNodeType::IncrementBlackboard {
3699 key: "inventory_count".to_string(),
3700 amount: 1.0,
3701 });
3702 tree.add_child(root, gather_seq);
3703 tree.add_child(gather_seq, find_resource);
3704 tree.add_child(gather_seq, move_to_res);
3705 tree.add_child(gather_seq, pickup);
3706 tree.add_child(gather_seq, inc_inv);
3707
3708 let idle = tree.add_node(BtNodeType::Wait { duration: 1.0 });
3710 tree.add_child(root, idle);
3711
3712 tree
3713 }
3714}
3715
3716pub struct GoapLibrary;
3721
3722impl GoapLibrary {
3723 pub fn build_combat_planner() -> GoapPlanner {
3725 let mut planner = GoapPlanner::new();
3726 planner.label_bit(0, "has_ammo");
3736 planner.label_bit(1, "enemy_visible");
3737 planner.label_bit(2, "enemy_dead");
3738 planner.label_bit(3, "in_cover");
3739 planner.label_bit(4, "health_low");
3740 planner.label_bit(5, "has_medpack");
3741 planner.label_bit(6, "enemy_alerted");
3742
3743 let mut shoot = GoapAction::new(1, "Shoot");
3745 shoot.preconditions = (1 << 0) | (1 << 1); shoot.effects_clear = 1 << 1; shoot.effects_set = 1 << 2; shoot.cost = 1.0;
3749 planner.add_action(shoot);
3750
3751 let mut find_cover = GoapAction::new(2, "FindCover");
3753 find_cover.preconditions = 1 << 1; find_cover.effects_set = 1 << 3; find_cover.cost = 2.0;
3756 planner.add_action(find_cover);
3757
3758 let mut heal = GoapAction::new(3, "Heal");
3760 heal.preconditions = (1 << 4) | (1 << 5); heal.effects_clear = (1 << 4) | (1 << 5); heal.cost = 1.0;
3763 planner.add_action(heal);
3764
3765 let mut reload = GoapAction::new(4, "Reload");
3767 reload.preconditions_false = 1 << 0; reload.effects_set = 1 << 0; reload.cost = 1.5;
3770 planner.add_action(reload);
3771
3772 let mut patrol = GoapAction::new(5, "Patrol");
3774 patrol.preconditions = 0;
3775 patrol.effects_set = 1 << 1; patrol.cost = 3.0;
3777 planner.add_action(patrol);
3778
3779 let mut alert = GoapAction::new(6, "AlertAllies");
3781 alert.preconditions = 1 << 1; alert.effects_set = 1 << 6; alert.cost = 0.5;
3784 planner.add_action(alert);
3785
3786 let mut melee = GoapAction::new(7, "Melee");
3788 melee.preconditions = 1 << 1; melee.preconditions_false = 1 << 0; melee.effects_set = 1 << 2; melee.effects_clear = 1 << 1;
3792 melee.cost = 1.5;
3793 planner.add_action(melee);
3794
3795 planner
3796 }
3797}
3798
3799pub struct UtilityLibrary;
3804
3805impl UtilityLibrary {
3806 pub fn build_combat_decision_maker() -> UtilityDecisionMaker {
3807 let mut dm = UtilityDecisionMaker::new();
3808
3809 let mut attack = UtilityAction::new(1, "Attack");
3811 attack.considerations.push(Consideration {
3812 name: "health".to_string(),
3813 input_key: "self_health".to_string(),
3814 input_min: 0.0,
3815 input_max: 100.0,
3816 curve: ResponseCurve::Linear { slope: 1.0, intercept: 0.0 },
3817 weight: 1.0,
3818 });
3819 attack.considerations.push(Consideration {
3820 name: "enemy_visible".to_string(),
3821 input_key: "enemy_visible".to_string(),
3822 input_min: 0.0,
3823 input_max: 1.0,
3824 curve: ResponseCurve::Step { threshold: 0.5, low: 0.0, high: 1.0 },
3825 weight: 2.0,
3826 });
3827 attack.considerations.push(Consideration {
3828 name: "ammo".to_string(),
3829 input_key: "ammo_count".to_string(),
3830 input_min: 0.0,
3831 input_max: 30.0,
3832 curve: ResponseCurve::Smoothstep { edge0: 0.0, edge1: 0.5 },
3833 weight: 1.5,
3834 });
3835 dm.add_action(attack);
3836
3837 let mut flee = UtilityAction::new(2, "Flee");
3839 flee.considerations.push(Consideration {
3840 name: "health_low".to_string(),
3841 input_key: "self_health".to_string(),
3842 input_min: 0.0,
3843 input_max: 100.0,
3844 curve: ResponseCurve::Logistic { steepness: -10.0, midpoint: 0.3 },
3845 weight: 2.0,
3846 });
3847 flee.considerations.push(Consideration {
3848 name: "threat_distance".to_string(),
3849 input_key: "threat_dist".to_string(),
3850 input_min: 0.0,
3851 input_max: 20.0,
3852 curve: ResponseCurve::Inverse { scale: 0.3 },
3853 weight: 1.0,
3854 });
3855 dm.add_action(flee);
3856
3857 let mut heal = UtilityAction::new(3, "Heal");
3859 heal.considerations.push(Consideration {
3860 name: "need_heal".to_string(),
3861 input_key: "self_health".to_string(),
3862 input_min: 0.0,
3863 input_max: 100.0,
3864 curve: ResponseCurve::Logistic { steepness: -8.0, midpoint: 0.4 },
3865 weight: 2.0,
3866 });
3867 heal.considerations.push(Consideration {
3868 name: "has_medpack".to_string(),
3869 input_key: "medpack_count".to_string(),
3870 input_min: 0.0,
3871 input_max: 5.0,
3872 curve: ResponseCurve::Step { threshold: 0.15, low: 0.0, high: 1.0 },
3873 weight: 1.5,
3874 });
3875 dm.add_action(heal);
3876
3877 let mut patrol = UtilityAction::new(4, "Patrol");
3879 patrol.considerations.push(Consideration {
3880 name: "boredom".to_string(),
3881 input_key: "idle_time".to_string(),
3882 input_min: 0.0,
3883 input_max: 30.0,
3884 curve: ResponseCurve::Exponential { base: 2.0, exponent: 1.5, scale: 0.5 },
3885 weight: 1.0,
3886 });
3887 patrol.bonus_score = 0.1;
3888 dm.add_action(patrol);
3889
3890 let mut reload = UtilityAction::new(5, "Reload");
3892 reload.considerations.push(Consideration {
3893 name: "ammo_low".to_string(),
3894 input_key: "ammo_count".to_string(),
3895 input_min: 0.0,
3896 input_max: 30.0,
3897 curve: ResponseCurve::Logistic { steepness: -8.0, midpoint: 0.2 },
3898 weight: 2.0,
3899 });
3900 reload.considerations.push(Consideration {
3901 name: "not_in_danger".to_string(),
3902 input_key: "threat_dist".to_string(),
3903 input_min: 0.0,
3904 input_max: 20.0,
3905 curve: ResponseCurve::Smoothstep { edge0: 0.3, edge1: 0.8 },
3906 weight: 1.0,
3907 });
3908 dm.add_action(reload);
3909
3910 dm
3911 }
3912}
3913
3914#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3919pub enum AiAgentMode {
3920 BehaviorTree,
3921 UtilityAi,
3922 Goap,
3923 Fsm,
3924 Hybrid, }
3926
3927pub struct AiAgent {
3928 pub id: u64,
3929 pub name: String,
3930 pub position: Vec3,
3931 pub velocity: Vec3,
3932 pub heading: Vec3,
3933 pub blackboard: Blackboard,
3934 pub mode: AiAgentMode,
3935 pub behavior_tree: Option<BehaviorTree>,
3936 pub utility_dm: Option<UtilityDecisionMaker>,
3937 pub goap_planner: Option<GoapPlanner>,
3938 pub goap_world_state: WorldState,
3939 pub goap_goal_state: WorldState,
3940 pub goap_current_plan: Option<Vec<usize>>,
3941 pub goap_plan_step: usize,
3942 pub fsm: Option<FsmInstance>,
3943 pub perception: PerceptionSystem,
3944 pub emotion_engine: EmotionEngine,
3945 pub steering_agent: SteeringAgent,
3946 pub formation_slot: Option<Vec3>,
3947 pub formation_type: FormationType,
3948 pub current_time: f32,
3949 pub debug_enabled: bool,
3950}
3951
3952impl AiAgent {
3953 pub fn new(id: u64, name: &str, position: Vec3, mode: AiAgentMode) -> Self {
3954 Self {
3955 id,
3956 name: name.to_string(),
3957 position,
3958 velocity: Vec3::ZERO,
3959 heading: Vec3::Z,
3960 blackboard: Blackboard::new(),
3961 mode,
3962 behavior_tree: None,
3963 utility_dm: None,
3964 goap_planner: None,
3965 goap_world_state: 0,
3966 goap_goal_state: 0,
3967 goap_current_plan: None,
3968 goap_plan_step: 0,
3969 fsm: None,
3970 perception: PerceptionSystem::new(id),
3971 emotion_engine: EmotionEngine::new(),
3972 steering_agent: SteeringAgent::new(id, position, 5.0, 10.0),
3973 formation_slot: None,
3974 formation_type: FormationType::Line,
3975 current_time: 0.0,
3976 debug_enabled: false,
3977 }
3978 }
3979
3980 pub fn update(&mut self, dt: f32, obstacles: &[Aabb]) {
3981 self.current_time += dt;
3982 self.blackboard.advance_time(dt as f64);
3983 self.blackboard.set("agent_position", BlackboardValue::Vec3(self.position));
3984 self.blackboard.set("current_time", BlackboardValue::Float(self.current_time));
3985
3986 self.emotion_engine.update(dt, self.current_time);
3988 let modifiers = self.emotion_engine.get_modifier();
3989 self.emotion_engine.state.serialize_to_blackboard(&mut self.blackboard, "emotion");
3990 self.blackboard.set("speed_mult", BlackboardValue::Float(modifiers.speed_multiplier));
3991
3992 match self.mode {
3993 AiAgentMode::BehaviorTree => {
3994 if let Some(ref mut tree) = self.behavior_tree {
3995 let mut ctx = BtTickContext::new(
3996 &mut self.blackboard, dt, self.current_time, self.position, self.id
3997 );
3998 tree.tick(&mut ctx);
3999 self.position = ctx.agent_position;
4000 }
4001 }
4002 AiAgentMode::UtilityAi => {
4003 if let Some(ref mut dm) = self.utility_dm {
4004 let selected = dm.evaluate(&self.blackboard, self.current_time);
4005 if let Some(action_id) = selected {
4006 self.blackboard.set("utility_selected_action", BlackboardValue::Int(action_id as i64));
4007 }
4008 }
4009 }
4010 AiAgentMode::Goap => {
4011 self.tick_goap(dt);
4012 }
4013 AiAgentMode::Fsm => {
4014 if let Some(ref mut fsm) = self.fsm {
4015 fsm.tick(&mut self.blackboard, dt, self.current_time);
4016 }
4017 }
4018 AiAgentMode::Hybrid => {
4019 if let Some(ref mut dm) = self.utility_dm {
4021 dm.evaluate(&self.blackboard, self.current_time);
4022 }
4023 if let Some(ref mut tree) = self.behavior_tree {
4024 let mut ctx = BtTickContext::new(
4025 &mut self.blackboard, dt, self.current_time, self.position, self.id
4026 );
4027 tree.tick(&mut ctx);
4028 self.position = ctx.agent_position;
4029 }
4030 }
4031 }
4032
4033 self.steering_agent.position = self.position;
4035 self.steering_agent.velocity = self.velocity;
4036 }
4037
4038 fn tick_goap(&mut self, dt: f32) {
4039 let planner = match &self.goap_planner { Some(p) => p, None => return };
4040
4041 if self.goap_current_plan.is_none() || self.goap_plan_step >= self.goap_current_plan.as_ref().map(|p| p.len()).unwrap_or(0) {
4043 let plan = planner.plan(self.goap_world_state, self.goap_goal_state, self.current_time);
4044 self.goap_current_plan = plan;
4045 self.goap_plan_step = 0;
4046 }
4047
4048 if let Some(ref plan) = self.goap_current_plan {
4049 if self.goap_plan_step < plan.len() {
4050 let action_idx = plan[self.goap_plan_step];
4051 if action_idx < planner.actions.len() {
4052 let action = &planner.actions[action_idx];
4053 self.goap_world_state = action.apply(self.goap_world_state);
4055 self.blackboard.set("goap_action", BlackboardValue::String(action.name.clone()));
4056 self.goap_plan_step += 1;
4057 }
4058 }
4059 }
4060 }
4061}
4062
4063pub struct AiBehaviorEditor {
4068 pub active_tree_index: usize,
4070 pub behavior_trees: Vec<BehaviorTree>,
4071 pub bt_layout: ReingoldTilford,
4072 pub bt_selection: GraphSelection,
4073 pub bt_camera: NodeGraphCamera,
4074 pub bt_connection_draft: Option<ConnectionDraft>,
4075 pub bt_tool: EditorTool,
4076 pub bt_node_palette: Vec<(String, BtNodeType)>,
4077 pub bt_comments: Vec<NodeComment>,
4078 pub bt_undo_stack: Vec<BtUndoEntry>,
4079 pub bt_redo_stack: Vec<BtUndoEntry>,
4080
4081 pub active_fsm_index: usize,
4083 pub fsm_instances: Vec<FsmInstance>,
4084 pub fsm_camera: NodeGraphCamera,
4085 pub fsm_selection: HashSet<u32>,
4086 pub fsm_tool: EditorTool,
4087 pub fsm_transition_draft: Option<(u32, Vec2)>,
4088
4089 pub goap_planner: GoapPlanner,
4091 pub goap_world_state: WorldState,
4092 pub goap_goal_state: WorldState,
4093 pub goap_last_plan: Option<Vec<usize>>,
4094 pub goap_action_editor_open: bool,
4095 pub goap_selected_action: Option<u32>,
4096
4097 pub utility_dm: UtilityDecisionMaker,
4099 pub utility_selected_action: Option<u32>,
4100 pub utility_curve_editor_open: bool,
4101 pub utility_selected_consideration: Option<(u32, usize)>, pub utility_curve_preview_points: Vec<Vec2>,
4103
4104 pub perception_systems: Vec<PerceptionSystem>,
4106 pub selected_perception_agent: Option<u64>,
4107 pub perception_debug_draw: bool,
4108
4109 pub formation_preview: FormationType,
4111 pub formation_n_agents: usize,
4112 pub formation_spacing: f32,
4113 pub formation_preview_slots: Vec<Vec3>,
4114
4115 pub steering_agents: Vec<SteeringAgent>,
4117 pub steering_debug_draw: bool,
4118 pub steering_selected_agent: Option<u64>,
4119
4120 pub emotion_engines: Vec<EmotionEngine>,
4122 pub selected_emotion_agent: usize,
4123 pub emotion_debug_draw: bool,
4124
4125 pub blackboard_inspector: BlackboardInspector,
4127 pub shared_blackboard: Blackboard,
4128
4129 pub debug_buffer: DebugVisualizationBuffer,
4131 pub show_debug_panel: bool,
4132
4133 pub agents: Vec<AiAgent>,
4135 pub selected_agent_id: Option<u64>,
4136 pub simulation_running: bool,
4137 pub simulation_speed: f32,
4138 pub obstacles: Vec<Aabb>,
4139 pub flow_field: HashMap<(i32, i32), Vec3>,
4140
4141 pub current_time: f32,
4143 pub frame_dt: f32,
4144 pub panel_sizes: HashMap<String, Vec2>,
4145 pub theme_color: Vec4,
4146 pub font_size: f32,
4147 pub grid_visible: bool,
4148 pub grid_size: f32,
4149 pub snap_to_grid: bool,
4150 pub status_message: String,
4151 pub status_timer: f32,
4152}
4153
4154#[derive(Clone, Debug)]
4156pub enum BtUndoEntry {
4157 AddNode { tree_idx: usize, node_id: u32, node: BtNode },
4158 RemoveNode { tree_idx: usize, node_id: u32, node: BtNode },
4159 AddChild { tree_idx: usize, parent_id: u32, child_id: u32, index: usize },
4160 RemoveChild { tree_idx: usize, parent_id: u32, child_id: u32 },
4161 MoveNode { tree_idx: usize, node_id: u32, old_pos: Vec2, new_pos: Vec2 },
4162 ChangeNodeType { tree_idx: usize, node_id: u32, old_type: BtNodeType, new_type: BtNodeType },
4163}
4164
4165impl AiBehaviorEditor {
4166 pub fn new() -> Self {
4167 let mut editor = Self {
4168 active_tree_index: 0,
4169 behavior_trees: Vec::new(),
4170 bt_layout: ReingoldTilford::new(),
4171 bt_selection: GraphSelection::new(),
4172 bt_camera: NodeGraphCamera::new(),
4173 bt_connection_draft: None,
4174 bt_tool: EditorTool::Select,
4175 bt_node_palette: Self::build_node_palette(),
4176 bt_comments: Vec::new(),
4177 bt_undo_stack: Vec::with_capacity(64),
4178 bt_redo_stack: Vec::with_capacity(64),
4179
4180 active_fsm_index: 0,
4181 fsm_instances: Vec::new(),
4182 fsm_camera: NodeGraphCamera::new(),
4183 fsm_selection: HashSet::new(),
4184 fsm_tool: EditorTool::Select,
4185 fsm_transition_draft: None,
4186
4187 goap_planner: GoapLibrary::build_combat_planner(),
4188 goap_world_state: 0b0000_0001, goap_goal_state: 0b0000_0100, goap_last_plan: None,
4191 goap_action_editor_open: false,
4192 goap_selected_action: None,
4193
4194 utility_dm: UtilityLibrary::build_combat_decision_maker(),
4195 utility_selected_action: None,
4196 utility_curve_editor_open: false,
4197 utility_selected_consideration: None,
4198 utility_curve_preview_points: Vec::new(),
4199
4200 perception_systems: Vec::new(),
4201 selected_perception_agent: None,
4202 perception_debug_draw: true,
4203
4204 formation_preview: FormationType::Wedge,
4205 formation_n_agents: 8,
4206 formation_spacing: 2.0,
4207 formation_preview_slots: Vec::new(),
4208
4209 steering_agents: Vec::new(),
4210 steering_debug_draw: true,
4211 steering_selected_agent: None,
4212
4213 emotion_engines: Vec::new(),
4214 selected_emotion_agent: 0,
4215 emotion_debug_draw: false,
4216
4217 blackboard_inspector: BlackboardInspector::new(),
4218 shared_blackboard: Blackboard::new(),
4219
4220 debug_buffer: DebugVisualizationBuffer::new(4096),
4221 show_debug_panel: true,
4222
4223 agents: Vec::new(),
4224 selected_agent_id: None,
4225 simulation_running: false,
4226 simulation_speed: 1.0,
4227 obstacles: Vec::new(),
4228 flow_field: HashMap::new(),
4229
4230 current_time: 0.0,
4231 frame_dt: 0.0,
4232 panel_sizes: HashMap::new(),
4233 theme_color: Vec4::new(0.18, 0.2, 0.25, 1.0),
4234 font_size: 14.0,
4235 grid_visible: true,
4236 grid_size: 20.0,
4237 snap_to_grid: false,
4238 status_message: String::new(),
4239 status_timer: 0.0,
4240 };
4241
4242 editor.behavior_trees.push(BtTemplates::combat_patrol_tree());
4244 editor.behavior_trees.push(BtTemplates::flee_tree());
4245 editor.behavior_trees.push(BtTemplates::gather_tree());
4246
4247 let mut fsm = FsmInstance::new(1, "CombatFsm");
4249 let patrol_s = fsm.add_state("Patrol");
4250 let engage_s = fsm.add_state("Engage");
4251 let cover_s = fsm.add_state("TakeCover");
4252 let dead_s = fsm.add_state("Dead");
4253 fsm.set_initial(patrol_s);
4254 if let Some(s) = fsm.states.get_mut(&dead_s) { s.is_final = true; }
4255 fsm.add_transition(patrol_s, engage_s, FsmConditionOp::BlackboardBool {
4256 key: "enemy_visible".to_string(), expected: true
4257 }, 10);
4258 fsm.add_transition(engage_s, cover_s, FsmConditionOp::BlackboardCompare {
4259 key: "self_health".to_string(),
4260 op: CompareOp::LessThan,
4261 value: BlackboardValue::Float(0.3),
4262 }, 20);
4263 fsm.add_transition(cover_s, engage_s, FsmConditionOp::TimeElapsed { duration: 5.0 }, 5);
4264 fsm.add_transition(engage_s, patrol_s, FsmConditionOp::BlackboardBool {
4265 key: "enemy_visible".to_string(), expected: false
4266 }, 5);
4267 fsm.add_transition(engage_s, dead_s, FsmConditionOp::BlackboardCompare {
4268 key: "self_health".to_string(),
4269 op: CompareOp::LessOrEqual,
4270 value: BlackboardValue::Float(0.0),
4271 }, 100);
4272 fsm.auto_layout();
4273 editor.fsm_instances.push(fsm);
4274
4275 for tree in &mut editor.behavior_trees {
4277 editor.bt_layout.layout(tree);
4278 }
4279
4280 for i in 0..4 {
4282 let pos = Vec3::new(i as f32 * 5.0, 0.0, 0.0);
4283 let mut agent = AiAgent::new(i as u64 + 1, &format!("Agent_{}", i), pos, AiAgentMode::BehaviorTree);
4284 agent.behavior_tree = Some(BtTemplates::combat_patrol_tree());
4285 agent.blackboard.set("self_health", BlackboardValue::Float(1.0));
4286 agent.blackboard.set("ammo_count", BlackboardValue::Float(30.0));
4287 for wp_i in 0..4 {
4289 let wp_pos = Vec3::new(
4290 pos.x + (wp_i as f32 * 4.0 - 8.0),
4291 0.0,
4292 ((wp_i as f32 + 0.5) * PI * 0.5).sin() * 5.0,
4293 );
4294 agent.blackboard.set(
4295 &format!("patrol_waypoints_{}", wp_i),
4296 BlackboardValue::Vec3(wp_pos),
4297 );
4298 }
4299 editor.agents.push(agent);
4300 }
4301
4302 editor.obstacles.push(Aabb::new(Vec3::new(10.0, 0.0, 0.0), Vec3::new(2.0, 1.0, 2.0)));
4304 editor.obstacles.push(Aabb::new(Vec3::new(-5.0, 0.0, 8.0), Vec3::new(1.5, 1.0, 1.5)));
4305
4306 editor.recompute_formation_preview();
4308
4309 editor.goap_last_plan = editor.goap_planner.plan(
4311 editor.goap_world_state,
4312 editor.goap_goal_state,
4313 0.0,
4314 );
4315
4316 editor
4317 }
4318
4319 fn build_node_palette() -> Vec<(String, BtNodeType)> {
4320 vec![
4321 ("Sequence".to_string(), BtNodeType::Sequence),
4322 ("Selector".to_string(), BtNodeType::Selector),
4323 ("Parallel (All)".to_string(), BtNodeType::ParallelAll),
4324 ("Parallel (Any)".to_string(), BtNodeType::ParallelAny),
4325 ("Random Selector".to_string(), BtNodeType::RandomSelector),
4326 ("Random Sequence".to_string(), BtNodeType::RandomSequence),
4327 ("Inverter".to_string(), BtNodeType::Inverter),
4328 ("Repeater x3".to_string(), BtNodeType::Repeater { times: 3 }),
4329 ("Repeat Forever".to_string(), BtNodeType::RepeatForever),
4330 ("Retry Until Success".to_string(), BtNodeType::RetryUntilSuccess { max_retries: 5 }),
4331 ("Timeout 5s".to_string(), BtNodeType::Timeout { duration: 5.0 }),
4332 ("Cooldown 2s".to_string(), BtNodeType::Cooldown { cooldown: 2.0 }),
4333 ("Succeeder".to_string(), BtNodeType::Succeeder),
4334 ("Failer".to_string(), BtNodeType::Failer),
4335 ("Until Fail".to_string(), BtNodeType::UntilFail),
4336 ("Until Success".to_string(), BtNodeType::UntilSuccess),
4337 ("BB Check".to_string(), BtNodeType::BlackboardCheck {
4338 key: "var".to_string(), op: CompareOp::GreaterThan, value: BlackboardValue::Float(0.0)
4339 }),
4340 ("BB Guard".to_string(), BtNodeType::BlackboardGuard { key: "var".to_string() }),
4341 ("Move To".to_string(), BtNodeType::MoveTo { target_key: "target_pos".to_string(), speed: 3.5, acceptance_radius: 1.0 }),
4342 ("Attack".to_string(), BtNodeType::Attack { target_key: "target_pos".to_string(), damage: 10.0, range: 2.0 }),
4343 ("Play Animation".to_string(), BtNodeType::PlayAnimation { clip: "idle".to_string(), layer: 0, blend_time: 0.2 }),
4344 ("Set Blackboard".to_string(), BtNodeType::SetBlackboard { key: "var".to_string(), value: BlackboardValue::Bool(true) }),
4345 ("Increment BB".to_string(), BtNodeType::IncrementBlackboard { key: "counter".to_string(), amount: 1.0 }),
4346 ("Wait 1s".to_string(), BtNodeType::Wait { duration: 1.0 }),
4347 ("Log".to_string(), BtNodeType::Log { message: "Hello".to_string() }),
4348 ("Idle".to_string(), BtNodeType::Idle),
4349 ("Find Target".to_string(), BtNodeType::FindTarget { radius: 15.0, faction_key: "enemy".to_string(), result_key: "target".to_string() }),
4350 ("Flee".to_string(), BtNodeType::Flee { threat_key: "threat_pos".to_string(), speed: 6.0, distance: 10.0 }),
4351 ("Patrol".to_string(), BtNodeType::Patrol { waypoints_key: "waypoints".to_string(), speed: 2.5 }),
4352 ("Take Cover".to_string(), BtNodeType::TakeCover { threat_key: "threat_pos".to_string(), result_key: "cover_pos".to_string() }),
4353 ("Alert Allies".to_string(), BtNodeType::AlertAllies { radius: 20.0, message: "Alert!".to_string() }),
4354 ("Play Sound".to_string(), BtNodeType::PlaySound { sound: "alert.wav".to_string(), volume: 1.0 }),
4355 ("Send Event".to_string(), BtNodeType::SendEvent { event_name: "on_spotted".to_string(), payload_key: "target_id".to_string() }),
4356 ("Succeed".to_string(), BtNodeType::SucceedAlways),
4357 ("Fail".to_string(), BtNodeType::FailAlways),
4358 ]
4359 }
4360
4361 pub fn bt_add_node(&mut self, tree_idx: usize, node_type: BtNodeType) -> Option<u32> {
4364 let tree = self.behavior_trees.get_mut(tree_idx)?;
4365 let id = tree.add_node(node_type.clone());
4366 self.bt_undo_stack.push(BtUndoEntry::AddNode {
4367 tree_idx,
4368 node_id: id,
4369 node: tree.nodes[&id].clone(),
4370 });
4371 self.bt_redo_stack.clear();
4372 Some(id)
4373 }
4374
4375 pub fn bt_remove_node(&mut self, tree_idx: usize, node_id: u32) {
4376 let tree = match self.behavior_trees.get_mut(tree_idx) { Some(t) => t, None => return };
4377 if let Some(node) = tree.nodes.remove(&node_id) {
4378 if let Some(parent_id) = node.parent {
4380 if let Some(parent) = tree.nodes.get_mut(&parent_id) {
4381 parent.children.retain(|&c| c != node_id);
4382 }
4383 }
4384 for child_id in &node.children {
4386 if let Some(child) = tree.nodes.get_mut(child_id) {
4387 child.parent = None;
4388 }
4389 }
4390 if tree.root_id == Some(node_id) { tree.root_id = None; }
4392 self.bt_undo_stack.push(BtUndoEntry::RemoveNode { tree_idx, node_id, node });
4393 }
4394 }
4395
4396 pub fn bt_connect_nodes(&mut self, tree_idx: usize, parent_id: u32, child_id: u32) {
4397 let tree = match self.behavior_trees.get_mut(tree_idx) { Some(t) => t, None => return };
4398 let old_parent = tree.nodes.get(&child_id).and_then(|n| n.parent);
4400 if let Some(op) = old_parent {
4401 if let Some(op_node) = tree.nodes.get_mut(&op) {
4402 op_node.children.retain(|&c| c != child_id);
4403 }
4404 }
4405 let child_idx = tree.nodes.get(&parent_id).map(|n| n.children.len()).unwrap_or(0);
4406 tree.add_child(parent_id, child_id);
4407 self.bt_undo_stack.push(BtUndoEntry::AddChild { tree_idx, parent_id, child_id, index: child_idx });
4408 self.bt_redo_stack.clear();
4409 if let Some(tree) = self.behavior_trees.get_mut(tree_idx) {
4411 self.bt_layout.layout(tree);
4412 }
4413 }
4414
4415 pub fn bt_move_node(&mut self, tree_idx: usize, node_id: u32, new_pos: Vec2) {
4416 let tree = match self.behavior_trees.get_mut(tree_idx) { Some(t) => t, None => return };
4417 let old_pos = tree.nodes.get(&node_id).map(|n| n.position).unwrap_or(Vec2::ZERO);
4418 if let Some(node) = tree.nodes.get_mut(&node_id) {
4419 node.position = if self.snap_to_grid {
4420 let g = self.grid_size;
4421 Vec2::new((new_pos.x / g).round() * g, (new_pos.y / g).round() * g)
4422 } else { new_pos };
4423 }
4424 self.bt_undo_stack.push(BtUndoEntry::MoveNode { tree_idx, node_id, old_pos, new_pos });
4425 self.bt_redo_stack.clear();
4426 }
4427
4428 pub fn bt_auto_layout(&mut self, tree_idx: usize) {
4429 if let Some(tree) = self.behavior_trees.get_mut(tree_idx) {
4430 self.bt_layout.layout(tree);
4431 }
4432 }
4433
4434 pub fn bt_undo(&mut self) {
4435 if let Some(entry) = self.bt_undo_stack.pop() {
4436 match &entry {
4437 BtUndoEntry::AddNode { tree_idx, node_id, .. } => {
4438 let t = *tree_idx;
4439 let nid = *node_id;
4440 self.bt_remove_node(t, nid);
4441 }
4442 BtUndoEntry::RemoveNode { tree_idx, node_id, node } => {
4443 let t = *tree_idx;
4444 let nid = *node_id;
4445 let n = node.clone();
4446 if let Some(tree) = self.behavior_trees.get_mut(t) {
4447 tree.nodes.insert(nid, n);
4448 }
4449 }
4450 BtUndoEntry::MoveNode { tree_idx, node_id, old_pos, .. } => {
4451 let t = *tree_idx;
4452 let nid = *node_id;
4453 let op = *old_pos;
4454 if let Some(tree) = self.behavior_trees.get_mut(t) {
4455 if let Some(node) = tree.nodes.get_mut(&nid) {
4456 node.position = op;
4457 }
4458 }
4459 }
4460 _ => {}
4461 }
4462 self.bt_redo_stack.push(entry);
4463 }
4464 }
4465
4466 pub fn bt_redo(&mut self) {
4467 if let Some(entry) = self.bt_redo_stack.pop() {
4468 match &entry {
4469 BtUndoEntry::MoveNode { tree_idx, node_id, new_pos, .. } => {
4470 let t = *tree_idx;
4471 let nid = *node_id;
4472 let np = *new_pos;
4473 self.bt_move_node(t, nid, np);
4474 }
4475 _ => {}
4476 }
4477 }
4478 }
4479
4480 pub fn bt_duplicate_node(&mut self, tree_idx: usize, node_id: u32) -> Option<u32> {
4481 let tree = self.behavior_trees.get(tree_idx)?;
4482 let original = tree.nodes.get(&node_id)?.clone();
4483 let new_node_type = original.node_type.clone();
4484 let new_id = self.bt_add_node(tree_idx, new_node_type)?;
4485 let tree = self.behavior_trees.get_mut(tree_idx)?;
4486 if let Some(new_node) = tree.nodes.get_mut(&new_id) {
4487 new_node.position = original.position + Vec2::new(REINGOLD_NODE_WIDTH + 10.0, 0.0);
4488 }
4489 Some(new_id)
4490 }
4491
4492 pub fn bt_select_all(&mut self, tree_idx: usize) {
4493 if let Some(tree) = self.behavior_trees.get(tree_idx) {
4494 self.bt_selection.selected_nodes = tree.nodes.keys().copied().collect();
4495 }
4496 }
4497
4498 pub fn bt_delete_selected(&mut self, tree_idx: usize) {
4499 let selected: Vec<u32> = self.bt_selection.selected_nodes.iter().copied().collect();
4500 for id in selected {
4501 self.bt_remove_node(tree_idx, id);
4502 }
4503 self.bt_selection.clear();
4504 if let Some(tree) = self.behavior_trees.get_mut(tree_idx) {
4505 self.bt_layout.layout(tree);
4506 }
4507 }
4508
4509 pub fn bt_hit_test(&self, tree_idx: usize, world_pos: Vec2) -> Option<u32> {
4510 let tree = self.behavior_trees.get(tree_idx)?;
4511 for (id, node) in &tree.nodes {
4512 let min = node.position;
4513 let max = node.position + node.size;
4514 if world_pos.x >= min.x && world_pos.x <= max.x
4515 && world_pos.y >= min.y && world_pos.y <= max.y {
4516 return Some(*id);
4517 }
4518 }
4519 None
4520 }
4521
4522 pub fn bt_get_node_color(&self, node: &BtNode) -> Vec4 {
4523 if node.is_selected {
4524 return Vec4::new(1.0, 0.9, 0.3, 1.0);
4525 }
4526 match node.status {
4527 BtStatus::Success => Vec4::new(0.2, 0.7, 0.2, 1.0),
4528 BtStatus::Failure => Vec4::new(0.7, 0.2, 0.2, 1.0),
4529 BtStatus::Running => Vec4::new(0.7, 0.7, 0.1, 1.0),
4530 BtStatus::Invalid => {
4531 if node.is_composite() { Vec4::new(0.3, 0.4, 0.7, 1.0) }
4532 else if node.is_decorator() { Vec4::new(0.5, 0.3, 0.7, 1.0) }
4533 else { Vec4::new(0.2, 0.5, 0.3, 1.0) }
4534 }
4535 }
4536 }
4537
4538 pub fn bt_get_bezier_control_points(from: Vec2, to: Vec2) -> (Vec2, Vec2, Vec2, Vec2) {
4539 let mid_y = (from.y + to.y) * 0.5;
4540 let p0 = from;
4541 let p1 = Vec2::new(from.x, mid_y);
4542 let p2 = Vec2::new(to.x, mid_y);
4543 let p3 = to;
4544 (p0, p1, p2, p3)
4545 }
4546
4547 pub fn bt_bezier_point(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) -> Vec2 {
4548 let u = 1.0 - t;
4549 p0 * (u * u * u)
4550 + p1 * (3.0 * u * u * t)
4551 + p2 * (3.0 * u * t * t)
4552 + p3 * (t * t * t)
4553 }
4554
4555 pub fn bt_get_edge_polyline(from: Vec2, to: Vec2, num_points: usize) -> Vec<Vec2> {
4556 let (p0, p1, p2, p3) = Self::bt_get_bezier_control_points(from, to);
4557 (0..num_points).map(|i| {
4558 let t = i as f32 / (num_points - 1).max(1) as f32;
4559 Self::bt_bezier_point(p0, p1, p2, p3, t)
4560 }).collect()
4561 }
4562
4563 pub fn fsm_add_state(&mut self, fsm_idx: usize, name: &str, pos: Vec2) -> Option<u32> {
4566 let fsm = self.fsm_instances.get_mut(fsm_idx)?;
4567 let id = fsm.add_state(name);
4568 if let Some(s) = fsm.states.get_mut(&id) { s.position = pos; }
4569 Some(id)
4570 }
4571
4572 pub fn fsm_add_transition(&mut self, fsm_idx: usize, from: u32, to: u32, condition: FsmConditionOp, priority: i32) -> Option<u32> {
4573 let fsm = self.fsm_instances.get_mut(fsm_idx)?;
4574 let id = fsm.add_transition(from, to, condition, priority);
4575 Some(id)
4576 }
4577
4578 pub fn fsm_remove_state(&mut self, fsm_idx: usize, state_id: u32) {
4579 let fsm = match self.fsm_instances.get_mut(fsm_idx) { Some(f) => f, None => return };
4580 fsm.states.remove(&state_id);
4581 fsm.transitions.retain(|t| t.from_state != state_id && t.to_state != state_id);
4582 }
4583
4584 pub fn fsm_remove_transition(&mut self, fsm_idx: usize, transition_id: u32) {
4585 let fsm = match self.fsm_instances.get_mut(fsm_idx) { Some(f) => f, None => return };
4586 fsm.transitions.retain(|t| t.id != transition_id);
4587 }
4588
4589 pub fn fsm_transition_midpoint(&self, fsm_idx: usize, transition: &FsmTransition) -> Vec2 {
4590 let fsm = match self.fsm_instances.get(fsm_idx) { Some(f) => f, None => return Vec2::ZERO };
4591 let from_pos = fsm.states.get(&transition.from_state).map(|s| s.position).unwrap_or(Vec2::ZERO);
4592 let to_pos = fsm.states.get(&transition.to_state).map(|s| s.position).unwrap_or(Vec2::ZERO);
4593 (from_pos + to_pos) * 0.5
4594 }
4595
4596 pub fn fsm_hit_test_state(&self, fsm_idx: usize, world_pos: Vec2, state_radius: f32) -> Option<u32> {
4597 let fsm = self.fsm_instances.get(fsm_idx)?;
4598 for (id, state) in &fsm.states {
4599 if (state.position - world_pos).length() <= state_radius {
4600 return Some(*id);
4601 }
4602 }
4603 None
4604 }
4605
4606 pub fn goap_add_action(&mut self, action: GoapAction) {
4609 self.goap_planner.add_action(action);
4610 self.goap_replan();
4611 }
4612
4613 pub fn goap_remove_action(&mut self, action_id: u32) {
4614 self.goap_planner.actions.retain(|a| a.id != action_id);
4615 self.goap_replan();
4616 }
4617
4618 pub fn goap_replan(&mut self) {
4619 self.goap_last_plan = self.goap_planner.plan(
4620 self.goap_world_state,
4621 self.goap_goal_state,
4622 self.current_time,
4623 );
4624 }
4625
4626 pub fn goap_toggle_world_state_bit(&mut self, bit: u8) {
4627 self.goap_world_state ^= 1 << bit;
4628 self.goap_replan();
4629 }
4630
4631 pub fn goap_toggle_goal_bit(&mut self, bit: u8) {
4632 self.goap_goal_state ^= 1 << bit;
4633 self.goap_replan();
4634 }
4635
4636 pub fn goap_plan_to_names(&self) -> Vec<String> {
4637 if let Some(ref plan) = self.goap_last_plan {
4638 plan.iter().filter_map(|&idx| {
4639 self.goap_planner.actions.get(idx).map(|a| a.name.clone())
4640 }).collect()
4641 } else {
4642 vec!["[No plan found]".to_string()]
4643 }
4644 }
4645
4646 pub fn goap_plan_total_cost(&self) -> f32 {
4647 if let Some(ref plan) = self.goap_last_plan {
4648 plan.iter().filter_map(|&idx| {
4649 self.goap_planner.actions.get(idx).map(|a| a.cost)
4650 }).sum()
4651 } else {
4652 0.0
4653 }
4654 }
4655
4656 pub fn goap_simulate_plan_states(&self) -> Vec<(String, WorldState)> {
4657 let plan = match &self.goap_last_plan { Some(p) => p, None => return vec![] };
4658 let mut state = self.goap_world_state;
4659 let mut result = vec![("Start".to_string(), state)];
4660 for &idx in plan {
4661 if let Some(action) = self.goap_planner.actions.get(idx) {
4662 state = action.apply(state);
4663 result.push((action.name.clone(), state));
4664 }
4665 }
4666 result
4667 }
4668
4669 pub fn utility_update_curve_preview(&mut self) {
4672 if let Some((action_id, consideration_idx)) = self.utility_selected_consideration {
4673 if let Some(action) = self.utility_dm.actions.iter().find(|a| a.id == action_id) {
4674 if let Some(consideration) = action.considerations.get(consideration_idx) {
4675 self.utility_curve_preview_points = consideration.curve.sample_points(64);
4676 }
4677 }
4678 }
4679 }
4680
4681 pub fn utility_score_all(&self) -> Vec<(u32, String, f32)> {
4682 self.utility_dm.actions.iter().map(|a| {
4683 let score = a.score(&self.shared_blackboard, self.current_time);
4684 (a.id, a.name.clone(), score)
4685 }).collect()
4686 }
4687
4688 pub fn utility_set_consideration_curve(
4689 &mut self,
4690 action_id: u32,
4691 consideration_idx: usize,
4692 curve: ResponseCurve,
4693 ) {
4694 if let Some(action) = self.utility_dm.actions.iter_mut().find(|a| a.id == action_id) {
4695 if let Some(c) = action.considerations.get_mut(consideration_idx) {
4696 c.curve = curve;
4697 }
4698 }
4699 self.utility_update_curve_preview();
4700 }
4701
4702 pub fn recompute_formation_preview(&mut self) {
4705 self.formation_preview_slots = FormationLayout::compute_slots(
4706 self.formation_preview,
4707 Vec3::ZERO,
4708 Vec3::Z,
4709 self.formation_n_agents,
4710 self.formation_spacing,
4711 );
4712 }
4713
4714 pub fn set_formation(&mut self, formation: FormationType) {
4715 self.formation_preview = formation;
4716 self.recompute_formation_preview();
4717 }
4718
4719 pub fn get_formation_debug_lines(&self) -> Vec<(Vec3, Vec3)> {
4720 let mut lines = Vec::new();
4721 let leader = Vec3::ZERO;
4722 for slot in &self.formation_preview_slots {
4723 lines.push((leader, *slot));
4724 }
4725 lines
4726 }
4727
4728 pub fn add_perception_system(&mut self, agent_id: u64) {
4731 self.perception_systems.push(PerceptionSystem::new(agent_id));
4732 }
4733
4734 pub fn perception_debug_draw(&mut self, agent_idx: usize, observer_pos: Vec3, observer_fwd: Vec3) {
4735 if agent_idx >= self.perception_systems.len() { return; }
4736 let ps = &self.perception_systems[agent_idx];
4737 let color_vision = Vec4::new(0.3, 0.8, 0.3, 0.7);
4738 let color_hearing = Vec4::new(0.3, 0.3, 0.8, 0.5);
4739 self.debug_buffer.draw_vision_cone(
4740 observer_pos, observer_fwd,
4741 ps.vision.half_angle, ps.vision.range, color_vision
4742 );
4743 self.debug_buffer.draw_hearing_radius(observer_pos, ps.hearing.base_radius, color_hearing);
4744 }
4745
4746 pub fn add_steering_agent(&mut self, id: u64, pos: Vec3) {
4749 self.steering_agents.push(SteeringAgent::new(id, pos, 5.0, 10.0));
4750 }
4751
4752 pub fn tick_steering_agents(&mut self, dt: f32) {
4753 let n = self.steering_agents.len();
4754 if n == 0 { return; }
4755
4756 let mut forces: Vec<Vec3> = vec![Vec3::ZERO; n];
4757 let agents_clone: Vec<SteeringAgent> = self.steering_agents.clone();
4758 let obstacles_clone = self.obstacles.clone();
4759
4760 for i in 0..n {
4761 let neighbors: Vec<&SteeringAgent> = agents_clone.iter().enumerate()
4762 .filter(|(j, _)| *j != i)
4763 .filter(|(_, a)| (a.position - agents_clone[i].position).length() < 8.0)
4764 .map(|(_, a)| a)
4765 .collect();
4766
4767 let mut rng_seed = agents_clone[i].id.wrapping_mul(0x9e3779b97f4a7c15);
4768 let mut agent_copy = agents_clone[i].clone();
4769 let force = SteeringBehaviors::compute_weighted(
4770 &mut agent_copy,
4771 None, None,
4773 None,
4774 None,
4775 None,
4776 true, &mut rng_seed,
4778 dt,
4779 &obstacles_clone,
4780 &[],
4781 &neighbors,
4782 None,
4783 None,
4784 None,
4785 None,
4786 None,
4787 );
4788 let avoid = SteeringBehaviors::obstacle_avoidance(&agents_clone[i], &obstacles_clone);
4790 let sep = SteeringBehaviors::separation(&agents_clone[i], &neighbors, 2.0);
4791 forces[i] = force + avoid * 2.0 + sep * 1.5;
4792 }
4793
4794 for (i, agent) in self.steering_agents.iter_mut().enumerate() {
4795 agent.apply_force(forces[i], dt);
4796 }
4797 }
4798
4799 pub fn add_emotion_engine(&mut self) {
4802 self.emotion_engines.push(EmotionEngine::new());
4803 }
4804
4805 pub fn trigger_emotion(&mut self, engine_idx: usize, emotion: PrimaryEmotion, intensity: f32) {
4806 if let Some(engine) = self.emotion_engines.get_mut(engine_idx) {
4807 engine.submit_stimulus(EmotionalStimulus {
4808 emotion,
4809 intensity,
4810 source_id: 0,
4811 decay_rate_override: None,
4812 });
4813 }
4814 }
4815
4816 pub fn get_emotion_wheel_points(&self, engine_idx: usize, scale: f32) -> Vec<Vec2> {
4817 if let Some(engine) = self.emotion_engines.get(engine_idx) {
4818 PrimaryEmotion::ALL.iter().map(|e| {
4819 let intensity = engine.state.get_intensity(*e);
4820 let wheel_pos = e.wheel_position();
4821 wheel_pos * intensity * scale
4822 }).collect()
4823 } else {
4824 vec![]
4825 }
4826 }
4827
4828 pub fn simulation_tick(&mut self, dt: f32) {
4831 if !self.simulation_running { return; }
4832 let effective_dt = dt * self.simulation_speed;
4833 self.current_time += effective_dt;
4834 self.frame_dt = effective_dt;
4835
4836 let obstacles_clone = self.obstacles.clone();
4837 for agent in &mut self.agents {
4838 agent.update(effective_dt, &obstacles_clone);
4839 }
4840
4841 self.tick_steering_agents(effective_dt);
4843
4844 for engine in &mut self.emotion_engines {
4846 engine.update(effective_dt, self.current_time);
4847 }
4848
4849 self.debug_buffer.tick(effective_dt);
4851
4852 if self.show_debug_panel {
4854 for agent in &self.agents {
4855 self.debug_buffer.draw_velocity_arrow(
4856 agent.position, agent.velocity,
4857 Vec4::new(0.8, 0.8, 0.0, 0.9)
4858 );
4859 if let Some(ref tree) = agent.behavior_tree {
4860 self.debug_buffer.draw_bt_status(agent.position, tree.last_status);
4861 }
4862 }
4863 }
4864
4865 if self.status_timer > 0.0 {
4867 self.status_timer -= effective_dt;
4868 if self.status_timer <= 0.0 {
4869 self.status_message.clear();
4870 }
4871 }
4872 }
4873
4874 pub fn show_status(&mut self, message: &str, duration: f32) {
4875 self.status_message = message.to_string();
4876 self.status_timer = duration;
4877 }
4878
4879 pub fn spawn_agent(&mut self, position: Vec3, mode: AiAgentMode) -> u64 {
4880 let id = (self.agents.len() as u64) + 100;
4881 let mut agent = AiAgent::new(id, &format!("Agent_{}", id), position, mode);
4882 agent.behavior_tree = Some(BtTemplates::combat_patrol_tree());
4883 agent.blackboard.set("self_health", BlackboardValue::Float(1.0));
4884 agent.blackboard.set("ammo_count", BlackboardValue::Float(30.0));
4885 agent.utility_dm = Some(UtilityLibrary::build_combat_decision_maker());
4886 self.agents.push(agent);
4887 id
4888 }
4889
4890 pub fn remove_agent(&mut self, id: u64) {
4891 self.agents.retain(|a| a.id != id);
4892 if self.selected_agent_id == Some(id) { self.selected_agent_id = None; }
4893 }
4894
4895 pub fn get_agent(&self, id: u64) -> Option<&AiAgent> {
4896 self.agents.iter().find(|a| a.id == id)
4897 }
4898
4899 pub fn get_agent_mut(&mut self, id: u64) -> Option<&mut AiAgent> {
4900 self.agents.iter_mut().find(|a| a.id == id)
4901 }
4902
4903 pub fn inspect_blackboard(&mut self, agent_id: Option<u64>) {
4906 if let Some(id) = agent_id {
4907 if let Some(agent) = self.agents.iter().find(|a| a.id == id) {
4908 for (key, value) in &agent.blackboard.entries {
4910 if !self.shared_blackboard.entries.contains_key(key)
4911 || self.shared_blackboard.entries[key] != *value
4912 {
4913 let v = value.clone();
4914 let t = agent.blackboard.current_time;
4915 self.blackboard_inspector.record_change(key, v, t);
4916 }
4917 }
4918 self.shared_blackboard = agent.blackboard.clone();
4919 }
4920 }
4921 }
4922
4923 pub fn blackboard_search_keys(&self, prefix: &str) -> Vec<String> {
4924 self.shared_blackboard.entries.keys()
4925 .filter(|k| k.starts_with(prefix))
4926 .cloned()
4927 .collect()
4928 }
4929
4930 pub fn generate_flow_field_toward(
4933 &mut self,
4934 target: Vec3,
4935 bounds_min: Vec2,
4936 bounds_max: Vec2,
4937 cell_size: f32,
4938 ) {
4939 self.flow_field.clear();
4940 let cols = ((bounds_max.x - bounds_min.x) / cell_size).ceil() as i32;
4941 let rows = ((bounds_max.y - bounds_min.y) / cell_size).ceil() as i32;
4942 for row in 0..rows {
4943 for col in 0..cols {
4944 let cx = bounds_min.x + col as f32 * cell_size + cell_size * 0.5;
4945 let cy = bounds_min.y + row as f32 * cell_size + cell_size * 0.5;
4946 let pos = Vec3::new(cx, 0.0, cy);
4947 let dir = (target - pos).normalize_or_zero();
4948 self.flow_field.insert((col, row), dir);
4949 }
4950 }
4951 }
4952
4953 pub fn generate_flow_field_rotational(
4954 &mut self,
4955 center: Vec3,
4956 bounds_min: Vec2,
4957 bounds_max: Vec2,
4958 cell_size: f32,
4959 clockwise: bool,
4960 ) {
4961 self.flow_field.clear();
4962 let cols = ((bounds_max.x - bounds_min.x) / cell_size).ceil() as i32;
4963 let rows = ((bounds_max.y - bounds_min.y) / cell_size).ceil() as i32;
4964 for row in 0..rows {
4965 for col in 0..cols {
4966 let cx = bounds_min.x + col as f32 * cell_size + cell_size * 0.5;
4967 let cy = bounds_min.y + row as f32 * cell_size + cell_size * 0.5;
4968 let pos = Vec3::new(cx, 0.0, cy);
4969 let to_center = (center - pos).normalize_or_zero();
4970 let tangent = if clockwise {
4971 Vec3::new(to_center.z, 0.0, -to_center.x)
4972 } else {
4973 Vec3::new(-to_center.z, 0.0, to_center.x)
4974 };
4975 self.flow_field.insert((col, row), tangent);
4976 }
4977 }
4978 }
4979
4980 pub fn set_dark_theme(&mut self) {
4983 self.theme_color = Vec4::new(0.15, 0.17, 0.2, 1.0);
4984 }
4985
4986 pub fn set_light_theme(&mut self) {
4987 self.theme_color = Vec4::new(0.85, 0.87, 0.9, 1.0);
4988 }
4989
4990 pub fn set_font_size(&mut self, size: f32) {
4991 self.font_size = size.clamp(8.0, 32.0);
4992 }
4993
4994 pub fn serialize_behavior_tree(&self, tree_idx: usize) -> Option<String> {
4997 let tree = self.behavior_trees.get(tree_idx)?;
4998 let mut out = String::new();
4999 out.push_str(&format!("BehaviorTree: {}\n", tree.name));
5000 out.push_str(&format!(" Nodes: {}\n", tree.nodes.len()));
5001 if let Some(root) = tree.root_id {
5002 self.serialize_bt_node_recursive(tree, root, &mut out, 0);
5003 }
5004 Some(out)
5005 }
5006
5007 fn serialize_bt_node_recursive(&self, tree: &BehaviorTree, node_id: u32, out: &mut String, depth: usize) {
5008 let indent = " ".repeat(depth + 1);
5009 if let Some(node) = tree.nodes.get(&node_id) {
5010 out.push_str(&format!("{}[{}] {}\n", indent, node.id, node.display_name()));
5011 for &child_id in &node.children {
5012 self.serialize_bt_node_recursive(tree, child_id, out, depth + 1);
5013 }
5014 }
5015 }
5016
5017 pub fn serialize_goap_actions(&self) -> String {
5018 let mut out = String::new();
5019 out.push_str("GOAP Actions:\n");
5020 for action in &self.goap_planner.actions {
5021 out.push_str(&format!(
5022 " [{}] {} | cost={:.1} | pre={:b} | eff_set={:b}\n",
5023 action.id, action.name, action.cost, action.preconditions, action.effects_set
5024 ));
5025 }
5026 out
5027 }
5028
5029 pub fn grid_snap(&self, pos: Vec2) -> Vec2 {
5032 let g = self.grid_size;
5033 Vec2::new((pos.x / g).round() * g, (pos.y / g).round() * g)
5034 }
5035
5036 pub fn grid_lines_in_view(&self, viewport_min: Vec2, viewport_max: Vec2, camera: &NodeGraphCamera, viewport_size: Vec2) -> (Vec<(Vec2, Vec2)>, Vec<(Vec2, Vec2)>) {
5037 let world_min = camera.screen_to_world(viewport_min, viewport_size);
5038 let world_max = camera.screen_to_world(viewport_max, viewport_size);
5039 let g = self.grid_size;
5040 let mut minor_lines = Vec::new();
5041 let mut major_lines = Vec::new();
5042
5043 let x_start = (world_min.x / g).floor() as i32;
5044 let x_end = (world_max.x / g).ceil() as i32;
5045 let y_start = (world_min.y / g).floor() as i32;
5046 let y_end = (world_max.y / g).ceil() as i32;
5047
5048 for i in x_start..=x_end {
5049 let x = i as f32 * g;
5050 let line = (Vec2::new(x, world_min.y), Vec2::new(x, world_max.y));
5051 if i % 5 == 0 { major_lines.push(line); } else { minor_lines.push(line); }
5052 }
5053 for j in y_start..=y_end {
5054 let y = j as f32 * g;
5055 let line = (Vec2::new(world_min.x, y), Vec2::new(world_max.x, y));
5056 if j % 5 == 0 { major_lines.push(line); } else { minor_lines.push(line); }
5057 }
5058 (minor_lines, major_lines)
5059 }
5060
5061 pub fn bt_tree_depth(&self, tree_idx: usize) -> usize {
5064 if let Some(tree) = self.behavior_trees.get(tree_idx) {
5065 if let Some(root) = tree.root_id {
5066 self.bt_node_depth(tree, root)
5067 } else { 0 }
5068 } else { 0 }
5069 }
5070
5071 fn bt_node_depth(&self, tree: &BehaviorTree, node_id: u32) -> usize {
5072 if let Some(node) = tree.nodes.get(&node_id) {
5073 if node.children.is_empty() { 1 }
5074 else {
5075 1 + node.children.iter()
5076 .map(|&c| self.bt_node_depth(tree, c))
5077 .max()
5078 .unwrap_or(0)
5079 }
5080 } else { 0 }
5081 }
5082
5083 pub fn bt_leaf_count(&self, tree_idx: usize) -> usize {
5084 if let Some(tree) = self.behavior_trees.get(tree_idx) {
5085 tree.nodes.values().filter(|n| n.is_leaf()).count()
5086 } else { 0 }
5087 }
5088
5089 pub fn goap_action_count(&self) -> usize { self.goap_planner.actions.len() }
5090 pub fn agent_count(&self) -> usize { self.agents.len() }
5091
5092 pub fn selected_agent_debug_info(&self) -> Option<String> {
5093 let id = self.selected_agent_id?;
5094 let agent = self.get_agent(id)?;
5095 let mut info = String::new();
5096 info.push_str(&format!("Agent: {} (id={})\n", agent.name, agent.id));
5097 info.push_str(&format!(" Position: ({:.2}, {:.2}, {:.2})\n", agent.position.x, agent.position.y, agent.position.z));
5098 info.push_str(&format!(" Mode: {:?}\n", agent.mode));
5099 if let Some(ref tree) = agent.behavior_tree {
5100 info.push_str(&format!(" BT: {} | status={:?} | ticks={}\n", tree.name, tree.last_status, tree.tick_count));
5101 }
5102 let dominant = agent.emotion_engine.state.dominant();
5103 info.push_str(&format!(" Dominant emotion: {:?}\n", dominant));
5104 info.push_str(&format!(" Blackboard entries: {}\n", agent.blackboard.entries.len()));
5105 Some(info)
5106 }
5107
5108 pub fn handle_key(&mut self, key: EditorKey, shift: bool, ctrl: bool) {
5111 match key {
5112 EditorKey::Delete => {
5113 self.bt_delete_selected(self.active_tree_index);
5114 }
5115 EditorKey::Z if ctrl && !shift => {
5116 self.bt_undo();
5117 self.show_status("Undo", 2.0);
5118 }
5119 EditorKey::Z if ctrl && shift => {
5120 self.bt_redo();
5121 self.show_status("Redo", 2.0);
5122 }
5123 EditorKey::A if ctrl => {
5124 self.bt_select_all(self.active_tree_index);
5125 }
5126 EditorKey::L if ctrl => {
5127 self.bt_auto_layout(self.active_tree_index);
5128 self.show_status("Layout computed", 2.0);
5129 }
5130 EditorKey::Space => {
5131 self.simulation_running = !self.simulation_running;
5132 let msg = if self.simulation_running { "Simulation started" } else { "Simulation paused" };
5133 self.show_status(msg, 2.0);
5134 }
5135 EditorKey::F5 => {
5136 for tree in &mut self.behavior_trees {
5137 tree.reset();
5138 }
5139 self.show_status("Trees reset", 2.0);
5140 }
5141 EditorKey::G if ctrl => {
5142 self.snap_to_grid = !self.snap_to_grid;
5143 let msg = if self.snap_to_grid { "Snap to grid ON" } else { "Snap to grid OFF" };
5144 self.show_status(msg, 2.0);
5145 }
5146 _ => {}
5147 }
5148 }
5149}
5150
5151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5152pub enum EditorKey {
5153 Delete, Z, A, L, Space, F5, G, Other,
5154}
5155
5156#[derive(Clone, Debug)]
5161pub struct GridPathfinder {
5162 pub width: usize,
5163 pub height: usize,
5164 pub cell_size: f32,
5165 pub origin: Vec2,
5166 pub passable: Vec<bool>,
5167 pub cost_map: Vec<f32>,
5168}
5169
5170impl GridPathfinder {
5171 pub fn new(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
5172 let n = width * height;
5173 Self {
5174 width,
5175 height,
5176 cell_size,
5177 origin,
5178 passable: vec![true; n],
5179 cost_map: vec![1.0; n],
5180 }
5181 }
5182
5183 pub fn world_to_cell(&self, pos: Vec2) -> (i32, i32) {
5184 let rel = pos - self.origin;
5185 let x = (rel.x / self.cell_size).floor() as i32;
5186 let y = (rel.y / self.cell_size).floor() as i32;
5187 (x, y)
5188 }
5189
5190 pub fn cell_to_world(&self, x: i32, y: i32) -> Vec2 {
5191 Vec2::new(
5192 self.origin.x + x as f32 * self.cell_size + self.cell_size * 0.5,
5193 self.origin.y + y as f32 * self.cell_size + self.cell_size * 0.5,
5194 )
5195 }
5196
5197 fn idx(&self, x: i32, y: i32) -> Option<usize> {
5198 if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 { return None; }
5199 Some(y as usize * self.width + x as usize)
5200 }
5201
5202 pub fn is_passable(&self, x: i32, y: i32) -> bool {
5203 self.idx(x, y).map(|i| self.passable[i]).unwrap_or(false)
5204 }
5205
5206 pub fn set_obstacle(&mut self, x: i32, y: i32, obstacle: bool) {
5207 if let Some(i) = self.idx(x, y) { self.passable[i] = !obstacle; }
5208 }
5209
5210 pub fn find_path(&self, from: Vec2, to: Vec2) -> Option<Vec<Vec2>> {
5212 let (sx, sy) = self.world_to_cell(from);
5213 let (ex, ey) = self.world_to_cell(to);
5214 if !self.is_passable(sx, sy) || !self.is_passable(ex, ey) { return None; }
5215 if sx == ex && sy == ey { return Some(vec![to]); }
5216
5217 #[derive(Clone, Debug)]
5218 struct Node { x: i32, y: i32, g: f32, h: f32, parent: Option<(i32, i32)> }
5219 impl Node { fn f(&self) -> f32 { self.g + self.h } }
5220
5221 let heuristic = |x: i32, y: i32| -> f32 {
5222 let dx = (x - ex).abs() as f32;
5223 let dy = (y - ey).abs() as f32;
5224 (dx + dy) * 1.001 };
5226
5227 let mut open: BTreeMap<(i32, i32), Node> = BTreeMap::new();
5228 let mut closed: HashMap<(i32, i32), Node> = HashMap::new();
5229
5230 open.insert((sx, sy), Node { x: sx, y: sy, g: 0.0, h: heuristic(sx, sy), parent: None });
5231
5232 let neighbors_offsets: [(i32, i32, f32); 8] = [
5233 (1, 0, 1.0), (-1, 0, 1.0), (0, 1, 1.0), (0, -1, 1.0),
5234 (1, 1, SQRT2), (-1, 1, SQRT2), (1, -1, SQRT2), (-1, -1, SQRT2),
5235 ];
5236
5237 while !open.is_empty() {
5238 let current_key = open.iter()
5240 .min_by(|a, b| a.1.f().partial_cmp(&b.1.f()).unwrap())
5241 .map(|(k, _)| *k)?;
5242 let current = open.remove(¤t_key)?;
5243
5244 if current.x == ex && current.y == ey {
5245 let mut path: Vec<Vec2> = Vec::new();
5247 let mut cur = (current.x, current.y);
5248 path.push(self.cell_to_world(cur.0, cur.1));
5249 closed.insert(cur, current);
5250 while let Some(parent) = closed.get(&cur).and_then(|n| n.parent) {
5251 path.push(self.cell_to_world(parent.0, parent.1));
5252 cur = parent;
5253 }
5254 path.reverse();
5255 return Some(path);
5256 }
5257
5258 closed.insert((current.x, current.y), current.clone());
5259
5260 for &(dx, dy, move_cost) in &neighbors_offsets {
5261 let nx = current.x + dx;
5262 let ny = current.y + dy;
5263 if !self.is_passable(nx, ny) { continue; }
5264 if closed.contains_key(&(nx, ny)) { continue; }
5265 let cell_cost = self.idx(nx, ny).map(|i| self.cost_map[i]).unwrap_or(1.0);
5266 let new_g = current.g + move_cost * cell_cost;
5267 let new_h = heuristic(nx, ny);
5268 let new_f = new_g + new_h;
5269 if let Some(existing) = open.get(&(nx, ny)) {
5270 if existing.f() <= new_f { continue; }
5271 }
5272 open.insert((nx, ny), Node { x: nx, y: ny, g: new_g, h: new_h, parent: Some((current.x, current.y)) });
5273 }
5274
5275 if closed.len() > 8192 { break; }
5276 }
5277 None
5278 }
5279
5280 pub fn smooth_path(path: &[Vec2], obstacles: &[Aabb]) -> Vec<Vec2> {
5281 if path.len() <= 2 { return path.to_vec(); }
5282 let mut smoothed = vec![path[0]];
5283 let mut current_idx = 0;
5284 while current_idx < path.len() - 1 {
5285 let mut furthest = current_idx + 1;
5286 for i in (current_idx + 1)..path.len() {
5287 let from = path[current_idx];
5288 let to = path[i];
5289 let from3 = Vec3::new(from.x, 0.0, from.y);
5290 let to3 = Vec3::new(to.x, 0.0, to.y);
5291 let dir3 = to3 - from3;
5292 let len3 = dir3.length();
5293 let inv = Vec3::new(1.0 / dir3.x, 1.0 / dir3.y, 1.0 / dir3.z);
5294 let clear = !obstacles.iter().any(|obs| obs.ray_intersects(from3, inv, len3));
5295 if clear { furthest = i; }
5296 }
5297 smoothed.push(path[furthest]);
5298 current_idx = furthest;
5299 }
5300 smoothed
5301 }
5302}
5303
5304pub fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t.clamp(0.0, 1.0) }
5309
5310pub fn smooth_damp(current: f32, target: f32, velocity: &mut f32, smooth_time: f32, max_speed: f32, dt: f32) -> f32 {
5311 let smooth_time = smooth_time.max(0.0001);
5312 let omega = 2.0 / smooth_time;
5313 let x = omega * dt;
5314 let exp = 1.0 / (1.0 + x + 0.48 * x * x + 0.235 * x * x * x);
5315 let change = current - target;
5316 let original_to = target;
5317 let max_change = max_speed * smooth_time;
5318 let change = change.clamp(-max_change, max_change);
5319 let target2 = current - change;
5320 let temp = (*velocity + omega * change) * dt;
5321 *velocity = (*velocity - omega * temp) * exp;
5322 let output = target2 + (change + temp) * exp;
5323 if original_to - current > 0.0 && output > original_to {
5324 *velocity = 0.0;
5325 return original_to;
5326 }
5327 if original_to - current < 0.0 && output < original_to {
5328 *velocity = 0.0;
5329 return original_to;
5330 }
5331 output
5332}
5333
5334pub fn smooth_damp_vec3(
5335 current: Vec3, target: Vec3,
5336 velocity: &mut Vec3,
5337 smooth_time: f32,
5338 max_speed: f32,
5339 dt: f32,
5340) -> Vec3 {
5341 Vec3::new(
5342 smooth_damp(current.x, target.x, &mut velocity.x, smooth_time, max_speed, dt),
5343 smooth_damp(current.y, target.y, &mut velocity.y, smooth_time, max_speed, dt),
5344 smooth_damp(current.z, target.z, &mut velocity.z, smooth_time, max_speed, dt),
5345 )
5346}
5347
5348pub fn angle_between_vectors(a: Vec3, b: Vec3) -> f32 {
5349 let dot = a.normalize_or_zero().dot(b.normalize_or_zero());
5350 dot.clamp(-1.0, 1.0).acos()
5351}
5352
5353pub fn signed_angle_2d(from: Vec2, to: Vec2) -> f32 {
5354 let cross = from.x * to.y - from.y * to.x;
5355 let dot = from.x * to.x + from.y * to.y;
5356 cross.atan2(dot)
5357}
5358
5359pub fn rotate_vec2(v: Vec2, angle: f32) -> Vec2 {
5360 let cos = angle.cos();
5361 let sin = angle.sin();
5362 Vec2::new(v.x * cos - v.y * sin, v.x * sin + v.y * cos)
5363}
5364
5365pub fn closest_point_on_segment(point: Vec3, seg_a: Vec3, seg_b: Vec3) -> Vec3 {
5366 let ab = seg_b - seg_a;
5367 let ap = point - seg_a;
5368 let len_sq = ab.length_squared();
5369 if len_sq < EPSILON { return seg_a; }
5370 let t = ap.dot(ab) / len_sq;
5371 seg_a + ab * t.clamp(0.0, 1.0)
5372}
5373
5374pub fn point_in_triangle(p: Vec2, a: Vec2, b: Vec2, c: Vec2) -> bool {
5375 let d1 = sign_2d(p, a, b);
5376 let d2 = sign_2d(p, b, c);
5377 let d3 = sign_2d(p, c, a);
5378 let has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
5379 let has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
5380 !(has_neg && has_pos)
5381}
5382
5383fn sign_2d(p1: Vec2, p2: Vec2, p3: Vec2) -> f32 {
5384 (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
5385}
5386
5387pub fn catmull_rom_point(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
5388 let t2 = t * t;
5389 let t3 = t2 * t;
5390 p0 * (-t3 + 2.0 * t2 - t) * 0.5
5391 + p1 * (3.0 * t3 - 5.0 * t2 + 2.0) * 0.5
5392 + p2 * (-3.0 * t3 + 4.0 * t2 + t) * 0.5
5393 + p3 * (t3 - t2) * 0.5
5394}
5395
5396pub fn catmull_rom_velocity(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
5397 let t2 = t * t;
5398 p0 * (-3.0 * t2 + 4.0 * t - 1.0) * 0.5
5399 + p1 * (9.0 * t2 - 10.0 * t) * 0.5
5400 + p2 * (-9.0 * t2 + 8.0 * t + 1.0) * 0.5
5401 + p3 * (3.0 * t2 - 2.0 * t) * 0.5
5402}
5403
5404pub struct SquadAi {
5409 pub agents: Vec<u64>,
5410 pub leader_id: u64,
5411 pub formation: FormationType,
5412 pub formation_spacing: f32,
5413 pub objective: SquadObjective,
5414 pub threat_map: HashMap<u64, f32>,
5415 pub suppression_targets: Vec<u64>,
5416 pub current_time: f32,
5417}
5418
5419#[derive(Clone, Debug)]
5420pub enum SquadObjective {
5421 Patrol { waypoints: Vec<Vec3>, current_wp: usize },
5422 Attack { target_id: u64, target_pos: Vec3 },
5423 Defend { position: Vec3, radius: f32 },
5424 Retreat { rally_point: Vec3 },
5425 Scout { area_center: Vec3, radius: f32 },
5426 Flank { target_pos: Vec3, flank_direction: Vec3 },
5427 Ambush { ambush_pos: Vec3, trigger_radius: f32 },
5428}
5429
5430impl SquadAi {
5431 pub fn new(leader_id: u64) -> Self {
5432 Self {
5433 agents: Vec::new(),
5434 leader_id,
5435 formation: FormationType::Wedge,
5436 formation_spacing: 2.5,
5437 objective: SquadObjective::Patrol { waypoints: Vec::new(), current_wp: 0 },
5438 threat_map: HashMap::new(),
5439 suppression_targets: Vec::new(),
5440 current_time: 0.0,
5441 }
5442 }
5443
5444 pub fn add_agent(&mut self, id: u64) {
5445 if !self.agents.contains(&id) { self.agents.push(id); }
5446 }
5447
5448 pub fn remove_agent(&mut self, id: u64) {
5449 self.agents.retain(|&a| a != id);
5450 self.threat_map.remove(&id);
5451 self.suppression_targets.retain(|&a| a != id);
5452 }
5453
5454 pub fn assign_formation_slots(&self, agent_positions: &HashMap<u64, Vec3>, leader_forward: Vec3) -> HashMap<u64, Vec3> {
5455 let leader_pos = agent_positions.get(&self.leader_id).copied().unwrap_or(Vec3::ZERO);
5456 let slots = FormationLayout::compute_slots(
5457 self.formation, leader_pos, leader_forward, self.agents.len(), self.formation_spacing
5458 );
5459 let positions: Vec<Vec3> = self.agents.iter().map(|id| agent_positions.get(id).copied().unwrap_or(Vec3::ZERO)).collect();
5460 let assignment = FormationLayout::assign_slots(&positions, &slots);
5461 self.agents.iter().zip(assignment.iter()).map(|(&id, &slot_idx)| {
5462 (id, slots.get(slot_idx).copied().unwrap_or(leader_pos))
5463 }).collect()
5464 }
5465
5466 pub fn assess_threat_level(&mut self, perceived_entities: &[PerceivedEntity]) -> f32 {
5467 self.threat_map.clear();
5468 let mut total_threat = 0.0f32;
5469 for entity in perceived_entities {
5470 let threat = entity.threat_level * entity.confidence;
5471 self.threat_map.insert(entity.entity_id, threat);
5472 total_threat += threat;
5473 }
5474 total_threat
5475 }
5476
5477 pub fn decide_objective(&mut self, threat_level: f32, perceived: &[PerceivedEntity]) {
5478 if threat_level > 3.0 {
5479 if let Some(highest) = perceived.iter()
5481 .max_by(|a, b| (a.threat_level * a.confidence).partial_cmp(&(b.threat_level * b.confidence)).unwrap())
5482 {
5483 self.objective = SquadObjective::Attack {
5484 target_id: highest.entity_id,
5485 target_pos: highest.last_known_position,
5486 };
5487 }
5488 } else if threat_level > 1.0 {
5489 } else {
5492 if !matches!(self.objective, SquadObjective::Patrol { .. }) {
5494 }
5496 }
5497 }
5498
5499 pub fn tick(&mut self, dt: f32, perceived: &[PerceivedEntity], agent_positions: &HashMap<u64, Vec3>) {
5500 self.current_time += dt;
5501 let threat = self.assess_threat_level(perceived);
5502 self.decide_objective(threat, perceived);
5503
5504 if let SquadObjective::Patrol { ref waypoints, ref mut current_wp } = &mut self.objective {
5506 if let Some(leader_pos) = agent_positions.get(&self.leader_id) {
5507 if let Some(wp) = waypoints.get(*current_wp) {
5508 if (*wp - *leader_pos).length() < 2.0 {
5509 *current_wp = (*current_wp + 1) % waypoints.len().max(1);
5510 }
5511 }
5512 }
5513 }
5514 }
5515
5516 pub fn get_leader_target(&self) -> Option<Vec3> {
5517 match &self.objective {
5518 SquadObjective::Patrol { waypoints, current_wp } => waypoints.get(*current_wp).copied(),
5519 SquadObjective::Attack { target_pos, .. } => Some(*target_pos),
5520 SquadObjective::Defend { position, .. } => Some(*position),
5521 SquadObjective::Retreat { rally_point } => Some(*rally_point),
5522 SquadObjective::Scout { area_center, .. } => Some(*area_center),
5523 SquadObjective::Flank { target_pos, flank_direction } => {
5524 Some(*target_pos + *flank_direction * 10.0)
5525 }
5526 SquadObjective::Ambush { ambush_pos, .. } => Some(*ambush_pos),
5527 }
5528 }
5529}
5530
5531pub struct BtDebugger {
5536 pub is_attached: bool,
5537 pub agent_id: Option<u64>,
5538 pub breakpoints: HashSet<u32>,
5539 pub step_mode: bool,
5540 pub last_tick_nodes: Vec<u32>,
5541 pub node_exec_counts: HashMap<u32, u64>,
5542 pub node_status_history: HashMap<u32, VecDeque<BtStatus>>,
5543 pub status_history_len: usize,
5544 pub paused_at_node: Option<u32>,
5545 pub play_speed: f32,
5546}
5547
5548impl BtDebugger {
5549 pub fn new() -> Self {
5550 Self {
5551 is_attached: false,
5552 agent_id: None,
5553 breakpoints: HashSet::new(),
5554 step_mode: false,
5555 last_tick_nodes: Vec::new(),
5556 node_exec_counts: HashMap::new(),
5557 node_status_history: HashMap::new(),
5558 status_history_len: 32,
5559 paused_at_node: None,
5560 play_speed: 1.0,
5561 }
5562 }
5563
5564 pub fn attach(&mut self, agent_id: u64) {
5565 self.agent_id = Some(agent_id);
5566 self.is_attached = true;
5567 }
5568
5569 pub fn detach(&mut self) {
5570 self.agent_id = None;
5571 self.is_attached = false;
5572 self.paused_at_node = None;
5573 }
5574
5575 pub fn toggle_breakpoint(&mut self, node_id: u32) {
5576 if self.breakpoints.contains(&node_id) {
5577 self.breakpoints.remove(&node_id);
5578 } else {
5579 self.breakpoints.insert(node_id);
5580 }
5581 }
5582
5583 pub fn record_tick(&mut self, visited: &[u32], tree: &BehaviorTree) {
5584 self.last_tick_nodes = visited.to_vec();
5585 for &node_id in visited {
5586 *self.node_exec_counts.entry(node_id).or_insert(0) += 1;
5587 if let Some(node) = tree.nodes.get(&node_id) {
5588 let history = self.node_status_history.entry(node_id).or_insert_with(|| VecDeque::with_capacity(self.status_history_len));
5589 history.push_back(node.status);
5590 if history.len() > self.status_history_len { history.pop_front(); }
5591 }
5592 }
5593 }
5594
5595 pub fn check_breakpoints(&mut self, visited: &[u32]) -> bool {
5596 for &node_id in visited {
5597 if self.breakpoints.contains(&node_id) {
5598 self.paused_at_node = Some(node_id);
5599 return true;
5600 }
5601 }
5602 false
5603 }
5604
5605 pub fn get_node_coverage(&self, tree: &BehaviorTree) -> f32 {
5606 let total = tree.nodes.len();
5607 if total == 0 { return 0.0; }
5608 let executed = self.node_exec_counts.len();
5609 executed as f32 / total as f32
5610 }
5611
5612 pub fn hot_nodes(&self, top_n: usize) -> Vec<(u32, u64)> {
5613 let mut vec: Vec<(u32, u64)> = self.node_exec_counts.iter().map(|(&k, &v)| (k, v)).collect();
5614 vec.sort_by(|a, b| b.1.cmp(&a.1));
5615 vec.truncate(top_n);
5616 vec
5617 }
5618
5619 pub fn node_success_rate(&self, node_id: u32) -> f32 {
5620 if let Some(history) = self.node_status_history.get(&node_id) {
5621 let successes = history.iter().filter(|&&s| s == BtStatus::Success).count();
5622 if history.is_empty() { 0.0 }
5623 else { successes as f32 / history.len() as f32 }
5624 } else { 0.0 }
5625 }
5626}
5627
5628pub struct ValueNoise {
5633 pub perm: [u8; 512],
5634}
5635
5636impl ValueNoise {
5637 pub fn new(seed: u64) -> Self {
5638 let mut perm = [0u8; 512];
5639 let mut rng = seed;
5640 let mut table: Vec<u8> = (0..=255u8).collect();
5641 for i in (1..256).rev() {
5642 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
5643 let j = (rng >> 33) as usize % (i + 1);
5644 table.swap(i, j);
5645 }
5646 for i in 0..256 { perm[i] = table[i]; perm[i + 256] = table[i]; }
5647 Self { perm }
5648 }
5649
5650 fn fade(t: f32) -> f32 { t * t * t * (t * (t * 6.0 - 15.0) + 10.0) }
5651 fn lerp_n(a: f32, b: f32, t: f32) -> f32 { a + t * (b - a) }
5652 fn grad(hash: u8, x: f32, y: f32, z: f32) -> f32 {
5653 let h = hash & 15;
5654 let u = if h < 8 { x } else { y };
5655 let v = if h < 4 { y } else if h == 12 || h == 14 { x } else { z };
5656 (if (h & 1) == 0 { u } else { -u }) + (if (h & 2) == 0 { v } else { -v })
5657 }
5658
5659 pub fn sample_3d(&self, x: f32, y: f32, z: f32) -> f32 {
5660 let xi = x.floor() as i32 & 255;
5661 let yi = y.floor() as i32 & 255;
5662 let zi = z.floor() as i32 & 255;
5663 let xf = x - x.floor();
5664 let yf = y - y.floor();
5665 let zf = z - z.floor();
5666 let u = Self::fade(xf);
5667 let v = Self::fade(yf);
5668 let w = Self::fade(zf);
5669 let a = self.perm[xi as usize] as i32 + yi;
5670 let aa = self.perm[a as usize] as i32 + zi;
5671 let ab = self.perm[(a+1) as usize] as i32 + zi;
5672 let b = self.perm[(xi+1) as usize] as i32 + yi;
5673 let ba = self.perm[b as usize] as i32 + zi;
5674 let bb = self.perm[(b+1) as usize] as i32 + zi;
5675
5676 let r = Self::lerp_n(
5677 Self::lerp_n(
5678 Self::lerp_n(Self::grad(self.perm[aa as usize], xf, yf, zf ), Self::grad(self.perm[ba as usize], xf-1.0, yf, zf ), u),
5679 Self::lerp_n(Self::grad(self.perm[ab as usize], xf, yf-1.0, zf), Self::grad(self.perm[bb as usize], xf-1.0, yf-1.0, zf ), u), v),
5680 Self::lerp_n(
5681 Self::lerp_n(Self::grad(self.perm[(aa+1) as usize], xf, yf, zf-1.0), Self::grad(self.perm[(ba+1) as usize], xf-1.0, yf, zf-1.0), u),
5682 Self::lerp_n(Self::grad(self.perm[(ab+1) as usize], xf, yf-1.0, zf-1.0), Self::grad(self.perm[(bb+1) as usize], xf-1.0, yf-1.0, zf-1.0), u), v), w);
5683 (r + 1.0) * 0.5
5684 }
5685
5686 pub fn octave_3d(&self, x: f32, y: f32, z: f32, octaves: usize, persistence: f32, lacunarity: f32) -> f32 {
5687 let mut value = 0.0f32;
5688 let mut amplitude = 1.0f32;
5689 let mut frequency = 1.0f32;
5690 let mut max_value = 0.0f32;
5691 for _ in 0..octaves {
5692 value += self.sample_3d(x * frequency, y * frequency, z * frequency) * amplitude;
5693 max_value += amplitude;
5694 amplitude *= persistence;
5695 frequency *= lacunarity;
5696 }
5697 value / max_value
5698 }
5699}
5700
5701pub struct BehaviorModulator {
5706 pub noise: ValueNoise,
5707 pub time_offset: f32,
5708 pub parameters: HashMap<String, ModulatedParam>,
5709}
5710
5711#[derive(Clone, Debug)]
5712pub struct ModulatedParam {
5713 pub base_value: f32,
5714 pub noise_scale: f32,
5715 pub noise_speed: f32,
5716 pub noise_seed: f32,
5717 pub clamp_min: f32,
5718 pub clamp_max: f32,
5719 pub current_value: f32,
5720}
5721
5722impl ModulatedParam {
5723 pub fn new(base: f32, noise_scale: f32, noise_speed: f32, seed: f32) -> Self {
5724 Self {
5725 base_value: base,
5726 noise_scale,
5727 noise_speed,
5728 noise_seed: seed,
5729 clamp_min: f32::NEG_INFINITY,
5730 clamp_max: f32::INFINITY,
5731 current_value: base,
5732 }
5733 }
5734
5735 pub fn with_clamp(mut self, min: f32, max: f32) -> Self {
5736 self.clamp_min = min;
5737 self.clamp_max = max;
5738 self
5739 }
5740}
5741
5742impl BehaviorModulator {
5743 pub fn new(seed: u64) -> Self {
5744 Self {
5745 noise: ValueNoise::new(seed),
5746 time_offset: 0.0,
5747 parameters: HashMap::new(),
5748 }
5749 }
5750
5751 pub fn add_param(&mut self, name: &str, param: ModulatedParam) {
5752 self.parameters.insert(name.to_string(), param);
5753 }
5754
5755 pub fn update(&mut self, dt: f32) {
5756 self.time_offset += dt;
5757 for param in self.parameters.values_mut() {
5758 let noise_val = self.noise.sample_3d(
5759 param.noise_seed + self.time_offset * param.noise_speed,
5760 param.noise_seed * 1.37,
5761 0.0,
5762 );
5763 let modulated = param.base_value + (noise_val * 2.0 - 1.0) * param.noise_scale;
5764 param.current_value = modulated.clamp(param.clamp_min, param.clamp_max);
5765 }
5766 }
5767
5768 pub fn get(&self, name: &str) -> f32 {
5769 self.parameters.get(name).map(|p| p.current_value).unwrap_or(0.0)
5770 }
5771}
5772
5773#[derive(Clone, Debug)]
5778pub struct MemoryRecord {
5779 pub key: String,
5780 pub value: BlackboardValue,
5781 pub created_at: f32,
5782 pub last_accessed: f32,
5783 pub importance: f32,
5784 pub decay_rate: f32,
5785 pub source_entity: Option<u64>,
5786 pub tags: HashSet<String>,
5787}
5788
5789impl MemoryRecord {
5790 pub fn new(key: &str, value: BlackboardValue, time: f32, importance: f32) -> Self {
5791 Self {
5792 key: key.to_string(),
5793 value,
5794 created_at: time,
5795 last_accessed: time,
5796 importance,
5797 decay_rate: 0.05,
5798 source_entity: None,
5799 tags: HashSet::new(),
5800 }
5801 }
5802
5803 pub fn decay_importance(&mut self, dt: f32) {
5804 self.importance = (self.importance - self.decay_rate * dt).max(0.0);
5805 }
5806
5807 pub fn is_forgotten(&self) -> bool { self.importance < 0.01 }
5808}
5809
5810pub struct AgentMemory {
5811 pub records: HashMap<String, MemoryRecord>,
5812 pub forget_threshold: f32,
5813 pub max_records: usize,
5814 pub current_time: f32,
5815}
5816
5817impl AgentMemory {
5818 pub fn new(max_records: usize) -> Self {
5819 Self { records: HashMap::new(), forget_threshold: 0.01, max_records, current_time: 0.0 }
5820 }
5821
5822 pub fn remember(&mut self, key: &str, value: BlackboardValue, importance: f32) {
5823 if self.records.len() >= self.max_records {
5824 let min_key = self.records.iter()
5826 .min_by(|a, b| a.1.importance.partial_cmp(&b.1.importance).unwrap())
5827 .map(|(k, _)| k.clone());
5828 if let Some(mk) = min_key { self.records.remove(&mk); }
5829 }
5830 let record = MemoryRecord::new(key, value, self.current_time, importance);
5831 self.records.insert(key.to_string(), record);
5832 }
5833
5834 pub fn recall(&mut self, key: &str) -> Option<&BlackboardValue> {
5835 if let Some(record) = self.records.get_mut(key) {
5836 record.last_accessed = self.current_time;
5837 record.importance = (record.importance + 0.1).min(1.0);
5839 Some(&record.value)
5840 } else { None }
5841 }
5842
5843 pub fn update(&mut self, dt: f32) {
5844 self.current_time += dt;
5845 let mut to_forget = Vec::new();
5846 for (key, record) in &mut self.records {
5847 record.decay_importance(dt);
5848 if record.is_forgotten() { to_forget.push(key.clone()); }
5849 }
5850 for key in to_forget { self.records.remove(&key); }
5851 }
5852
5853 pub fn most_important(&self, n: usize) -> Vec<&MemoryRecord> {
5854 let mut records: Vec<&MemoryRecord> = self.records.values().collect();
5855 records.sort_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap());
5856 records.truncate(n);
5857 records
5858 }
5859
5860 pub fn with_tag(&self, tag: &str) -> Vec<&MemoryRecord> {
5861 self.records.values().filter(|r| r.tags.contains(tag)).collect()
5862 }
5863}
5864
5865pub struct WorldStateTracker {
5870 pub facts: HashMap<String, BlackboardValue>,
5871 pub last_changed: HashMap<String, f32>,
5872 pub listeners: Vec<WorldStateFact>,
5873 pub current_time: f32,
5874}
5875
5876#[derive(Clone, Debug)]
5877pub struct WorldStateFact {
5878 pub key: String,
5879 pub condition: CompareOp,
5880 pub value: BlackboardValue,
5881 pub triggered: bool,
5882 pub callback_label: String,
5883}
5884
5885impl WorldStateTracker {
5886 pub fn new() -> Self {
5887 Self {
5888 facts: HashMap::new(),
5889 last_changed: HashMap::new(),
5890 listeners: Vec::new(),
5891 current_time: 0.0,
5892 }
5893 }
5894
5895 pub fn set(&mut self, key: &str, value: BlackboardValue) {
5896 self.facts.insert(key.to_string(), value);
5897 self.last_changed.insert(key.to_string(), self.current_time);
5898 for listener in &mut self.listeners {
5900 if listener.key == key {
5901 let val = self.facts.get(key).unwrap_or(&BlackboardValue::None);
5902 listener.triggered = listener.condition.evaluate(val, &listener.value);
5903 }
5904 }
5905 }
5906
5907 pub fn get(&self, key: &str) -> &BlackboardValue {
5908 self.facts.get(key).unwrap_or(&BlackboardValue::None)
5909 }
5910
5911 pub fn tick(&mut self, dt: f32) { self.current_time += dt; }
5912
5913 pub fn add_listener(&mut self, key: &str, condition: CompareOp, value: BlackboardValue, callback: &str) {
5914 self.listeners.push(WorldStateFact {
5915 key: key.to_string(),
5916 condition,
5917 value,
5918 triggered: false,
5919 callback_label: callback.to_string(),
5920 });
5921 }
5922
5923 pub fn triggered_callbacks(&self) -> Vec<String> {
5924 self.listeners.iter().filter(|l| l.triggered).map(|l| l.callback_label.clone()).collect()
5925 }
5926}
5927
5928#[derive(Clone, Debug)]
5933pub struct SocialRelationship {
5934 pub other_id: u64,
5935 pub affinity: f32, pub trust: f32, pub fear: f32, pub last_interaction: f32,
5939 pub interaction_count: u32,
5940}
5941
5942impl SocialRelationship {
5943 pub fn new(other_id: u64) -> Self {
5944 Self { other_id, affinity: 0.0, trust: 0.5, fear: 0.0, last_interaction: 0.0, interaction_count: 0 }
5945 }
5946
5947 pub fn update_after_interaction(&mut self, positive: bool, intensity: f32, time: f32) {
5948 self.last_interaction = time;
5949 self.interaction_count += 1;
5950 let delta = if positive { intensity } else { -intensity };
5951 self.affinity = (self.affinity + delta * 0.2).clamp(-1.0, 1.0);
5952 if positive {
5953 self.trust = (self.trust + intensity * 0.1).min(1.0);
5954 } else {
5955 self.trust = (self.trust - intensity * 0.15).max(0.0);
5956 self.fear = (self.fear + intensity * 0.1).min(1.0);
5957 }
5958 }
5959
5960 pub fn decay(&mut self, dt: f32, current_time: f32) {
5961 let age = current_time - self.last_interaction;
5962 let decay_factor = (-age * 0.001 * dt).exp();
5963 self.affinity *= decay_factor;
5964 self.fear = (self.fear - dt * 0.01).max(0.0);
5965 }
5966}
5967
5968pub struct SocialGraph {
5969 pub agent_id: u64,
5970 pub relationships: HashMap<u64, SocialRelationship>,
5971 pub faction_id: u32,
5972 pub faction_relations: HashMap<u32, f32>, }
5974
5975impl SocialGraph {
5976 pub fn new(agent_id: u64, faction_id: u32) -> Self {
5977 Self { agent_id, relationships: HashMap::new(), faction_id, faction_relations: HashMap::new() }
5978 }
5979
5980 pub fn get_or_create_relationship(&mut self, other_id: u64) -> &mut SocialRelationship {
5981 self.relationships.entry(other_id).or_insert_with(|| SocialRelationship::new(other_id))
5982 }
5983
5984 pub fn affinity_toward(&self, other_id: u64) -> f32 {
5985 self.relationships.get(&other_id).map(|r| r.affinity).unwrap_or(0.0)
5986 }
5987
5988 pub fn is_ally(&self, other_id: u64, other_faction: u32) -> bool {
5989 let personal = self.relationships.get(&other_id).map(|r| r.affinity).unwrap_or(0.0);
5990 let faction_aff = self.faction_relations.get(&other_faction).copied().unwrap_or(0.0);
5991 (personal + faction_aff) > 0.2
5992 }
5993
5994 pub fn is_enemy(&self, other_id: u64, other_faction: u32) -> bool {
5995 let personal = self.relationships.get(&other_id).map(|r| r.affinity).unwrap_or(0.0);
5996 let faction_aff = self.faction_relations.get(&other_faction).copied().unwrap_or(0.0);
5997 (personal + faction_aff) < -0.2
5998 }
5999
6000 pub fn update(&mut self, dt: f32, current_time: f32) {
6001 for rel in self.relationships.values_mut() {
6002 rel.decay(dt, current_time);
6003 }
6004 }
6005}
6006
6007#[derive(Clone, Debug, PartialEq)]
6012pub enum LocoState {
6013 Idle,
6014 Walk,
6015 Run,
6016 Crouch,
6017 CrouchWalk,
6018 Jump,
6019 Fall,
6020 Land,
6021 Strafe(f32), Dead,
6023}
6024
6025pub struct LocomotionAnimController {
6026 pub state: LocoState,
6027 pub blend_weights: HashMap<String, f32>,
6028 pub transition_time: f32,
6029 pub transition_remaining: f32,
6030 pub prev_state: LocoState,
6031 pub speed: f32,
6032 pub turn_rate: f32,
6033 pub is_grounded: bool,
6034}
6035
6036impl LocomotionAnimController {
6037 pub fn new() -> Self {
6038 Self {
6039 state: LocoState::Idle,
6040 blend_weights: HashMap::new(),
6041 transition_time: 0.2,
6042 transition_remaining: 0.0,
6043 prev_state: LocoState::Idle,
6044 speed: 0.0,
6045 turn_rate: 0.0,
6046 is_grounded: true,
6047 }
6048 }
6049
6050 pub fn update(&mut self, velocity: Vec3, is_grounded: bool, is_crouching: bool, dt: f32) {
6051 self.speed = velocity.length();
6052 self.is_grounded = is_grounded;
6053 self.transition_remaining = (self.transition_remaining - dt).max(0.0);
6054
6055 let new_state = if !is_grounded {
6056 if velocity.y > 0.1 { LocoState::Jump }
6057 else { LocoState::Fall }
6058 } else if is_crouching {
6059 if self.speed > 0.5 { LocoState::CrouchWalk } else { LocoState::Crouch }
6060 } else if self.speed < 0.1 {
6061 LocoState::Idle
6062 } else if self.speed < 2.5 {
6063 LocoState::Walk
6064 } else {
6065 LocoState::Run
6066 };
6067
6068 if new_state != self.state {
6069 self.prev_state = self.state.clone();
6070 self.state = new_state;
6071 self.transition_remaining = self.transition_time;
6072 }
6073
6074 let t = if self.transition_time > 0.0 {
6076 1.0 - (self.transition_remaining / self.transition_time)
6077 } else { 1.0 };
6078
6079 self.blend_weights.insert("walk".to_string(), if matches!(self.state, LocoState::Walk) { t } else { 0.0 });
6080 self.blend_weights.insert("run".to_string(), if matches!(self.state, LocoState::Run) { t } else { 0.0 });
6081 self.blend_weights.insert("idle".to_string(), if matches!(self.state, LocoState::Idle) { t } else { 0.0 });
6082 self.blend_weights.insert("crouch".to_string(), if matches!(self.state, LocoState::Crouch | LocoState::CrouchWalk) { t } else { 0.0 });
6083 }
6084
6085 pub fn get_blend_weight(&self, anim: &str) -> f32 {
6086 self.blend_weights.get(anim).copied().unwrap_or(0.0)
6087 }
6088}
6089
6090pub struct AiEditorTests;
6095
6096impl AiEditorTests {
6097 pub fn run_all() -> Vec<(String, bool)> {
6098 let mut results = Vec::new();
6099 results.push(("blackboard_basic".to_string(), Self::test_blackboard_basic()));
6100 results.push(("bt_sequence_success".to_string(), Self::test_bt_sequence_success()));
6101 results.push(("bt_selector_fallthrough".to_string(), Self::test_bt_selector_fallthrough()));
6102 results.push(("bt_inverter".to_string(), Self::test_bt_inverter()));
6103 results.push(("bt_cooldown".to_string(), Self::test_bt_cooldown()));
6104 results.push(("bt_repeater".to_string(), Self::test_bt_repeater()));
6105 results.push(("bt_wait".to_string(), Self::test_bt_wait()));
6106 results.push(("goap_basic_plan".to_string(), Self::test_goap_basic_plan()));
6107 results.push(("utility_scoring".to_string(), Self::test_utility_scoring()));
6108 results.push(("perception_vision".to_string(), Self::test_perception_vision()));
6109 results.push(("formation_line".to_string(), Self::test_formation_line()));
6110 results.push(("steering_seek".to_string(), Self::test_steering_seek()));
6111 results.push(("emotion_decay".to_string(), Self::test_emotion_decay()));
6112 results.push(("response_curve_logistic".to_string(), Self::test_response_curve_logistic()));
6113 results.push(("astar_pathfinding".to_string(), Self::test_astar_pathfinding()));
6114 results
6115 }
6116
6117 fn test_blackboard_basic() -> bool {
6118 let mut bb = Blackboard::new();
6119 bb.set("health", BlackboardValue::Float(100.0));
6120 let v = bb.get_float("health");
6121 (v - 100.0).abs() < EPSILON
6122 }
6123
6124 fn test_bt_sequence_success() -> bool {
6125 let mut tree = BehaviorTree::new("test");
6126 let root = tree.add_node(BtNodeType::Sequence);
6127 tree.set_root(root);
6128 let s1 = tree.add_node(BtNodeType::SucceedAlways);
6129 let s2 = tree.add_node(BtNodeType::SucceedAlways);
6130 tree.add_child(root, s1);
6131 tree.add_child(root, s2);
6132 let mut bb = Blackboard::new();
6133 let mut ctx = BtTickContext::new(&mut bb, 0.016, 0.0, Vec3::ZERO, 1);
6134 let status = tree.tick(&mut ctx);
6135 status == BtStatus::Success
6136 }
6137
6138 fn test_bt_selector_fallthrough() -> bool {
6139 let mut tree = BehaviorTree::new("test");
6140 let root = tree.add_node(BtNodeType::Selector);
6141 tree.set_root(root);
6142 let f1 = tree.add_node(BtNodeType::FailAlways);
6143 let s1 = tree.add_node(BtNodeType::SucceedAlways);
6144 tree.add_child(root, f1);
6145 tree.add_child(root, s1);
6146 let mut bb = Blackboard::new();
6147 let mut ctx = BtTickContext::new(&mut bb, 0.016, 0.0, Vec3::ZERO, 1);
6148 let status = tree.tick(&mut ctx);
6149 status == BtStatus::Success
6150 }
6151
6152 fn test_bt_inverter() -> bool {
6153 let mut tree = BehaviorTree::new("test");
6154 let root = tree.add_node(BtNodeType::Inverter);
6155 tree.set_root(root);
6156 let child = tree.add_node(BtNodeType::SucceedAlways);
6157 tree.add_child(root, child);
6158 let mut bb = Blackboard::new();
6159 let mut ctx = BtTickContext::new(&mut bb, 0.016, 0.0, Vec3::ZERO, 1);
6160 let status = tree.tick(&mut ctx);
6161 status == BtStatus::Failure
6162 }
6163
6164 fn test_bt_cooldown() -> bool {
6165 let mut tree = BehaviorTree::new("test");
6166 let root = tree.add_node(BtNodeType::Cooldown { cooldown: 2.0 });
6167 tree.set_root(root);
6168 let child = tree.add_node(BtNodeType::SucceedAlways);
6169 tree.add_child(root, child);
6170 let mut bb = Blackboard::new();
6171 let mut ctx = BtTickContext::new(&mut bb, 0.016, 0.0, Vec3::ZERO, 1);
6172 let s1 = tree.tick(&mut ctx); let s2 = tree.tick(&mut ctx); s1 == BtStatus::Success && s2 == BtStatus::Failure
6175 }
6176
6177 fn test_bt_repeater() -> bool {
6178 let mut tree = BehaviorTree::new("test");
6179 let root = tree.add_node(BtNodeType::Repeater { times: 3 });
6180 tree.set_root(root);
6181 let child = tree.add_node(BtNodeType::SucceedAlways);
6182 tree.add_child(root, child);
6183 let mut bb = Blackboard::new();
6184 let mut ctx = BtTickContext::new(&mut bb, 0.016, 0.0, Vec3::ZERO, 1);
6185 let s1 = tree.tick(&mut ctx);
6186 let s2 = tree.tick(&mut ctx);
6187 let s3 = tree.tick(&mut ctx);
6188 s3 == BtStatus::Success
6190 }
6191
6192 fn test_bt_wait() -> bool {
6193 let mut tree = BehaviorTree::new("test");
6194 let root = tree.add_node(BtNodeType::Wait { duration: 0.5 });
6195 tree.set_root(root);
6196 let mut bb = Blackboard::new();
6197 let mut ctx1 = BtTickContext::new(&mut bb, 0.1, 0.0, Vec3::ZERO, 1);
6198 let s1 = tree.tick(&mut ctx1);
6199 let mut bb2 = Blackboard::new();
6200 let mut ctx2 = BtTickContext::new(&mut bb2, 0.5, 0.5, Vec3::ZERO, 1);
6201 let s2 = tree.tick(&mut ctx2);
6202 s1 == BtStatus::Running
6203 }
6204
6205 fn test_goap_basic_plan() -> bool {
6206 let planner = GoapLibrary::build_combat_planner();
6207 let start: WorldState = 0b0000_0011;
6210 let goal: WorldState = 0b0000_0100;
6211 let plan = planner.plan(start, goal, 0.0);
6212 plan.is_some()
6213 }
6214
6215 fn test_utility_scoring() -> bool {
6216 let dm = UtilityLibrary::build_combat_decision_maker();
6217 let mut bb = Blackboard::new();
6218 bb.set("self_health", BlackboardValue::Float(80.0));
6219 bb.set("enemy_visible", BlackboardValue::Float(1.0));
6220 bb.set("ammo_count", BlackboardValue::Float(20.0));
6221 bb.set("threat_dist", BlackboardValue::Float(10.0));
6222 let scores: Vec<f32> = dm.actions.iter().map(|a| a.score(&bb, 0.0)).collect();
6223 scores.iter().any(|&s| s > 0.0)
6224 }
6225
6226 fn test_perception_vision() -> bool {
6227 let ps = PerceptionSystem::new(1);
6228 let observer_pos = Vec3::ZERO;
6229 let observer_fwd = Vec3::Z;
6230 let target_pos = Vec3::new(0.0, 0.0, 10.0); let (vis, conf) = ps.can_see(observer_pos, observer_fwd, target_pos, Vec3::ZERO, &[]);
6232 vis && conf > 0.0
6233 }
6234
6235 fn test_formation_line() -> bool {
6236 let slots = FormationLayout::compute_slots(FormationType::Line, Vec3::ZERO, Vec3::Z, 5, 2.0);
6237 slots.len() == 5
6238 }
6239
6240 fn test_steering_seek() -> bool {
6241 let agent = SteeringAgent::new(1, Vec3::ZERO, 5.0, 10.0);
6242 let target = Vec3::new(0.0, 0.0, 10.0);
6243 let force = SteeringBehaviors::seek(&agent, target);
6244 force.length() > 0.0
6245 }
6246
6247 fn test_emotion_decay() -> bool {
6248 let mut state = EmotionState::new();
6249 state.add_emotion(PrimaryEmotion::Fear, 1.0);
6250 let initial = state.get_intensity(PrimaryEmotion::Fear);
6251 state.update(1.0);
6252 let after = state.get_intensity(PrimaryEmotion::Fear);
6253 after < initial
6254 }
6255
6256 fn test_response_curve_logistic() -> bool {
6257 let curve = ResponseCurve::Logistic { steepness: 5.0, midpoint: 0.5 };
6258 let low = curve.evaluate(0.0);
6259 let mid = curve.evaluate(0.5);
6260 let high = curve.evaluate(1.0);
6261 low < mid && mid < high
6262 }
6263
6264 fn test_astar_pathfinding() -> bool {
6265 let pf = GridPathfinder::new(20, 20, 1.0, Vec2::ZERO);
6266 let path = pf.find_path(Vec2::new(0.5, 0.5), Vec2::new(18.5, 18.5));
6267 path.is_some()
6268 }
6269}
6270
6271pub fn create_default_ai_editor() -> AiBehaviorEditor {
6276 AiBehaviorEditor::new()
6277}
6278
6279pub fn run_editor_tests() -> usize {
6280 let results = AiEditorTests::run_all();
6281 let passed = results.iter().filter(|(_, ok)| *ok).count();
6282 passed
6283}
6284
6285#[derive(Clone, Debug)]
6290pub struct CoverPoint {
6291 pub id: u32,
6292 pub position: Vec3,
6293 pub normal: Vec3,
6294 pub height: f32,
6295 pub is_occupied: Option<u64>,
6296 pub quality: f32,
6297 pub flanked_by: Vec<Vec3>,
6298}
6299
6300impl CoverPoint {
6301 pub fn new(id: u32, position: Vec3, normal: Vec3, height: f32) -> Self {
6302 Self { id, position, normal, height, is_occupied: None, quality: 1.0, flanked_by: Vec::new() }
6303 }
6304
6305 pub fn is_good_cover_from(&self, threat_pos: Vec3) -> bool {
6306 let to_threat = (threat_pos - self.position).normalize_or_zero();
6307 self.normal.dot(to_threat) > 0.5
6308 }
6309
6310 pub fn cover_quality_from(&self, threat_pos: Vec3) -> f32 {
6311 let to_threat = (threat_pos - self.position).normalize_or_zero();
6312 let dot = self.normal.dot(to_threat).max(0.0);
6313 let dist_factor = {
6314 let d = (threat_pos - self.position).length();
6315 (d / 20.0).clamp(0.1, 1.0)
6316 };
6317 let flank_penalty = self.flanked_by.iter()
6318 .map(|&fdir| (fdir - self.position).normalize_or_zero().dot(to_threat).max(0.0))
6319 .fold(0.0f32, |a, b| a.max(b));
6320 (dot * dist_factor * self.quality * (1.0 - flank_penalty * 0.5)).clamp(0.0, 1.0)
6321 }
6322
6323 pub fn peek_position(&self, peek_amount: f32) -> Vec3 {
6324 self.position + self.normal * peek_amount
6325 }
6326}
6327
6328pub struct CoverSystem {
6329 pub cover_points: Vec<CoverPoint>,
6330 pub next_id: u32,
6331 pub occupation_radius: f32,
6332}
6333
6334impl CoverSystem {
6335 pub fn new() -> Self {
6336 Self { cover_points: Vec::new(), next_id: 1, occupation_radius: 1.5 }
6337 }
6338
6339 pub fn add_cover(&mut self, position: Vec3, normal: Vec3, height: f32) -> u32 {
6340 let id = self.next_id;
6341 self.next_id += 1;
6342 self.cover_points.push(CoverPoint::new(id, position, normal, height));
6343 id
6344 }
6345
6346 pub fn find_best_cover(&self, seeker_pos: Vec3, threats: &[Vec3], occupied_by: u64, max_distance: f32) -> Option<&CoverPoint> {
6347 if threats.is_empty() { return None; }
6348 self.cover_points.iter()
6349 .filter(|c| {
6350 let dist = (c.position - seeker_pos).length();
6351 dist <= max_distance && (c.is_occupied.is_none() || c.is_occupied == Some(occupied_by))
6352 })
6353 .filter(|c| threats.iter().any(|&t| c.is_good_cover_from(t)))
6354 .max_by(|a, b| {
6355 let qa: f32 = threats.iter().map(|&t| a.cover_quality_from(t)).sum::<f32>() / (1.0 + (a.position - seeker_pos).length() * 0.1);
6356 let qb: f32 = threats.iter().map(|&t| b.cover_quality_from(t)).sum::<f32>() / (1.0 + (b.position - seeker_pos).length() * 0.1);
6357 qa.partial_cmp(&qb).unwrap()
6358 })
6359 }
6360
6361 pub fn occupy(&mut self, cover_id: u32, agent_id: u64) {
6362 if let Some(c) = self.cover_points.iter_mut().find(|c| c.id == cover_id) {
6363 c.is_occupied = Some(agent_id);
6364 }
6365 }
6366
6367 pub fn vacate(&mut self, agent_id: u64) {
6368 for c in &mut self.cover_points { if c.is_occupied == Some(agent_id) { c.is_occupied = None; } }
6369 }
6370
6371 pub fn generate_cover_from_obstacles(&mut self, obstacles: &[Aabb], normal_directions: &[Vec3]) {
6372 for obs in obstacles {
6373 for &normal in normal_directions {
6374 let position = obs.center() + normal * (obs.half_extents().length() + 0.5);
6375 self.add_cover(position, -normal, 1.0);
6376 }
6377 }
6378 }
6379
6380 pub fn debug_draw(&self, buf: &mut DebugVisualizationBuffer) {
6381 for cover in &self.cover_points {
6382 let color = if cover.is_occupied.is_some() { Vec4::new(1.0, 0.5, 0.0, 0.8) } else { Vec4::new(0.0, 0.8, 0.8, 0.8) };
6383 buf.add(DebugShapeType::Arrow { from: cover.position, to: cover.position + cover.normal * 1.0, head_size: 0.2 }, color, 0.0);
6384 buf.add(DebugShapeType::Cross { center: cover.position, size: 0.4 }, color, 0.0);
6385 }
6386 }
6387}
6388
6389#[derive(Clone, Debug)]
6394pub struct ThreatEntry {
6395 pub entity_id: u64,
6396 pub position: Vec3,
6397 pub velocity: Vec3,
6398 pub threat_score: f32,
6399 pub last_damage_dealt: f32,
6400 pub can_see_me: bool,
6401 pub is_flanking: bool,
6402 pub last_updated: f32,
6403}
6404
6405pub struct ThreatAssessor {
6406 pub threats: Vec<ThreatEntry>,
6407 pub current_time: f32,
6408 pub stale_threshold: f32,
6409 pub damage_weight: f32,
6410 pub distance_weight: f32,
6411 pub flanking_weight: f32,
6412 pub facing_weight: f32,
6413}
6414
6415impl ThreatAssessor {
6416 pub fn new() -> Self {
6417 Self {
6418 threats: Vec::new(),
6419 current_time: 0.0,
6420 stale_threshold: 5.0,
6421 damage_weight: 2.5,
6422 distance_weight: 2.0,
6423 flanking_weight: 2.0,
6424 facing_weight: 1.5,
6425 }
6426 }
6427
6428 pub fn compute_threat_score(&self, perceiver_pos: Vec3, target: &PerceivedEntity, target_facing: Vec3, damage_dealt: f32) -> f32 {
6429 let dist = (target.position - perceiver_pos).length();
6430 let dist_score = 1.0 / (1.0 + dist * 0.1);
6431 let to_target = (target.position - perceiver_pos).normalize_or_zero();
6432 let facing_dot = target_facing.dot(to_target).max(0.0);
6433 let behind_dot = (-to_target).dot((perceiver_pos - target.position).normalize_or_zero()).max(0.0);
6434 let flanking = behind_dot > 0.7;
6435 let speed = target.velocity.length();
6436 (dist_score * self.distance_weight
6437 + facing_dot * self.facing_weight
6438 + (damage_dealt / 100.0) * self.damage_weight
6439 + if flanking { self.flanking_weight } else { 0.0 }
6440 + (speed / 10.0) * 0.5) * target.confidence
6441 }
6442
6443 pub fn update_threat(&mut self, entity_id: u64, position: Vec3, velocity: Vec3, score: f32, damage_dealt: f32, can_see_me: bool, is_flanking: bool) {
6444 self.threats.retain(|t| t.entity_id != entity_id);
6445 self.threats.push(ThreatEntry { entity_id, position, velocity, threat_score: score, last_damage_dealt: damage_dealt, can_see_me, is_flanking, last_updated: self.current_time });
6446 self.threats.sort_by(|a, b| b.threat_score.partial_cmp(&a.threat_score).unwrap());
6447 }
6448
6449 pub fn remove_stale(&mut self) {
6450 let stale_time = self.current_time - self.stale_threshold;
6451 self.threats.retain(|t| t.last_updated >= stale_time);
6452 }
6453
6454 pub fn primary_threat(&self) -> Option<&ThreatEntry> { self.threats.first() }
6455
6456 pub fn tick(&mut self, dt: f32) {
6457 self.current_time += dt;
6458 self.remove_stale();
6459 }
6460}
6461
6462#[derive(Clone, Debug)]
6467pub enum DecisionTreeNode {
6468 Decision {
6469 attribute_key: String,
6470 threshold: f32,
6471 left_branch: Box<DecisionTreeNode>,
6472 right_branch: Box<DecisionTreeNode>,
6473 },
6474 Leaf {
6475 action_label: String,
6476 action_id: u32,
6477 confidence: f32,
6478 },
6479}
6480
6481impl DecisionTreeNode {
6482 pub fn evaluate(&self, blackboard: &Blackboard) -> (u32, String, f32) {
6483 match self {
6484 DecisionTreeNode::Leaf { action_id, action_label, confidence } => (*action_id, action_label.clone(), *confidence),
6485 DecisionTreeNode::Decision { attribute_key, threshold, left_branch, right_branch } => {
6486 if blackboard.get_float(attribute_key) < *threshold { left_branch.evaluate(blackboard) }
6487 else { right_branch.evaluate(blackboard) }
6488 }
6489 }
6490 }
6491
6492 pub fn depth(&self) -> usize {
6493 match self {
6494 DecisionTreeNode::Leaf { .. } => 1,
6495 DecisionTreeNode::Decision { left_branch, right_branch, .. } => 1 + left_branch.depth().max(right_branch.depth()),
6496 }
6497 }
6498}
6499
6500pub struct DecisionTreeBuilder;
6501impl DecisionTreeBuilder {
6502 pub fn build_combat_tree() -> DecisionTreeNode {
6503 DecisionTreeNode::Decision {
6504 attribute_key: "self_health".to_string(),
6505 threshold: 0.3,
6506 left_branch: Box::new(DecisionTreeNode::Decision {
6507 attribute_key: "medpack_count".to_string(),
6508 threshold: 1.0,
6509 left_branch: Box::new(DecisionTreeNode::Leaf { action_label: "Heal".to_string(), action_id: 10, confidence: 0.95 }),
6510 right_branch: Box::new(DecisionTreeNode::Decision {
6511 attribute_key: "threat_dist".to_string(),
6512 threshold: 8.0,
6513 left_branch: Box::new(DecisionTreeNode::Leaf { action_label: "Flee".to_string(), action_id: 11, confidence: 0.9 }),
6514 right_branch: Box::new(DecisionTreeNode::Leaf { action_label: "TakeCover".to_string(), action_id: 12, confidence: 0.8 }),
6515 }),
6516 }),
6517 right_branch: Box::new(DecisionTreeNode::Decision {
6518 attribute_key: "enemy_visible".to_string(),
6519 threshold: 0.5,
6520 left_branch: Box::new(DecisionTreeNode::Leaf { action_label: "Patrol".to_string(), action_id: 13, confidence: 0.7 }),
6521 right_branch: Box::new(DecisionTreeNode::Decision {
6522 attribute_key: "ammo_count".to_string(),
6523 threshold: 5.0,
6524 left_branch: Box::new(DecisionTreeNode::Leaf { action_label: "Reload".to_string(), action_id: 15, confidence: 0.85 }),
6525 right_branch: Box::new(DecisionTreeNode::Leaf { action_label: "Attack".to_string(), action_id: 16, confidence: 0.9 }),
6526 }),
6527 }),
6528 }
6529 }
6530}
6531
6532#[derive(Clone, Debug)]
6537pub enum FuzzyMembershipType {
6538 Triangular { left: f32, center: f32, right: f32 },
6539 Trapezoidal { left_edge: f32, left_plateau: f32, right_plateau: f32, right_edge: f32 },
6540 Gaussian { center: f32, sigma: f32 },
6541 Singleton { value: f32 },
6542}
6543
6544#[derive(Clone, Debug)]
6545pub struct FuzzySet {
6546 pub name: String,
6547 pub membership_type: FuzzyMembershipType,
6548}
6549
6550impl FuzzySet {
6551 pub fn membership(&self, x: f32) -> f32 {
6552 match &self.membership_type {
6553 FuzzyMembershipType::Triangular { left, center, right } => {
6554 if x <= *left || x >= *right { 0.0 }
6555 else if x <= *center { (x - left) / (center - left + EPSILON) }
6556 else { (right - x) / (right - center + EPSILON) }
6557 }
6558 FuzzyMembershipType::Trapezoidal { left_edge, left_plateau, right_plateau, right_edge } => {
6559 if x <= *left_edge || x >= *right_edge { 0.0 }
6560 else if x <= *left_plateau { (x - left_edge) / (left_plateau - left_edge + EPSILON) }
6561 else if x <= *right_plateau { 1.0 }
6562 else { (right_edge - x) / (right_edge - right_plateau + EPSILON) }
6563 }
6564 FuzzyMembershipType::Gaussian { center, sigma } => {
6565 let d = (x - center) / (sigma + EPSILON);
6566 (-0.5 * d * d).exp()
6567 }
6568 FuzzyMembershipType::Singleton { value } => {
6569 if (x - value).abs() < EPSILON { 1.0 } else { 0.0 }
6570 }
6571 }
6572 }
6573}
6574
6575#[derive(Clone, Debug)]
6576pub struct FuzzyRule {
6577 pub input_set_indices: Vec<usize>,
6578 pub output_set_index: usize,
6579 pub weight: f32,
6580}
6581
6582pub struct FuzzyInferenceSystem {
6583 pub input_sets: Vec<Vec<FuzzySet>>,
6584 pub output_sets: Vec<FuzzySet>,
6585 pub rules: Vec<FuzzyRule>,
6586 pub input_variables: Vec<String>,
6587 pub output_variable: String,
6588}
6589
6590impl FuzzyInferenceSystem {
6591 pub fn new(output_var: &str) -> Self {
6592 Self { input_sets: Vec::new(), output_sets: Vec::new(), rules: Vec::new(), input_variables: Vec::new(), output_variable: output_var.to_string() }
6593 }
6594
6595 pub fn add_input(&mut self, name: &str, sets: Vec<FuzzySet>) -> usize {
6596 let idx = self.input_sets.len();
6597 self.input_variables.push(name.to_string());
6598 self.input_sets.push(sets);
6599 idx
6600 }
6601
6602 pub fn add_output_sets(&mut self, sets: Vec<FuzzySet>) { self.output_sets = sets; }
6603
6604 pub fn add_rule(&mut self, input_set_indices: Vec<usize>, output_set_index: usize, weight: f32) {
6605 self.rules.push(FuzzyRule { input_set_indices, output_set_index, weight });
6606 }
6607
6608 pub fn infer(&self, inputs: &[f32], output_range: (f32, f32), resolution: usize) -> f32 {
6609 let mut output_activations: Vec<f32> = vec![0.0; self.output_sets.len()];
6610 for rule in &self.rules {
6611 let mut activation = rule.weight;
6612 for (input_idx, &set_idx) in rule.input_set_indices.iter().enumerate() {
6613 if input_idx >= inputs.len() || input_idx >= self.input_sets.len() { break; }
6614 let m = if set_idx < self.input_sets[input_idx].len() { self.input_sets[input_idx][set_idx].membership(inputs[input_idx]) } else { 0.0 };
6615 activation = activation.min(m);
6616 }
6617 if rule.output_set_index < output_activations.len() {
6618 output_activations[rule.output_set_index] = output_activations[rule.output_set_index].max(activation);
6619 }
6620 }
6621 let (lo, hi) = output_range;
6622 let step = (hi - lo) / resolution.max(1) as f32;
6623 let mut num = 0.0f32;
6624 let mut den = 0.0f32;
6625 for i in 0..resolution {
6626 let x = lo + i as f32 * step + step * 0.5;
6627 let mut max_mem = 0.0f32;
6628 for (j, set) in self.output_sets.iter().enumerate() {
6629 let clipped = set.membership(x).min(output_activations.get(j).copied().unwrap_or(0.0));
6630 max_mem = max_mem.max(clipped);
6631 }
6632 num += x * max_mem;
6633 den += max_mem;
6634 }
6635 if den < EPSILON { (lo + hi) * 0.5 } else { num / den }
6636 }
6637}
6638
6639pub struct FuzzyBehaviorController;
6640impl FuzzyBehaviorController {
6641 pub fn build_aggressiveness_fis() -> FuzzyInferenceSystem {
6642 let mut fis = FuzzyInferenceSystem::new("aggressiveness");
6643 fis.add_input("health", vec![
6644 FuzzySet { name: "low".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 0.0, center: 0.0, right: 0.4 } },
6645 FuzzySet { name: "medium".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 0.2, center: 0.5, right: 0.8 } },
6646 FuzzySet { name: "high".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 0.6, center: 1.0, right: 1.0 } },
6647 ]);
6648 fis.add_input("threat_count", vec![
6649 FuzzySet { name: "few".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 0.0, center: 0.0, right: 3.0 } },
6650 FuzzySet { name: "moderate".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 1.0, center: 4.0, right: 7.0 } },
6651 FuzzySet { name: "many".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 5.0, center: 10.0, right: 10.0 } },
6652 ]);
6653 fis.add_output_sets(vec![
6654 FuzzySet { name: "cowardly".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 0.0, center: 0.0, right: 0.3 } },
6655 FuzzySet { name: "cautious".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 0.1, center: 0.4, right: 0.7 } },
6656 FuzzySet { name: "aggressive".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 0.5, center: 0.8, right: 1.0 } },
6657 FuzzySet { name: "berserker".to_string(), membership_type: FuzzyMembershipType::Triangular { left: 0.8, center: 1.0, right: 1.0 } },
6658 ]);
6659 fis.add_rule(vec![2, 0], 2, 1.0);
6660 fis.add_rule(vec![2, 2], 1, 1.0);
6661 fis.add_rule(vec![1, 0], 2, 0.8);
6662 fis.add_rule(vec![1, 1], 1, 0.9);
6663 fis.add_rule(vec![0, 0], 1, 0.7);
6664 fis.add_rule(vec![0, 1], 0, 1.0);
6665 fis.add_rule(vec![0, 2], 0, 1.0);
6666 fis.add_rule(vec![2, 0], 3, 0.5);
6667 fis
6668 }
6669}
6670
6671#[derive(Clone, Debug)]
6676pub enum HtnTask {
6677 Primitive { name: String, action_id: u32, preconditions: WorldState, effects_set: WorldState, effects_clear: WorldState, cost: f32 },
6678 Compound { name: String, methods: Vec<HtnMethod> },
6679}
6680
6681#[derive(Clone, Debug)]
6682pub struct HtnMethod {
6683 pub name: String,
6684 pub preconditions: WorldState,
6685 pub subtasks: Vec<String>,
6686 pub priority: i32,
6687}
6688
6689pub struct HtnPlanner {
6690 pub tasks: HashMap<String, HtnTask>,
6691 pub root_task: String,
6692}
6693
6694impl HtnPlanner {
6695 pub fn new(root_task: &str) -> Self { Self { tasks: HashMap::new(), root_task: root_task.to_string() } }
6696
6697 pub fn add_task(&mut self, name: &str, task: HtnTask) { self.tasks.insert(name.to_string(), task); }
6698
6699 pub fn plan(&self, world_state: WorldState) -> Vec<u32> {
6700 let mut plan = Vec::new();
6701 let mut tasks_to_process: VecDeque<String> = VecDeque::new();
6702 tasks_to_process.push_back(self.root_task.clone());
6703 let mut current_state = world_state;
6704 let mut depth = 0;
6705
6706 while let Some(task_name) = tasks_to_process.pop_front() {
6707 if depth > 50 { break; }
6708 depth += 1;
6709 if let Some(task) = self.tasks.get(&task_name) {
6710 match task {
6711 HtnTask::Primitive { action_id, preconditions, effects_set, effects_clear, .. } => {
6712 if (current_state & preconditions) == *preconditions {
6713 plan.push(*action_id);
6714 current_state = (current_state | effects_set) & !effects_clear;
6715 }
6716 }
6717 HtnTask::Compound { methods, .. } => {
6718 let mut sorted_methods: Vec<&HtnMethod> = methods.iter().collect();
6719 sorted_methods.sort_by(|a, b| b.priority.cmp(&a.priority));
6720 for method in sorted_methods {
6721 if (current_state & method.preconditions) == method.preconditions {
6722 let existing: Vec<String> = tasks_to_process.iter().cloned().collect();
6723 tasks_to_process.clear();
6724 for st in &method.subtasks { tasks_to_process.push_back(st.clone()); }
6725 for et in existing { tasks_to_process.push_back(et); }
6726 break;
6727 }
6728 }
6729 }
6730 }
6731 }
6732 }
6733 plan
6734 }
6735
6736 pub fn build_combat_network() -> HtnPlanner {
6737 let mut planner = HtnPlanner::new("BeSoldier");
6738 planner.add_task("BeSoldier", HtnTask::Compound {
6739 name: "BeSoldier".to_string(),
6740 methods: vec![
6741 HtnMethod { name: "Fight".to_string(), preconditions: 0b0000_0011, subtasks: vec!["EngageEnemy".to_string()], priority: 10 },
6742 HtnMethod { name: "GetAmmo".to_string(), preconditions: 0, subtasks: vec!["FindAmmo".to_string(), "Reload".to_string()], priority: 5 },
6743 HtnMethod { name: "Patrol".to_string(), preconditions: 0, subtasks: vec!["PatrolArea".to_string()], priority: 1 },
6744 ],
6745 });
6746 planner.add_task("EngageEnemy", HtnTask::Compound {
6747 name: "EngageEnemy".to_string(),
6748 methods: vec![
6749 HtnMethod { name: "ShootEnemy".to_string(), preconditions: 0b0000_0001, subtasks: vec!["MoveToAttackPos".to_string(), "Shoot".to_string()], priority: 10 },
6750 HtnMethod { name: "MeleeEnemy".to_string(), preconditions: 0, subtasks: vec!["MoveToMeleePos".to_string(), "MeleeAttack".to_string()], priority: 5 },
6751 ],
6752 });
6753 planner.add_task("Shoot", HtnTask::Primitive { name: "Shoot".to_string(), action_id: 101, preconditions: 0b11, effects_set: 0b100, effects_clear: 0b10, cost: 1.0 });
6754 planner.add_task("MeleeAttack", HtnTask::Primitive { name: "MeleeAttack".to_string(), action_id: 102, preconditions: 0b10, effects_set: 0b100, effects_clear: 0b10, cost: 1.5 });
6755 planner.add_task("MoveToAttackPos", HtnTask::Primitive { name: "MoveToAttackPos".to_string(), action_id: 103, preconditions: 0b10, effects_set: 0b1_0000, effects_clear: 0, cost: 2.0 });
6756 planner.add_task("MoveToMeleePos", HtnTask::Primitive { name: "MoveToMeleePos".to_string(), action_id: 104, preconditions: 0b10, effects_set: 0b10_0000, effects_clear: 0, cost: 3.0 });
6757 planner.add_task("Reload", HtnTask::Primitive { name: "Reload".to_string(), action_id: 105, preconditions: 0, effects_set: 1, effects_clear: 0, cost: 1.5 });
6758 planner.add_task("FindAmmo", HtnTask::Primitive { name: "FindAmmo".to_string(), action_id: 106, preconditions: 0, effects_set: 0b100_0000, effects_clear: 0, cost: 2.5 });
6759 planner.add_task("PatrolArea", HtnTask::Primitive { name: "PatrolArea".to_string(), action_id: 107, preconditions: 0, effects_set: 0b10, effects_clear: 0, cost: 1.0 });
6760 planner
6761 }
6762}
6763
6764pub struct BtSerializer;
6769impl BtSerializer {
6770 pub fn serialize(tree: &BehaviorTree) -> String {
6771 let mut out = String::new();
6772 out.push_str(&format!("tree \"{}\" {{\n", tree.name));
6773 if let Some(root) = tree.root_id { Self::serialize_node(tree, root, &mut out, 1); }
6774 out.push_str("}\n");
6775 out
6776 }
6777
6778 fn serialize_node(tree: &BehaviorTree, node_id: u32, out: &mut String, depth: usize) {
6779 if depth > 30 { return; }
6780 let indent = " ".repeat(depth);
6781 if let Some(node) = tree.nodes.get(&node_id) {
6782 out.push_str(&format!("{}node {} [id={}] {{\n", indent, node.display_name(), node.id));
6783 for &child_id in &node.children { Self::serialize_node(tree, child_id, out, depth + 1); }
6784 out.push_str(&format!("{}}}\n", indent));
6785 }
6786 }
6787}
6788
6789#[derive(Clone, Debug)]
6794pub enum SoundType { Footstep, Gunshot, Explosion, Voice, Ambient }
6795
6796#[derive(Clone, Debug)]
6797pub enum AiSignal {
6798 EnemySpotted { spotter_id: u64, enemy_id: u64, position: Vec3 },
6799 AllyKilled { ally_id: u64, position: Vec3, killer_id: u64 },
6800 SoundHeard { listener_id: u64, source_pos: Vec3, sound_type: SoundType, intensity: f32 },
6801 ItemPickedUp { agent_id: u64, item_id: String },
6802 ObjectiveReached { agent_id: u64, objective_id: u32 },
6803 FormationBreak { squad_id: u32, reason: String },
6804 EmotionalEvent { agent_id: u64, emotion: PrimaryEmotion, intensity: f32 },
6805 DamageTaken { agent_id: u64, damage: f32, source_id: u64, source_pos: Vec3 },
6806 AgentDied { agent_id: u64, position: Vec3 },
6807 TargetLost { agent_id: u64, last_known_pos: Vec3 },
6808 CoverReached { agent_id: u64, cover_id: u32 },
6809 BehaviorChanged { agent_id: u64, from_mode: String, to_mode: String },
6810}
6811
6812pub struct AiEventBus {
6813 pub events: VecDeque<(f32, AiSignal)>,
6814 pub history: VecDeque<(f32, AiSignal)>,
6815 pub max_history: usize,
6816 pub current_time: f32,
6817}
6818
6819impl AiEventBus {
6820 pub fn new() -> Self {
6821 Self { events: VecDeque::with_capacity(256), history: VecDeque::with_capacity(512), max_history: 512, current_time: 0.0 }
6822 }
6823
6824 pub fn publish(&mut self, signal: AiSignal) {
6825 self.events.push_back((self.current_time, signal.clone()));
6826 self.history.push_back((self.current_time, signal));
6827 if self.history.len() > self.max_history { self.history.pop_front(); }
6828 }
6829
6830 pub fn drain(&mut self) -> Vec<(f32, AiSignal)> { self.events.drain(..).collect() }
6831 pub fn tick(&mut self, dt: f32) { self.current_time += dt; }
6832}
6833
6834pub struct SpatialGrid {
6839 pub cell_size: f32,
6840 pub cells: HashMap<(i32, i32), Vec<u64>>,
6841 pub agent_cells: HashMap<u64, (i32, i32)>,
6842}
6843
6844impl SpatialGrid {
6845 pub fn new(cell_size: f32) -> Self { Self { cell_size, cells: HashMap::new(), agent_cells: HashMap::new() } }
6846
6847 pub fn cell_of(&self, pos: Vec3) -> (i32, i32) {
6848 ((pos.x / self.cell_size).floor() as i32, (pos.z / self.cell_size).floor() as i32)
6849 }
6850
6851 pub fn insert(&mut self, id: u64, pos: Vec3) {
6852 let cell = self.cell_of(pos);
6853 self.cells.entry(cell).or_default().push(id);
6854 self.agent_cells.insert(id, cell);
6855 }
6856
6857 pub fn remove(&mut self, id: u64) {
6858 if let Some(cell) = self.agent_cells.remove(&id) {
6859 if let Some(v) = self.cells.get_mut(&cell) { v.retain(|&x| x != id); }
6860 }
6861 }
6862
6863 pub fn update(&mut self, id: u64, pos: Vec3) {
6864 let new_cell = self.cell_of(pos);
6865 if let Some(&old_cell) = self.agent_cells.get(&id) {
6866 if old_cell != new_cell {
6867 if let Some(v) = self.cells.get_mut(&old_cell) { v.retain(|&x| x != id); }
6868 self.cells.entry(new_cell).or_default().push(id);
6869 self.agent_cells.insert(id, new_cell);
6870 }
6871 }
6872 }
6873
6874 pub fn query_radius(&self, pos: Vec3, radius: f32) -> Vec<u64> {
6875 let cell_radius = (radius / self.cell_size).ceil() as i32 + 1;
6876 let center_cell = self.cell_of(pos);
6877 let mut results = Vec::new();
6878 for dx in -cell_radius..=cell_radius {
6879 for dz in -cell_radius..=cell_radius {
6880 if let Some(agents) = self.cells.get(&(center_cell.0 + dx, center_cell.1 + dz)) {
6881 results.extend_from_slice(agents);
6882 }
6883 }
6884 }
6885 results
6886 }
6887
6888 pub fn clear(&mut self) { self.cells.clear(); self.agent_cells.clear(); }
6889
6890 pub fn rebuild(&mut self, agents: &[(u64, Vec3)]) {
6891 self.clear();
6892 for &(id, pos) in agents { self.insert(id, pos); }
6893 }
6894}
6895
6896#[derive(Clone, Debug)]
6901pub struct NavRegion {
6902 pub id: u32,
6903 pub vertices: Vec<Vec2>,
6904 pub center: Vec2,
6905 pub connections: Vec<NavConnection>,
6906 pub cost_modifier: f32,
6907}
6908
6909#[derive(Clone, Debug)]
6910pub struct NavConnection {
6911 pub to_region: u32,
6912 pub portal_start: Vec2,
6913 pub portal_end: Vec2,
6914 pub traversal_cost: f32,
6915}
6916
6917impl NavRegion {
6918 pub fn new(id: u32, vertices: Vec<Vec2>) -> Self {
6919 let center = if vertices.is_empty() { Vec2::ZERO }
6920 else { vertices.iter().copied().fold(Vec2::ZERO, |a, b| a + b) / vertices.len() as f32 };
6921 Self { id, vertices, center, connections: Vec::new(), cost_modifier: 1.0 }
6922 }
6923
6924 pub fn contains_point(&self, p: Vec2) -> bool {
6925 let n = self.vertices.len();
6926 if n < 3 { return false; }
6927 let mut inside = false;
6928 let mut j = n - 1;
6929 for i in 0..n {
6930 let vi = self.vertices[i];
6931 let vj = self.vertices[j];
6932 if ((vi.y > p.y) != (vj.y > p.y)) && (p.x < (vj.x - vi.x) * (p.y - vi.y) / (vj.y - vi.y + EPSILON) + vi.x) {
6933 inside = !inside;
6934 }
6935 j = i;
6936 }
6937 inside
6938 }
6939}
6940
6941pub struct NavMesh {
6942 pub regions: HashMap<u32, NavRegion>,
6943 pub next_id: u32,
6944}
6945
6946impl NavMesh {
6947 pub fn new() -> Self { Self { regions: HashMap::new(), next_id: 1 } }
6948
6949 pub fn add_region(&mut self, vertices: Vec<Vec2>) -> u32 {
6950 let id = self.next_id;
6951 self.next_id += 1;
6952 self.regions.insert(id, NavRegion::new(id, vertices));
6953 id
6954 }
6955
6956 pub fn connect_regions(&mut self, a: u32, b: u32, portal_start: Vec2, portal_end: Vec2, cost: f32) {
6957 if let Some(ra) = self.regions.get_mut(&a) {
6958 ra.connections.push(NavConnection { to_region: b, portal_start, portal_end, traversal_cost: cost });
6959 }
6960 if let Some(rb) = self.regions.get_mut(&b) {
6961 rb.connections.push(NavConnection { to_region: a, portal_start: portal_end, portal_end: portal_start, traversal_cost: cost });
6962 }
6963 }
6964
6965 pub fn find_region(&self, pos: Vec2) -> Option<u32> {
6966 self.regions.iter().find(|(_, r)| r.contains_point(pos)).map(|(&id, _)| id)
6967 }
6968
6969 pub fn find_path_regions(&self, from_region: u32, to_region: u32) -> Option<Vec<u32>> {
6970 if from_region == to_region { return Some(vec![from_region]); }
6971 let goal_center = self.regions.get(&to_region)?.center;
6972 let h = |rid: u32| self.regions.get(&rid).map(|r| (r.center - goal_center).length()).unwrap_or(f32::MAX);
6973
6974 let mut open: HashMap<u32, (f32, f32, Option<u32>)> = HashMap::new(); let mut closed: HashMap<u32, (f32, Option<u32>)> = HashMap::new();
6976 open.insert(from_region, (0.0, h(from_region), None));
6977
6978 while !open.is_empty() {
6979 let (&cur_id, _) = open.iter().min_by(|a, b| {
6980 let fa = a.1.0 + a.1.1;
6981 let fb = b.1.0 + b.1.1;
6982 fa.partial_cmp(&fb).unwrap()
6983 })?;
6984 let (g, _, parent) = open.remove(&cur_id)?;
6985 closed.insert(cur_id, (g, parent));
6986
6987 if cur_id == to_region {
6988 let mut path = vec![cur_id];
6989 let mut c = cur_id;
6990 while let Some((_, Some(p))) = closed.get(&c) { path.push(*p); c = *p; }
6991 path.reverse();
6992 return Some(path);
6993 }
6994 if let Some(region) = self.regions.get(&cur_id) {
6995 for conn in ®ion.connections {
6996 let nid = conn.to_region;
6997 if closed.contains_key(&nid) { continue; }
6998 let new_g = g + conn.traversal_cost;
6999 let new_h = h(nid);
7000 if let Some((og, _, _)) = open.get(&nid) { if *og <= new_g { continue; } }
7001 open.insert(nid, (new_g, new_h, Some(cur_id)));
7002 }
7003 }
7004 if closed.len() > 2048 { break; }
7005 }
7006 None
7007 }
7008}
7009
7010
7011#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7016pub enum AiLodLevel { Full, Medium, Low, Dormant }
7017
7018pub struct AiLodManager {
7019 pub agent_lods: HashMap<u64, AiLodLevel>,
7020 pub camera_pos: Vec3,
7021 pub full_radius: f32,
7022 pub medium_radius: f32,
7023 pub low_radius: f32,
7024 pub force_full: HashSet<u64>,
7025}
7026
7027impl AiLodManager {
7028 pub fn new(full_radius: f32, medium_radius: f32, low_radius: f32) -> Self {
7029 Self { agent_lods: HashMap::new(), camera_pos: Vec3::ZERO, full_radius, medium_radius, low_radius, force_full: HashSet::new() }
7030 }
7031
7032 pub fn update(&mut self, agent_positions: &HashMap<u64, Vec3>) {
7033 for (&id, &pos) in agent_positions {
7034 let lod = if self.force_full.contains(&id) { AiLodLevel::Full }
7035 else {
7036 let d = (pos - self.camera_pos).length();
7037 if d < self.full_radius { AiLodLevel::Full }
7038 else if d < self.medium_radius { AiLodLevel::Medium }
7039 else if d < self.low_radius { AiLodLevel::Low }
7040 else { AiLodLevel::Dormant }
7041 };
7042 self.agent_lods.insert(id, lod);
7043 }
7044 }
7045
7046 pub fn get_lod(&self, id: u64) -> AiLodLevel { self.agent_lods.get(&id).copied().unwrap_or(AiLodLevel::Dormant) }
7047
7048 pub fn lod_update_freq(&self, lod: AiLodLevel) -> f32 {
7049 match lod {
7050 AiLodLevel::Full => 1.0 / BT_TICK_RATE_HZ,
7051 AiLodLevel::Medium => 0.1,
7052 AiLodLevel::Low => 0.5,
7053 AiLodLevel::Dormant => f32::MAX,
7054 }
7055 }
7056
7057 pub fn should_update(&self, id: u64, last_update: f32, current_time: f32) -> bool {
7058 (current_time - last_update) >= self.lod_update_freq(self.get_lod(id))
7059 }
7060
7061 pub fn counts_by_lod(&self) -> (usize, usize, usize, usize) {
7062 let f = self.agent_lods.values().filter(|&&l| l == AiLodLevel::Full).count();
7063 let m = self.agent_lods.values().filter(|&&l| l == AiLodLevel::Medium).count();
7064 let l = self.agent_lods.values().filter(|&&l| l == AiLodLevel::Low).count();
7065 let d = self.agent_lods.values().filter(|&&l| l == AiLodLevel::Dormant).count();
7066 (f, m, l, d)
7067 }
7068}
7069
7070pub struct PerformanceMonitor {
7075 pub bt_times: VecDeque<f32>,
7076 pub perception_times: VecDeque<f32>,
7077 pub steering_times: VecDeque<f32>,
7078 pub total_times: VecDeque<f32>,
7079 pub history_len: usize,
7080 pub frame: u64,
7081}
7082
7083impl PerformanceMonitor {
7084 pub fn new(history_len: usize) -> Self {
7085 Self { bt_times: VecDeque::with_capacity(history_len), perception_times: VecDeque::with_capacity(history_len), steering_times: VecDeque::with_capacity(history_len), total_times: VecDeque::with_capacity(history_len), history_len, frame: 0 }
7086 }
7087
7088 pub fn record(&mut self, bt: f32, percept: f32, steering: f32, _goap: f32) {
7089 self.frame += 1;
7090 macro_rules! push_b { ($q:expr, $v:expr) => { $q.push_back($v); if $q.len() > self.history_len { $q.pop_front(); } } }
7091 push_b!(self.bt_times, bt);
7092 push_b!(self.perception_times, percept);
7093 push_b!(self.steering_times, steering);
7094 push_b!(self.total_times, bt + percept + steering);
7095 }
7096
7097 pub fn avg_total(&self) -> f32 { if self.total_times.is_empty() { 0.0 } else { self.total_times.iter().sum::<f32>() / self.total_times.len() as f32 } }
7098 pub fn peak_total(&self) -> f32 { self.total_times.iter().copied().fold(0.0f32, f32::max) }
7099 pub fn avg_bt(&self) -> f32 { if self.bt_times.is_empty() { 0.0 } else { self.bt_times.iter().sum::<f32>() / self.bt_times.len() as f32 } }
7100}
7101
7102#[derive(Clone, Debug)]
7107pub enum FramingRule {
7108 ThirdPerson { angle_yaw: f32, angle_pitch: f32 },
7109 OverShoulder { shoulder_offset: Vec3 },
7110 TopDown { height: f32 },
7111 FreeOrbit { orbit_angle: f32, orbit_pitch: f32 },
7112}
7113
7114pub struct CameraDirectorAi {
7115 pub camera_pos: Vec3,
7116 pub camera_velocity: Vec3,
7117 pub camera_target: Vec3,
7118 pub smoothing: f32,
7119 pub look_ahead_factor: f32,
7120 pub distance: f32,
7121 pub height_offset: f32,
7122 pub max_speed: f32,
7123 pub framing_rule: FramingRule,
7124 pub cut_threshold: f32,
7125}
7126
7127impl CameraDirectorAi {
7128 pub fn new() -> Self {
7129 Self { camera_pos: Vec3::new(0.0, 5.0, -10.0), camera_velocity: Vec3::ZERO, camera_target: Vec3::ZERO, smoothing: 5.0, look_ahead_factor: 2.0, distance: 8.0, height_offset: 3.0, max_speed: 20.0, framing_rule: FramingRule::ThirdPerson { angle_yaw: 0.0, angle_pitch: 0.3 }, cut_threshold: 30.0 }
7130 }
7131
7132 pub fn update(&mut self, target_pos: Vec3, target_velocity: Vec3, dt: f32) {
7133 let predicted = target_pos + target_velocity * self.look_ahead_factor * 0.5;
7134 let desired = match &self.framing_rule {
7135 FramingRule::ThirdPerson { angle_yaw, angle_pitch } => {
7136 let yaw = *angle_yaw; let pitch = *angle_pitch;
7137 let offset = Vec3::new(yaw.sin() * self.distance * pitch.cos(), self.height_offset + self.distance * pitch.sin(), yaw.cos() * self.distance * pitch.cos());
7138 predicted + offset
7139 }
7140 FramingRule::TopDown { height } => Vec3::new(predicted.x, *height, predicted.z),
7141 FramingRule::OverShoulder { shoulder_offset } => predicted + *shoulder_offset,
7142 FramingRule::FreeOrbit { orbit_angle, orbit_pitch } => {
7143 let ang = *orbit_angle; let pitch = *orbit_pitch;
7144 let offset = Vec3::new(ang.cos() * self.distance * pitch.cos(), self.distance * pitch.sin() + self.height_offset, ang.sin() * self.distance * pitch.cos());
7145 predicted + offset
7146 }
7147 };
7148 let dist = (desired - self.camera_pos).length();
7149 if dist > self.cut_threshold {
7150 self.camera_pos = desired;
7151 self.camera_velocity = Vec3::ZERO;
7152 } else {
7153 self.camera_pos = smooth_damp_vec3(self.camera_pos, desired, &mut self.camera_velocity, 1.0 / self.smoothing, self.max_speed, dt);
7154 }
7155 }
7156
7157 pub fn view_matrix(&self) -> Mat4 {
7158 Mat4::look_at_rh(self.camera_pos, self.camera_target, Vec3::Y)
7159 }
7160}
7161
7162#[derive(Clone, Debug)]
7167pub struct FusedBelief {
7168 pub entity_id: u64,
7169 pub position: Vec3,
7170 pub velocity: Vec3,
7171 pub confidence: f32,
7172 pub sensor_contributions: [f32; 4],
7173 pub last_fused: f32,
7174 pub threat: f32,
7175}
7176
7177pub struct SensorFusion {
7178 pub vision_weight: f32,
7179 pub hearing_weight: f32,
7180 pub smell_weight: f32,
7181 pub memory_weight: f32,
7182 pub fused_beliefs: HashMap<u64, FusedBelief>,
7183 pub decay_rate: f32,
7184 pub current_time: f32,
7185}
7186
7187impl SensorFusion {
7188 pub fn new() -> Self {
7189 Self { vision_weight: 1.0, hearing_weight: 0.6, smell_weight: 0.3, memory_weight: 0.4, fused_beliefs: HashMap::new(), decay_rate: 0.1, current_time: 0.0 }
7190 }
7191
7192 pub fn fuse(&mut self, entity_id: u64, vision_pos: Option<(Vec3, f32)>, hearing_pos: Option<(Vec3, f32)>, smell_pos: Option<(Vec3, f32)>, memory_pos: Option<(Vec3, f32)>, threat: f32) {
7193 let mut total_weight = 0.0f32;
7194 let mut fused_pos = Vec3::ZERO;
7195 let mut contributions = [0.0f32; 4];
7196 macro_rules! add_s { ($sensor:expr, $weight:expr, $idx:expr) => { if let Some((pos, conf)) = $sensor { let w = $weight * conf; fused_pos += pos * w; total_weight += w; contributions[$idx] = w; } } }
7197 add_s!(vision_pos, self.vision_weight, 0);
7198 add_s!(hearing_pos, self.hearing_weight, 1);
7199 add_s!(smell_pos, self.smell_weight, 2);
7200 add_s!(memory_pos, self.memory_weight, 3);
7201 if total_weight > EPSILON {
7202 fused_pos /= total_weight;
7203 let confidence = (total_weight / (self.vision_weight + self.hearing_weight + self.smell_weight + self.memory_weight)).min(1.0);
7204 let belief = self.fused_beliefs.entry(entity_id).or_insert_with(|| FusedBelief { entity_id, position: fused_pos, velocity: Vec3::ZERO, confidence: 0.0, sensor_contributions: [0.0; 4], last_fused: self.current_time, threat: 0.0 });
7205 let dt = (self.current_time - belief.last_fused).max(EPSILON as f32);
7206 belief.velocity = (fused_pos - belief.position) / dt;
7207 belief.position = fused_pos;
7208 belief.confidence = confidence;
7209 belief.sensor_contributions = contributions;
7210 belief.last_fused = self.current_time;
7211 belief.threat = threat;
7212 }
7213 }
7214
7215 pub fn update(&mut self, dt: f32) {
7216 self.current_time += dt;
7217 let to_remove: Vec<u64> = self.fused_beliefs.iter_mut().filter_map(|(id, b)| { b.confidence = (b.confidence - self.decay_rate * dt).max(0.0); b.position += b.velocity * dt; if b.confidence < 0.02 { Some(*id) } else { None } }).collect();
7218 for id in to_remove { self.fused_beliefs.remove(&id); }
7219 }
7220
7221 pub fn most_confident(&self) -> Option<&FusedBelief> {
7222 self.fused_beliefs.values().max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap())
7223 }
7224}
7225
7226
7227
7228
7229#[derive(Clone, Debug)]
7234pub struct EditorWindowLayout {
7235 pub viewport_size: Vec2,
7236}
7237
7238impl EditorWindowLayout {
7239 pub fn default_layout(viewport_size: Vec2) -> Self {
7240 Self { viewport_size }
7241 }
7242}
7243
7244pub struct AiSystemIntegrator {
7249 pub editor: AiBehaviorEditor,
7250 pub spatial_grid: SpatialGrid,
7251 pub cover_system: CoverSystem,
7252 pub event_bus: AiEventBus,
7253 pub lod_manager: AiLodManager,
7254 pub perf_monitor: PerformanceMonitor,
7255 pub nav_mesh: NavMesh,
7256 pub world_tracker: WorldStateTracker,
7257 pub htn_planner: HtnPlanner,
7258 pub pathfinder: GridPathfinder,
7259 pub camera_director: CameraDirectorAi,
7260 pub agent_memories: HashMap<u64, AgentMemory>,
7261 pub agent_social_graphs: HashMap<u64, SocialGraph>,
7262 pub agent_fusion: HashMap<u64, SensorFusion>,
7263 pub squad_ais: Vec<SquadAi>,
7264 pub decision_trees: HashMap<String, DecisionTreeNode>,
7265 pub fuzzy_systems: HashMap<String, FuzzyInferenceSystem>,
7266 pub behavior_modulators: HashMap<u64, BehaviorModulator>,
7267 pub noise: ValueNoise,
7268 pub window_layout: EditorWindowLayout,
7269}
7270
7271impl AiSystemIntegrator {
7272 pub fn new() -> Self {
7273 let editor = AiBehaviorEditor::new();
7274 let pathfinder = GridPathfinder::new(100, 100, 1.0, Vec2::new(-50.0, -50.0));
7275 let htn_planner = HtnPlanner::build_combat_network();
7276 let nav_mesh = NavMesh::new();
7277 let mut cover_system = CoverSystem::new();
7278 let obstacles: Vec<Aabb> = vec![
7279 Aabb::new(Vec3::new(10.0, 0.0, 0.0), Vec3::new(2.0, 1.0, 2.0)),
7280 Aabb::new(Vec3::new(-5.0, 0.0, 8.0), Vec3::new(1.5, 1.0, 1.5)),
7281 ];
7282 cover_system.generate_cover_from_obstacles(&obstacles, &[Vec3::X, Vec3::NEG_X, Vec3::Z, Vec3::NEG_Z]);
7283 let mut decision_trees = HashMap::new();
7284 decision_trees.insert("combat".to_string(), DecisionTreeBuilder::build_combat_tree());
7285 let mut fuzzy_systems = HashMap::new();
7286 fuzzy_systems.insert("aggressiveness".to_string(), FuzzyBehaviorController::build_aggressiveness_fis());
7287 let window_layout = EditorWindowLayout::default_layout(Vec2::new(1920.0, 1080.0));
7288
7289 Self {
7290 spatial_grid: SpatialGrid::new(5.0),
7291 cover_system,
7292 event_bus: AiEventBus::new(),
7293 lod_manager: AiLodManager::new(15.0, 40.0, 80.0),
7294 perf_monitor: PerformanceMonitor::new(128),
7295 nav_mesh,
7296 world_tracker: WorldStateTracker::new(),
7297 htn_planner,
7298 pathfinder,
7299 camera_director: CameraDirectorAi::new(),
7300 agent_memories: HashMap::new(),
7301 agent_social_graphs: HashMap::new(),
7302 agent_fusion: HashMap::new(),
7303 squad_ais: Vec::new(),
7304 decision_trees,
7305 fuzzy_systems,
7306 behavior_modulators: HashMap::new(),
7307 noise: ValueNoise::new(42),
7308 window_layout,
7309 editor,
7310 }
7311 }
7312
7313 pub fn full_update(&mut self, dt: f32) {
7314 let positions: Vec<(u64, Vec3)> = self.editor.agents.iter().map(|a| (a.id, a.position)).collect();
7315 self.spatial_grid.rebuild(&positions);
7316 let pos_map: HashMap<u64, Vec3> = positions.iter().copied().collect();
7317 self.lod_manager.camera_pos = Vec3::new(0.0, 5.0, 0.0);
7318 self.lod_manager.update(&pos_map);
7319 self.event_bus.tick(dt);
7320 self.world_tracker.tick(dt);
7321 let perceived_empty: Vec<PerceivedEntity> = Vec::new();
7322 for squad in &mut self.squad_ais { squad.tick(dt, &perceived_empty, &pos_map); }
7323 self.editor.simulation_tick(dt);
7324 for memory in self.agent_memories.values_mut() { memory.update(dt); }
7325 for graph in self.agent_social_graphs.values_mut() { graph.update(dt, self.editor.current_time); }
7326 for fusion in self.agent_fusion.values_mut() { fusion.update(dt); }
7327 for modulator in self.behavior_modulators.values_mut() { modulator.update(dt); }
7328 if let Some(id) = self.editor.selected_agent_id {
7329 if let Some(agent) = self.editor.agents.iter().find(|a| a.id == id) {
7330 self.camera_director.update(agent.position, agent.velocity, dt);
7331 }
7332 }
7333 self.perf_monitor.record(0.1, 0.05, 0.03, 0.02);
7334 }
7335
7336 pub fn spawn_squad(&mut self, leader_id: u64, formation: FormationType) {
7337 let mut squad = SquadAi::new(leader_id);
7338 squad.formation = formation;
7339 if let Some(leader) = self.editor.agents.iter().find(|a| a.id == leader_id) {
7340 let leader_pos = leader.position;
7341 let nearby = self.spatial_grid.query_radius(leader_pos, 10.0);
7342 for id in nearby { squad.add_agent(id); }
7343 }
7344 self.squad_ais.push(squad);
7345 }
7346
7347 pub fn query_decision_tree(&self, tree_name: &str, blackboard: &Blackboard) -> Option<(u32, String)> {
7348 let tree = self.decision_trees.get(tree_name)?;
7349 let (id, label, _) = tree.evaluate(blackboard);
7350 Some((id, label))
7351 }
7352
7353 pub fn query_fuzzy(&self, system_name: &str, inputs: &[f32]) -> Option<f32> {
7354 let fis = self.fuzzy_systems.get(system_name)?;
7355 Some(fis.infer(inputs, (0.0, 1.0), 100))
7356 }
7357
7358 pub fn broadcast_event(&mut self, signal: AiSignal) { self.event_bus.publish(signal); }
7359
7360 pub fn stats_summary(&self) -> String {
7361 let mut out = String::new();
7362 out.push_str("=== AI System Status ===\n");
7363 out.push_str(&format!("Agents: {}\n", self.editor.agents.len()));
7364 out.push_str(&format!("BTs: {} FSMs: {} Squads: {}\n", self.editor.behavior_trees.len(), self.editor.fsm_instances.len(), self.squad_ais.len()));
7365 out.push_str(&format!("Cover Points: {}\n", self.cover_system.cover_points.len()));
7366 let (f, m, l, d) = self.lod_manager.counts_by_lod();
7367 out.push_str(&format!("LOD: Full={} Med={} Low={} Dormant={}\n", f, m, l, d));
7368 out.push_str(&format!("Avg AI: {:.3}ms Peak: {:.3}ms\n", self.perf_monitor.avg_total(), self.perf_monitor.peak_total()));
7369 out.push_str(&format!("Sim time: {:.2}s\n", self.editor.current_time));
7370 out
7371 }
7372}
7373
7374pub fn compute_intercept_point(shooter_pos: Vec3, projectile_speed: f32, target_pos: Vec3, target_vel: Vec3) -> Option<Vec3> {
7375 let to_target = target_pos - shooter_pos;
7376 let a = target_vel.length_squared() - projectile_speed * projectile_speed;
7377 let b = 2.0 * to_target.dot(target_vel);
7378 let c = to_target.length_squared();
7379 let discriminant = b * b - 4.0 * a * c;
7380 if discriminant < 0.0 { return None; }
7381 let sqrt_disc = discriminant.sqrt();
7382 let t1 = (-b + sqrt_disc) / (2.0 * a + EPSILON);
7383 let t2 = (-b - sqrt_disc) / (2.0 * a + EPSILON);
7384 let t = [t1, t2].iter().filter(|&&t| t > 0.0).copied().fold(f32::MAX, f32::min);
7385 if t == f32::MAX { None } else { Some(target_pos + target_vel * t) }
7386}
7387
7388pub fn effective_range_modifier(distance: f32, weapon_range: f32, falloff_start: f32) -> f32 {
7389 if distance > weapon_range { return 0.0; }
7390 if distance <= falloff_start { return 1.0; }
7391 let t = (distance - falloff_start) / (weapon_range - falloff_start + EPSILON);
7392 1.0 - t * t
7393}
7394
7395pub fn compute_flanking_score(attacker_pos: Vec3, defender_pos: Vec3, defender_forward: Vec3) -> f32 {
7396 let to_attacker = (attacker_pos - defender_pos).normalize_or_zero();
7397 (1.0 - defender_forward.dot(to_attacker)) * 0.5
7398}
7399
7400pub fn clamp_angle(angle: f32) -> f32 {
7401 let mut a = angle % TWO_PI;
7402 if a > PI { a -= TWO_PI; }
7403 if a < -PI { a += TWO_PI; }
7404 a
7405}
7406
7407pub fn project_onto_plane(v: Vec3, plane_normal: Vec3) -> Vec3 {
7408 v - plane_normal * v.dot(plane_normal)
7409}
7410
7411pub fn reflect_vector(v: Vec3, normal: Vec3) -> Vec3 {
7412 v - normal * (2.0 * v.dot(normal))
7413}
7414
7415pub fn frustum_cull_sphere(center: Vec3, radius: f32, frustum_planes: &[(Vec3, f32)]) -> bool {
7416 for &(normal, d) in frustum_planes {
7417 if normal.dot(center) + d < -radius { return false; }
7418 }
7419 true
7420}
7421
7422pub fn pack_behavior_config(agent: &AiAgent) -> HashMap<String, f32> {
7423 let mut cfg = HashMap::new();
7424 cfg.insert("health".to_string(), agent.blackboard.get_float("self_health"));
7425 cfg.insert("ammo".to_string(), agent.blackboard.get_float("ammo_count"));
7426 cfg.insert("emotion_valence".to_string(), agent.emotion_engine.state.mood_valence);
7427 cfg.insert("emotion_arousal".to_string(), agent.emotion_engine.state.mood_arousal);
7428 cfg.insert("speed_mult".to_string(), agent.emotion_engine.get_modifier().speed_multiplier);
7429 cfg
7430}
7431
7432pub fn global_editor_init() -> AiSystemIntegrator {
7433 let mut integrator = AiSystemIntegrator::new();
7434 integrator.editor.simulation_running = true;
7435 for i in 0..8 {
7436 let angle = (i as f32 / 8.0) * TWO_PI;
7437 let pos = Vec3::new(angle.cos() * 8.0, 0.0, angle.sin() * 8.0);
7438 let mode = if i % 2 == 0 { AiAgentMode::BehaviorTree } else { AiAgentMode::UtilityAi };
7439 integrator.editor.spawn_agent(pos, mode);
7440 }
7441 if let Some(first) = integrator.editor.agents.first() {
7442 let leader_id = first.id;
7443 integrator.spawn_squad(leader_id, FormationType::Wedge);
7444 }
7445 integrator
7446}
7447
7448
7449#[derive(Clone, Debug)]
7454pub struct AnimLayer {
7455 pub clip_name: String,
7456 pub weight: f32,
7457 pub time: f32,
7458 pub speed: f32,
7459 pub looping: bool,
7460 pub duration: f32,
7461 pub blend_in_time: f32,
7462}
7463
7464impl AnimLayer {
7465 pub fn new(clip_name: &str, duration: f32, looping: bool) -> Self {
7466 Self { clip_name: clip_name.to_string(), weight: 0.0, time: 0.0, speed: 1.0, looping, duration, blend_in_time: 0.2 }
7467 }
7468
7469 pub fn normalized_time(&self) -> f32 { if self.duration < EPSILON { 0.0 } else { self.time / self.duration } }
7470 pub fn is_finished(&self) -> bool { !self.looping && self.time >= self.duration }
7471
7472 pub fn tick(&mut self, dt: f32) {
7473 self.time += dt * self.speed;
7474 if self.looping && self.duration > EPSILON { self.time %= self.duration; }
7475 }
7476}
7477
7478pub struct AnimBlendTree {
7479 pub layers: Vec<AnimLayer>,
7480 pub layer_weights: Vec<f32>,
7481 pub active_layer: usize,
7482 pub transition_time: f32,
7483}
7484
7485impl AnimBlendTree {
7486 pub fn new() -> Self { Self { layers: Vec::new(), layer_weights: Vec::new(), active_layer: 0, transition_time: 0.0 } }
7487
7488 pub fn add_layer(&mut self, layer: AnimLayer) { self.layer_weights.push(0.0); self.layers.push(layer); }
7489
7490 pub fn play(&mut self, layer_idx: usize, blend_time: f32) {
7491 if layer_idx >= self.layers.len() { return; }
7492 self.active_layer = layer_idx;
7493 self.transition_time = blend_time;
7494 self.layers[layer_idx].time = 0.0;
7495 }
7496
7497 pub fn tick(&mut self, dt: f32) {
7498 let n = self.layers.len();
7499 if n == 0 { return; }
7500 for i in 0..n {
7501 let target = if i == self.active_layer { 1.0 } else { 0.0 };
7502 let speed = if self.transition_time > EPSILON { dt / self.transition_time } else { 1.0 };
7503 self.layer_weights[i] += (target - self.layer_weights[i]) * speed.min(1.0);
7504 }
7505 let total: f32 = self.layer_weights.iter().sum();
7506 if total > EPSILON { for w in &mut self.layer_weights { *w /= total; } }
7507 for layer in &mut self.layers { layer.tick(dt); }
7508 }
7509
7510 pub fn root_motion_velocity(&self, velocities: &[Vec3]) -> Vec3 {
7511 velocities.iter().enumerate().map(|(i, &v)| v * self.layer_weights.get(i).copied().unwrap_or(0.0)).fold(Vec3::ZERO, |a, b| a + b)
7512 }
7513}
7514
7515#[derive(Clone, Debug)]
7520pub struct DialogOption {
7521 pub id: u32,
7522 pub text: String,
7523 pub next_node_id: Option<u32>,
7524 pub condition_key: Option<String>,
7525 pub condition_op: Option<CompareOp>,
7526 pub condition_value: Option<BlackboardValue>,
7527 pub effects: Vec<FsmAction>,
7528 pub ai_weight: f32,
7529}
7530
7531#[derive(Clone, Debug)]
7532pub struct DialogNode {
7533 pub id: u32,
7534 pub speaker: String,
7535 pub text: String,
7536 pub options: Vec<DialogOption>,
7537 pub auto_advance: bool,
7538 pub advance_time: f32,
7539 pub entry_effects: Vec<FsmAction>,
7540}
7541
7542pub struct DialogGraph {
7543 pub nodes: HashMap<u32, DialogNode>,
7544 pub start_node: Option<u32>,
7545 pub current_node: Option<u32>,
7546 pub next_id: u32,
7547 pub blackboard: Blackboard,
7548 pub history: Vec<u32>,
7549}
7550
7551impl DialogGraph {
7552 pub fn new() -> Self {
7553 Self { nodes: HashMap::new(), start_node: None, current_node: None, next_id: 1, blackboard: Blackboard::new(), history: Vec::new() }
7554 }
7555
7556 pub fn add_node(&mut self, speaker: &str, text: &str) -> u32 {
7557 let id = self.next_id; self.next_id += 1;
7558 self.nodes.insert(id, DialogNode { id, speaker: speaker.to_string(), text: text.to_string(), options: Vec::new(), auto_advance: false, advance_time: 3.0, entry_effects: Vec::new() });
7559 id
7560 }
7561
7562 pub fn add_option(&mut self, node_id: u32, text: &str, next_node: Option<u32>, weight: f32) {
7563 let id = self.next_id; self.next_id += 1;
7564 if let Some(node) = self.nodes.get_mut(&node_id) {
7565 node.options.push(DialogOption { id, text: text.to_string(), next_node_id: next_node, condition_key: None, condition_op: None, condition_value: None, effects: Vec::new(), ai_weight: weight });
7566 }
7567 }
7568
7569 pub fn start(&mut self) {
7570 if let Some(node_id) = self.start_node {
7571 self.current_node = Some(node_id);
7572 self.history.push(node_id);
7573 if let Some(node) = self.nodes.get(&node_id) {
7574 for effect in &node.entry_effects.clone() { effect.execute(&mut self.blackboard); }
7575 }
7576 }
7577 }
7578
7579 fn option_available(&self, opt: &DialogOption) -> bool {
7580 if let (Some(key), Some(op), Some(val)) = (&opt.condition_key, &opt.condition_op, &opt.condition_value) {
7581 op.evaluate(self.blackboard.get(key), val)
7582 } else { true }
7583 }
7584
7585 pub fn choose_option(&mut self, option_idx: usize) -> bool {
7586 let current = match self.current_node { Some(c) => c, None => return false };
7587 let (next_node, effects) = if let Some(node) = self.nodes.get(¤t) {
7588 let available: Vec<&DialogOption> = node.options.iter().filter(|o| self.option_available(o)).collect();
7589 if option_idx >= available.len() { return false; }
7590 let opt = available[option_idx];
7591 (opt.next_node_id, opt.effects.clone())
7592 } else { return false; };
7593
7594 for effect in &effects { effect.execute(&mut self.blackboard); }
7595 self.current_node = next_node;
7596 if let Some(nn) = next_node {
7597 self.history.push(nn);
7598 if let Some(node) = self.nodes.get(&nn) {
7599 for effect in &node.entry_effects.clone() { effect.execute(&mut self.blackboard); }
7600 }
7601 }
7602 true
7603 }
7604
7605 pub fn ai_choose_response(&self) -> Option<usize> {
7606 let current = self.current_node?;
7607 let node = self.nodes.get(¤t)?;
7608 let available: Vec<(usize, f32)> = node.options.iter().enumerate()
7609 .filter(|(_, o)| self.option_available(o))
7610 .map(|(i, o)| (i, o.ai_weight))
7611 .collect();
7612 available.iter().max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).map(|&(i, _)| i)
7613 }
7614
7615 pub fn current_text(&self) -> Option<(&str, &str)> {
7616 let node = self.nodes.get(&self.current_node?)?;
7617 Some((&node.speaker, &node.text))
7618 }
7619
7620 pub fn available_options(&self) -> Vec<(usize, &str)> {
7621 let current = match self.current_node { Some(c) => c, None => return vec![] };
7622 if let Some(node) = self.nodes.get(¤t) {
7623 node.options.iter().enumerate().filter(|(_, o)| self.option_available(o)).map(|(i, o)| (i, o.text.as_str())).collect()
7624 } else { vec![] }
7625 }
7626}
7627
7628#[derive(Clone, Debug)]
7633pub enum PlayerIntent {
7634 Attack { target_pos: Vec3 },
7635 Defend { position: Vec3, radius: f32 },
7636 Follow { leader_id: u64 },
7637 Retreat { direction: Vec3 },
7638 UseAbility { ability_id: u32, target_pos: Vec3 },
7639 Idle,
7640}
7641
7642pub struct IntentRecognizer {
7643 pub window: VecDeque<(f32, PlayerIntent)>,
7644 pub window_duration: f32,
7645 pub current_intent: PlayerIntent,
7646 pub confidence: f32,
7647}
7648
7649impl IntentRecognizer {
7650 pub fn new(window_duration: f32) -> Self {
7651 Self { window: VecDeque::new(), window_duration, current_intent: PlayerIntent::Idle, confidence: 0.0 }
7652 }
7653
7654 pub fn observe(&mut self, time: f32, intent: PlayerIntent) {
7655 self.window.push_back((time, intent));
7656 while self.window.front().map(|&(t, _)| time - t > self.window_duration).unwrap_or(false) { self.window.pop_front(); }
7657 }
7658
7659 pub fn infer_intent(&mut self) -> &PlayerIntent {
7660 let mut attack_c = 0usize;
7661 let mut defend_c = 0usize;
7662 let mut follow_c = 0usize;
7663 let mut retreat_c = 0usize;
7664 let mut idle_c = 0usize;
7665
7666 for (_, intent) in &self.window {
7667 match intent {
7668 PlayerIntent::Attack { .. } => attack_c += 1,
7669 PlayerIntent::Defend { .. } => defend_c += 1,
7670 PlayerIntent::Follow { .. } => follow_c += 1,
7671 PlayerIntent::Retreat { .. } => retreat_c += 1,
7672 PlayerIntent::Idle => idle_c += 1,
7673 _ => {}
7674 }
7675 }
7676
7677 let total = self.window.len().max(1) as f32;
7678 let counts = [attack_c, defend_c, follow_c, retreat_c, idle_c];
7679 let best = counts.iter().enumerate().max_by_key(|(_, &c)| c).map(|(i, _)| i).unwrap_or(4);
7680 self.confidence = counts[best] as f32 / total;
7681
7682 for (_, intent) in self.window.iter().rev() {
7683 let matched = match (best, intent) {
7684 (0, PlayerIntent::Attack { .. }) | (1, PlayerIntent::Defend { .. })
7685 | (2, PlayerIntent::Follow { .. }) | (3, PlayerIntent::Retreat { .. }) => true,
7686 _ => false,
7687 };
7688 if matched { self.current_intent = intent.clone(); return &self.current_intent; }
7689 }
7690 self.current_intent = PlayerIntent::Idle;
7691 &self.current_intent
7692 }
7693}
7694
7695pub struct BtAnalyzer;
7700
7701impl BtAnalyzer {
7702 pub fn find_unreachable_nodes(tree: &BehaviorTree) -> Vec<u32> {
7703 let root = match tree.root_id { Some(r) => r, None => return tree.nodes.keys().copied().collect() };
7704 let mut reachable = HashSet::new();
7705 let mut stack = vec![root];
7706 while let Some(id) = stack.pop() {
7707 if reachable.contains(&id) { continue; }
7708 reachable.insert(id);
7709 if let Some(node) = tree.nodes.get(&id) {
7710 for &child in &node.children { stack.push(child); }
7711 }
7712 }
7713 tree.nodes.keys().filter(|&&id| !reachable.contains(&id)).copied().collect()
7714 }
7715
7716 pub fn find_cycles(tree: &BehaviorTree) -> Vec<Vec<u32>> {
7717 let mut cycles = Vec::new();
7719 let mut visited = HashSet::new();
7720 let mut stack = HashSet::new();
7721 let mut path = Vec::new();
7722 if let Some(root) = tree.root_id {
7723 Self::dfs_cycle(tree, root, &mut visited, &mut stack, &mut path, &mut cycles);
7724 }
7725 cycles
7726 }
7727
7728 fn dfs_cycle(tree: &BehaviorTree, node_id: u32, visited: &mut HashSet<u32>, stack: &mut HashSet<u32>, path: &mut Vec<u32>, cycles: &mut Vec<Vec<u32>>) {
7729 if stack.contains(&node_id) {
7730 if let Some(pos) = path.iter().position(|&x| x == node_id) {
7731 cycles.push(path[pos..].to_vec());
7732 }
7733 return;
7734 }
7735 if visited.contains(&node_id) { return; }
7736 visited.insert(node_id);
7737 stack.insert(node_id);
7738 path.push(node_id);
7739 if let Some(node) = tree.nodes.get(&node_id) {
7740 for &child in &node.children { Self::dfs_cycle(tree, child, visited, stack, path, cycles); }
7741 }
7742 stack.remove(&node_id);
7743 path.pop();
7744 }
7745
7746 pub fn get_subtree_size(tree: &BehaviorTree, node_id: u32) -> usize {
7747 if let Some(node) = tree.nodes.get(&node_id) {
7748 1 + node.children.iter().map(|&c| Self::get_subtree_size(tree, c)).sum::<usize>()
7749 } else { 0 }
7750 }
7751
7752 pub fn max_branching_factor(tree: &BehaviorTree) -> usize {
7753 tree.nodes.values().map(|n| n.children.len()).max().unwrap_or(0)
7754 }
7755
7756 pub fn count_by_type(tree: &BehaviorTree) -> (usize, usize, usize) {
7757 let composites = tree.nodes.values().filter(|n| n.is_composite()).count();
7758 let decorators = tree.nodes.values().filter(|n| n.is_decorator()).count();
7759 let leaves = tree.nodes.values().filter(|n| n.is_leaf()).count();
7760 (composites, decorators, leaves)
7761 }
7762
7763 pub fn validate(tree: &BehaviorTree) -> Vec<String> {
7764 let mut errors = Vec::new();
7765 if tree.root_id.is_none() { errors.push("No root node".to_string()); }
7767 let unreachable = Self::find_unreachable_nodes(tree);
7769 if !unreachable.is_empty() { errors.push(format!("{} unreachable nodes: {:?}", unreachable.len(), unreachable)); }
7770 for node in tree.nodes.values() {
7772 if node.is_decorator() && node.children.len() > 1 {
7773 errors.push(format!("Decorator node {} [{}] has {} children (should have 1)", node.id, node.display_name(), node.children.len()));
7774 }
7775 }
7776 let cycles = Self::find_cycles(tree);
7778 for cycle in cycles { errors.push(format!("Cycle detected: {:?}", cycle)); }
7779 errors
7780 }
7781}
7782
7783pub struct CommandHistory {
7788 pub undo_stack: VecDeque<EditorCommand>,
7789 pub redo_stack: VecDeque<EditorCommand>,
7790 pub max_history: usize,
7791}
7792
7793#[derive(Clone, Debug)]
7794pub enum EditorCommand {
7795 AddBtNode { tree_idx: usize, node_id: u32, node_type: BtNodeType, position: Vec2 },
7796 RemoveBtNode { tree_idx: usize, node_id: u32 },
7797 MoveBtNode { tree_idx: usize, node_id: u32, from: Vec2, to: Vec2 },
7798 ConnectBtNodes { tree_idx: usize, parent_id: u32, child_id: u32 },
7799 DisconnectBtNodes { tree_idx: usize, parent_id: u32, child_id: u32 },
7800 AddFsmState { fsm_idx: usize, state_id: u32, name: String, pos: Vec2 },
7801 RemoveFsmState { fsm_idx: usize, state_id: u32 },
7802 AddFsmTransition { fsm_idx: usize, transition_id: u32, from: u32, to: u32 },
7803 RemoveFsmTransition { fsm_idx: usize, transition_id: u32 },
7804 SetBlackboardValue { key: String, old_value: BlackboardValue, new_value: BlackboardValue },
7805 AddGoapAction { action_id: u32 },
7806 RemoveGoapAction { action_id: u32 },
7807 ChangeFsmStateColor { fsm_idx: usize, state_id: u32, old_color: Vec4, new_color: Vec4 },
7808 Composite(Vec<EditorCommand>),
7809}
7810
7811impl CommandHistory {
7812 pub fn new(max_history: usize) -> Self {
7813 Self { undo_stack: VecDeque::with_capacity(max_history), redo_stack: VecDeque::with_capacity(max_history), max_history }
7814 }
7815
7816 pub fn push(&mut self, cmd: EditorCommand) {
7817 self.undo_stack.push_back(cmd);
7818 if self.undo_stack.len() > self.max_history { self.undo_stack.pop_front(); }
7819 self.redo_stack.clear();
7820 }
7821
7822 pub fn undo(&mut self) -> Option<EditorCommand> {
7823 let cmd = self.undo_stack.pop_back()?;
7824 self.redo_stack.push_back(cmd.clone());
7825 Some(cmd)
7826 }
7827
7828 pub fn redo(&mut self) -> Option<EditorCommand> {
7829 let cmd = self.redo_stack.pop_back()?;
7830 self.undo_stack.push_back(cmd.clone());
7831 Some(cmd)
7832 }
7833
7834 pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
7835 pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
7836
7837 pub fn history_summary(&self) -> String {
7838 format!("Undo: {} commands Redo: {} commands", self.undo_stack.len(), self.redo_stack.len())
7839 }
7840}
7841
7842pub struct BtClipboard {
7847 pub copied_nodes: HashMap<u32, BtNode>,
7848 pub root_of_copy: Option<u32>,
7849 pub offset: Vec2,
7850}
7851
7852impl BtClipboard {
7853 pub fn new() -> Self { Self { copied_nodes: HashMap::new(), root_of_copy: None, offset: Vec2::ZERO } }
7854
7855 pub fn copy_subtree(&mut self, tree: &BehaviorTree, root_node_id: u32) {
7856 self.copied_nodes.clear();
7857 self.root_of_copy = None;
7858 let mut stack = vec![root_node_id];
7859 while let Some(id) = stack.pop() {
7860 if let Some(node) = tree.nodes.get(&id) {
7861 self.copied_nodes.insert(id, node.clone());
7862 for &child_id in &node.children { stack.push(child_id); }
7863 }
7864 }
7865 self.root_of_copy = Some(root_node_id);
7866 if let Some(root) = tree.nodes.get(&root_node_id) { self.offset = root.position; }
7867 }
7868
7869 pub fn paste_into(&self, tree: &mut BehaviorTree, paste_pos: Vec2) -> Option<u32> {
7870 if self.copied_nodes.is_empty() { return None; }
7871 let old_root = self.root_of_copy?;
7872 let pos_delta = paste_pos - self.offset;
7873
7874 let mut id_map: HashMap<u32, u32> = HashMap::new();
7876 for &old_id in self.copied_nodes.keys() {
7877 let new_id = tree.next_id;
7878 tree.next_id += 1;
7879 id_map.insert(old_id, new_id);
7880 }
7881
7882 for (&old_id, old_node) in &self.copied_nodes {
7884 let new_id = id_map[&old_id];
7885 let mut new_node = old_node.clone();
7886 new_node.id = new_id;
7887 new_node.position = old_node.position + pos_delta;
7888 new_node.status = BtStatus::Invalid;
7889 new_node.elapsed_time = 0.0;
7890 new_node.repeat_count = 0;
7891 new_node.parent = old_node.parent.and_then(|p| id_map.get(&p).copied());
7892 new_node.children = old_node.children.iter().filter_map(|c| id_map.get(c).copied()).collect();
7893 tree.nodes.insert(new_id, new_node);
7894 }
7895
7896 id_map.get(&old_root).copied()
7897 }
7898
7899 pub fn is_empty(&self) -> bool { self.copied_nodes.is_empty() }
7900}
7901
7902pub struct TerrainQuery {
7907 pub height_map: Vec<f32>,
7908 pub width: usize,
7909 pub height: usize,
7910 pub cell_size: f32,
7911 pub origin: Vec2,
7912 pub slope_threshold: f32,
7913}
7914
7915impl TerrainQuery {
7916 pub fn new(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
7917 Self { height_map: vec![0.0; width * height], width, height, cell_size, origin, slope_threshold: 0.5 }
7918 }
7919
7920 fn sample(&self, x: i32, y: i32) -> f32 {
7921 if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 { return 0.0; }
7922 self.height_map[y as usize * self.width + x as usize]
7923 }
7924
7925 pub fn get_height(&self, pos: Vec2) -> f32 {
7926 let rel = pos - self.origin;
7927 let xi = (rel.x / self.cell_size) as i32;
7928 let yi = (rel.y / self.cell_size) as i32;
7929 let tx = (rel.x / self.cell_size) - xi as f32;
7930 let ty = (rel.y / self.cell_size) - yi as f32;
7931 let h00 = self.sample(xi, yi);
7932 let h10 = self.sample(xi + 1, yi);
7933 let h01 = self.sample(xi, yi + 1);
7934 let h11 = self.sample(xi + 1, yi + 1);
7935 h00 * (1.0 - tx) * (1.0 - ty) + h10 * tx * (1.0 - ty) + h01 * (1.0 - tx) * ty + h11 * tx * ty
7936 }
7937
7938 pub fn get_normal(&self, pos: Vec2) -> Vec3 {
7939 let step = self.cell_size;
7940 let hx0 = self.get_height(pos - Vec2::new(step, 0.0));
7941 let hx1 = self.get_height(pos + Vec2::new(step, 0.0));
7942 let hy0 = self.get_height(pos - Vec2::new(0.0, step));
7943 let hy1 = self.get_height(pos + Vec2::new(0.0, step));
7944 let dx = (hx1 - hx0) / (2.0 * step);
7945 let dy = (hy1 - hy0) / (2.0 * step);
7946 Vec3::new(-dx, 1.0, -dy).normalize_or_zero()
7947 }
7948
7949 pub fn is_traversable(&self, pos: Vec2) -> bool {
7950 let normal = self.get_normal(pos);
7951 normal.y >= (1.0 - self.slope_threshold * self.slope_threshold).sqrt()
7952 }
7953
7954 pub fn get_slope_angle(&self, pos: Vec2) -> f32 {
7955 let normal = self.get_normal(pos);
7956 normal.y.clamp(-1.0, 1.0).acos()
7957 }
7958
7959 pub fn find_high_ground_near(&self, center: Vec2, search_radius: f32) -> Option<Vec2> {
7960 let cells = (search_radius / self.cell_size) as i32;
7961 let center_cell_x = ((center.x - self.origin.x) / self.cell_size) as i32;
7962 let center_cell_y = ((center.y - self.origin.y) / self.cell_size) as i32;
7963 let mut best_h = f32::NEG_INFINITY;
7964 let mut best_pos = None;
7965 for dy in -cells..=cells {
7966 for dx in -cells..=cells {
7967 let cx = center_cell_x + dx;
7968 let cy = center_cell_y + dy;
7969 let h = self.sample(cx, cy);
7970 if h > best_h {
7971 best_h = h;
7972 let world_pos = Vec2::new(self.origin.x + cx as f32 * self.cell_size, self.origin.y + cy as f32 * self.cell_size);
7973 if (world_pos - center).length() <= search_radius { best_pos = Some(world_pos); }
7974 }
7975 }
7976 }
7977 best_pos
7978 }
7979}
7980
7981pub struct DdaSystem {
7986 pub player_skill_estimate: f32, pub kill_death_ratio: f32,
7988 pub time_to_die_avg: f32,
7989 pub time_to_kill_avg: f32,
7990 pub current_difficulty: f32, pub target_difficulty: f32,
7992 pub adjustment_rate: f32,
7993 pub history_window: VecDeque<DdaEvent>,
7994 pub window_size: usize,
7995}
7996
7997#[derive(Clone, Debug)]
7998pub enum DdaEvent {
7999 PlayerKilled { time: f32 },
8000 EnemyKilled { time: f32, time_to_kill: f32 },
8001 PlayerDamaged { amount: f32, time: f32 },
8002 PlayerHealed { amount: f32, time: f32 },
8003 ObjectiveCompleted { time: f32 },
8004 ObjectiveFailed { time: f32 },
8005}
8006
8007impl DdaSystem {
8008 pub fn new() -> Self {
8009 Self {
8010 player_skill_estimate: 0.5,
8011 kill_death_ratio: 1.0,
8012 time_to_die_avg: 30.0,
8013 time_to_kill_avg: 5.0,
8014 current_difficulty: 0.5,
8015 target_difficulty: 0.5,
8016 adjustment_rate: 0.05,
8017 history_window: VecDeque::with_capacity(50),
8018 window_size: 50,
8019 }
8020 }
8021
8022 pub fn record_event(&mut self, event: DdaEvent) {
8023 self.history_window.push_back(event);
8024 if self.history_window.len() > self.window_size { self.history_window.pop_front(); }
8025 self.recompute_skill();
8026 }
8027
8028 fn recompute_skill(&mut self) {
8029 let kills: Vec<f32> = self.history_window.iter().filter_map(|e| if let DdaEvent::EnemyKilled { time_to_kill, .. } = e { Some(*time_to_kill) } else { None }).collect();
8030 let deaths = self.history_window.iter().filter(|e| matches!(e, DdaEvent::PlayerKilled { .. })).count() as f32;
8031 let n_kills = kills.len() as f32;
8032
8033 if n_kills > 0.0 {
8034 let avg_ttk = kills.iter().sum::<f32>() / n_kills;
8035 self.time_to_kill_avg = avg_ttk;
8036 let kdr = n_kills / (deaths + 1.0);
8037 self.kill_death_ratio = kdr;
8038 let ttk_score = (1.0 - (avg_ttk / 30.0).min(1.0));
8040 let kdr_score = (kdr / (kdr + 1.0)).min(1.0);
8041 self.player_skill_estimate = (ttk_score * 0.4 + kdr_score * 0.6).clamp(0.0, 1.0);
8042 }
8043
8044 let target = 0.4 + self.player_skill_estimate * 0.4;
8047 self.target_difficulty = target.clamp(0.1, 0.9);
8048 }
8049
8050 pub fn update(&mut self, dt: f32) {
8051 let diff = self.target_difficulty - self.current_difficulty;
8053 self.current_difficulty += diff * self.adjustment_rate * dt;
8054 self.current_difficulty = self.current_difficulty.clamp(0.0, 1.0);
8055 }
8056
8057 pub fn get_enemy_health_multiplier(&self) -> f32 { 0.5 + self.current_difficulty * 1.0 }
8058 pub fn get_enemy_damage_multiplier(&self) -> f32 { 0.6 + self.current_difficulty * 0.8 }
8059 pub fn get_enemy_accuracy(&self) -> f32 { 0.3 + self.current_difficulty * 0.5 }
8060 pub fn get_enemy_reaction_time(&self) -> f32 { 0.8 - self.current_difficulty * 0.5 }
8061 pub fn get_enemy_aggression(&self) -> f32 { 0.2 + self.current_difficulty * 0.6 }
8062
8063 pub fn apply_to_blackboard(&self, bb: &mut Blackboard) {
8064 bb.set("dda_difficulty", BlackboardValue::Float(self.current_difficulty));
8065 bb.set("dda_health_mult", BlackboardValue::Float(self.get_enemy_health_multiplier()));
8066 bb.set("dda_damage_mult", BlackboardValue::Float(self.get_enemy_damage_multiplier()));
8067 bb.set("dda_accuracy", BlackboardValue::Float(self.get_enemy_accuracy()));
8068 bb.set("dda_reaction_time", BlackboardValue::Float(self.get_enemy_reaction_time()));
8069 bb.set("dda_aggression", BlackboardValue::Float(self.get_enemy_aggression()));
8070 }
8071}
8072
8073pub struct ResponseCurveTests;
8078
8079impl ResponseCurveTests {
8080 pub fn test_all() -> Vec<(String, bool)> {
8081 let mut results = Vec::new();
8082 let curves = [
8083 ("linear", ResponseCurve::Linear { slope: 1.0, intercept: 0.0 }),
8084 ("exponential", ResponseCurve::Exponential { base: 2.0, exponent: 1.0, scale: 0.5 }),
8085 ("logistic", ResponseCurve::Logistic { steepness: 5.0, midpoint: 0.5 }),
8086 ("sine", ResponseCurve::Sine { frequency: 1.0, phase: 0.0, amplitude: 0.5, offset: 0.5 }),
8087 ("polynomial", ResponseCurve::Polynomial { coefficients: vec![0.0, 0.0, 1.0] }),
8088 ("inverse", ResponseCurve::Inverse { scale: 0.5 }),
8089 ("step", ResponseCurve::Step { threshold: 0.5, low: 0.0, high: 1.0 }),
8090 ("smoothstep", ResponseCurve::Smoothstep { edge0: 0.2, edge1: 0.8 }),
8091 ("bell", ResponseCurve::Bell { center: 0.5, width: 0.3 }),
8092 ("constant", ResponseCurve::Constant { value: 0.7 }),
8093 ];
8094 for (name, curve) in &curves {
8095 let valid = (0..=10).map(|i| i as f32 / 10.0).all(|x| {
8097 let v = curve.evaluate(x);
8098 v >= 0.0 && v <= 1.0
8099 });
8100 results.push((name.to_string(), valid));
8101 }
8102 results
8103 }
8104}
8105
8106pub struct GoapValidator;
8111
8112impl GoapValidator {
8113 pub fn validate_action_chain(planner: &GoapPlanner, goal: WorldState) -> Vec<String> {
8115 let mut warnings = Vec::new();
8116 for action in &planner.actions {
8117 let effective_state = action.effects_set;
8118 if effective_state == 0 { warnings.push(format!("Action '{}' has no effects", action.name)); continue; }
8119 let satisfies_goal = (effective_state & goal) != 0;
8121 let satisfies_precond = planner.actions.iter().any(|other| {
8122 other.id != action.id && (effective_state & other.preconditions) != 0
8123 });
8124 if !satisfies_goal && !satisfies_precond {
8125 warnings.push(format!("Action '{}' effects don't satisfy any goal or precondition", action.name));
8126 }
8127 }
8128 warnings
8129 }
8130
8131 pub fn check_dead_ends(planner: &GoapPlanner, start: WorldState, goal: WorldState) -> Vec<WorldState> {
8132 let mut dead_ends = Vec::new();
8134 let mut to_check = vec![start];
8135 let mut seen = HashSet::new();
8136 seen.insert(start);
8137
8138 while let Some(state) = to_check.pop() {
8139 if (state & goal) == goal { continue; }
8140 let applicable: Vec<&GoapAction> = planner.actions.iter().filter(|a| a.can_execute(state, 0.0)).collect();
8141 if applicable.is_empty() {
8142 dead_ends.push(state);
8143 } else {
8144 for action in applicable {
8145 let new_state = action.apply(state);
8146 if !seen.contains(&new_state) {
8147 seen.insert(new_state);
8148 to_check.push(new_state);
8149 }
8150 }
8151 }
8152 }
8153 dead_ends
8154 }
8155}
8156
8157pub fn create_full_ai_system() -> AiSystemIntegrator {
8162 global_editor_init()
8163}
8164
8165pub fn validate_editor(editor: &AiBehaviorEditor) -> Vec<String> {
8166 let mut issues = Vec::new();
8167 for (i, tree) in editor.behavior_trees.iter().enumerate() {
8168 let tree_issues = BtAnalyzer::validate(tree);
8169 for issue in tree_issues {
8170 issues.push(format!("Tree[{}] '{}': {}", i, tree.name, issue));
8171 }
8172 }
8173 if editor.goap_planner.actions.is_empty() {
8174 issues.push("GOAP planner has no actions".to_string());
8175 }
8176 let goap_warnings = GoapValidator::validate_action_chain(&editor.goap_planner, editor.goap_goal_state);
8177 issues.extend(goap_warnings);
8178 issues
8179}
8180
8181pub fn run_all_validations() -> (usize, usize) {
8182 let test_results = AiEditorTests::run_all();
8183 let curve_tests = ResponseCurveTests::test_all();
8184 let passed = test_results.iter().filter(|(_, ok)| *ok).count() + curve_tests.iter().filter(|(_, ok)| *ok).count();
8185 let total = test_results.len() + curve_tests.len();
8186 (passed, total)
8187}
8188
8189pub struct InfluenceMap {
8194 pub width: usize,
8195 pub height: usize,
8196 pub cell_size: f32,
8197 pub origin: Vec2,
8198 pub friendly_influence: Vec<f32>,
8199 pub enemy_influence: Vec<f32>,
8200 pub danger_map: Vec<f32>,
8201 pub opportunity_map: Vec<f32>,
8202 pub decay: f32,
8203 pub propagation_iterations: usize,
8204}
8205
8206impl InfluenceMap {
8207 pub fn new(width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
8208 let n = width * height;
8209 Self {
8210 width, height, cell_size, origin,
8211 friendly_influence: vec![0.0; n],
8212 enemy_influence: vec![0.0; n],
8213 danger_map: vec![0.0; n],
8214 opportunity_map: vec![0.0; n],
8215 decay: 0.9,
8216 propagation_iterations: 3,
8217 }
8218 }
8219
8220 fn idx(&self, x: i32, y: i32) -> Option<usize> {
8221 if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 { return None; }
8222 Some(y as usize * self.width + x as usize)
8223 }
8224
8225 pub fn cell_of(&self, pos: Vec2) -> (i32, i32) {
8226 let rel = pos - self.origin;
8227 ((rel.x / self.cell_size).floor() as i32, (rel.y / self.cell_size).floor() as i32)
8228 }
8229
8230 pub fn stamp_influence(&mut self, pos: Vec2, value: f32, radius: f32, friendly: bool) {
8231 let (cx, cy) = self.cell_of(pos);
8232 let cell_radius = (radius / self.cell_size) as i32 + 1;
8233 let width = self.width;
8234 let height = self.height;
8235 let cell_size = self.cell_size;
8236 let map = if friendly { &mut self.friendly_influence } else { &mut self.enemy_influence };
8237 for dy in -cell_radius..=cell_radius {
8238 for dx in -cell_radius..=cell_radius {
8239 let nx = cx + dx;
8240 let ny = cy + dy;
8241 let idx_opt = if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
8242 None
8243 } else {
8244 Some(ny as usize * width + nx as usize)
8245 };
8246 if let Some(idx) = idx_opt {
8247 let dist = ((dx * dx + dy * dy) as f32).sqrt() * cell_size;
8248 if dist <= radius {
8249 let falloff = 1.0 - (dist / radius);
8250 map[idx] = (map[idx] + value * falloff).clamp(-1.0, 1.0);
8251 }
8252 }
8253 }
8254 }
8255 }
8256
8257 pub fn propagate(&mut self) {
8258 let w = self.width;
8259 let h = self.height;
8260 for _ in 0..self.propagation_iterations {
8261 let mut new_friendly = self.friendly_influence.clone();
8262 let mut new_enemy = self.enemy_influence.clone();
8263 for y in 0..(h as i32) {
8264 for x in 0..(w as i32) {
8265 if let Some(idx) = self.idx(x, y) {
8266 let neighbors = [(x-1, y), (x+1, y), (x, y-1), (x, y+1)];
8267 let mut sum_f = 0.0f32;
8268 let mut sum_e = 0.0f32;
8269 let mut count = 0;
8270 for &(nx, ny) in &neighbors {
8271 if let Some(ni) = self.idx(nx, ny) {
8272 sum_f += self.friendly_influence[ni];
8273 sum_e += self.enemy_influence[ni];
8274 count += 1;
8275 }
8276 }
8277 if count > 0 {
8278 let avg_f = sum_f / count as f32;
8279 let avg_e = sum_e / count as f32;
8280 new_friendly[idx] = (new_friendly[idx] + avg_f * self.decay * 0.25).clamp(-1.0, 1.0);
8281 new_enemy[idx] = (new_enemy[idx] + avg_e * self.decay * 0.25).clamp(-1.0, 1.0);
8282 }
8283 }
8284 }
8285 }
8286 self.friendly_influence = new_friendly;
8287 self.enemy_influence = new_enemy;
8288 }
8289 for i in 0..(self.width * self.height) {
8291 self.danger_map[i] = (self.enemy_influence[i] - self.friendly_influence[i]).max(0.0);
8292 self.opportunity_map[i] = (self.friendly_influence[i] - self.enemy_influence[i]).max(0.0);
8293 }
8294 }
8295
8296 pub fn decay_all(&mut self, dt: f32) {
8297 let decay = (1.0 - dt * 0.5).max(0.0);
8298 for v in &mut self.friendly_influence { *v *= decay; }
8299 for v in &mut self.enemy_influence { *v *= decay; }
8300 }
8301
8302 pub fn get_tension(&self, pos: Vec2) -> f32 {
8303 let (cx, cy) = self.cell_of(pos);
8304 if let Some(idx) = self.idx(cx, cy) {
8305 (self.friendly_influence[idx] + self.enemy_influence[idx]).abs()
8306 } else { 0.0 }
8307 }
8308
8309 pub fn get_vulnerability(&self, pos: Vec2) -> f32 {
8310 let (cx, cy) = self.cell_of(pos);
8311 if let Some(idx) = self.idx(cx, cy) { self.danger_map[idx] } else { 0.0 }
8312 }
8313
8314 pub fn find_safest_direction(&self, pos: Vec2) -> Vec2 {
8315 let (cx, cy) = self.cell_of(pos);
8316 let directions = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, 1), (1, -1), (-1, -1)];
8317 let mut safest_dir = Vec2::ZERO;
8318 let mut min_danger = f32::MAX;
8319 for &(dx, dy) in &directions {
8320 if let Some(idx) = self.idx(cx + dx, cy + dy) {
8321 let danger = self.danger_map[idx];
8322 if danger < min_danger {
8323 min_danger = danger;
8324 safest_dir = Vec2::new(dx as f32, dy as f32).normalize_or_zero();
8325 }
8326 }
8327 }
8328 safest_dir
8329 }
8330
8331 pub fn find_most_opportune_position(&self, center: Vec2, search_radius: f32) -> Option<Vec2> {
8332 let (cx, cy) = self.cell_of(center);
8333 let cell_r = (search_radius / self.cell_size) as i32;
8334 let mut best = f32::NEG_INFINITY;
8335 let mut best_pos = None;
8336 for dy in -cell_r..=cell_r {
8337 for dx in -cell_r..=cell_r {
8338 if let Some(idx) = self.idx(cx + dx, cy + dy) {
8339 let opp = self.opportunity_map[idx];
8340 if opp > best {
8341 best = opp;
8342 best_pos = Some(Vec2::new(
8343 self.origin.x + (cx + dx) as f32 * self.cell_size,
8344 self.origin.y + (cy + dy) as f32 * self.cell_size,
8345 ));
8346 }
8347 }
8348 }
8349 }
8350 best_pos
8351 }
8352}
8353
8354pub struct ContextSteering {
8359 pub resolution: usize, pub interest: Vec<f32>, pub danger: Vec<f32>, pub result_dir: Vec2,
8363 pub result_speed: f32,
8364}
8365
8366impl ContextSteering {
8367 pub fn new(resolution: usize) -> Self {
8368 Self {
8369 resolution,
8370 interest: vec![0.0; resolution],
8371 danger: vec![0.0; resolution],
8372 result_dir: Vec2::ZERO,
8373 result_speed: 0.0,
8374 }
8375 }
8376
8377 pub fn direction_for_slot(&self, slot: usize) -> Vec2 {
8378 let angle = (slot as f32 / self.resolution as f32) * TWO_PI;
8379 Vec2::new(angle.cos(), angle.sin())
8380 }
8381
8382 pub fn add_interest(&mut self, desired_direction: Vec2, weight: f32) {
8383 let desired_norm = desired_direction.normalize_or_zero();
8384 for i in 0..self.resolution {
8385 let slot_dir = self.direction_for_slot(i);
8386 let dot = slot_dir.dot(desired_norm).max(0.0);
8387 self.interest[i] += dot * weight;
8388 }
8389 }
8390
8391 pub fn add_danger(&mut self, danger_direction: Vec2, weight: f32) {
8392 let danger_norm = danger_direction.normalize_or_zero();
8393 for i in 0..self.resolution {
8394 let slot_dir = self.direction_for_slot(i);
8395 let dot = slot_dir.dot(danger_norm).max(0.0);
8396 self.danger[i] = (self.danger[i] + dot * weight).min(1.0);
8397 }
8398 }
8399
8400 pub fn solve(&mut self) -> Vec2 {
8401 let masked: Vec<f32> = self.interest.iter().zip(self.danger.iter())
8403 .map(|(&i, &d)| if d > 0.7 { 0.0 } else { i * (1.0 - d) })
8404 .collect();
8405
8406 let best_slot = masked.iter().enumerate()
8408 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8409 .map(|(i, _)| i)
8410 .unwrap_or(0);
8411
8412 let best_weight = masked[best_slot];
8413 if best_weight < EPSILON {
8414 self.result_dir = Vec2::ZERO;
8415 self.result_speed = 0.0;
8416 return Vec2::ZERO;
8417 }
8418
8419 let mut dir_sum = Vec2::ZERO;
8421 let mut weight_sum = 0.0f32;
8422 for i in 0..self.resolution {
8423 if masked[i] > best_weight * 0.5 {
8424 dir_sum += self.direction_for_slot(i) * masked[i];
8425 weight_sum += masked[i];
8426 }
8427 }
8428
8429 self.result_dir = if weight_sum > EPSILON { (dir_sum / weight_sum).normalize_or_zero() } else { Vec2::ZERO };
8430 self.result_speed = best_weight.min(1.0);
8431 self.result_dir
8432 }
8433
8434 pub fn reset(&mut self) {
8435 for v in &mut self.interest { *v = 0.0; }
8436 for v in &mut self.danger { *v = 0.0; }
8437 }
8438
8439 pub fn debug_draw(&self, center: Vec3, scale: f32, buf: &mut DebugVisualizationBuffer) {
8440 for i in 0..self.resolution {
8441 let dir_2d = self.direction_for_slot(i);
8442 let dir_3d = Vec3::new(dir_2d.x, 0.0, dir_2d.y);
8443 let interest_color = Vec4::new(0.0, self.interest[i], 0.0, 0.8);
8444 let danger_color = Vec4::new(self.danger[i], 0.0, 0.0, 0.8);
8445 buf.add(DebugShapeType::Arrow { from: center, to: center + dir_3d * self.interest[i] * scale, head_size: 0.1 }, interest_color, 0.0);
8446 buf.add(DebugShapeType::Arrow { from: center, to: center + dir_3d * self.danger[i] * scale * 0.5, head_size: 0.08 }, danger_color, 0.0);
8447 }
8448 let result_3d = Vec3::new(self.result_dir.x, 0.0, self.result_dir.y);
8450 buf.add(DebugShapeType::Arrow { from: center, to: center + result_3d * self.result_speed * scale * 1.2, head_size: 0.15 }, Vec4::new(1.0, 1.0, 0.0, 1.0), 0.0);
8451 }
8452}
8453
8454#[derive(Clone, Debug)]
8459pub struct AiAbility {
8460 pub id: u32,
8461 pub name: String,
8462 pub cooldown: f32,
8463 pub cooldown_remaining: f32,
8464 pub cast_time: f32,
8465 pub range: f32,
8466 pub area_radius: f32,
8467 pub damage: f32,
8468 pub healing: f32,
8469 pub energy_cost: f32,
8470 pub is_casting: bool,
8471 pub cast_elapsed: f32,
8472 pub target_pos: Vec3,
8473 pub target_entity: Option<u64>,
8474 pub tags: HashSet<String>,
8475}
8476
8477impl AiAbility {
8478 pub fn new(id: u32, name: &str, cooldown: f32, range: f32) -> Self {
8479 Self {
8480 id, name: name.to_string(), cooldown, cooldown_remaining: 0.0,
8481 cast_time: 0.5, range, area_radius: 0.0, damage: 0.0, healing: 0.0,
8482 energy_cost: 10.0, is_casting: false, cast_elapsed: 0.0,
8483 target_pos: Vec3::ZERO, target_entity: None, tags: HashSet::new(),
8484 }
8485 }
8486
8487 pub fn is_ready(&self) -> bool { self.cooldown_remaining <= 0.0 && !self.is_casting }
8488
8489 pub fn can_reach(&self, user_pos: Vec3, target_pos: Vec3) -> bool {
8490 (target_pos - user_pos).length() <= self.range
8491 }
8492
8493 pub fn start_cast(&mut self, target_pos: Vec3, target_entity: Option<u64>) {
8494 if !self.is_ready() { return; }
8495 self.is_casting = true;
8496 self.cast_elapsed = 0.0;
8497 self.target_pos = target_pos;
8498 self.target_entity = target_entity;
8499 }
8500
8501 pub fn tick(&mut self, dt: f32) -> bool {
8502 self.cooldown_remaining = (self.cooldown_remaining - dt).max(0.0);
8504 if self.is_casting {
8505 self.cast_elapsed += dt;
8506 if self.cast_elapsed >= self.cast_time {
8507 self.is_casting = false;
8508 self.cooldown_remaining = self.cooldown;
8509 return true;
8510 }
8511 }
8512 false
8513 }
8514
8515 pub fn interrupt(&mut self) {
8516 self.is_casting = false;
8517 self.cast_elapsed = 0.0;
8518 }
8519
8520 pub fn cast_progress(&self) -> f32 {
8521 if self.cast_time < EPSILON { 1.0 } else { self.cast_elapsed / self.cast_time }
8522 }
8523}
8524
8525pub struct AbilityManager {
8526 pub abilities: Vec<AiAbility>,
8527 pub energy: f32,
8528 pub max_energy: f32,
8529 pub energy_regen: f32,
8530}
8531
8532impl AbilityManager {
8533 pub fn new(max_energy: f32) -> Self {
8534 Self { abilities: Vec::new(), energy: max_energy, max_energy, energy_regen: 5.0 }
8535 }
8536
8537 pub fn add_ability(&mut self, ability: AiAbility) { self.abilities.push(ability); }
8538
8539 pub fn tick(&mut self, dt: f32) -> Vec<u32> {
8540 self.energy = (self.energy + self.energy_regen * dt).min(self.max_energy);
8542 self.abilities.iter_mut().filter_map(|a| if a.tick(dt) { Some(a.id) } else { None }).collect()
8544 }
8545
8546 pub fn try_use(&mut self, ability_id: u32, target_pos: Vec3, user_pos: Vec3) -> bool {
8547 if let Some(ability) = self.abilities.iter_mut().find(|a| a.id == ability_id) {
8548 if ability.is_ready() && ability.can_reach(user_pos, target_pos) && self.energy >= ability.energy_cost {
8549 self.energy -= ability.energy_cost;
8550 ability.start_cast(target_pos, None);
8551 return true;
8552 }
8553 }
8554 false
8555 }
8556
8557 pub fn best_offensive_ability(&self, user_pos: Vec3, target_pos: Vec3) -> Option<u32> {
8558 self.abilities.iter()
8559 .filter(|a| a.is_ready() && a.damage > 0.0 && a.can_reach(user_pos, target_pos))
8560 .max_by(|a, b| a.damage.partial_cmp(&b.damage).unwrap())
8561 .map(|a| a.id)
8562 }
8563
8564 pub fn best_healing_ability(&self) -> Option<u32> {
8565 self.abilities.iter()
8566 .filter(|a| a.is_ready() && a.healing > 0.0 && self.energy >= a.energy_cost)
8567 .max_by(|a, b| a.healing.partial_cmp(&b.healing).unwrap())
8568 .map(|a| a.id)
8569 }
8570
8571 pub fn interrupt_all(&mut self) {
8572 for ability in &mut self.abilities { ability.interrupt(); }
8573 }
8574}
8575
8576impl AiBehaviorEditor {
8581 pub fn add_influence_map_panel(&mut self) {
8582 self.panel_sizes.insert("influence_map".to_string(), Vec2::new(300.0, 300.0));
8584 }
8585
8586 pub fn get_bt_node_tooltip(&self, tree_idx: usize, node_id: u32) -> String {
8587 let tree = match self.behavior_trees.get(tree_idx) { Some(t) => t, None => return String::new() };
8588 let node = match tree.nodes.get(&node_id) { Some(n) => n, None => return String::new() };
8589 let mut tip = format!("[{}] {}\n", node.id, node.display_name());
8590 tip.push_str(&format!(" Status: {:?}\n", node.status));
8591 tip.push_str(&format!(" Children: {}\n", node.children.len()));
8592 tip.push_str(&format!(" Elapsed: {:.2}s\n", node.elapsed_time));
8593 if node.repeat_count > 0 { tip.push_str(&format!(" Repeat count: {}\n", node.repeat_count)); }
8594 tip
8595 }
8596
8597 pub fn center_camera_on_tree(&mut self, tree_idx: usize) {
8598 if let Some(tree) = self.behavior_trees.get(tree_idx) {
8599 if tree.nodes.is_empty() { return; }
8600 let mut min_x = f32::MAX; let mut max_x = f32::MIN;
8601 let mut min_y = f32::MAX; let mut max_y = f32::MIN;
8602 for node in tree.nodes.values() {
8603 min_x = min_x.min(node.position.x);
8604 max_x = max_x.max(node.position.x + node.size.x);
8605 min_y = min_y.min(node.position.y);
8606 max_y = max_y.max(node.position.y + node.size.y);
8607 }
8608 let center = Vec2::new((min_x + max_x) * 0.5, (min_y + max_y) * 0.5);
8609 self.bt_camera.target_pan = -center;
8610 }
8611 }
8612
8613 pub fn align_nodes_horizontal(&mut self, tree_idx: usize) {
8614 let selected: Vec<u32> = self.bt_selection.selected_nodes.iter().copied().collect();
8615 if selected.is_empty() { return; }
8616 let tree = match self.behavior_trees.get(tree_idx) { Some(t) => t, None => return };
8617 let avg_y: f32 = selected.iter().filter_map(|id| tree.nodes.get(id)).map(|n| n.position.y).sum::<f32>() / selected.len() as f32;
8618 let tree = match self.behavior_trees.get_mut(tree_idx) { Some(t) => t, None => return };
8619 for id in &selected {
8620 if let Some(node) = tree.nodes.get_mut(id) { node.position.y = avg_y; }
8621 }
8622 }
8623
8624 pub fn align_nodes_vertical(&mut self, tree_idx: usize) {
8625 let selected: Vec<u32> = self.bt_selection.selected_nodes.iter().copied().collect();
8626 if selected.is_empty() { return; }
8627 let tree = match self.behavior_trees.get(tree_idx) { Some(t) => t, None => return };
8628 let avg_x: f32 = selected.iter().filter_map(|id| tree.nodes.get(id)).map(|n| n.position.x).sum::<f32>() / selected.len() as f32;
8629 let tree = match self.behavior_trees.get_mut(tree_idx) { Some(t) => t, None => return };
8630 for id in &selected {
8631 if let Some(node) = tree.nodes.get_mut(id) { node.position.x = avg_x; }
8632 }
8633 }
8634
8635 pub fn distribute_nodes_horizontally(&mut self, tree_idx: usize) {
8636 let mut selected: Vec<u32> = self.bt_selection.selected_nodes.iter().copied().collect();
8637 if selected.len() < 2 { return; }
8638 let tree = match self.behavior_trees.get(tree_idx) { Some(t) => t, None => return };
8639 selected.sort_by(|&a, &b| {
8640 let xa = tree.nodes.get(&a).map(|n| n.position.x).unwrap_or(0.0);
8641 let xb = tree.nodes.get(&b).map(|n| n.position.x).unwrap_or(0.0);
8642 xa.partial_cmp(&xb).unwrap()
8643 });
8644 let first_x = tree.nodes.get(&selected[0]).map(|n| n.position.x).unwrap_or(0.0);
8645 let last_x = tree.nodes.get(selected.last().unwrap()).map(|n| n.position.x + n.size.x).unwrap_or(0.0);
8646 let total_width: f32 = selected.iter().filter_map(|id| tree.nodes.get(id)).map(|n| n.size.x).sum();
8647 let gap = (last_x - first_x - total_width) / (selected.len() as f32 - 1.0).max(1.0);
8648 let tree = match self.behavior_trees.get_mut(tree_idx) { Some(t) => t, None => return };
8649 let mut cursor = first_x;
8650 for id in &selected {
8651 if let Some(node) = tree.nodes.get_mut(id) {
8652 node.position.x = cursor;
8653 cursor += node.size.x + gap;
8654 }
8655 }
8656 }
8657
8658 pub fn set_node_color_by_status(&self) -> HashMap<u32, Vec4> {
8659 let tree = match self.behavior_trees.get(self.active_tree_index) { Some(t) => t, None => return HashMap::new() };
8660 tree.nodes.iter().map(|(&id, node)| (id, self.bt_get_node_color(node))).collect()
8661 }
8662
8663 pub fn export_tree_as_dot(&self, tree_idx: usize) -> String {
8664 let tree = match self.behavior_trees.get(tree_idx) { Some(t) => t, None => return String::new() };
8665 let mut out = String::from("digraph BehaviorTree {\n rankdir=TB;\n");
8666 for (id, node) in &tree.nodes {
8667 let color = match node.status {
8668 BtStatus::Success => "green",
8669 BtStatus::Failure => "red",
8670 BtStatus::Running => "yellow",
8671 BtStatus::Invalid => "gray",
8672 };
8673 let shape = if node.is_composite() { "diamond" } else if node.is_decorator() { "hexagon" } else { "box" };
8674 out.push_str(&format!(" {} [label=\"{}\" style=filled fillcolor={} shape={}];\n", id, node.display_name(), color, shape));
8675 }
8676 for (parent_id, node) in &tree.nodes {
8677 for child_id in &node.children {
8678 out.push_str(&format!(" {} -> {};\n", parent_id, child_id));
8679 }
8680 }
8681 out.push_str("}\n");
8682 out
8683 }
8684
8685 pub fn compute_heatmap_positions(&self, tree_idx: usize, debugger: &BtDebugger) -> Vec<(Vec2, f32)> {
8686 let tree = match self.behavior_trees.get(tree_idx) { Some(t) => t, None => return Vec::new() };
8687 let max_count = debugger.node_exec_counts.values().copied().max().unwrap_or(1) as f32;
8688 tree.nodes.iter().map(|(&id, node)| {
8689 let count = debugger.node_exec_counts.get(&id).copied().unwrap_or(0) as f32;
8690 let heat = count / max_count;
8691 (node.position + node.size * 0.5, heat)
8692 }).collect()
8693 }
8694
8695 pub fn fsm_get_transition_arrow(&self, fsm_idx: usize, transition_id: u32) -> Option<(Vec2, Vec2)> {
8696 let fsm = self.fsm_instances.get(fsm_idx)?;
8697 let t = fsm.transitions.iter().find(|t| t.id == transition_id)?;
8698 let from_pos = fsm.states.get(&t.from_state)?.position;
8699 let to_pos = fsm.states.get(&t.to_state)?.position;
8700 if t.from_state == t.to_state {
8702 let offset = Vec2::new(60.0, -40.0);
8703 Some((from_pos + offset, from_pos + offset * 2.0))
8704 } else {
8705 Some((from_pos, to_pos))
8706 }
8707 }
8708
8709 pub fn blackboard_diff(&self, other: &Blackboard) -> Vec<(String, BlackboardValue, BlackboardValue)> {
8710 let mut diffs = Vec::new();
8711 for (key, value) in &self.shared_blackboard.entries {
8712 if let Some(other_val) = other.entries.get(key) {
8713 if other_val != value {
8714 diffs.push((key.clone(), value.clone(), other_val.clone()));
8715 }
8716 } else {
8717 diffs.push((key.clone(), value.clone(), BlackboardValue::None));
8718 }
8719 }
8720 diffs
8721 }
8722
8723 pub fn get_formation_agent_positions(&self, leader_pos: Vec3, leader_fwd: Vec3) -> Vec<Vec3> {
8724 FormationLayout::compute_slots(self.formation_preview, leader_pos, leader_fwd, self.formation_n_agents, self.formation_spacing)
8725 }
8726
8727 pub fn compute_all_utility_scores(&self) -> Vec<(String, f32)> {
8728 self.utility_dm.actions.iter().map(|a| {
8729 (a.name.clone(), a.score(&self.shared_blackboard, self.current_time))
8730 }).collect()
8731 }
8732
8733 pub fn tick_all_emotion_engines(&mut self, dt: f32) {
8734 for engine in &mut self.emotion_engines {
8735 engine.update(dt, self.current_time);
8736 }
8737 }
8738
8739 pub fn get_global_threat_level(&self) -> f32 {
8740 self.agents.iter().map(|a| {
8741 a.perception.perceived.values().map(|p| p.threat_level * p.confidence).sum::<f32>()
8742 }).sum::<f32>() / self.agents.len().max(1) as f32
8743 }
8744
8745 pub fn snapshot_agent_states(&self) -> Vec<HashMap<String, f32>> {
8746 self.agents.iter().map(|agent| {
8747 let mut snap = HashMap::new();
8748 snap.insert("health".to_string(), agent.blackboard.get_float("self_health"));
8749 snap.insert("ammo".to_string(), agent.blackboard.get_float("ammo_count"));
8750 snap.insert("pos_x".to_string(), agent.position.x);
8751 snap.insert("pos_y".to_string(), agent.position.y);
8752 snap.insert("pos_z".to_string(), agent.position.z);
8753 snap.insert("speed".to_string(), agent.steering_agent.speed());
8754 snap.insert("emotion_valence".to_string(), agent.emotion_engine.state.mood_valence);
8755 snap.insert("emotion_arousal".to_string(), agent.emotion_engine.state.mood_arousal);
8756 snap
8757 }).collect()
8758 }
8759}
8760
8761pub fn simulate_combat_round(
8766 attacker_pos: Vec3, attacker_damage: f32, attacker_accuracy: f32,
8767 defender_pos: Vec3, defender_health: f32, defender_cover: f32,
8768 rng: &mut u64,
8769) -> (f32, bool) {
8770 *rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
8772 let roll = ((*rng >> 33) as f32) / (u32::MAX as f32);
8773
8774 let hit_chance = (attacker_accuracy * (1.0 - defender_cover * 0.5)).clamp(0.0, 1.0);
8775 let dist = (defender_pos - attacker_pos).length();
8776 let range_penalty = effective_range_modifier(dist, 20.0, 5.0);
8777 let effective_hit_chance = hit_chance * range_penalty;
8778
8779 if roll < effective_hit_chance {
8780 let damage = attacker_damage * (0.8 + roll * 0.4); let new_health = (defender_health - damage).max(0.0);
8782 let killed = new_health <= 0.0;
8783 (new_health, killed)
8784 } else {
8785 (defender_health, false)
8786 }
8787}
8788
8789pub fn estimate_time_to_kill(attacker_damage: f32, attacker_fire_rate: f32, attacker_accuracy: f32, defender_health: f32, defender_cover: f32) -> f32 {
8790 if attacker_fire_rate <= 0.0 || attacker_damage <= 0.0 { return f32::MAX; }
8791 let shots_needed = (defender_health / attacker_damage).ceil();
8792 let effective_accuracy = attacker_accuracy * (1.0 - defender_cover * 0.3);
8793 let shots_to_fire = shots_needed / effective_accuracy.max(0.01);
8794 shots_to_fire / attacker_fire_rate
8795}
8796
8797pub fn check_line_of_sight_multi(from: Vec3, to: Vec3, obstacles: &[Aabb]) -> (bool, Option<Vec3>) {
8798 let dir = to - from;
8799 let len = dir.length();
8800 if len < EPSILON { return (true, None); }
8801 let inv_dir = Vec3::new(1.0 / dir.x, 1.0 / dir.y, 1.0 / dir.z);
8802
8803 let mut nearest_hit: Option<Vec3> = None;
8804 let mut nearest_t = f32::MAX;
8805
8806 for obs in obstacles {
8807 let t1 = (obs.min - from) * inv_dir;
8808 let t2 = (obs.max - from) * inv_dir;
8809 let t_enter = Vec3::new(t1.x.min(t2.x), t1.y.min(t2.y), t1.z.min(t2.z));
8810 let t_exit = Vec3::new(t1.x.max(t2.x), t1.y.max(t2.y), t1.z.max(t2.z));
8811 let t_in = t_enter.x.max(t_enter.y).max(t_enter.z);
8812 let t_out = t_exit.x.min(t_exit.y).min(t_exit.z);
8813 if t_in <= t_out && t_out >= 0.0 && t_in <= len {
8814 let t = t_in.max(0.0);
8815 if t < nearest_t {
8816 nearest_t = t;
8817 nearest_hit = Some(from + dir.normalize() * t);
8818 }
8819 }
8820 }
8821
8822 if nearest_hit.is_some() { (false, nearest_hit) } else { (true, None) }
8823}
8824
8825pub struct AiPresets;
8830
8831impl AiPresets {
8832 pub fn apply_sniper_config(agent: &mut AiAgent) {
8833 agent.perception.vision.range = 50.0;
8834 agent.perception.vision.half_angle = PI / 6.0; agent.perception.hearing.base_radius = 20.0;
8836 agent.steering_agent.max_speed = 2.5;
8837 agent.blackboard.set("preferred_range", BlackboardValue::Float(25.0));
8838 agent.blackboard.set("aggression", BlackboardValue::Float(0.3));
8839 agent.blackboard.set("cover_preference", BlackboardValue::Float(0.9));
8840 }
8841
8842 pub fn apply_berserker_config(agent: &mut AiAgent) {
8843 agent.perception.vision.range = 15.0;
8844 agent.perception.vision.half_angle = PI * 0.6; agent.steering_agent.max_speed = 8.0;
8846 agent.blackboard.set("preferred_range", BlackboardValue::Float(2.0));
8847 agent.blackboard.set("aggression", BlackboardValue::Float(0.95));
8848 agent.blackboard.set("cover_preference", BlackboardValue::Float(0.1));
8849 agent.emotion_engine.submit_stimulus(EmotionalStimulus { emotion: PrimaryEmotion::Anger, intensity: 0.8, source_id: 0, decay_rate_override: Some(0.005) });
8850 }
8851
8852 pub fn apply_medic_config(agent: &mut AiAgent) {
8853 agent.perception.vision.range = 25.0;
8854 agent.steering_agent.max_speed = 4.0;
8855 agent.blackboard.set("preferred_range", BlackboardValue::Float(10.0));
8856 agent.blackboard.set("aggression", BlackboardValue::Float(0.1));
8857 agent.blackboard.set("heal_priority", BlackboardValue::Float(0.9));
8858 agent.blackboard.set("medpack_count", BlackboardValue::Int(5));
8859 }
8860
8861 pub fn apply_scout_config(agent: &mut AiAgent) {
8862 agent.perception.vision.range = 35.0;
8863 agent.perception.vision.half_angle = PI * 0.4;
8864 agent.perception.hearing.base_radius = 30.0;
8865 agent.steering_agent.max_speed = 7.0;
8866 agent.blackboard.set("preferred_range", BlackboardValue::Float(15.0));
8867 agent.blackboard.set("aggression", BlackboardValue::Float(0.4));
8868 agent.blackboard.set("report_sightings", BlackboardValue::Bool(true));
8869 agent.emotion_engine.submit_stimulus(EmotionalStimulus { emotion: PrimaryEmotion::Anticipation, intensity: 0.6, source_id: 0, decay_rate_override: None });
8870 }
8871
8872 pub fn apply_guardian_config(agent: &mut AiAgent) {
8873 agent.perception.vision.range = 20.0;
8874 agent.perception.vision.near_range = 3.0;
8875 agent.steering_agent.max_speed = 3.5;
8876 agent.blackboard.set("preferred_range", BlackboardValue::Float(5.0));
8877 agent.blackboard.set("aggression", BlackboardValue::Float(0.6));
8878 agent.blackboard.set("defend_position", BlackboardValue::Vec3(agent.position));
8879 agent.blackboard.set("defend_radius", BlackboardValue::Float(8.0));
8880 agent.emotion_engine.submit_stimulus(EmotionalStimulus { emotion: PrimaryEmotion::Trust, intensity: 0.7, source_id: 0, decay_rate_override: None });
8881 }
8882}
8883
8884#[derive(Clone, Debug)]
8889pub struct AgentSnapshot {
8890 pub time: f32,
8891 pub agent_id: u64,
8892 pub position: Vec3,
8893 pub velocity: Vec3,
8894 pub heading: Vec3,
8895 pub bt_status: BtStatus,
8896 pub active_node_id: Option<u32>,
8897 pub emotion_intensities: [f32; 8],
8898 pub blackboard_floats: HashMap<String, f32>,
8899 pub goap_world_state: WorldState,
8900}
8901
8902pub struct ReplayBuffer {
8903 pub snapshots: VecDeque<AgentSnapshot>,
8904 pub max_duration: f32,
8905 pub snapshot_interval: f32,
8906 pub last_snapshot_time: f32,
8907 pub is_recording: bool,
8908 pub is_replaying: bool,
8909 pub replay_time: f32,
8910 pub replay_speed: f32,
8911}
8912
8913impl ReplayBuffer {
8914 pub fn new(max_duration: f32, snapshot_interval: f32) -> Self {
8915 let capacity = (max_duration / snapshot_interval) as usize * 8;
8916 Self { snapshots: VecDeque::with_capacity(capacity), max_duration, snapshot_interval, last_snapshot_time: 0.0, is_recording: false, is_replaying: false, replay_time: 0.0, replay_speed: 1.0 }
8917 }
8918
8919 pub fn record_agent(&mut self, agent: &AiAgent, current_time: f32) {
8920 if !self.is_recording { return; }
8921 if current_time - self.last_snapshot_time < self.snapshot_interval { return; }
8922
8923 let active_node = agent.behavior_tree.as_ref().and_then(|bt| bt.root_id);
8924 let mut bb_floats = HashMap::new();
8925 for (key, value) in &agent.blackboard.entries {
8926 if let BlackboardValue::Float(f) = value { bb_floats.insert(key.clone(), *f); }
8927 }
8928
8929 self.snapshots.push_back(AgentSnapshot {
8930 time: current_time,
8931 agent_id: agent.id,
8932 position: agent.position,
8933 velocity: agent.velocity,
8934 heading: agent.heading,
8935 bt_status: agent.behavior_tree.as_ref().map(|bt| bt.last_status).unwrap_or(BtStatus::Invalid),
8936 active_node_id: active_node,
8937 emotion_intensities: agent.emotion_engine.state.intensities,
8938 blackboard_floats: bb_floats,
8939 goap_world_state: agent.goap_world_state,
8940 });
8941
8942 while let Some(s) = self.snapshots.front() {
8944 if current_time - s.time > self.max_duration { self.snapshots.pop_front(); } else { break; }
8945 }
8946 self.last_snapshot_time = current_time;
8947 }
8948
8949 pub fn get_snapshot_at(&self, time: f32, agent_id: u64) -> Option<&AgentSnapshot> {
8950 let mut best: Option<&AgentSnapshot> = None;
8951 for snap in &self.snapshots {
8952 if snap.agent_id == agent_id && snap.time <= time {
8953 best = Some(snap);
8954 }
8955 }
8956 best
8957 }
8958
8959 pub fn interpolate_position(&self, time: f32, agent_id: u64) -> Option<Vec3> {
8960 let mut before: Option<&AgentSnapshot> = None;
8961 let mut after: Option<&AgentSnapshot> = None;
8962 for snap in &self.snapshots {
8963 if snap.agent_id != agent_id { continue; }
8964 if snap.time <= time { before = Some(snap); }
8965 if snap.time >= time && after.is_none() { after = Some(snap); }
8966 }
8967 match (before, after) {
8968 (Some(b), Some(a)) if b.time != a.time => {
8969 let t = (time - b.time) / (a.time - b.time);
8970 Some(b.position.lerp(a.position, t))
8971 }
8972 (Some(b), _) => Some(b.position),
8973 (_, Some(a)) => Some(a.position),
8974 _ => None,
8975 }
8976 }
8977
8978 pub fn start_recording(&mut self) { self.is_recording = true; self.is_replaying = false; }
8979 pub fn stop_recording(&mut self) { self.is_recording = false; }
8980 pub fn start_replay(&mut self) { self.is_replaying = true; self.is_recording = false; if let Some(s) = self.snapshots.front() { self.replay_time = s.time; } }
8981 pub fn stop_replay(&mut self) { self.is_replaying = false; }
8982
8983 pub fn tick_replay(&mut self, dt: f32) {
8984 if self.is_replaying { self.replay_time += dt * self.replay_speed; }
8985 }
8986
8987 pub fn snapshot_count(&self) -> usize { self.snapshots.len() }
8988 pub fn duration_recorded(&self) -> f32 {
8989 match (self.snapshots.front(), self.snapshots.back()) {
8990 (Some(f), Some(b)) => b.time - f.time,
8991 _ => 0.0,
8992 }
8993 }
8994}
8995
8996pub struct FlockSimulation {
9001 pub agents: Vec<SteeringAgent>,
9002 pub obstacles: Vec<Aabb>,
9003 pub neighbor_radius: f32,
9004 pub separation_radius: f32,
9005 pub rng_seeds: Vec<u64>,
9006 pub seek_target: Option<Vec3>,
9007 pub bounds_min: Vec3,
9008 pub bounds_max: Vec3,
9009}
9010
9011impl FlockSimulation {
9012 pub fn new(n: usize, bounds_min: Vec3, bounds_max: Vec3) -> Self {
9013 let agents: Vec<SteeringAgent> = (0..n).map(|i| {
9014 let x = bounds_min.x + (i as f32 / n as f32) * (bounds_max.x - bounds_min.x);
9015 let z = bounds_min.z + ((i * 7 % n) as f32 / n as f32) * (bounds_max.z - bounds_min.z);
9016 SteeringAgent::new(i as u64, Vec3::new(x, 0.0, z), 4.0, 8.0)
9017 }).collect();
9018 let rng_seeds: Vec<u64> = (0..n).map(|i| (i as u64 + 1) * 6364136223846793005).collect();
9019 Self { agents, obstacles: Vec::new(), neighbor_radius: 5.0, separation_radius: 1.5, rng_seeds, seek_target: None, bounds_min, bounds_max }
9020 }
9021
9022 pub fn tick(&mut self, dt: f32) {
9023 let n = self.agents.len();
9024 let agents_clone = self.agents.clone();
9025 let obs_clone = self.obstacles.clone();
9026
9027 for i in 0..n {
9028 let neighbors: Vec<&SteeringAgent> = agents_clone.iter().enumerate()
9029 .filter(|(j, _)| *j != i)
9030 .filter(|(_, a)| (a.position - agents_clone[i].position).length() < self.neighbor_radius)
9031 .map(|(_, a)| a)
9032 .collect();
9033
9034 let mut force = Vec3::ZERO;
9035
9036 force += SteeringBehaviors::alignment(&agents_clone[i], &neighbors) * ALIGNMENT_WEIGHT;
9038 force += SteeringBehaviors::cohesion(&agents_clone[i], &neighbors) * COHESION_WEIGHT;
9039 force += SteeringBehaviors::separation(&agents_clone[i], &neighbors, self.separation_radius) * SEPARATION_WEIGHT;
9040
9041 if let Some(target) = self.seek_target {
9043 force += SteeringBehaviors::arrive(&agents_clone[i], target, ARRIVE_DECELERATION_RADIUS * 2.0) * 0.5;
9044 }
9045
9046 if self.seek_target.is_none() {
9048 force += SteeringBehaviors::wander(&mut self.agents[i], &mut self.rng_seeds[i], dt) * 0.3;
9049 }
9050
9051 force += SteeringBehaviors::obstacle_avoidance(&agents_clone[i], &obs_clone) * 2.0;
9053
9054 let bmin = self.bounds_min;
9056 let bmax = self.bounds_max;
9057 let pos = agents_clone[i].position;
9058 let margin = 3.0;
9059 if pos.x < bmin.x + margin { force += Vec3::X * (bmin.x + margin - pos.x) * 2.0; }
9060 if pos.x > bmax.x - margin { force -= Vec3::X * (pos.x - (bmax.x - margin)) * 2.0; }
9061 if pos.z < bmin.z + margin { force += Vec3::Z * (bmin.z + margin - pos.z) * 2.0; }
9062 if pos.z > bmax.z - margin { force -= Vec3::Z * (pos.z - (bmax.z - margin)) * 2.0; }
9063
9064 self.agents[i].apply_force(force, dt);
9065 }
9066 }
9067
9068 pub fn average_velocity(&self) -> Vec3 {
9069 if self.agents.is_empty() { return Vec3::ZERO; }
9070 let sum = self.agents.iter().map(|a| a.velocity).fold(Vec3::ZERO, |a, b| a + b);
9071 sum / self.agents.len() as f32
9072 }
9073
9074 pub fn centroid(&self) -> Vec3 {
9075 if self.agents.is_empty() { return Vec3::ZERO; }
9076 let sum = self.agents.iter().map(|a| a.position).fold(Vec3::ZERO, |a, b| a + b);
9077 sum / self.agents.len() as f32
9078 }
9079
9080 pub fn spread(&self) -> f32 {
9081 let center = self.centroid();
9082 if self.agents.is_empty() { return 0.0; }
9083 self.agents.iter().map(|a| (a.position - center).length()).sum::<f32>() / self.agents.len() as f32
9084 }
9085}
9086
9087pub struct AiStateProfiler {
9092 pub mode_time: HashMap<String, f32>,
9093 pub bt_node_time: HashMap<u32, f32>,
9094 pub current_mode: String,
9095 pub mode_entry_time: f32,
9096 pub current_time: f32,
9097 pub sample_count: u64,
9098}
9099
9100impl AiStateProfiler {
9101 pub fn new() -> Self {
9102 Self { mode_time: HashMap::new(), bt_node_time: HashMap::new(), current_mode: "idle".to_string(), mode_entry_time: 0.0, current_time: 0.0, sample_count: 0 }
9103 }
9104
9105 pub fn enter_mode(&mut self, mode: &str) {
9106 let elapsed = self.current_time - self.mode_entry_time;
9107 if elapsed > 0.0 {
9108 *self.mode_time.entry(self.current_mode.clone()).or_insert(0.0) += elapsed;
9109 }
9110 self.current_mode = mode.to_string();
9111 self.mode_entry_time = self.current_time;
9112 }
9113
9114 pub fn tick(&mut self, dt: f32) {
9115 self.current_time += dt;
9116 self.sample_count += 1;
9117 }
9118
9119 pub fn mode_percentage(&self, mode: &str) -> f32 {
9120 let total: f32 = self.mode_time.values().sum();
9121 if total < EPSILON { return 0.0; }
9122 self.mode_time.get(mode).copied().unwrap_or(0.0) / total
9123 }
9124
9125 pub fn most_common_mode(&self) -> Option<(&str, f32)> {
9126 let total: f32 = self.mode_time.values().sum();
9127 if total < EPSILON { return None; }
9128 self.mode_time.iter()
9129 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9130 .map(|(k, &v)| (k.as_str(), v / total))
9131 }
9132
9133 pub fn report(&self) -> String {
9134 let mut out = String::from("AI State Profile:\n");
9135 let total: f32 = self.mode_time.values().sum();
9136 let mut sorted: Vec<(&String, &f32)> = self.mode_time.iter().collect();
9137 sorted.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap());
9138 for (mode, time) in sorted {
9139 let pct = if total > 0.0 { time / total * 100.0 } else { 0.0 };
9140 out.push_str(&format!(" {}: {:.2}s ({:.1}%)\n", mode, time, pct));
9141 }
9142 out.push_str(&format!(" Total: {:.2}s Samples: {}\n", total, self.sample_count));
9143 out
9144 }
9145}
9146
9147pub struct HeatMapData {
9152 pub width: usize,
9153 pub height: usize,
9154 pub values: Vec<f32>,
9155 pub cell_size: f32,
9156 pub origin: Vec2,
9157 pub max_value: f32,
9158 pub label: String,
9159}
9160
9161impl HeatMapData {
9162 pub fn new(width: usize, height: usize, cell_size: f32, origin: Vec2, label: &str) -> Self {
9163 Self { width, height, values: vec![0.0; width * height], cell_size, origin, max_value: 1.0, label: label.to_string() }
9164 }
9165
9166 pub fn add_point(&mut self, pos: Vec2, value: f32, radius: f32) {
9167 let cx = ((pos.x - self.origin.x) / self.cell_size).floor() as i32;
9168 let cy = ((pos.y - self.origin.y) / self.cell_size).floor() as i32;
9169 let cell_r = (radius / self.cell_size).ceil() as i32;
9170 for dy in -cell_r..=cell_r {
9171 for dx in -cell_r..=cell_r {
9172 let nx = cx + dx;
9173 let ny = cy + dy;
9174 if nx < 0 || ny < 0 || nx >= self.width as i32 || ny >= self.height as i32 { continue; }
9175 let dist = ((dx * dx + dy * dy) as f32).sqrt() * self.cell_size;
9176 if dist <= radius {
9177 let falloff = 1.0 - dist / (radius + EPSILON);
9178 let idx = ny as usize * self.width + nx as usize;
9179 self.values[idx] += value * falloff * falloff;
9180 if self.values[idx] > self.max_value { self.max_value = self.values[idx]; }
9181 }
9182 }
9183 }
9184 }
9185
9186 pub fn normalize(&mut self) {
9187 if self.max_value > EPSILON {
9188 for v in &mut self.values { *v /= self.max_value; }
9189 self.max_value = 1.0;
9190 }
9191 }
9192
9193 pub fn get_normalized(&self, x: usize, y: usize) -> f32 {
9194 if x >= self.width || y >= self.height { return 0.0; }
9195 let v = self.values[y * self.width + x];
9196 if self.max_value > EPSILON { v / self.max_value } else { 0.0 }
9197 }
9198
9199 pub fn to_rgba_gradient(&self, low: Vec4, high: Vec4) -> Vec<Vec4> {
9200 self.values.iter().map(|&v| {
9201 let t = (v / self.max_value.max(EPSILON)).clamp(0.0, 1.0);
9202 Vec4::new(
9203 low.x + (high.x - low.x) * t,
9204 low.y + (high.y - low.y) * t,
9205 low.z + (high.z - low.z) * t,
9206 low.w + (high.w - low.w) * t,
9207 )
9208 }).collect()
9209 }
9210
9211 pub fn from_agent_positions(agents: &[AiAgent], width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
9212 let mut hmap = HeatMapData::new(width, height, cell_size, origin, "Agent Positions");
9213 for agent in agents {
9214 let pos_2d = Vec2::new(agent.position.x, agent.position.z);
9215 hmap.add_point(pos_2d, 1.0, cell_size * 2.0);
9216 }
9217 hmap.normalize();
9218 hmap
9219 }
9220
9221 pub fn from_threat_data(threat_assessors: &[(Vec3, f32)], width: usize, height: usize, cell_size: f32, origin: Vec2) -> Self {
9222 let mut hmap = HeatMapData::new(width, height, cell_size, origin, "Threat");
9223 for &(pos, threat) in threat_assessors {
9224 hmap.add_point(Vec2::new(pos.x, pos.z), threat, cell_size * 3.0);
9225 }
9226 hmap.normalize();
9227 hmap
9228 }
9229}
9230
9231pub struct EditorSearch {
9236 pub query: String,
9237 pub results: Vec<SearchResult>,
9238 pub selected_result: Option<usize>,
9239 pub search_bt_nodes: bool,
9240 pub search_blackboard: bool,
9241 pub search_goap_actions: bool,
9242 pub search_fsm_states: bool,
9243}
9244
9245#[derive(Clone, Debug)]
9246pub struct SearchResult {
9247 pub label: String,
9248 pub category: String,
9249 pub location: SearchLocation,
9250 pub relevance: f32,
9251}
9252
9253#[derive(Clone, Debug)]
9254pub enum SearchLocation {
9255 BtNode { tree_idx: usize, node_id: u32 },
9256 FsmState { fsm_idx: usize, state_id: u32 },
9257 GoapAction { action_id: u32 },
9258 BlackboardKey { key: String },
9259 UtilityAction { action_id: u32 },
9260}
9261
9262impl EditorSearch {
9263 pub fn new() -> Self {
9264 Self { query: String::new(), results: Vec::new(), selected_result: None, search_bt_nodes: true, search_blackboard: true, search_goap_actions: true, search_fsm_states: true }
9265 }
9266
9267 pub fn search(&mut self, editor: &AiBehaviorEditor) {
9268 self.results.clear();
9269 if self.query.is_empty() { return; }
9270 let q = self.query.to_lowercase();
9271
9272 if self.search_bt_nodes {
9273 for (tree_idx, tree) in editor.behavior_trees.iter().enumerate() {
9274 for (node_id, node) in &tree.nodes {
9275 let name = node.display_name().to_lowercase();
9276 if name.contains(&q) {
9277 let relevance = if name == q { 1.0 } else if name.starts_with(&q) { 0.8 } else { 0.5 };
9278 self.results.push(SearchResult {
9279 label: format!("{} [{}]", node.display_name(), node_id),
9280 category: format!("BT: {}", tree.name),
9281 location: SearchLocation::BtNode { tree_idx, node_id: *node_id },
9282 relevance,
9283 });
9284 }
9285 }
9286 }
9287 }
9288
9289 if self.search_fsm_states {
9290 for (fsm_idx, fsm) in editor.fsm_instances.iter().enumerate() {
9291 for (state_id, state) in &fsm.states {
9292 if state.name.to_lowercase().contains(&q) {
9293 self.results.push(SearchResult {
9294 label: state.name.clone(),
9295 category: format!("FSM: {}", fsm.name),
9296 location: SearchLocation::FsmState { fsm_idx, state_id: *state_id },
9297 relevance: 0.7,
9298 });
9299 }
9300 }
9301 }
9302 }
9303
9304 if self.search_goap_actions {
9305 for action in &editor.goap_planner.actions {
9306 if action.name.to_lowercase().contains(&q) {
9307 self.results.push(SearchResult {
9308 label: action.name.clone(),
9309 category: "GOAP Action".to_string(),
9310 location: SearchLocation::GoapAction { action_id: action.id },
9311 relevance: 0.6,
9312 });
9313 }
9314 }
9315 }
9316
9317 if self.search_blackboard {
9318 for key in editor.shared_blackboard.entries.keys() {
9319 if key.to_lowercase().contains(&q) {
9320 self.results.push(SearchResult {
9321 label: key.clone(),
9322 category: "Blackboard".to_string(),
9323 location: SearchLocation::BlackboardKey { key: key.clone() },
9324 relevance: 0.5,
9325 });
9326 }
9327 }
9328 }
9329
9330 self.results.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap());
9331 self.results.truncate(50);
9332 self.selected_result = if self.results.is_empty() { None } else { Some(0) };
9333 }
9334
9335 pub fn select_next(&mut self) {
9336 if let Some(idx) = self.selected_result {
9337 self.selected_result = Some((idx + 1) % self.results.len().max(1));
9338 }
9339 }
9340
9341 pub fn select_prev(&mut self) {
9342 if let Some(idx) = self.selected_result {
9343 self.selected_result = Some(if idx == 0 { self.results.len().saturating_sub(1) } else { idx - 1 });
9344 }
9345 }
9346}
9347
9348pub const AI_EDITOR_VERSION: &str = "1.0.0";
9353pub const AI_EDITOR_MAX_AGENTS: usize = 1024;
9354pub const AI_EDITOR_MAX_TREES: usize = 256;
9355pub const AI_EDITOR_MAX_FSMS: usize = 128;
9356pub const AI_EDITOR_MAX_GOAP_ACTIONS: usize = 64;
9357pub const AI_EDITOR_MAX_UTILITY_ACTIONS: usize = 32;
9358
9359pub struct AiEditorCapabilities {
9360 pub supports_bt: bool,
9361 pub supports_goap: bool,
9362 pub supports_utility: bool,
9363 pub supports_fsm: bool,
9364 pub supports_htn: bool,
9365 pub supports_fuzzy: bool,
9366 pub supports_perception: bool,
9367 pub supports_steering: bool,
9368 pub supports_emotions: bool,
9369 pub supports_formations: bool,
9370 pub supports_cover: bool,
9371 pub supports_influence_maps: bool,
9372 pub supports_navmesh: bool,
9373 pub supports_replay: bool,
9374 pub supports_dda: bool,
9375 pub supports_dialog: bool,
9376 pub max_agents: usize,
9377 pub max_bt_nodes_per_tree: usize,
9378 pub max_fsm_states: usize,
9379}
9380
9381impl Default for AiEditorCapabilities {
9382 fn default() -> Self {
9383 Self {
9384 supports_bt: true,
9385 supports_goap: true,
9386 supports_utility: true,
9387 supports_fsm: true,
9388 supports_htn: true,
9389 supports_fuzzy: true,
9390 supports_perception: true,
9391 supports_steering: true,
9392 supports_emotions: true,
9393 supports_formations: true,
9394 supports_cover: true,
9395 supports_influence_maps: true,
9396 supports_navmesh: true,
9397 supports_replay: true,
9398 supports_dda: true,
9399 supports_dialog: true,
9400 max_agents: AI_EDITOR_MAX_AGENTS,
9401 max_bt_nodes_per_tree: 512,
9402 max_fsm_states: AI_EDITOR_MAX_FSMS,
9403 }
9404 }
9405}
9406
9407pub fn get_capabilities() -> AiEditorCapabilities { AiEditorCapabilities::default() }
9408
9409pub fn ai_editor_info() -> String {
9410 format!(
9411 "AI Behavior Editor v{}\nCapabilities: BT={}, GOAP={}, Utility={}, FSM={}, HTN={}, Fuzzy={}\nPerception, Steering(18 types), Emotions(Plutchik), Formations(10), Cover, InfluenceMaps, NavMesh, Replay, DDA, Dialog\nMax Agents: {}",
9412 AI_EDITOR_VERSION, true, true, true, true, true, true, AI_EDITOR_MAX_AGENTS
9413 )
9414}