Skip to main content

proof_engine/editor/
ai_behavior_editor.rs

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
6// ============================================================
7// CONSTANTS
8// ============================================================
9
10const 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// ============================================================
48// BLACKBOARD
49// ============================================================
50
51#[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// ============================================================
159// BEHAVIOR TREE — NODE STATUS
160// ============================================================
161
162#[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// ============================================================
177// BEHAVIOR TREE — NODE TYPES
178// ============================================================
179
180#[derive(Clone, Debug)]
181pub enum BtNodeType {
182    // Composite
183    Sequence,
184    Selector,
185    ParallelAll,       // succeed when ALL children succeed
186    ParallelAny,       // succeed when ANY child succeeds
187    RandomSelector,
188    RandomSequence,
189    // Decorators
190    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    // Leaf — action
203    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// ============================================================
258// BEHAVIOR TREE — NODE
259// ============================================================
260
261#[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    // Runtime state
269    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    // Layout
275    pub position: Vec2,
276    pub size: Vec2,
277    pub is_selected: bool,
278    pub is_collapsed: bool,
279    // Reingold-Tilford
280    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// ============================================================
413// BEHAVIOR TREE — TICK CONTEXT
414// ============================================================
415
416#[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        // LCG random
446        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
458// ============================================================
459// BEHAVIOR TREE — EXECUTOR
460// ============================================================
461
462pub 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        // Clone node data needed for dispatch
522        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            // ---- COMPOSITES ----
542            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                // Fisher-Yates shuffle index list using ctx rng
647                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            // ---- DECORATORS ----
686            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            // ---- LEAF NODES ----
837            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                    // Apply damage in blackboard
938                    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                // In a real system this would query a spatial index; here we check blackboard
961                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                // Waypoints stored as concatenated Vec3 in blackboard as array index
997                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                // Simple: find a point perpendicular to threat direction
1026                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
1137// ============================================================
1138// REINGOLD-TILFORD LAYOUT ALGORITHM
1139// ============================================================
1140
1141pub 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            // First pass
1153            let depth = 0;
1154            let siblings_count = 1;
1155            self.first_walk(tree, root_id, 0, 0);
1156            // Second pass
1157            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            // Leaf node
1169            let prelim = if sibling_index == 0 {
1170                0.0
1171            } else {
1172                // Find previous sibling
1173                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            // Internal node — recurse first
1194            for (i, &child_id) in children.iter().enumerate() {
1195                self.first_walk(tree, child_id, i, depth + 1);
1196            }
1197
1198            // Apportion
1199            let children2 = tree.nodes.get(&node_id).map(|n| n.children.clone()).unwrap_or_default();
1200            self.apportion(tree, node_id);
1201
1202            // Place node between first and last child
1203            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                // Shift right subtree
1248                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
1281// ============================================================
1282// GOAP — WORLD STATE
1283// ============================================================
1284
1285pub type WorldState = u64; // bitmask of facts
1286
1287#[derive(Clone, Debug)]
1288pub struct GoapAction {
1289    pub id: u32,
1290    pub name: String,
1291    pub preconditions: WorldState,   // required bits set
1292    pub preconditions_false: WorldState, // required bits clear
1293    pub effects_set: WorldState,     // bits to set
1294    pub effects_clear: WorldState,   // bits to clear
1295    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// ============================================================
1330// GOAP — A* PLANNER
1331// ============================================================
1332
1333#[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    /// Heuristic: count number of goal bits not yet set
1368    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        // A* search over world states
1380        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            // Find lowest f
1397            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            // Check if goal satisfied
1406            if (current.world_state & goal) == goal {
1407                // Reconstruct path
1408                let mut plan: Vec<usize> = Vec::new();
1409                let mut node = &closed[closed.len() - 1]; // will push current first
1410                // Push current to closed temporarily to allow backtrack
1411                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            // Expand
1433            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                // Check if already in closed
1437                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                // Check if already in open with lower cost
1444                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// ============================================================
1481// UTILITY AI — RESPONSE CURVES
1482// ============================================================
1483
1484#[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                // Evaluate polynomial using Horner's method
1520                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// ============================================================
1555// UTILITY AI — CONSIDERATION
1556// ============================================================
1557
1558#[derive(Clone, Debug)]
1559pub struct Consideration {
1560    pub name: String,
1561    pub input_key: String,       // blackboard key
1562    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// ============================================================
1593// UTILITY AI — UTILITY ACTION
1594// ============================================================
1595
1596#[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,     // inertia factor to prevent thrashing
1605    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        // Geometric mean of all considerations (avoids all-or-nothing bias)
1630        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        // Compensation factor: geometric mean normalization
1637        let avg = product.powf(1.0 / n);
1638        // Modification factor makes the score approach arithmetic mean as considerations grow
1639        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
1645// ============================================================
1646// UTILITY AI — DECISION MAKER
1647// ============================================================
1648
1649pub struct UtilityDecisionMaker {
1650    pub actions: Vec<UtilityAction>,
1651    pub selected_action_id: Option<u32>,
1652    pub selection_history: VecDeque<(u32, f32)>,   // (action_id, time)
1653    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        // Update active states
1692        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// ============================================================
1713// PERCEPTION SYSTEM
1714// ============================================================
1715
1716#[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,    // 0..1
1724    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        // Predict position based on last known velocity
1757        self.last_known_position = self.last_known_position + self.velocity * dt;
1758        // Slow velocity decay (entity might stop)
1759        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,         // radians
1767    pub near_range: f32,         // always sees within this range regardless of angle
1768    pub darkness_penalty: f32,   // 0 = full dark, 1 = full light
1769    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,    // 120-degree FOV
1777            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,   // Hz filtering analog
1788    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    /// Vision cone check with distance falloff
1844    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        // Near range always visible
1858        if dist <= self.vision.near_range { return (true, 1.0); }
1859
1860        if dist > self.vision.range { return (false, 0.0); }
1861
1862        // Angle check
1863        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        // Distance falloff: linear from range_start to range
1871        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        // Angle falloff: cos falloff from center to edge of cone
1876        let angle_factor = 1.0 - (angle / self.vision.half_angle);
1877
1878        // Moving target bonus
1879        let vel_factor = 1.0 + (target_velocity.length().min(5.0) / 5.0) * self.vision.moving_target_bonus;
1880
1881        // Darkness penalty
1882        let light_factor = self.vision.darkness_penalty;
1883
1884        // Occlusion: raycast against obstacles
1885        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    /// Simple AABB ray intersection for occlusion
1893    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    /// Hearing: sound attenuation model
1908    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        // Inverse square law with min attenuation
1913        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    /// Smell: wind-adjusted radius
1925    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        // Wind direction shifts the detectable range
1931        let wind_dot = self.smell.wind_direction.normalize_or_zero().dot(to_source / dist);
1932        // Downwind multiplier: 1.5x range downwind, 0.5x range upwind
1933        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        // Smell intensity: exponential falloff
1939        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)],  // (id, pos, vel, sound_int, smell_int, threat)
1949        dt: f32,
1950        current_time: f32,
1951        obstacles: &[Aabb],
1952    ) {
1953        // Decay existing perceptions
1954        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        // Check new candidates
1966        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// ============================================================
2008// AABB (used by perception and steering)
2009// ============================================================
2010
2011#[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// ============================================================
2043// SQUAD FORMATIONS
2044// ============================================================
2045
2046#[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    /// Returns world-space slot positions for N agents given leader pos/forward
2064    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                // Horizontal line perpendicular to forward
2078                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                // Single file behind leader
2086                for i in 0..n_agents {
2087                    slots.push(leader_pos - fwd * (i as f32 * spacing));
2088                }
2089            }
2090            FormationType::Wedge => {
2091                // V-shape with leader at front
2092                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                // Rectangular grid, roughly square
2129                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                // Leader front
2170                slots.push(leader_pos + fwd * spacing);
2171                // Left & right
2172                if n_agents > 1 { slots.push(leader_pos - right * spacing); }
2173                if n_agents > 2 { slots.push(leader_pos + right * spacing); }
2174                // Rear
2175                if n_agents > 3 { slots.push(leader_pos - fwd * spacing); }
2176                // Fill remaining
2177                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        // Ensure we have exactly n_agents slots
2188        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    /// Optimal slot assignment using min-cost bipartite matching (Hungarian greedy approximation)
2197    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// ============================================================
2222// STEERING BEHAVIORS
2223// ============================================================
2224
2225#[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    // ---- 1. SEEK ----
2277    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    // ---- 2. FLEE ----
2283    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    // ---- 3. ARRIVE ----
2289    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        // Slow down as we approach
2294        let speed = (dist / deceleration).min(agent.max_speed);
2295        let desired = (to_target / dist) * speed;
2296        desired - agent.velocity
2297    }
2298
2299    // ---- 4. PURSUE ----
2300    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        // Prediction time: distance / (own_speed + target_speed) approximately
2305        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    // ---- 5. EVADE ----
2314    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    // ---- 6. WANDER ----
2323    pub fn wander(agent: &mut SteeringAgent, rng_seed: &mut u64, dt: f32) -> Vec3 {
2324        // Move wander angle randomly
2325        *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        // Wander circle center is ahead of agent
2330        let circle_center = agent.position + agent.heading * WANDER_CIRCLE_DISTANCE;
2331        // Displacement on the circle
2332        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    // ---- 7. OBSTACLE AVOIDANCE ----
2342    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);  // approximate radius
2354
2355            // Check if ray from position to ahead intersects obstacle sphere
2356            let to_center = center - agent.position;
2357            let proj = to_center.dot(agent.heading);
2358            if proj < 0.0 { continue; }  // behind agent
2359
2360            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            // Steer away
2374            let avoid_dir = (ahead - center).normalize_or_zero();
2375            avoid_dir * agent.max_force
2376        } else {
2377            Vec3::ZERO
2378        }
2379    }
2380
2381    // ---- 8. WALL FOLLOWING ----
2382    pub fn wall_following(agent: &SteeringAgent, walls: &[(Vec3, Vec3)]) -> Vec3 {
2383        // walls: list of (point_on_wall, wall_normal)
2384        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                // Push agent along the wall (perpendicular to normal)
2392                let along_wall = Vec3::new(-wall_normal.z, 0.0, wall_normal.x);
2393                // Desired velocity: along wall direction + slight push away
2394                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    // ---- 9. PATH FOLLOWING ----
2403    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    // ---- 10. FLOW FIELD FOLLOWING ----
2415    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    // ---- 11. ALIGNMENT ----
2431    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    // ---- 12. COHESION ----
2446    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    // ---- 13. SEPARATION ----
2461    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                // Weighted by inverse distance
2470                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    // ---- 14. LEADER FOLLOWING ----
2483    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        // Evade if ahead of leader (in way)
2497        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            // Get out of the way
2501            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    // ---- 15. QUEUE BEHAVIOR ----
2508    pub fn queue_behavior(
2509        agent: &SteeringAgent,
2510        neighbors: &[&SteeringAgent],
2511        target: Vec3,
2512    ) -> Vec3 {
2513        // Move toward target but slow down if neighbor ahead is too close
2514        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                // Too close — brake
2531                return -agent.velocity;
2532            }
2533        }
2534        Self::arrive(agent, target, ARRIVE_DECELERATION_RADIUS)
2535    }
2536
2537    // ---- 16. COLLISION AVOIDANCE ----
2538    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            // Time to closest approach
2548            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    // ---- 17. HIDE ----
2570    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            // Hiding spot is on the far side of obstacle from threat
2581            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    // ---- 18. INTERPOSE ----
2595    pub fn interpose(
2596        agent: &SteeringAgent,
2597        agent_a: &SteeringAgent,
2598        agent_b: &SteeringAgent,
2599    ) -> Vec3 {
2600        // Get future midpoint between A and B
2601        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    /// Weighted sum of all applicable behaviors
2610    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        // Clamp total force
2673        if total.length() > agent.max_force {
2674            total = total.normalize() * agent.max_force;
2675        }
2676        total
2677    }
2678}
2679
2680// ============================================================
2681// FSM — FINITE STATE MACHINE EDITOR
2682// ============================================================
2683
2684#[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                // In real engine: log to console
2750                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,  // editor layout
2770    pub is_initial: bool,
2771    pub is_final: bool,
2772    pub color: Vec4,
2773    pub sub_fsm: Option<u32>,  // sub FSM id for hierarchical FSMs
2774}
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)>,   // (from, to, time)
2802    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        // Execute tick actions
2871        if let Some(state) = self.states.get(&current) {
2872            let tick_actions: Vec<FsmAction> = state.tick_actions.clone();
2873            for action in &tick_actions { action.execute(blackboard); }
2874        }
2875
2876        // Check transitions sorted by priority
2877        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    /// Auto-layout states using circular layout
2894    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// ============================================================
2908// EMOTION SYSTEM — PLUTCHIK WHEEL
2909// ============================================================
2910
2911#[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,       // Joy + Trust
2926    Submission, // Trust + Fear
2927    Awe,        // Fear + Surprise
2928    Disapproval,// Surprise + Sadness
2929    Remorse,    // Sadness + Disgust
2930    Contempt,   // Disgust + Anger
2931    Aggressiveness, // Anger + Anticipation
2932    Optimism,   // Anticipation + Joy
2933}
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    /// Opposite emotion on the wheel (180 degrees)
2961    pub fn opposite(&self) -> PrimaryEmotion {
2962        PrimaryEmotion::ALL[(self.index() + 4) % 8]
2963    }
2964
2965    /// Wheel position as Vec2 (unit circle, 8 segments)
2966    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    /// Get the secondary emotion formed with the next emotion
2972    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],   // One per primary emotion
2989    pub secondary_intensities: [f32; 8],
2990    pub mood_valence: f32,       // -1 negative .. +1 positive
2991    pub mood_arousal: f32,       // 0 calm .. 1 excited
2992    pub decay_rates: [f32; 8],
2993    pub threshold: f32,          // Below this: negligible
2994}
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        // Suppress opposite emotion
3012        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        // Decay emotions
3030        for i in 0..8 {
3031            self.intensities[i] = (self.intensities[i] - self.decay_rates[i] * dt).max(0.0);
3032        }
3033
3034        // Compute secondary emotions
3035        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        // Compute mood valence: positive = joy, trust, anticipation; negative = fear, sadness, disgust, anger
3041        let positive = self.intensities[0] + self.intensities[1] + self.intensities[7]; // joy, trust, anticipation
3042        let negative = self.intensities[2] + self.intensities[4] + self.intensities[5] + self.intensities[6]; // fear, sadness, disgust, anger
3043        let total = positive + negative;
3044        if total > EPSILON {
3045            self.mood_valence = (positive - negative) / total;
3046        }
3047
3048        // Arousal: surprise and fear drive high arousal; sadness drives low
3049        let high_arousal = self.intensities[2] + self.intensities[3] + self.intensities[6]; // fear, surprise, anger
3050        let low_arousal = self.intensities[4];  // sadness
3051        self.mood_arousal = ((high_arousal - low_arousal * 0.5) / (8.0f32.sqrt())).clamp(0.0, 1.0);
3052    }
3053
3054    /// Map emotion state to behavior parameter modifiers
3055    pub fn behavior_modifiers(&self) -> EmotionBehaviorModifiers {
3056        EmotionBehaviorModifiers {
3057            speed_multiplier: 1.0 + self.intensities[6] * 0.3      // anger increases speed
3058                - self.intensities[4] * 0.2                         // sadness decreases
3059                + self.intensities[7] * 0.15,                       // anticipation
3060            aggression_bias: self.intensities[6] * 0.5 + self.intensities[3] * 0.2, // anger + surprise
3061            flee_threshold_modifier: self.intensities[2] * 0.4,    // fear: flee sooner
3062            search_radius_multiplier: 1.0 + self.intensities[7] * 0.3, // anticipation: search wider
3063            reaction_time_modifier: -self.intensities[2] * 0.2     // fear: faster reaction
3064                + self.intensities[4] * 0.3,                        // sadness: slower
3065            accuracy_modifier: 1.0 - self.intensities[2] * 0.15    // fear reduces accuracy
3066                - self.intensities[3] * 0.1,                        // surprise
3067            cooperation_bias: self.intensities[1] * 0.4            // trust improves cooperation
3068                - self.intensities[5] * 0.3,                        // disgust reduces
3069            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// Emotional stimulus
3118#[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])>,  // (time, intensities)
3130    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        // Process stimuli
3149        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        // Record history
3159        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    /// Compute emotional contagion: spread emotion from nearby agents
3170    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// ============================================================
3187// NODE GRAPH EDITOR — UI STATE
3188// ============================================================
3189
3190#[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        // Adjust pan so zoom centers on cursor
3223        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),   // top-left, bottom-right
3252    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// ============================================================
3309// BLACKBOARD INSPECTOR STATE
3310// ============================================================
3311
3312#[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        // Pinned keys first
3369        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// ============================================================
3408// DEBUG VISUALIZATION DATA
3409// ============================================================
3410
3411#[derive(Clone, Debug)]
3412pub struct DebugVizShape {
3413    pub shape_type: DebugShapeType,
3414    pub color: Vec4,
3415    pub duration: f32,       // seconds; 0 = one frame
3416    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        // Draw arc at range
3455        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        // Left edge
3470        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        // Right edge
3473        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
3520// Color utility
3521fn 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
3539// ============================================================
3540// BEHAVIOR TREE LIBRARY — PRE-BUILT TEMPLATES
3541// ============================================================
3542
3543pub struct BtTemplates;
3544
3545impl BtTemplates {
3546    /// Simple patrol + attack tree
3547    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        // Branch 1: Combat
3554        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        // Branch 2: Investigate sound
3588        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        // Branch 3: Patrol
3611        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    /// Flee and take cover tree
3621    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    /// Gather resources tree
3657    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        // Have inventory full?
3663        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        // Gather resource
3684        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        // Wander
3709        let idle = tree.add_node(BtNodeType::Wait { duration: 1.0 });
3710        tree.add_child(root, idle);
3711
3712        tree
3713    }
3714}
3715
3716// ============================================================
3717// GOAP ACTION LIBRARY
3718// ============================================================
3719
3720pub struct GoapLibrary;
3721
3722impl GoapLibrary {
3723    /// Combat agent GOAP
3724    pub fn build_combat_planner() -> GoapPlanner {
3725        let mut planner = GoapPlanner::new();
3726        // World state bits
3727        // 0: has_ammo
3728        // 1: enemy_visible
3729        // 2: enemy_dead
3730        // 3: in_cover
3731        // 4: health_low
3732        // 5: has_medpack
3733        // 6: enemy_alerted
3734
3735        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        // Action: Shoot enemy
3744        let mut shoot = GoapAction::new(1, "Shoot");
3745        shoot.preconditions = (1 << 0) | (1 << 1); // has_ammo + enemy_visible
3746        shoot.effects_clear = 1 << 1;               // clear enemy_visible (they die or flee)
3747        shoot.effects_set = 1 << 2;                 // enemy_dead (optimistic)
3748        shoot.cost = 1.0;
3749        planner.add_action(shoot);
3750
3751        // Action: Find cover
3752        let mut find_cover = GoapAction::new(2, "FindCover");
3753        find_cover.preconditions = 1 << 1;           // enemy_visible
3754        find_cover.effects_set = 1 << 3;             // in_cover
3755        find_cover.cost = 2.0;
3756        planner.add_action(find_cover);
3757
3758        // Action: Use medpack
3759        let mut heal = GoapAction::new(3, "Heal");
3760        heal.preconditions = (1 << 4) | (1 << 5);   // health_low + has_medpack
3761        heal.effects_clear = (1 << 4) | (1 << 5);   // clear both
3762        heal.cost = 1.0;
3763        planner.add_action(heal);
3764
3765        // Action: Reload
3766        let mut reload = GoapAction::new(4, "Reload");
3767        reload.preconditions_false = 1 << 0;         // doesn't have ammo
3768        reload.effects_set = 1 << 0;                 // has ammo
3769        reload.cost = 1.5;
3770        planner.add_action(reload);
3771
3772        // Action: Patrol
3773        let mut patrol = GoapAction::new(5, "Patrol");
3774        patrol.preconditions = 0;
3775        patrol.effects_set = 1 << 1;                 // might spot enemy
3776        patrol.cost = 3.0;
3777        planner.add_action(patrol);
3778
3779        // Action: Alert allies
3780        let mut alert = GoapAction::new(6, "AlertAllies");
3781        alert.preconditions = 1 << 1;               // enemy_visible
3782        alert.effects_set = 1 << 6;                  // enemy_alerted
3783        alert.cost = 0.5;
3784        planner.add_action(alert);
3785
3786        // Action: Melee attack
3787        let mut melee = GoapAction::new(7, "Melee");
3788        melee.preconditions = 1 << 1;               // enemy visible
3789        melee.preconditions_false = 1 << 0;         // no ammo
3790        melee.effects_set = 1 << 2;                  // enemy dead
3791        melee.effects_clear = 1 << 1;
3792        melee.cost = 1.5;
3793        planner.add_action(melee);
3794
3795        planner
3796    }
3797}
3798
3799// ============================================================
3800// UTILITY AI LIBRARY
3801// ============================================================
3802
3803pub struct UtilityLibrary;
3804
3805impl UtilityLibrary {
3806    pub fn build_combat_decision_maker() -> UtilityDecisionMaker {
3807        let mut dm = UtilityDecisionMaker::new();
3808
3809        // Attack action
3810        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        // Flee action
3838        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        // Heal action
3858        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        // Patrol action
3878        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        // Reload
3891        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// ============================================================
3915// AI AGENT — TOP-LEVEL INTEGRATION
3916// ============================================================
3917
3918#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3919pub enum AiAgentMode {
3920    BehaviorTree,
3921    UtilityAi,
3922    Goap,
3923    Fsm,
3924    Hybrid,  // BT orchestrates, Utility selects leaves
3925}
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        // Update emotion engine
3987        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                // Run utility AI to determine high-level goal, then BT executes
4020                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        // Update steering
4034        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        // Re-plan if needed
4042        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                    // Execute action (tick for dt — simplified: complete in one tick)
4054                    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
4063// ============================================================
4064// FULL AI BEHAVIOR EDITOR STRUCT
4065// ============================================================
4066
4067pub struct AiBehaviorEditor {
4068    // Behavior Tree editor
4069    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    // FSM editor
4082    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    // GOAP editor
4090    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    // Utility AI editor
4098    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)>,  // (action_id, consideration_idx)
4102    pub utility_curve_preview_points: Vec<Vec2>,
4103
4104    // Perception inspector
4105    pub perception_systems: Vec<PerceptionSystem>,
4106    pub selected_perception_agent: Option<u64>,
4107    pub perception_debug_draw: bool,
4108
4109    // Formation editor
4110    pub formation_preview: FormationType,
4111    pub formation_n_agents: usize,
4112    pub formation_spacing: f32,
4113    pub formation_preview_slots: Vec<Vec3>,
4114
4115    // Steering editor
4116    pub steering_agents: Vec<SteeringAgent>,
4117    pub steering_debug_draw: bool,
4118    pub steering_selected_agent: Option<u64>,
4119
4120    // Emotion editor
4121    pub emotion_engines: Vec<EmotionEngine>,
4122    pub selected_emotion_agent: usize,
4123    pub emotion_debug_draw: bool,
4124
4125    // Blackboard inspector
4126    pub blackboard_inspector: BlackboardInspector,
4127    pub shared_blackboard: Blackboard,
4128
4129    // Debug visualization
4130    pub debug_buffer: DebugVisualizationBuffer,
4131    pub show_debug_panel: bool,
4132
4133    // Agents (live sim)
4134    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    // Editor global state
4142    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// Undo / redo for BT editor
4155#[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,   // has_ammo
4189            goap_goal_state:  0b0000_0100,   // enemy_dead
4190            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        // Add default trees
4243        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        // Add default FSM
4248        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        // Layout trees
4276        for tree in &mut editor.behavior_trees {
4277            editor.bt_layout.layout(tree);
4278        }
4279
4280        // Spawn a few demo agents
4281        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            // Set patrol waypoints
4288            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        // Add some obstacles
4303        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        // Compute initial formation preview
4307        editor.recompute_formation_preview();
4308
4309        // Run initial GOAP plan
4310        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    // ---- BT EDITOR OPERATIONS ----
4362
4363    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            // Remove from parent
4379            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            // Orphan children
4385            for child_id in &node.children {
4386                if let Some(child) = tree.nodes.get_mut(child_id) {
4387                    child.parent = None;
4388                }
4389            }
4390            // Reset root if needed
4391            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        // Remove from old parent if any
4399        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        // Re-run layout
4410        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    // ---- FSM EDITOR OPERATIONS ----
4564
4565    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    // ---- GOAP EDITOR OPERATIONS ----
4607
4608    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    // ---- UTILITY AI EDITOR ----
4670
4671    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    // ---- FORMATION EDITOR ----
4703
4704    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    // ---- PERCEPTION EDITOR ----
4729
4730    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    // ---- STEERING EDITOR ----
4747
4748    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,        // no seek target for demo
4772                None,
4773                None,
4774                None,
4775                None,
4776                true,        // wander
4777                &mut rng_seed,
4778                dt,
4779                &obstacles_clone,
4780                &[],
4781                &neighbors,
4782                None,
4783                None,
4784                None,
4785                None,
4786                None,
4787            );
4788            // Also avoid obstacles
4789            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    // ---- EMOTION EDITOR ----
4800
4801    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    // ---- SIMULATION ----
4829
4830    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        // Update steering agents
4842        self.tick_steering_agents(effective_dt);
4843
4844        // Update emotion engines
4845        for engine in &mut self.emotion_engines {
4846            engine.update(effective_dt, self.current_time);
4847        }
4848
4849        // Update debug buffer
4850        self.debug_buffer.tick(effective_dt);
4851
4852        // Draw debug for agents
4853        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        // Decay status message
4866        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    // ---- BLACKBOARD INSPECTOR ----
4904
4905    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                // Diff blackboard for changes
4909                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    // ---- FLOW FIELD GENERATION ----
4931
4932    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    // ---- EDITOR THEMING ----
4981
4982    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    // ---- SERIALIZATION HELPERS ----
4995
4996    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    // ---- GRID UTILITIES ----
5030
5031    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    // ---- STATISTICS / METRICS ----
5062
5063    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    // ---- KEYBOARD SHORTCUTS ----
5109
5110    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// ============================================================
5157// PATHFINDING — A* on a grid (used by agents for navigation)
5158// ============================================================
5159
5160#[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    /// A* search; returns world-space waypoints
5211    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  // tie-breaking
5225        };
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            // Find lowest f in open
5239            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(&current_key)?;
5243
5244            if current.x == ex && current.y == ey {
5245                // Reconstruct
5246                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
5304// ============================================================
5305// ADDITIONAL MATH UTILITIES
5306// ============================================================
5307
5308pub 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
5404// ============================================================
5405// SQUAD INTELLIGENCE — TACTICAL AI
5406// ============================================================
5407
5408pub 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            // High threat: attack highest priority target
5480            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            // Moderate: defend current position or flank
5490            // Keep current objective
5491        } else {
5492            // Low threat: patrol
5493            if !matches!(self.objective, SquadObjective::Patrol { .. }) {
5494                // Transition to patrol — keep existing waypoints
5495            }
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        // Update patrol waypoint
5505        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
5531// ============================================================
5532// BEHAVIOR TREE DEBUGGER
5533// ============================================================
5534
5535pub 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
5628// ============================================================
5629// NOISE UTILS (for procedural behavior variation)
5630// ============================================================
5631
5632pub 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
5701// ============================================================
5702// BEHAVIOR MODULATION SYSTEM
5703// ============================================================
5704
5705pub 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// ============================================================
5774// MEMORY SYSTEM (longer-term agent memory beyond blackboard)
5775// ============================================================
5776
5777#[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            // Forget least important
5825            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            // Accessing refreshes importance slightly
5838            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
5865// ============================================================
5866// WORLD STATE TRACKER (global shared state for GOAP + agents)
5867// ============================================================
5868
5869pub 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        // Check listeners
5899        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// ============================================================
5929// SOCIAL BEHAVIOR GRAPH
5930// ============================================================
5931
5932#[derive(Clone, Debug)]
5933pub struct SocialRelationship {
5934    pub other_id: u64,
5935    pub affinity: f32,      // -1..1
5936    pub trust: f32,         // 0..1
5937    pub fear: f32,          // 0..1
5938    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>,  // faction_id -> affinity
5973}
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// ============================================================
6008// ANIMATION STATE MACHINE (simple, for AI locomotion)
6009// ============================================================
6010
6011#[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),  // angle
6022    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        // Update blend weights
6075        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
6090// ============================================================
6091// AI EDITOR TEST SUITE
6092// ============================================================
6093
6094pub 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);  // Should succeed (first call, cooldown 0)
6173        let s2 = tree.tick(&mut ctx);  // Should fail (on cooldown)
6174        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        // After 3 completions returns Success
6189        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        // Start: has_ammo=1, enemy_visible=1
6208        // Goal: enemy_dead=1
6209        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);  // directly ahead
6231        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
6271// ============================================================
6272// MODULE REGISTRATION / ENTRY POINT
6273// ============================================================
6274
6275pub 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// ============================================================
6286// COVER SYSTEM
6287// ============================================================
6288
6289#[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// ============================================================
6390// THREAT ASSESSMENT
6391// ============================================================
6392
6393#[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// ============================================================
6463// DECISION TREE
6464// ============================================================
6465
6466#[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// ============================================================
6533// FUZZY LOGIC
6534// ============================================================
6535
6536#[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// ============================================================
6672// HTN PLANNER
6673// ============================================================
6674
6675#[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
6764// ============================================================
6765// BEHAVIOR TREE SERIALIZER
6766// ============================================================
6767
6768pub 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// ============================================================
6790// AI EVENT BUS
6791// ============================================================
6792
6793#[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
6834// ============================================================
6835// SPATIAL GRID
6836// ============================================================
6837
6838pub 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// ============================================================
6897// NAV MESH (simplified)
6898// ============================================================
6899
6900#[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(); // id -> (g, h, parent)
6975        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 &region.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// ============================================================
7012// AI LOD MANAGER
7013// ============================================================
7014
7015#[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
7070// ============================================================
7071// PERFORMANCE MONITOR
7072// ============================================================
7073
7074pub 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// ============================================================
7103// CAMERA DIRECTOR AI
7104// ============================================================
7105
7106#[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// ============================================================
7163// SENSOR FUSION
7164// ============================================================
7165
7166#[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// ============================================================
7230// EDITOR WINDOW LAYOUT
7231// ============================================================
7232
7233#[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
7244// ============================================================
7245// FULL AI SYSTEM INTEGRATOR
7246// ============================================================
7247
7248pub 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// ============================================================
7450// ANIMATION BLEND TREE
7451// ============================================================
7452
7453#[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// ============================================================
7516// DIALOG GRAPH
7517// ============================================================
7518
7519#[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(&current) {
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(&current)?;
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(&current) {
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// ============================================================
7629// INTENT RECOGNIZER (companion AI)
7630// ============================================================
7631
7632#[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
7695// ============================================================
7696// ADDITIONAL BT ANALYSIS
7697// ============================================================
7698
7699pub 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        // BTs should be DAGs; detect any cycles
7718        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        // Check root exists
7766        if tree.root_id.is_none() { errors.push("No root node".to_string()); }
7767        // Check no orphans
7768        let unreachable = Self::find_unreachable_nodes(tree);
7769        if !unreachable.is_empty() { errors.push(format!("{} unreachable nodes: {:?}", unreachable.len(), unreachable)); }
7770        // Check decorators have exactly one child
7771        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        // Check cycles
7777        let cycles = Self::find_cycles(tree);
7778        for cycle in cycles { errors.push(format!("Cycle detected: {:?}", cycle)); }
7779        errors
7780    }
7781}
7782
7783// ============================================================
7784// EDITOR UNDO/REDO SYSTEM (Extended)
7785// ============================================================
7786
7787pub 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
7842// ============================================================
7843// BEHAVIOR TREE COPY/PASTE BUFFER
7844// ============================================================
7845
7846pub 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        // Remap old ids to new ids
7875        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        // Insert nodes with new ids
7883        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
7902// ============================================================
7903// TERRAIN QUERY (for AI navigation decisions)
7904// ============================================================
7905
7906pub 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
7981// ============================================================
7982// DYNAMIC DIFFICULTY ADJUSTMENT
7983// ============================================================
7984
7985pub struct DdaSystem {
7986    pub player_skill_estimate: f32,  // 0..1
7987    pub kill_death_ratio: f32,
7988    pub time_to_die_avg: f32,
7989    pub time_to_kill_avg: f32,
7990    pub current_difficulty: f32,    // 0..1
7991    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            // Skill: fast kills + high KDR = high skill
8039            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        // Adjust target difficulty toward challenging but not frustrating
8045        // Target: player wins ~60% of encounters
8046        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        // Smoothly approach target difficulty
8052        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
8073// ============================================================
8074// ADDITIONAL RESPONSE CURVE TESTS
8075// ============================================================
8076
8077pub 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            // Verify output in [0,1] for inputs 0..1
8096            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
8106// ============================================================
8107// GOAP VALIDATOR
8108// ============================================================
8109
8110pub struct GoapValidator;
8111
8112impl GoapValidator {
8113    /// Checks that every action's effects can satisfy at least one other action's preconditions or goal
8114    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            // Check if any effect bit satisfies goal or another action's precondition
8120            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        // Find world states reachable from start but from which goal is not reachable
8133        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
8157// ============================================================
8158// FINAL: COMPLETE INTEGRATION CREATION
8159// ============================================================
8160
8161pub 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
8189// ============================================================
8190// INFLUENCE MAP SYSTEM
8191// ============================================================
8192
8193pub 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        // Compute derived maps
8290        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
8354// ============================================================
8355// ADVANCED STEERING — CONTEXT STEERING
8356// ============================================================
8357
8358pub struct ContextSteering {
8359    pub resolution: usize,          // Number of directions to sample (e.g., 8 or 16)
8360    pub interest: Vec<f32>,          // How much we want to move in each direction
8361    pub danger: Vec<f32>,            // Obstacles/dangers in each direction
8362    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        // Mask interest with danger
8402        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        // Find best slot
8407        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        // Weighted average of top directions
8420        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        // Result
8449        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// ============================================================
8455// ABILITY SYSTEM (for AI skill activation)
8456// ============================================================
8457
8458#[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        // Returns true when ability fires
8503        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        // Energy regen
8541        self.energy = (self.energy + self.energy_regen * dt).min(self.max_energy);
8542        // Tick each ability, collect fired IDs
8543        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
8576// ============================================================
8577// FINAL AI EDITOR EXTENSIONS
8578// ============================================================
8579
8580impl AiBehaviorEditor {
8581    pub fn add_influence_map_panel(&mut self) {
8582        // Register panel
8583        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        // Offset for self-loops
8701        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
8761// ============================================================
8762// COMBAT SIMULATION HELPERS
8763// ============================================================
8764
8765pub 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 roll
8771    *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); // slight variance
8781        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
8825// ============================================================
8826// EDITOR QUICK-START PRESETS
8827// ============================================================
8828
8829pub 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;   // narrow but far
8835        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;   // wide peripheral
8845        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// ============================================================
8885// TIMELINE / REPLAY SYSTEM
8886// ============================================================
8887
8888#[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        // Prune old snapshots
8943        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
8996// ============================================================
8997// STEERING AGENT GROUP — FLOCKING SIMULATION
8998// ============================================================
8999
9000pub 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            // Flocking
9037            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            // Seek center target
9042            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            // Wander if no target
9047            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            // Obstacle avoidance
9052            force += SteeringBehaviors::obstacle_avoidance(&agents_clone[i], &obs_clone) * 2.0;
9053
9054            // Boundary avoidance
9055            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
9087// ============================================================
9088// AI STATE PROFILER
9089// ============================================================
9090
9091pub 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
9147// ============================================================
9148// HEAT MAP RENDERER DATA
9149// ============================================================
9150
9151pub 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
9231// ============================================================
9232// EDITOR SEARCH SYSTEM
9233// ============================================================
9234
9235pub 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
9348// ============================================================
9349// FINAL MODULE EXPORTS AND CONSTANTS SUMMARY
9350// ============================================================
9351
9352pub 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}