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, thread_rng};
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            .gen_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                if let Some(attacker) = bdata.read_data.id_maps.uid_entity(by.uid()) {
282                    // If target is dead or invulnerable (for now, this only
283                    // means safezone), untarget them and idle.
284                    if is_dead_or_invulnerable(attacker, bdata.read_data) {
285                        bdata.agent.target = None;
286                    } else {
287                        if bdata.agent.target.is_none() {
288                            bdata
289                                .controller
290                                .push_event(ControlEvent::Utterance(UtteranceKind::Angry));
291                        }
292
293                        bdata.agent.awareness.change_by(1.0);
294
295                        // Determine whether the new target should be a priority
296                        // over the old one (i.e: because it's either close or
297                        // because they attacked us).
298                        if bdata.agent.target.is_none_or(|target| {
299                            bdata.agent_data.is_more_dangerous_than_target(
300                                attacker,
301                                target,
302                                bdata.read_data,
303                            )
304                        }) {
305                            bdata.agent.target = Some(Target {
306                                target: attacker,
307                                hostile: true,
308                                selected_at: bdata.read_data.time.0,
309                                aggro_on: true,
310                                last_known_pos: bdata
311                                    .read_data
312                                    .positions
313                                    .get(attacker)
314                                    .map(|pos| pos.0),
315                            });
316                        }
317
318                        // Remember this attack if we're an RtSim entity
319                        /*
320                        if let Some(attacker_stats) =
321                            bdata.rtsim_entity.and(bdata.read_data.stats.get(attacker))
322                        {
323                            bdata
324                                .agent
325                                .add_fight_to_memory(&attacker_stats.name, bdata.read_data.time.0);
326                        }
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        if alert && hostile {
389            BehaviorTree::hostile().run(bdata);
390            return true;
391        }
392    }
393    false
394}
395
396/// if owned, do the pet tree and stop the current BehaviorTree
397fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool {
398    if let (Some(Target { target, .. }), Some(Alignment::Owned(uid))) =
399        (bdata.agent.target, bdata.agent_data.alignment)
400    {
401        if bdata.read_data.uids.get(target) == Some(uid) {
402            BehaviorTree::pet().run(bdata);
403        } else {
404            bdata.agent.target = None;
405            BehaviorTree::idle().run(bdata);
406        }
407        return true;
408    }
409    false
410}
411
412/// If the target is an ItemDrop, go pick it up
413fn do_pickup_loot(bdata: &mut BehaviorData) -> bool {
414    if let Some(Target { target, .. }) = bdata.agent.target
415        && let Some(Body::Item(body)) = bdata.read_data.bodies.get(target)
416        && !matches!(body, body::item::Body::Thrown(_))
417    {
418        if let Some(tgt_pos) = bdata.read_data.positions.get(target) {
419            let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
420            if dist_sqrd < NPC_PICKUP_RANGE.powi(2) {
421                if let Some(uid) = bdata.read_data.uids.get(target) {
422                    bdata
423                        .controller
424                        .push_event(ControlEvent::InventoryEvent(InventoryEvent::Pickup(*uid)));
425                }
426                bdata.agent.target = None;
427            } else if let Some((bearing, speed)) = bdata.agent.chaser.chase(
428                &*bdata.read_data.terrain,
429                bdata.agent_data.pos.0,
430                bdata.agent_data.vel.0,
431                tgt_pos.0,
432                TraversalConfig {
433                    min_tgt_dist: NPC_PICKUP_RANGE - 1.0,
434                    ..bdata.agent_data.traversal_config
435                },
436            ) {
437                bdata.controller.inputs.move_dir =
438                    bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
439                        * speed.min(0.2 + (dist_sqrd - (NPC_PICKUP_RANGE - 1.5).powi(2)) / 8.0);
440                bdata.agent_data.jump_if(bearing.z > 1.5, bdata.controller);
441                bdata.controller.inputs.move_z = bearing.z;
442            }
443        }
444        return true;
445    }
446    false
447}
448
449/// If there are nearby downed allies, save them.
450fn do_save_allies(bdata: &mut BehaviorData) -> bool {
451    if let Some(Target {
452        target,
453        hostile: false,
454        aggro_on: false,
455        ..
456    }) = bdata.agent.target
457        && let Some(target_uid) = bdata.read_data.uids.get(target)
458    {
459        let needs_saving = is_downed(
460            bdata.read_data.healths.get(target),
461            bdata.read_data.char_states.get(target),
462        );
463
464        let wants_to_save = match (bdata.agent_data.alignment, bdata.read_data.alignments.get(target)) {
465                        // Npcs generally do want to save players. Could have extra checks for
466                        // sentiment in the future.
467                        (Some(Alignment::Npc), _) if bdata.read_data.presences.get(target).is_some_and(|presence| matches!(presence.kind, PresenceKind::Character(_))) => true,
468                        (Some(Alignment::Npc), Some(Alignment::Npc)) => true,
469                        (Some(Alignment::Enemy), Some(Alignment::Enemy)) => true,
470                        _ => false,
471                    } && bdata.agent.allowed_to_speak()
472                        // Check that anyone else isn't already saving them.
473                        && bdata.read_data
474                            .interactors
475                            .get(target).is_none_or(|interactors| {
476                                !interactors.has_interaction(InteractionKind::HelpDowned)
477                            }) && bdata.agent_data.char_state.can_interact();
478
479        if needs_saving
480            && wants_to_save
481            && let Some(target_pos) = bdata.read_data.positions.get(target)
482        {
483            let dist_sqr = bdata.agent_data.pos.0.distance_squared(target_pos.0);
484            if dist_sqr < (MAX_INTERACT_RANGE * 0.5).powi(2) {
485                bdata.controller.push_event(ControlEvent::InteractWith {
486                    target: *target_uid,
487                    kind: common::interaction::InteractionKind::HelpDowned,
488                });
489                bdata.agent.target = None;
490            } else if let Some((bearing, speed)) = bdata.agent.chaser.chase(
491                &*bdata.read_data.terrain,
492                bdata.agent_data.pos.0,
493                bdata.agent_data.vel.0,
494                target_pos.0,
495                TraversalConfig {
496                    min_tgt_dist: MAX_INTERACT_RANGE * 0.5,
497                    ..bdata.agent_data.traversal_config
498                },
499            ) {
500                bdata.controller.inputs.move_dir =
501                    bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
502                        * speed
503                            .min(0.2 + (dist_sqr - (MAX_INTERACT_RANGE * 0.5 - 0.5).powi(2)) / 8.0);
504                bdata.agent_data.jump_if(bearing.z > 1.5, bdata.controller);
505                bdata.controller.inputs.move_z = bearing.z;
506            }
507            return true;
508        }
509    }
510    false
511}
512
513/// If too far away, then follow the target
514fn follow_if_far_away(bdata: &mut BehaviorData) -> bool {
515    if let Some(Target { target, .. }) = bdata.agent.target {
516        if let Some(tgt_pos) = bdata.read_data.positions.get(target) {
517            if let Some(stay_pos) = bdata.agent.stay_pos {
518                let distance_from_stay = stay_pos.0.distance_squared(bdata.agent_data.pos.0);
519                bdata.controller.push_action(ControlAction::Sit);
520                if distance_from_stay > (MAX_STAY_DISTANCE).powi(2) {
521                    bdata.agent_data.follow(
522                        bdata.agent,
523                        bdata.controller,
524                        bdata.read_data,
525                        &stay_pos,
526                    );
527                    return true;
528                }
529            } else {
530                bdata.controller.push_action(ControlAction::Stand);
531                let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
532                if dist_sqrd > (MAX_PATROL_DIST * bdata.agent.psyche.idle_wander_factor).powi(2) {
533                    bdata.agent_data.follow(
534                        bdata.agent,
535                        bdata.controller,
536                        bdata.read_data,
537                        tgt_pos,
538                    );
539                    return true;
540                }
541            }
542        }
543    }
544    false
545}
546
547/// Attack target's attacker (if there is one)
548/// Target is the owner in this case
549fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool {
550    if let Some(Target { target, .. }) = bdata.agent.target {
551        if bdata.read_data.positions.get(target).is_some() {
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    }
572    false
573}
574
575/// Set owner if no target
576fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool {
577    let small_chance = bdata.rng.gen_bool(0.1);
578
579    if bdata.agent.target.is_none() && small_chance {
580        if let Some(Alignment::Owned(owner)) = bdata.agent_data.alignment {
581            if let Some(owner) = get_entity_by_id(*owner, bdata.read_data) {
582                let owner_pos = bdata.read_data.positions.get(owner).map(|pos| pos.0);
583
584                bdata.agent.target = Some(Target::new(
585                    owner,
586                    false,
587                    bdata.read_data.time.0,
588                    false,
589                    owner_pos,
590                ));
591                // Always become aware of our owner no matter what
592                bdata.agent.awareness.set_maximally_aware();
593            }
594        }
595    }
596    false
597}
598
599/// Handle action requests from rtsim, such as talking to NPCs or attacking
600fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
601    if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() {
602        match action {
603            NpcAction::Say(target, msg) => {
604                if bdata.agent.allowed_to_speak() {
605                    // Aim the speech toward a target
606                    if let Some(target) =
607                        target.and_then(|tgt| bdata.read_data.id_maps.actor_entity(tgt))
608                    {
609                        bdata.agent.target = Some(Target::new(
610                            target,
611                            false,
612                            bdata.read_data.time.0,
613                            false,
614                            bdata.read_data.positions.get(target).map(|p| p.0),
615                        ));
616                        // We're always aware of someone we're talking to
617                        bdata.agent.awareness.set_maximally_aware();
618                        // Start a timer so that we eventually stop interacting
619                        bdata
620                            .agent
621                            .timer
622                            .start(bdata.read_data.time.0, TimerAction::Interact);
623                        bdata.controller.push_action(ControlAction::Stand);
624
625                        if let Some(target_uid) = bdata.read_data.uids.get(target) {
626                            bdata
627                                .controller
628                                .push_event(ControlEvent::Interact(*target_uid));
629                        }
630                    }
631                    bdata.controller.push_utterance(UtteranceKind::Greeting);
632                    bdata.agent_data.chat_npc(msg, bdata.emitters);
633                }
634            },
635            NpcAction::Attack(target) => {
636                if let Some(target) = bdata.read_data.id_maps.actor_entity(target) {
637                    bdata.agent.target = Some(Target::new(
638                        target,
639                        true,
640                        bdata.read_data.time.0,
641                        false,
642                        bdata.read_data.positions.get(target).map(|p| p.0),
643                    ));
644                    bdata.agent.awareness.set_maximally_aware();
645                }
646            },
647            NpcAction::Dialogue(target, dialogue) => {
648                if let Some(target) = bdata.read_data.id_maps.actor_entity(target)
649                    && let Some(target_uid) = bdata.read_data.uids.get(target)
650                {
651                    bdata
652                        .controller
653                        .push_event(ControlEvent::Dialogue(*target_uid, dialogue));
654                    bdata.controller.push_utterance(UtteranceKind::Greeting);
655                } else {
656                    warn!("NPC dialogue sent to non-existent target entity");
657                }
658            },
659        }
660        true
661    } else {
662        false
663    }
664}
665
666/// Handle timed events, like looking at the player we are talking to
667fn handle_timed_events(bdata: &mut BehaviorData) -> bool {
668    let timeout = if bdata.agent.behavior.is(BehaviorState::TRADING) {
669        TRADE_INTERACTION_TIME
670    } else {
671        DEFAULT_INTERACTION_TIME
672    };
673
674    match bdata.agent.timer.timeout_elapsed(
675        bdata.read_data.time.0,
676        TimerAction::Interact,
677        timeout as f64,
678    ) {
679        None => {
680            // Look toward the interacting entity for a while
681            if let Some(Target { target, .. }) = &bdata.agent.target {
682                bdata
683                    .agent_data
684                    .look_toward(bdata.controller, bdata.read_data, *target);
685                bdata.controller.push_action(ControlAction::Talk);
686            }
687        },
688        Some(just_ended) => {
689            if just_ended {
690                bdata.agent.target = None;
691                bdata.controller.push_action(ControlAction::Stand);
692            }
693
694            if bdata.rng.gen::<f32>() < 0.1 {
695                bdata.agent_data.choose_target(
696                    bdata.agent,
697                    bdata.controller,
698                    bdata.read_data,
699                    AgentData::is_enemy,
700                );
701            } else {
702                bdata.agent_data.handle_sounds_heard(
703                    bdata.agent,
704                    bdata.controller,
705                    bdata.read_data,
706                    bdata.emitters,
707                    bdata.rng,
708                );
709            }
710        },
711    }
712    false
713}
714
715fn update_last_known_pos(bdata: &mut BehaviorData) -> bool {
716    let BehaviorData {
717        agent,
718        agent_data,
719        read_data,
720        controller,
721        ..
722    } = bdata;
723
724    if let Some(target_info) = agent.target {
725        let target = target_info.target;
726
727        if let Some(target_pos) = read_data.positions.get(target) {
728            if agent_data.detects_other(
729                agent,
730                controller,
731                &target,
732                target_pos,
733                read_data.scales.get(target),
734                read_data,
735            ) {
736                let updated_pos = Some(target_pos.0);
737
738                let Target {
739                    hostile,
740                    selected_at,
741                    aggro_on,
742                    ..
743                } = target_info;
744
745                agent.target = Some(Target::new(
746                    target,
747                    hostile,
748                    selected_at,
749                    aggro_on,
750                    updated_pos,
751                ));
752            }
753        }
754    }
755
756    false
757}
758
759/// Try to heal self if our damage went below a certain threshold
760fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool {
761    if bdata.agent_data.char_state.can_interact()
762        && bdata.agent_data.damage < HEALING_ITEM_THRESHOLD
763        && bdata
764            .agent_data
765            .heal_self(bdata.agent, bdata.controller, false)
766    {
767        bdata.agent.behavior_state.timers
768            [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.01;
769        return true;
770    }
771    false
772}
773
774/// Hurt utterances at random upon receiving damage
775fn hurt_utterance(bdata: &mut BehaviorData) -> bool {
776    if matches!(bdata.agent.inbox.front(), Some(AgentEvent::Hurt)) {
777        if bdata.rng.gen::<f32>() < 0.4 {
778            bdata.controller.push_utterance(UtteranceKind::Hurt);
779        }
780        bdata.agent.inbox.pop_front();
781    }
782    false
783}
784
785fn update_target_awareness(bdata: &mut BehaviorData) -> bool {
786    let BehaviorData {
787        agent,
788        agent_data,
789        read_data,
790        controller,
791        ..
792    } = bdata;
793
794    let target = agent.target.map(|t| t.target);
795    let tgt_pos = target.and_then(|t| read_data.positions.get(t));
796    let tgt_scale = target.and_then(|t| read_data.scales.get(t));
797
798    if let (Some(target), Some(tgt_pos)) = (target, tgt_pos) {
799        if agent_data.can_see_entity(agent, controller, target, tgt_pos, tgt_scale, read_data) {
800            agent.awareness.change_by(1.75 * read_data.dt.0);
801        } else if agent_data.can_sense_directly_near(tgt_pos) {
802            agent.awareness.change_by(0.25);
803        } else {
804            agent
805                .awareness
806                .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
807        }
808    } else {
809        agent
810            .awareness
811            .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
812    }
813
814    if bdata.agent.awareness.state() == AwarenessState::Unaware
815        && !bdata.agent.behavior.is(BehaviorState::TRADING)
816    {
817        bdata.agent.target = None;
818    }
819
820    false
821}
822
823fn search_last_known_pos_if_not_alert(bdata: &mut BehaviorData) -> bool {
824    let awareness = &bdata.agent.awareness;
825    if awareness.reached() || awareness.state() < AwarenessState::Low {
826        return false;
827    }
828
829    let BehaviorData {
830        agent,
831        agent_data,
832        controller,
833        read_data,
834        ..
835    } = bdata;
836
837    if let Some(target) = agent.target {
838        if let Some(last_known_pos) = target.last_known_pos {
839            agent_data.follow(agent, controller, read_data, &Pos(last_known_pos));
840
841            return true;
842        }
843    }
844
845    false
846}
847
848fn do_combat(bdata: &mut BehaviorData) -> bool {
849    let BehaviorData {
850        agent,
851        agent_data,
852        read_data,
853        emitters,
854        controller,
855        rng,
856    } = bdata;
857
858    if let Some(Target {
859        target,
860        selected_at,
861        aggro_on,
862        ..
863    }) = &mut agent.target
864    {
865        let target = *target;
866        let selected_at = *selected_at;
867        if let Some(tgt_pos) = read_data.positions.get(target) {
868            let dist_sqrd = agent_data.pos.0.distance_squared(tgt_pos.0);
869            let origin_dist_sqrd = match agent.patrol_origin {
870                Some(pos) => pos.distance_squared(agent_data.pos.0),
871                None => 1.0,
872            };
873
874            let own_health_fraction = match agent_data.health {
875                Some(val) => val.fraction(),
876                None => 1.0,
877            };
878            let target_health_fraction = match read_data.healths.get(target) {
879                Some(val) => val.fraction(),
880                None => 1.0,
881            };
882            let in_aggro_range = agent
883                .psyche
884                .aggro_dist
885                .is_none_or(|ad| dist_sqrd < (ad * agent.psyche.aggro_range_multiplier).powi(2));
886
887            if in_aggro_range {
888                *aggro_on = true;
889            }
890            let aggro_on = *aggro_on;
891
892            let (flee, flee_dur_mul) = match agent_data.char_state {
893                CharacterState::Crawl => {
894                    controller.push_action(ControlAction::Stand);
895
896                    // Stay still if we're being helped up.
897                    if let Some(interactors) = read_data.interactors.get(*agent_data.entity)
898                        && interactors.has_interaction(InteractionKind::HelpDowned)
899                    {
900                        return true;
901                    }
902
903                    (true, 5.0)
904                },
905                _ => (agent_data.below_flee_health(agent), 1.0),
906            };
907
908            if flee {
909                let flee_timer_done = agent.behavior_state.timers
910                    [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize]
911                    > FLEE_DURATION * flee_dur_mul;
912                let within_normal_flee_dir_dist = dist_sqrd < NORMAL_FLEE_DIR_DIST.powi(2);
913
914                // FIXME: Using action state timer to see if allowed to speak is a hack.
915                if agent.behavior_state.timers
916                    [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize]
917                    == 0.0
918                {
919                    agent_data.cry_out(agent, emitters, read_data);
920                    agent.behavior_state.timers
921                        [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.01;
922                    agent.flee_from_pos = {
923                        let random = || thread_rng().gen_range(-1.0..1.0);
924                        Some(Pos(
925                            agent_data.pos.0 + Vec3::new(random(), random(), random())
926                        ))
927                    };
928                } else if !flee_timer_done {
929                    if within_normal_flee_dir_dist {
930                        agent_data.flee(agent, controller, read_data, tgt_pos);
931                    } else if let Some(random_pos) = agent.flee_from_pos {
932                        agent_data.flee(agent, controller, read_data, &random_pos);
933                    } else {
934                        agent_data.flee(agent, controller, read_data, tgt_pos);
935                    }
936
937                    agent.behavior_state.timers
938                        [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] +=
939                        read_data.dt.0;
940                } else {
941                    agent.behavior_state.timers
942                        [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.0;
943                    agent.target = None;
944                    agent.flee_from_pos = None;
945                    agent_data.idle(agent, controller, read_data, emitters, rng);
946                }
947            } else if is_dead(target, read_data) {
948                agent_data.exclaim_relief_about_enemy_dead(agent, emitters);
949                agent.target = None;
950                agent_data.idle(agent, controller, read_data, emitters, rng);
951            } else if is_invulnerable(target, read_data)
952                || stop_pursuing(
953                    dist_sqrd,
954                    origin_dist_sqrd,
955                    own_health_fraction,
956                    target_health_fraction,
957                    read_data.time.0 - selected_at,
958                    &agent.psyche,
959                )
960            {
961                agent.target = None;
962                agent_data.idle(agent, controller, read_data, emitters, rng);
963            } else {
964                let is_time_to_retarget =
965                    read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
966
967                if (!agent.psyche.should_stop_pursuing || !in_aggro_range) && is_time_to_retarget {
968                    agent_data.choose_target(agent, controller, read_data, AgentData::is_enemy);
969                }
970
971                if aggro_on {
972                    let target_data = TargetData::new(tgt_pos, target, read_data);
973                    // let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
974
975                    // TODO: Reimplement in rtsim2
976                    // tgt_name.map(|tgt_name| agent.add_fight_to_memory(&tgt_name,
977                    // read_data.time.0));
978                    agent_data.attack(agent, controller, &target_data, read_data, rng);
979                } else {
980                    agent_data.menacing(
981                        agent,
982                        controller,
983                        target,
984                        read_data,
985                        emitters,
986                        rng,
987                        remembers_fight_with(agent_data.rtsim_entity, read_data, target),
988                    );
989                    // TODO: Reimplement in rtsim2
990                    // remember_fight(agent_data.rtsim_entity, read_data, agent,
991                    // target);
992                }
993            }
994        }
995        // make sure world bosses and roaming entities stay aware, to continue pursuit
996        if !agent.psyche.should_stop_pursuing {
997            bdata.agent.awareness.set_maximally_aware();
998        }
999    }
1000    false
1001}
1002
1003fn remembers_fight_with(
1004    _rtsim_entity: Option<&RtSimEntity>,
1005    _read_data: &ReadData,
1006    _other: EcsEntity,
1007) -> bool {
1008    // TODO: implement for rtsim2
1009    // let name = || read_data.stats.get(other).map(|stats| stats.name.clone());
1010
1011    // rtsim_entity.map_or(false, |rtsim_entity| {
1012    //     name().map_or(false, |name| {
1013    //         rtsim_entity.brain.remembers_fight_with_character(&name)
1014    //     })
1015    // })
1016    false
1017}
1018
1019// /// Remember target.
1020// fn remember_fight(
1021//     rtsim_entity: Option<&RtSimEntity>,
1022//     read_data: &ReadData,
1023//     agent: &mut Agent,
1024//     target: EcsEntity,
1025// ) { rtsim_entity.is_some().then(|| { read_data .stats .get(target)
1026//   .map(|stats| agent.add_fight_to_memory(&stats.name,
1027// read_data.time.0))     });
1028// }