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