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