veloren_server/sys/agent/behavior_tree/
mod.rs

1use common::{
2    comp::{
3        Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, CharacterState,
4        ControlAction, ControlEvent, Controller, InputKind, InventoryEvent, Pos, PresenceKind,
5        UtteranceKind,
6        agent::{
7            AgentEvent, AwarenessState, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, Target,
8            TimerAction,
9        },
10        body, is_downed,
11    },
12    consts::MAX_INTERACT_RANGE,
13    interaction::InteractionKind,
14    path::TraversalConfig,
15    rtsim::{NpcAction, RtSimEntity},
16};
17use rand::{Rng, prelude::ThreadRng};
18use server_agent::{data::AgentEmitters, util::is_steering};
19use specs::Entity as EcsEntity;
20use tracing::warn;
21use vek::{Vec2, Vec3};
22
23use self::interaction::{
24    handle_inbox_cancel_interactions, handle_inbox_dialogue, handle_inbox_finished_trade,
25    handle_inbox_talk, handle_inbox_trade_accepted, handle_inbox_trade_invite,
26    handle_inbox_update_pending_trade, increment_timer_deltatime, process_inbox_interaction,
27    process_inbox_sound_and_hurt,
28};
29
30use super::{
31    consts::{
32        DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_PATROL_DIST,
33        MAX_STAY_DISTANCE, NORMAL_FLEE_DIR_DIST, NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS,
34        STD_AWARENESS_DECAY_RATE,
35    },
36    data::{AgentData, ReadData, TargetData},
37    util::{get_entity_by_id, is_dead, is_dead_or_invulnerable, is_invulnerable, stop_pursuing},
38};
39
40mod interaction;
41
42/// Struct containing essential data for running a behavior tree
43pub struct BehaviorData<'a, 'b, 'c> {
44    pub agent: &'a mut Agent,
45    pub agent_data: AgentData<'a>,
46    pub read_data: &'a ReadData<'a>,
47    pub emitters: &'a mut AgentEmitters<'c>,
48    pub controller: &'a mut Controller,
49    pub rng: &'b mut ThreadRng,
50}
51
52/// Behavior function
53/// Determines if the current situation can be handled and act accordingly
54/// Returns true if an action has been taken, stopping the tree execution
55type BehaviorFn = fn(&mut BehaviorData) -> bool;
56
57/// ~~list~~ ""tree"" of behavior functions
58/// This struct will allow you to run through multiple behavior function until
59/// one finally handles an event
60pub struct BehaviorTree {
61    tree: Vec<BehaviorFn>,
62}
63
64/// Enumeration of the timers used by the behavior tree.
65// FIXME: We shouldnt have a global timer enumeration for the whole behavior
66// tree. It isnt entirely clear where a lot of the agents in some of the bdata
67// objects in behavior tree functions come from, so it's hard to granularly
68// define these timers per action node. As such, the behavior tree currently has
69// one global enumeration for mapping timers in all functions, regardless as to
70// use case or action node currently executed -- even if the agent might be
71// different between calls. This doesn't break anything as each agent has its
72// own instance of timers, but it is much less clear than I would like.
73//
74// This may require some refactoring to fix, and I don't feel confident doing
75// so.
76enum ActionStateBehaviorTreeTimers {
77    TimerBehaviorTree = 0,
78}
79
80impl BehaviorTree {
81    /// Base BehaviorTree
82    ///
83    /// React to immediate dangers (fire, fall & attacks) then call subtrees
84    pub fn root() -> Self {
85        Self {
86            tree: vec![
87                maintain_if_gliding,
88                react_on_dangerous_fall,
89                react_if_on_fire,
90                target_if_attacked,
91                process_inbox_sound_and_hurt,
92                process_inbox_interaction,
93                do_target_tree_if_target_else_do_idle_tree,
94            ],
95        }
96    }
97
98    /// Target BehaviorTree
99    ///
100    /// React to the agent's target.
101    /// Either redirect to hostile or pet tree
102    pub fn target() -> Self {
103        Self {
104            tree: vec![
105                update_last_known_pos,
106                untarget_if_dead,
107                update_target_awareness,
108                search_last_known_pos_if_not_alert,
109                do_hostile_tree_if_hostile_and_aware,
110                do_save_allies,
111                do_pet_tree_if_owned,
112                do_pickup_loot,
113                do_idle_tree,
114            ],
115        }
116    }
117
118    /// Pet BehaviorTree
119    ///
120    /// Follow the owner and attack enemies
121    pub fn pet() -> Self {
122        Self {
123            tree: vec![follow_if_far_away, attack_if_owner_hurt, do_idle_tree],
124        }
125    }
126
127    /// Interaction BehaviorTree
128    ///
129    /// Either process the inbox for talk and trade events if the agent can
130    /// talk. If not, or if we are in combat, deny all talk and trade
131    /// events.
132    pub fn interaction(agent: &Agent) -> Self {
133        let is_in_combat = agent.target.is_some_and(|t| t.hostile);
134        if !is_in_combat
135            && (agent.behavior.can(BehaviorCapability::SPEAK)
136                || agent.behavior.can(BehaviorCapability::TRADE))
137        {
138            let mut tree: Vec<BehaviorFn> = vec![increment_timer_deltatime];
139            if agent.behavior.can(BehaviorCapability::SPEAK) {
140                tree.extend([handle_inbox_dialogue, handle_inbox_talk]);
141            }
142            tree.extend_from_slice(&[
143                handle_inbox_trade_invite,
144                handle_inbox_trade_accepted,
145                handle_inbox_finished_trade,
146                handle_inbox_update_pending_trade,
147            ]);
148            Self { tree }
149        } else {
150            Self {
151                tree: vec![handle_inbox_cancel_interactions],
152            }
153        }
154    }
155
156    /// Hostile BehaviorTree
157    ///
158    /// Attack the target, and heal self if applicable
159    pub fn hostile() -> Self {
160        Self {
161            tree: vec![heal_self_if_hurt, hurt_utterance, do_combat],
162        }
163    }
164
165    /// Idle BehaviorTree
166    pub fn idle() -> Self {
167        Self {
168            tree: vec![
169                set_owner_if_no_target,
170                handle_rtsim_actions,
171                handle_timed_events,
172            ],
173        }
174    }
175
176    /// Run the behavior tree until an event has been handled
177    pub fn run(&self, behavior_data: &mut BehaviorData) -> bool {
178        for behavior_fn in self.tree.iter() {
179            if behavior_fn(behavior_data) {
180                return true;
181            }
182        }
183        false
184    }
185}
186
187/// If in gliding, properly maintain it
188/// If on ground, unwield glider
189fn maintain_if_gliding(bdata: &mut BehaviorData) -> bool {
190    let Some(char_state) = bdata.read_data.char_states.get(*bdata.agent_data.entity) else {
191        return false;
192    };
193
194    match char_state {
195        CharacterState::Glide(_) => {
196            bdata
197                .agent_data
198                .glider_flight(bdata.controller, bdata.read_data);
199            true
200        },
201        CharacterState::GlideWield(_) => {
202            if bdata.agent_data.physics_state.on_ground.is_some() {
203                bdata.controller.push_action(ControlAction::Unwield);
204            }
205            // Always stop execution if during GlideWield.
206            // - If on ground, the line above will unwield the glider on next
207            // tick
208            // - If in air, we probably wouldn't want to do anything anyway, as
209            // character state code will shift itself to glide on next tick
210            true
211        },
212        _ => false,
213    }
214}
215
216/// If falling velocity is critical, throw everything
217/// and save yourself!
218///
219/// If can fly - fly.
220/// If have glider - glide.
221/// Else, rest in peace.
222fn react_on_dangerous_fall(bdata: &mut BehaviorData) -> bool {
223    // Falling damage starts from 30.0 as of time of writing
224    // But keep in mind our 25 m/s gravity
225    let is_falling_dangerous = bdata.agent_data.vel.0.z < -20.0;
226
227    if is_falling_dangerous {
228        bdata.agent_data.dismount(bdata.controller, bdata.read_data);
229        if bdata.agent_data.traversal_config.can_fly {
230            bdata
231                .agent_data
232                .fly_upward(bdata.controller, bdata.read_data);
233            return true;
234        } else if bdata.agent_data.glider_equipped {
235            bdata
236                .agent_data
237                .glider_equip(bdata.controller, bdata.read_data);
238            return true;
239        }
240    }
241    false
242}
243
244/// If on fire and able, stop, drop, and roll
245fn react_if_on_fire(bdata: &mut BehaviorData) -> bool {
246    let is_on_fire = bdata
247        .read_data
248        .buffs
249        .get(*bdata.agent_data.entity)
250        .is_some_and(|b| b.kinds[BuffKind::Burning].is_some());
251
252    if is_on_fire
253        && bdata.agent_data.body.is_some_and(|b| b.is_humanoid())
254        && bdata.agent_data.physics_state.on_ground.is_some()
255        && bdata
256            .rng
257            .random_bool((2.0 * bdata.read_data.dt.0).clamp(0.0, 1.0) as f64)
258    {
259        bdata.controller.inputs.move_dir = bdata
260            .agent_data
261            .ori
262            .look_vec()
263            .xy()
264            .try_normalized()
265            .unwrap_or_else(Vec2::zero);
266        bdata.controller.push_basic_input(InputKind::Roll);
267        return true;
268    }
269    false
270}
271
272/// Target an entity that's attacking us if the attack was recent and we have
273/// a health component
274fn target_if_attacked(bdata: &mut BehaviorData) -> bool {
275    match bdata.agent_data.health {
276        Some(health)
277            if bdata.read_data.time.0 - health.last_change.time.0 < DAMAGE_MEMORY_DURATION
278                && health.last_change.amount < 0.0 =>
279        {
280            if let Some(by) = health.last_change.damage_by()
281                && let Some(attacker) = bdata.read_data.id_maps.uid_entity(by.uid())
282            {
283                // If target is dead or invulnerable (for now, this only
284                // means safezone), untarget them and idle.
285                if is_dead_or_invulnerable(attacker, bdata.read_data) {
286                    bdata.agent.target = None;
287                } else {
288                    if bdata.agent.target.is_none() {
289                        bdata
290                            .controller
291                            .push_event(ControlEvent::Utterance(UtteranceKind::Angry));
292                    }
293
294                    bdata.agent.awareness.change_by(1.0);
295
296                    // Determine whether the new target should be a priority
297                    // over the old one (i.e: because it's either close or
298                    // because they attacked us).
299                    if bdata.agent.target.is_none_or(|target| {
300                        bdata.agent_data.is_more_dangerous_than_target(
301                            attacker,
302                            target,
303                            bdata.read_data,
304                        )
305                    }) {
306                        bdata.agent.target = Some(Target {
307                            target: attacker,
308                            hostile: true,
309                            selected_at: bdata.read_data.time.0,
310                            aggro_on: true,
311                            last_known_pos: bdata
312                                .read_data
313                                .positions
314                                .get(attacker)
315                                .map(|pos| pos.0),
316                        });
317                    }
318
319                    // Remember this attack if we're an RtSim entity
320                    /*
321                    if let Some(attacker_stats) =
322                        bdata.rtsim_entity.and(bdata.read_data.stats.get(attacker))
323                    {
324                        bdata
325                            .agent
326                            .add_fight_to_memory(&attacker_stats.name, bdata.read_data.time.0);
327                    }
328                    */
329                }
330            }
331        },
332        _ => {},
333    }
334    false
335}
336
337/// If the agent has a target, do the target tree, else do the idle tree
338///
339/// This function will never stop the BehaviorTree
340fn do_target_tree_if_target_else_do_idle_tree(bdata: &mut BehaviorData) -> bool {
341    if bdata.agent.target.is_some() && !is_steering(*bdata.agent_data.entity, bdata.read_data) {
342        BehaviorTree::target().run(bdata);
343    } else {
344        BehaviorTree::idle().run(bdata);
345    }
346    false
347}
348
349/// Run the Idle BehaviorTree
350///
351/// This function can stop the BehaviorTree
352fn do_idle_tree(bdata: &mut BehaviorData) -> bool { BehaviorTree::idle().run(bdata) }
353
354/// If target is dead, forget them
355fn untarget_if_dead(bdata: &mut BehaviorData) -> bool {
356    if let Some(Target { target, .. }) = bdata.agent.target {
357        // If target is dead or no longer exists, forget them. If the target is an item
358        // we don't expect it to have a health.
359        if bdata
360            .read_data
361            .bodies
362            .get(target)
363            .is_none_or(|b| !matches!(b, Body::Item(_)))
364            && bdata
365                .read_data
366                .healths
367                .get(target)
368                .is_none_or(|tgt_health| tgt_health.is_dead)
369        {
370            /*
371            if let Some(tgt_stats) = bdata.rtsim_entity.and(bdata.read_data.stats.get(target)) {
372                bdata.agent.forget_enemy(&tgt_stats.name);
373            }
374            */
375            bdata.agent.target = None;
376            return true;
377        }
378    }
379    false
380}
381
382/// If target is hostile and agent is aware of target, do the hostile tree and
383/// stop the current BehaviorTree
384fn do_hostile_tree_if_hostile_and_aware(bdata: &mut BehaviorData) -> bool {
385    let alert = bdata.agent.awareness.reached();
386
387    if let Some(Target { hostile, .. }) = bdata.agent.target
388        && alert
389        && hostile
390    {
391        BehaviorTree::hostile().run(bdata);
392        return true;
393    }
394    false
395}
396
397/// if owned, do the pet tree and stop the current BehaviorTree
398fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool {
399    if let (Some(Target { target, .. }), Some(Alignment::Owned(uid))) =
400        (bdata.agent.target, bdata.agent_data.alignment)
401    {
402        if bdata.read_data.uids.get(target) == Some(uid) {
403            BehaviorTree::pet().run(bdata);
404        } else {
405            bdata.agent.target = None;
406            BehaviorTree::idle().run(bdata);
407        }
408        return true;
409    }
410    false
411}
412
413/// If the target is an ItemDrop, go pick it up
414fn do_pickup_loot(bdata: &mut BehaviorData) -> bool {
415    if let Some(Target { target, .. }) = bdata.agent.target
416        && let Some(Body::Item(body)) = bdata.read_data.bodies.get(target)
417        && !matches!(body, body::item::Body::Thrown(_))
418    {
419        if let Some(tgt_pos) = bdata.read_data.positions.get(target) {
420            let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
421            if dist_sqrd < NPC_PICKUP_RANGE.powi(2) {
422                if let Some(uid) = bdata.read_data.uids.get(target) {
423                    bdata
424                        .controller
425                        .push_event(ControlEvent::InventoryEvent(InventoryEvent::Pickup(*uid)));
426                }
427                bdata.agent.target = None;
428            } else if let Some((bearing, speed, stuck)) = bdata.agent.chaser.chase(
429                &*bdata.read_data.terrain,
430                bdata.agent_data.pos.0,
431                bdata.agent_data.vel.0,
432                tgt_pos.0,
433                TraversalConfig {
434                    min_tgt_dist: NPC_PICKUP_RANGE - 1.0,
435                    ..bdata.agent_data.traversal_config
436                },
437                &bdata.read_data.time,
438            ) {
439                bdata.agent_data.unstuck_if(stuck, bdata.controller);
440                bdata.controller.inputs.move_dir =
441                    bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
442                        * speed.min(0.2 + (dist_sqrd - (NPC_PICKUP_RANGE - 1.5).powi(2)) / 8.0);
443                bdata.agent_data.jump_if(bearing.z > 1.5, bdata.controller);
444                bdata.controller.inputs.move_z = bearing.z;
445            }
446        }
447        return true;
448    }
449    false
450}
451
452/// If there are nearby downed allies, save them.
453fn do_save_allies(bdata: &mut BehaviorData) -> bool {
454    if let Some(Target {
455        target,
456        hostile: false,
457        aggro_on: false,
458        ..
459    }) = bdata.agent.target
460        && let Some(target_uid) = bdata.read_data.uids.get(target)
461    {
462        let needs_saving = is_downed(
463            bdata.read_data.healths.get(target),
464            bdata.read_data.char_states.get(target),
465        );
466
467        let wants_to_save = match (bdata.agent_data.alignment, bdata.read_data.alignments.get(target)) {
468                        // Npcs generally do want to save players. Could have extra checks for
469                        // sentiment in the future.
470                        (Some(Alignment::Npc), _) if bdata.read_data.presences.get(target).is_some_and(|presence| matches!(presence.kind, PresenceKind::Character(_))) => true,
471                        (Some(Alignment::Npc), Some(Alignment::Npc)) => true,
472                        (Some(Alignment::Enemy), Some(Alignment::Enemy)) => true,
473                        _ => false,
474                    } && bdata.agent.allowed_to_speak()
475                        // Check that anyone else isn't already saving them.
476                        && bdata.read_data
477                            .interactors
478                            .get(target).is_none_or(|interactors| {
479                                !interactors.has_interaction(InteractionKind::HelpDowned)
480                            }) && bdata.agent_data.char_state.can_interact();
481
482        if needs_saving
483            && wants_to_save
484            && let Some(target_pos) = bdata.read_data.positions.get(target)
485        {
486            let dist_sqr = bdata.agent_data.pos.0.distance_squared(target_pos.0);
487            if dist_sqr < (MAX_INTERACT_RANGE * 0.5).powi(2) {
488                bdata.controller.push_event(ControlEvent::InteractWith {
489                    target: *target_uid,
490                    kind: common::interaction::InteractionKind::HelpDowned,
491                });
492                bdata.agent.target = None;
493            } else if let Some((bearing, speed, stuck)) = bdata.agent.chaser.chase(
494                &*bdata.read_data.terrain,
495                bdata.agent_data.pos.0,
496                bdata.agent_data.vel.0,
497                target_pos.0,
498                TraversalConfig {
499                    min_tgt_dist: MAX_INTERACT_RANGE * 0.5,
500                    ..bdata.agent_data.traversal_config
501                },
502                &bdata.read_data.time,
503            ) {
504                bdata.agent_data.unstuck_if(stuck, bdata.controller);
505                bdata.controller.inputs.move_dir =
506                    bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
507                        * speed
508                            .min(0.2 + (dist_sqr - (MAX_INTERACT_RANGE * 0.5 - 0.5).powi(2)) / 8.0);
509                bdata.agent_data.jump_if(bearing.z > 1.5, bdata.controller);
510                bdata.controller.inputs.move_z = bearing.z;
511            }
512            return true;
513        }
514    }
515    false
516}
517
518/// If too far away, then follow the target
519fn follow_if_far_away(bdata: &mut BehaviorData) -> bool {
520    if let Some(Target { target, .. }) = bdata.agent.target
521        && let Some(tgt_pos) = bdata.read_data.positions.get(target)
522    {
523        if let Some(stay_pos) = bdata.agent.stay_pos {
524            let distance_from_stay = stay_pos.0.distance_squared(bdata.agent_data.pos.0);
525            bdata.controller.push_action(ControlAction::Sit);
526            if distance_from_stay > (MAX_STAY_DISTANCE).powi(2) {
527                bdata
528                    .agent_data
529                    .follow(bdata.agent, bdata.controller, bdata.read_data, &stay_pos);
530                return true;
531            }
532        } else {
533            bdata.controller.push_action(ControlAction::Stand);
534            let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
535            if dist_sqrd > (MAX_PATROL_DIST * bdata.agent.psyche.idle_wander_factor).powi(2) {
536                bdata
537                    .agent_data
538                    .follow(bdata.agent, bdata.controller, bdata.read_data, tgt_pos);
539                return true;
540            }
541        }
542    }
543    false
544}
545
546/// Attack target's attacker (if there is one)
547/// Target is the owner in this case
548fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool {
549    if let Some(Target { target, .. }) = bdata.agent.target
550        && bdata.read_data.positions.get(target).is_some()
551    {
552        let owner_recently_attacked =
553            if let Some(target_health) = bdata.read_data.healths.get(target) {
554                bdata.read_data.time.0 - target_health.last_change.time.0 < 5.0
555                    && target_health.last_change.amount < 0.0
556            } else {
557                false
558            };
559        let stay = bdata.agent.stay_pos.is_some();
560        if owner_recently_attacked && !stay {
561            bdata.agent_data.attack_target_attacker(
562                bdata.agent,
563                bdata.read_data,
564                bdata.controller,
565                bdata.emitters,
566                bdata.rng,
567            );
568            return true;
569        }
570    }
571    false
572}
573
574/// Set owner if no target
575fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool {
576    let small_chance = bdata.rng.random_bool(0.1);
577
578    if bdata.agent.target.is_none()
579        && small_chance
580        && let Some(Alignment::Owned(owner)) = bdata.agent_data.alignment
581        && let Some(owner) = get_entity_by_id(*owner, bdata.read_data)
582    {
583        let owner_pos = bdata.read_data.positions.get(owner).map(|pos| pos.0);
584
585        bdata.agent.target = Some(Target::new(
586            owner,
587            false,
588            bdata.read_data.time.0,
589            false,
590            owner_pos,
591        ));
592        // Always become aware of our owner no matter what
593        bdata.agent.awareness.set_maximally_aware();
594    }
595    false
596}
597
598/// Handle action requests from rtsim, such as talking to NPCs or attacking
599fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
600    if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() {
601        match action {
602            NpcAction::Say(target, msg) => {
603                if bdata.agent.allowed_to_speak() {
604                    // Aim the speech toward a target
605                    if let Some(target) =
606                        target.and_then(|tgt| bdata.read_data.id_maps.actor_entity(tgt))
607                    {
608                        bdata.agent.target = Some(Target::new(
609                            target,
610                            false,
611                            bdata.read_data.time.0,
612                            false,
613                            bdata.read_data.positions.get(target).map(|p| p.0),
614                        ));
615                        // We're always aware of someone we're talking to
616                        bdata.agent.awareness.set_maximally_aware();
617                        // Start a timer so that we eventually stop interacting
618                        bdata
619                            .agent
620                            .timer
621                            .start(bdata.read_data.time.0, TimerAction::Interact);
622                        bdata.controller.push_action(ControlAction::Stand);
623
624                        if let Some(target_uid) = bdata.read_data.uids.get(target) {
625                            bdata
626                                .controller
627                                .push_event(ControlEvent::Interact(*target_uid));
628                        }
629                    }
630                    bdata.controller.push_utterance(UtteranceKind::Greeting);
631                    bdata.agent_data.chat_npc(msg, bdata.emitters);
632                }
633            },
634            NpcAction::Attack(target) => {
635                if let Some(target) = bdata.read_data.id_maps.actor_entity(target) {
636                    bdata.agent.target = Some(Target::new(
637                        target,
638                        true,
639                        bdata.read_data.time.0,
640                        false,
641                        bdata.read_data.positions.get(target).map(|p| p.0),
642                    ));
643                    bdata.agent.awareness.set_maximally_aware();
644                }
645            },
646            NpcAction::Dialogue(target, dialogue) => {
647                if let Some(target) = bdata.read_data.id_maps.actor_entity(target)
648                    && let Some(target_uid) = bdata.read_data.uids.get(target)
649                {
650                    bdata
651                        .controller
652                        .push_event(ControlEvent::Dialogue(*target_uid, dialogue));
653                    bdata.controller.push_utterance(UtteranceKind::Greeting);
654                } else {
655                    warn!("NPC dialogue sent to non-existent target entity");
656                }
657            },
658        }
659        true
660    } else {
661        false
662    }
663}
664
665/// Handle timed events, like looking at the player we are talking to
666fn handle_timed_events(bdata: &mut BehaviorData) -> bool {
667    let timeout = if bdata.agent.behavior.is(BehaviorState::TRADING) {
668        TRADE_INTERACTION_TIME
669    } else {
670        DEFAULT_INTERACTION_TIME
671    };
672
673    match bdata.agent.timer.timeout_elapsed(
674        bdata.read_data.time.0,
675        TimerAction::Interact,
676        timeout as f64,
677    ) {
678        None => {
679            // Look toward the interacting entity for a while
680            if let Some(Target { target, .. }) = &bdata.agent.target
681                && let Some(target_uid) = bdata.read_data.uids.get(*target)
682            {
683                bdata
684                    .agent_data
685                    .look_toward(bdata.controller, bdata.read_data, *target);
686                bdata
687                    .controller
688                    .push_action(ControlAction::Talk(Some(*target_uid)));
689            }
690        },
691        Some(just_ended) => {
692            if just_ended {
693                bdata.agent.target = None;
694                bdata.controller.push_action(ControlAction::Stand);
695            }
696
697            if bdata.rng.random::<f32>() < 0.1 {
698                bdata.agent_data.choose_target(
699                    bdata.agent,
700                    bdata.controller,
701                    bdata.read_data,
702                    AgentData::is_enemy,
703                );
704            } else {
705                bdata.agent_data.handle_sounds_heard(
706                    bdata.agent,
707                    bdata.controller,
708                    bdata.read_data,
709                    bdata.emitters,
710                    bdata.rng,
711                );
712            }
713        },
714    }
715    false
716}
717
718fn update_last_known_pos(bdata: &mut BehaviorData) -> bool {
719    let BehaviorData {
720        agent,
721        agent_data,
722        read_data,
723        controller,
724        ..
725    } = bdata;
726
727    if let Some(target_info) = agent.target {
728        let target = target_info.target;
729
730        if let Some(target_pos) = read_data.positions.get(target)
731            && agent_data.detects_other(
732                agent,
733                controller,
734                &target,
735                target_pos,
736                read_data.scales.get(target),
737                read_data,
738            )
739        {
740            let updated_pos = Some(target_pos.0);
741
742            let Target {
743                hostile,
744                selected_at,
745                aggro_on,
746                ..
747            } = target_info;
748
749            agent.target = Some(Target::new(
750                target,
751                hostile,
752                selected_at,
753                aggro_on,
754                updated_pos,
755            ));
756        }
757    }
758
759    false
760}
761
762/// Try to heal self if our damage went below a certain threshold
763fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool {
764    if bdata.agent_data.char_state.can_interact()
765        && bdata.agent_data.damage < HEALING_ITEM_THRESHOLD
766        && bdata
767            .agent_data
768            .heal_self(bdata.agent, bdata.controller, false)
769    {
770        bdata.agent.behavior_state.timers
771            [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.01;
772        return true;
773    }
774    false
775}
776
777/// Hurt utterances at random upon receiving damage
778fn hurt_utterance(bdata: &mut BehaviorData) -> bool {
779    if matches!(bdata.agent.inbox.front(), Some(AgentEvent::Hurt)) {
780        if bdata.rng.random::<f32>() < 0.4 {
781            bdata.controller.push_utterance(UtteranceKind::Hurt);
782        }
783        bdata.agent.inbox.pop_front();
784    }
785    false
786}
787
788fn update_target_awareness(bdata: &mut BehaviorData) -> bool {
789    let BehaviorData {
790        agent,
791        agent_data,
792        read_data,
793        controller,
794        ..
795    } = bdata;
796
797    let target = agent.target.map(|t| t.target);
798    let tgt_pos = target.and_then(|t| read_data.positions.get(t));
799    let tgt_scale = target.and_then(|t| read_data.scales.get(t));
800
801    if let (Some(target), Some(tgt_pos)) = (target, tgt_pos) {
802        if agent_data.can_see_entity(agent, controller, target, tgt_pos, tgt_scale, read_data) {
803            agent.awareness.change_by(1.75 * read_data.dt.0);
804        } else if agent_data.can_sense_directly_near(tgt_pos) {
805            agent.awareness.change_by(0.25);
806        } else {
807            agent
808                .awareness
809                .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
810        }
811    } else {
812        agent
813            .awareness
814            .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
815    }
816
817    if bdata.agent.awareness.state() == AwarenessState::Unaware
818        && !bdata.agent.behavior.is(BehaviorState::TRADING)
819    {
820        bdata.agent.target = None;
821    }
822
823    false
824}
825
826fn search_last_known_pos_if_not_alert(bdata: &mut BehaviorData) -> bool {
827    let awareness = &bdata.agent.awareness;
828    if awareness.reached() || awareness.state() < AwarenessState::Low {
829        return false;
830    }
831
832    let BehaviorData {
833        agent,
834        agent_data,
835        controller,
836        read_data,
837        ..
838    } = bdata;
839
840    if let Some(target) = agent.target
841        && let Some(last_known_pos) = target.last_known_pos
842    {
843        agent_data.follow(agent, controller, read_data, &Pos(last_known_pos));
844
845        return true;
846    }
847
848    false
849}
850
851fn do_combat(bdata: &mut BehaviorData) -> bool {
852    let BehaviorData {
853        agent,
854        agent_data,
855        read_data,
856        emitters,
857        controller,
858        rng,
859    } = bdata;
860
861    if let Some(Target {
862        target,
863        selected_at,
864        aggro_on,
865        ..
866    }) = &mut agent.target
867    {
868        let target = *target;
869        let selected_at = *selected_at;
870        if let Some(tgt_pos) = read_data.positions.get(target) {
871            let dist_sqrd = agent_data.pos.0.distance_squared(tgt_pos.0);
872            let origin_dist_sqrd = match agent.patrol_origin {
873                Some(pos) => pos.distance_squared(agent_data.pos.0),
874                None => 1.0,
875            };
876
877            let own_health_fraction = match agent_data.health {
878                Some(val) => val.fraction(),
879                None => 1.0,
880            };
881            let target_health_fraction = match read_data.healths.get(target) {
882                Some(val) => val.fraction(),
883                None => 1.0,
884            };
885            let in_aggro_range = agent
886                .psyche
887                .aggro_dist
888                .is_none_or(|ad| dist_sqrd < (ad * agent.psyche.aggro_range_multiplier).powi(2));
889
890            if in_aggro_range {
891                *aggro_on = true;
892            }
893            let aggro_on = *aggro_on;
894
895            let (flee, flee_dur_mul) = match agent_data.char_state {
896                CharacterState::Crawl => {
897                    controller.push_action(ControlAction::Stand);
898
899                    // Stay still if we're being helped up.
900                    if let Some(interactors) = read_data.interactors.get(*agent_data.entity)
901                        && interactors.has_interaction(InteractionKind::HelpDowned)
902                    {
903                        return true;
904                    }
905
906                    (true, 5.0)
907                },
908                _ => (agent_data.below_flee_health(agent), 1.0),
909            };
910
911            if flee {
912                let flee_timer_done = agent.behavior_state.timers
913                    [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize]
914                    > FLEE_DURATION * flee_dur_mul;
915                let within_normal_flee_dir_dist = dist_sqrd < NORMAL_FLEE_DIR_DIST.powi(2);
916
917                // FIXME: Using action state timer to see if allowed to speak is a hack.
918                if agent.behavior_state.timers
919                    [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize]
920                    == 0.0
921                {
922                    agent_data.cry_out(agent, emitters, read_data);
923                    agent.behavior_state.timers
924                        [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.01;
925                    agent.flee_from_pos = {
926                        let random = || rand::rng().random_range(-1.0..1.0);
927                        Some(Pos(
928                            agent_data.pos.0 + Vec3::new(random(), random(), random())
929                        ))
930                    };
931                } else if !flee_timer_done {
932                    if within_normal_flee_dir_dist {
933                        agent_data.flee(agent, controller, read_data, tgt_pos);
934                    } else if let Some(random_pos) = agent.flee_from_pos {
935                        agent_data.flee(agent, controller, read_data, &random_pos);
936                    } else {
937                        agent_data.flee(agent, controller, read_data, tgt_pos);
938                    }
939
940                    agent.behavior_state.timers
941                        [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] +=
942                        read_data.dt.0;
943                } else {
944                    agent.behavior_state.timers
945                        [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.0;
946                    agent.target = None;
947                    agent.flee_from_pos = None;
948                    agent_data.idle(agent, controller, read_data, emitters, rng);
949                }
950            } else if is_dead(target, read_data) {
951                agent_data.exclaim_relief_about_enemy_dead(agent, emitters);
952                agent.target = None;
953                agent_data.idle(agent, controller, read_data, emitters, rng);
954            } else if is_invulnerable(target, read_data)
955                || stop_pursuing(
956                    dist_sqrd,
957                    origin_dist_sqrd,
958                    own_health_fraction,
959                    target_health_fraction,
960                    read_data.time.0 - selected_at,
961                    &agent.psyche,
962                )
963            {
964                agent.target = None;
965                agent_data.idle(agent, controller, read_data, emitters, rng);
966            } else {
967                let is_time_to_retarget =
968                    read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
969
970                if (!agent.psyche.should_stop_pursuing || !in_aggro_range) && is_time_to_retarget {
971                    agent_data.choose_target(agent, controller, read_data, AgentData::is_enemy);
972                }
973
974                let target_data = TargetData::new(tgt_pos, target, read_data);
975
976                if aggro_on {
977                    // let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
978
979                    // TODO: Reimplement in rtsim2
980                    // tgt_name.map(|tgt_name| agent.add_fight_to_memory(&tgt_name,
981                    // read_data.time.0));
982                    agent_data.attack(agent, controller, &target_data, read_data, rng);
983                } else {
984                    agent_data.menacing(
985                        agent,
986                        controller,
987                        target,
988                        &target_data,
989                        read_data,
990                        emitters,
991                        remembers_fight_with(agent_data.rtsim_entity, read_data, target),
992                    );
993                    // TODO: Reimplement in rtsim2
994                    // remember_fight(agent_data.rtsim_entity, read_data, agent,
995                    // target);
996                }
997            }
998        }
999        // make sure world bosses and roaming entities stay aware, to continue pursuit
1000        if !agent.psyche.should_stop_pursuing {
1001            bdata.agent.awareness.set_maximally_aware();
1002        }
1003    }
1004    false
1005}
1006
1007fn remembers_fight_with(
1008    _rtsim_entity: Option<&RtSimEntity>,
1009    _read_data: &ReadData,
1010    _other: EcsEntity,
1011) -> bool {
1012    // TODO: implement for rtsim2
1013    // let name = || read_data.stats.get(other).map(|stats| stats.name.clone());
1014
1015    // rtsim_entity.map_or(false, |rtsim_entity| {
1016    //     name().map_or(false, |name| {
1017    //         rtsim_entity.brain.remembers_fight_with_character(&name)
1018    //     })
1019    // })
1020    false
1021}
1022
1023// /// Remember target.
1024// fn remember_fight(
1025//     rtsim_entity: Option<&RtSimEntity>,
1026//     read_data: &ReadData,
1027//     agent: &mut Agent,
1028//     target: EcsEntity,
1029// ) { rtsim_entity.is_some().then(|| { read_data .stats .get(target)
1030//   .map(|stats| agent.add_fight_to_memory(&stats.name,
1031// read_data.time.0))     });
1032// }