1use common::{
2 comp::{
3 Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, CharacterState,
4 ControlAction, ControlEvent, Controller, InputKind, InventoryEvent, Pos, PresenceKind,
5 UtteranceKind,
6 agent::{
7 AgentEvent, AwarenessState, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, Target,
8 TimerAction,
9 },
10 body, is_downed,
11 },
12 consts::MAX_INTERACT_RANGE,
13 interaction::InteractionKind,
14 path::TraversalConfig,
15 rtsim::{NpcAction, RtSimEntity},
16};
17use rand::{Rng, prelude::ThreadRng};
18use server_agent::{data::AgentEmitters, util::is_steering};
19use specs::Entity as EcsEntity;
20use tracing::warn;
21use vek::{Vec2, Vec3};
22
23use self::interaction::{
24 handle_inbox_cancel_interactions, handle_inbox_dialogue, handle_inbox_finished_trade,
25 handle_inbox_talk, handle_inbox_trade_accepted, handle_inbox_trade_invite,
26 handle_inbox_update_pending_trade, increment_timer_deltatime, process_inbox_interaction,
27 process_inbox_sound_and_hurt,
28};
29
30use super::{
31 consts::{
32 DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_PATROL_DIST,
33 MAX_STAY_DISTANCE, NORMAL_FLEE_DIR_DIST, NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS,
34 STD_AWARENESS_DECAY_RATE,
35 },
36 data::{AgentData, ReadData, TargetData},
37 util::{get_entity_by_id, is_dead, is_dead_or_invulnerable, is_invulnerable, stop_pursuing},
38};
39
40mod interaction;
41
42pub 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
52type BehaviorFn = fn(&mut BehaviorData) -> bool;
56
57pub struct BehaviorTree {
61 tree: Vec<BehaviorFn>,
62}
63
64enum ActionStateBehaviorTreeTimers {
77 TimerBehaviorTree = 0,
78}
79
80impl BehaviorTree {
81 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 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 pub fn pet() -> Self {
122 Self {
123 tree: vec![follow_if_far_away, attack_if_owner_hurt, do_idle_tree],
124 }
125 }
126
127 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 pub fn hostile() -> Self {
160 Self {
161 tree: vec![heal_self_if_hurt, hurt_utterance, do_combat],
162 }
163 }
164
165 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 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
187fn 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 true
211 },
212 _ => false,
213 }
214}
215
216fn react_on_dangerous_fall(bdata: &mut BehaviorData) -> bool {
223 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
244fn react_if_on_fire(bdata: &mut BehaviorData) -> bool {
246 let is_on_fire = bdata
247 .read_data
248 .buffs
249 .get(*bdata.agent_data.entity)
250 .is_some_and(|b| b.kinds[BuffKind::Burning].is_some());
251
252 if is_on_fire
253 && bdata.agent_data.body.is_some_and(|b| b.is_humanoid())
254 && bdata.agent_data.physics_state.on_ground.is_some()
255 && bdata
256 .rng
257 .random_bool((2.0 * bdata.read_data.dt.0).clamp(0.0, 1.0) as f64)
258 {
259 bdata.controller.inputs.move_dir = bdata
260 .agent_data
261 .ori
262 .look_vec()
263 .xy()
264 .try_normalized()
265 .unwrap_or_else(Vec2::zero);
266 bdata.controller.push_basic_input(InputKind::Roll);
267 return true;
268 }
269 false
270}
271
272fn target_if_attacked(bdata: &mut BehaviorData) -> bool {
275 match bdata.agent_data.health {
276 Some(health)
277 if bdata.read_data.time.0 - health.last_change.time.0 < DAMAGE_MEMORY_DURATION
278 && health.last_change.amount < 0.0 =>
279 {
280 if let Some(by) = health.last_change.damage_by()
281 && let Some(attacker) = bdata.read_data.id_maps.uid_entity(by.uid())
282 {
283 if is_dead_or_invulnerable(attacker, bdata.read_data) {
286 bdata.agent.target = None;
287 } else {
288 if bdata.agent.target.is_none() {
289 bdata
290 .controller
291 .push_event(ControlEvent::Utterance(UtteranceKind::Angry));
292 }
293
294 bdata.agent.awareness.change_by(1.0);
295
296 if bdata.agent.target.is_none_or(|target| {
300 bdata.agent_data.is_more_dangerous_than_target(
301 attacker,
302 target,
303 bdata.read_data,
304 )
305 }) {
306 bdata.agent.target = Some(Target {
307 target: attacker,
308 hostile: true,
309 selected_at: bdata.read_data.time.0,
310 aggro_on: true,
311 last_known_pos: bdata
312 .read_data
313 .positions
314 .get(attacker)
315 .map(|pos| pos.0),
316 });
317 }
318
319 }
330 }
331 },
332 _ => {},
333 }
334 false
335}
336
337fn 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
349fn do_idle_tree(bdata: &mut BehaviorData) -> bool { BehaviorTree::idle().run(bdata) }
353
354fn untarget_if_dead(bdata: &mut BehaviorData) -> bool {
356 if let Some(Target { target, .. }) = bdata.agent.target {
357 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 bdata.agent.target = None;
376 return true;
377 }
378 }
379 false
380}
381
382fn do_hostile_tree_if_hostile_and_aware(bdata: &mut BehaviorData) -> bool {
385 let alert = bdata.agent.awareness.reached();
386
387 if let Some(Target { hostile, .. }) = bdata.agent.target
388 && alert
389 && hostile
390 {
391 BehaviorTree::hostile().run(bdata);
392 return true;
393 }
394 false
395}
396
397fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool {
399 if let (Some(Target { target, .. }), Some(Alignment::Owned(uid))) =
400 (bdata.agent.target, bdata.agent_data.alignment)
401 {
402 if bdata.read_data.uids.get(target) == Some(uid) {
403 BehaviorTree::pet().run(bdata);
404 } else {
405 bdata.agent.target = None;
406 BehaviorTree::idle().run(bdata);
407 }
408 return true;
409 }
410 false
411}
412
413fn do_pickup_loot(bdata: &mut BehaviorData) -> bool {
415 if let Some(Target { target, .. }) = bdata.agent.target
416 && let Some(Body::Item(body)) = bdata.read_data.bodies.get(target)
417 && !matches!(body, body::item::Body::Thrown(_))
418 {
419 if let Some(tgt_pos) = bdata.read_data.positions.get(target) {
420 let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
421 if dist_sqrd < NPC_PICKUP_RANGE.powi(2) {
422 if let Some(uid) = bdata.read_data.uids.get(target) {
423 bdata
424 .controller
425 .push_event(ControlEvent::InventoryEvent(InventoryEvent::Pickup(*uid)));
426 }
427 bdata.agent.target = None;
428 } else if let Some((bearing, speed, stuck)) = bdata.agent.chaser.chase(
429 &*bdata.read_data.terrain,
430 bdata.agent_data.pos.0,
431 bdata.agent_data.vel.0,
432 tgt_pos.0,
433 TraversalConfig {
434 min_tgt_dist: NPC_PICKUP_RANGE - 1.0,
435 ..bdata.agent_data.traversal_config
436 },
437 &bdata.read_data.time,
438 ) {
439 bdata.agent_data.unstuck_if(stuck, bdata.controller);
440 bdata.controller.inputs.move_dir =
441 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
442 * speed.min(0.2 + (dist_sqrd - (NPC_PICKUP_RANGE - 1.5).powi(2)) / 8.0);
443 bdata.agent_data.jump_if(bearing.z > 1.5, bdata.controller);
444 bdata.controller.inputs.move_z = bearing.z;
445 }
446 }
447 return true;
448 }
449 false
450}
451
452fn do_save_allies(bdata: &mut BehaviorData) -> bool {
454 if let Some(Target {
455 target,
456 hostile: false,
457 aggro_on: false,
458 ..
459 }) = bdata.agent.target
460 && let Some(target_uid) = bdata.read_data.uids.get(target)
461 {
462 let needs_saving = is_downed(
463 bdata.read_data.healths.get(target),
464 bdata.read_data.char_states.get(target),
465 );
466
467 let wants_to_save = match (bdata.agent_data.alignment, bdata.read_data.alignments.get(target)) {
468 (Some(Alignment::Npc), _) if bdata.read_data.presences.get(target).is_some_and(|presence| matches!(presence.kind, PresenceKind::Character(_))) => true,
471 (Some(Alignment::Npc), Some(Alignment::Npc)) => true,
472 (Some(Alignment::Enemy), Some(Alignment::Enemy)) => true,
473 _ => false,
474 } && bdata.agent.allowed_to_speak()
475 && bdata.read_data
477 .interactors
478 .get(target).is_none_or(|interactors| {
479 !interactors.has_interaction(InteractionKind::HelpDowned)
480 }) && bdata.agent_data.char_state.can_interact();
481
482 if needs_saving
483 && wants_to_save
484 && let Some(target_pos) = bdata.read_data.positions.get(target)
485 {
486 let dist_sqr = bdata.agent_data.pos.0.distance_squared(target_pos.0);
487 if dist_sqr < (MAX_INTERACT_RANGE * 0.5).powi(2) {
488 bdata.controller.push_event(ControlEvent::InteractWith {
489 target: *target_uid,
490 kind: common::interaction::InteractionKind::HelpDowned,
491 });
492 bdata.agent.target = None;
493 } else if let Some((bearing, speed, stuck)) = bdata.agent.chaser.chase(
494 &*bdata.read_data.terrain,
495 bdata.agent_data.pos.0,
496 bdata.agent_data.vel.0,
497 target_pos.0,
498 TraversalConfig {
499 min_tgt_dist: MAX_INTERACT_RANGE * 0.5,
500 ..bdata.agent_data.traversal_config
501 },
502 &bdata.read_data.time,
503 ) {
504 bdata.agent_data.unstuck_if(stuck, bdata.controller);
505 bdata.controller.inputs.move_dir =
506 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
507 * speed
508 .min(0.2 + (dist_sqr - (MAX_INTERACT_RANGE * 0.5 - 0.5).powi(2)) / 8.0);
509 bdata.agent_data.jump_if(bearing.z > 1.5, bdata.controller);
510 bdata.controller.inputs.move_z = bearing.z;
511 }
512 return true;
513 }
514 }
515 false
516}
517
518fn follow_if_far_away(bdata: &mut BehaviorData) -> bool {
520 if let Some(Target { target, .. }) = bdata.agent.target
521 && let Some(tgt_pos) = bdata.read_data.positions.get(target)
522 {
523 if let Some(stay_pos) = bdata.agent.stay_pos {
524 let distance_from_stay = stay_pos.0.distance_squared(bdata.agent_data.pos.0);
525 bdata.controller.push_action(ControlAction::Sit);
526 if distance_from_stay > (MAX_STAY_DISTANCE).powi(2) {
527 bdata
528 .agent_data
529 .follow(bdata.agent, bdata.controller, bdata.read_data, &stay_pos);
530 return true;
531 }
532 } else {
533 bdata.controller.push_action(ControlAction::Stand);
534 let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0);
535 if dist_sqrd > (MAX_PATROL_DIST * bdata.agent.psyche.idle_wander_factor).powi(2) {
536 bdata
537 .agent_data
538 .follow(bdata.agent, bdata.controller, bdata.read_data, tgt_pos);
539 return true;
540 }
541 }
542 }
543 false
544}
545
546fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool {
549 if let Some(Target { target, .. }) = bdata.agent.target
550 && bdata.read_data.positions.get(target).is_some()
551 {
552 let owner_recently_attacked =
553 if let Some(target_health) = bdata.read_data.healths.get(target) {
554 bdata.read_data.time.0 - target_health.last_change.time.0 < 5.0
555 && target_health.last_change.amount < 0.0
556 } else {
557 false
558 };
559 let stay = bdata.agent.stay_pos.is_some();
560 if owner_recently_attacked && !stay {
561 bdata.agent_data.attack_target_attacker(
562 bdata.agent,
563 bdata.read_data,
564 bdata.controller,
565 bdata.emitters,
566 bdata.rng,
567 );
568 return true;
569 }
570 }
571 false
572}
573
574fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool {
576 let small_chance = bdata.rng.random_bool(0.1);
577
578 if bdata.agent.target.is_none()
579 && small_chance
580 && let Some(Alignment::Owned(owner)) = bdata.agent_data.alignment
581 && let Some(owner) = get_entity_by_id(*owner, bdata.read_data)
582 {
583 let owner_pos = bdata.read_data.positions.get(owner).map(|pos| pos.0);
584
585 bdata.agent.target = Some(Target::new(
586 owner,
587 false,
588 bdata.read_data.time.0,
589 false,
590 owner_pos,
591 ));
592 bdata.agent.awareness.set_maximally_aware();
594 }
595 false
596}
597
598fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
600 if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() {
601 match action {
602 NpcAction::Say(target, msg) => {
603 if bdata.agent.allowed_to_speak() {
604 if let Some(target) =
606 target.and_then(|tgt| bdata.read_data.id_maps.actor_entity(tgt))
607 {
608 bdata.agent.target = Some(Target::new(
609 target,
610 false,
611 bdata.read_data.time.0,
612 false,
613 bdata.read_data.positions.get(target).map(|p| p.0),
614 ));
615 bdata.agent.awareness.set_maximally_aware();
617 bdata
619 .agent
620 .timer
621 .start(bdata.read_data.time.0, TimerAction::Interact);
622 bdata.controller.push_action(ControlAction::Stand);
623
624 if let Some(target_uid) = bdata.read_data.uids.get(target) {
625 bdata
626 .controller
627 .push_event(ControlEvent::Interact(*target_uid));
628 }
629 }
630 bdata.controller.push_utterance(UtteranceKind::Greeting);
631 bdata.agent_data.chat_npc(msg, bdata.emitters);
632 }
633 },
634 NpcAction::Attack(target) => {
635 if let Some(target) = bdata.read_data.id_maps.actor_entity(target) {
636 bdata.agent.target = Some(Target::new(
637 target,
638 true,
639 bdata.read_data.time.0,
640 false,
641 bdata.read_data.positions.get(target).map(|p| p.0),
642 ));
643 bdata.agent.awareness.set_maximally_aware();
644 }
645 },
646 NpcAction::Dialogue(target, dialogue) => {
647 if let Some(target) = bdata.read_data.id_maps.actor_entity(target)
648 && let Some(target_uid) = bdata.read_data.uids.get(target)
649 {
650 bdata
651 .controller
652 .push_event(ControlEvent::Dialogue(*target_uid, dialogue));
653 bdata.controller.push_utterance(UtteranceKind::Greeting);
654 } else {
655 warn!("NPC dialogue sent to non-existent target entity");
656 }
657 },
658 }
659 true
660 } else {
661 false
662 }
663}
664
665fn handle_timed_events(bdata: &mut BehaviorData) -> bool {
667 let timeout = if bdata.agent.behavior.is(BehaviorState::TRADING) {
668 TRADE_INTERACTION_TIME
669 } else {
670 DEFAULT_INTERACTION_TIME
671 };
672
673 match bdata.agent.timer.timeout_elapsed(
674 bdata.read_data.time.0,
675 TimerAction::Interact,
676 timeout as f64,
677 ) {
678 None => {
679 if let Some(Target { target, .. }) = &bdata.agent.target
681 && let Some(target_uid) = bdata.read_data.uids.get(*target)
682 {
683 bdata
684 .agent_data
685 .look_toward(bdata.controller, bdata.read_data, *target);
686 bdata
687 .controller
688 .push_action(ControlAction::Talk(Some(*target_uid)));
689 }
690 },
691 Some(just_ended) => {
692 if just_ended {
693 bdata.agent.target = None;
694 bdata.controller.push_action(ControlAction::Stand);
695 }
696
697 if bdata.rng.random::<f32>() < 0.1 {
698 bdata.agent_data.choose_target(
699 bdata.agent,
700 bdata.controller,
701 bdata.read_data,
702 AgentData::is_enemy,
703 );
704 } else {
705 bdata.agent_data.handle_sounds_heard(
706 bdata.agent,
707 bdata.controller,
708 bdata.read_data,
709 bdata.emitters,
710 bdata.rng,
711 );
712 }
713 },
714 }
715 false
716}
717
718fn update_last_known_pos(bdata: &mut BehaviorData) -> bool {
719 let BehaviorData {
720 agent,
721 agent_data,
722 read_data,
723 controller,
724 ..
725 } = bdata;
726
727 if let Some(target_info) = agent.target {
728 let target = target_info.target;
729
730 if let Some(target_pos) = read_data.positions.get(target)
731 && agent_data.detects_other(
732 agent,
733 controller,
734 &target,
735 target_pos,
736 read_data.scales.get(target),
737 read_data,
738 )
739 {
740 let updated_pos = Some(target_pos.0);
741
742 let Target {
743 hostile,
744 selected_at,
745 aggro_on,
746 ..
747 } = target_info;
748
749 agent.target = Some(Target::new(
750 target,
751 hostile,
752 selected_at,
753 aggro_on,
754 updated_pos,
755 ));
756 }
757 }
758
759 false
760}
761
762fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool {
764 if bdata.agent_data.char_state.can_interact()
765 && bdata.agent_data.damage < HEALING_ITEM_THRESHOLD
766 && bdata
767 .agent_data
768 .heal_self(bdata.agent, bdata.controller, false)
769 {
770 bdata.agent.behavior_state.timers
771 [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.01;
772 return true;
773 }
774 false
775}
776
777fn hurt_utterance(bdata: &mut BehaviorData) -> bool {
779 if matches!(bdata.agent.inbox.front(), Some(AgentEvent::Hurt)) {
780 if bdata.rng.random::<f32>() < 0.4 {
781 bdata.controller.push_utterance(UtteranceKind::Hurt);
782 }
783 bdata.agent.inbox.pop_front();
784 }
785 false
786}
787
788fn update_target_awareness(bdata: &mut BehaviorData) -> bool {
789 let BehaviorData {
790 agent,
791 agent_data,
792 read_data,
793 controller,
794 ..
795 } = bdata;
796
797 let target = agent.target.map(|t| t.target);
798 let tgt_pos = target.and_then(|t| read_data.positions.get(t));
799 let tgt_scale = target.and_then(|t| read_data.scales.get(t));
800
801 if let (Some(target), Some(tgt_pos)) = (target, tgt_pos) {
802 if agent_data.can_see_entity(agent, controller, target, tgt_pos, tgt_scale, read_data) {
803 agent.awareness.change_by(1.75 * read_data.dt.0);
804 } else if agent_data.can_sense_directly_near(tgt_pos) {
805 agent.awareness.change_by(0.25);
806 } else {
807 agent
808 .awareness
809 .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
810 }
811 } else {
812 agent
813 .awareness
814 .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
815 }
816
817 if bdata.agent.awareness.state() == AwarenessState::Unaware
818 && !bdata.agent.behavior.is(BehaviorState::TRADING)
819 {
820 bdata.agent.target = None;
821 }
822
823 false
824}
825
826fn search_last_known_pos_if_not_alert(bdata: &mut BehaviorData) -> bool {
827 let awareness = &bdata.agent.awareness;
828 if awareness.reached() || awareness.state() < AwarenessState::Low {
829 return false;
830 }
831
832 let BehaviorData {
833 agent,
834 agent_data,
835 controller,
836 read_data,
837 ..
838 } = bdata;
839
840 if let Some(target) = agent.target
841 && let Some(last_known_pos) = target.last_known_pos
842 {
843 agent_data.follow(agent, controller, read_data, &Pos(last_known_pos));
844
845 return true;
846 }
847
848 false
849}
850
851fn do_combat(bdata: &mut BehaviorData) -> bool {
852 let BehaviorData {
853 agent,
854 agent_data,
855 read_data,
856 emitters,
857 controller,
858 rng,
859 } = bdata;
860
861 if let Some(Target {
862 target,
863 selected_at,
864 aggro_on,
865 ..
866 }) = &mut agent.target
867 {
868 let target = *target;
869 let selected_at = *selected_at;
870 if let Some(tgt_pos) = read_data.positions.get(target) {
871 let dist_sqrd = agent_data.pos.0.distance_squared(tgt_pos.0);
872 let origin_dist_sqrd = match agent.patrol_origin {
873 Some(pos) => pos.distance_squared(agent_data.pos.0),
874 None => 1.0,
875 };
876
877 let own_health_fraction = match agent_data.health {
878 Some(val) => val.fraction(),
879 None => 1.0,
880 };
881 let target_health_fraction = match read_data.healths.get(target) {
882 Some(val) => val.fraction(),
883 None => 1.0,
884 };
885 let in_aggro_range = agent
886 .psyche
887 .aggro_dist
888 .is_none_or(|ad| dist_sqrd < (ad * agent.psyche.aggro_range_multiplier).powi(2));
889
890 if in_aggro_range {
891 *aggro_on = true;
892 }
893 let aggro_on = *aggro_on;
894
895 let (flee, flee_dur_mul) = match agent_data.char_state {
896 CharacterState::Crawl => {
897 controller.push_action(ControlAction::Stand);
898
899 if let Some(interactors) = read_data.interactors.get(*agent_data.entity)
901 && interactors.has_interaction(InteractionKind::HelpDowned)
902 {
903 return true;
904 }
905
906 (true, 5.0)
907 },
908 _ => (agent_data.below_flee_health(agent), 1.0),
909 };
910
911 if flee {
912 let flee_timer_done = agent.behavior_state.timers
913 [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize]
914 > FLEE_DURATION * flee_dur_mul;
915 let within_normal_flee_dir_dist = dist_sqrd < NORMAL_FLEE_DIR_DIST.powi(2);
916
917 if agent.behavior_state.timers
919 [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize]
920 == 0.0
921 {
922 agent_data.cry_out(agent, emitters, read_data);
923 agent.behavior_state.timers
924 [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.01;
925 agent.flee_from_pos = {
926 let random = || rand::rng().random_range(-1.0..1.0);
927 Some(Pos(
928 agent_data.pos.0 + Vec3::new(random(), random(), random())
929 ))
930 };
931 } else if !flee_timer_done {
932 if within_normal_flee_dir_dist {
933 agent_data.flee(agent, controller, read_data, tgt_pos);
934 } else if let Some(random_pos) = agent.flee_from_pos {
935 agent_data.flee(agent, controller, read_data, &random_pos);
936 } else {
937 agent_data.flee(agent, controller, read_data, tgt_pos);
938 }
939
940 agent.behavior_state.timers
941 [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] +=
942 read_data.dt.0;
943 } else {
944 agent.behavior_state.timers
945 [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.0;
946 agent.target = None;
947 agent.flee_from_pos = None;
948 agent_data.idle(agent, controller, read_data, emitters, rng);
949 }
950 } else if is_dead(target, read_data) {
951 agent_data.exclaim_relief_about_enemy_dead(agent, emitters);
952 agent.target = None;
953 agent_data.idle(agent, controller, read_data, emitters, rng);
954 } else if is_invulnerable(target, read_data)
955 || stop_pursuing(
956 dist_sqrd,
957 origin_dist_sqrd,
958 own_health_fraction,
959 target_health_fraction,
960 read_data.time.0 - selected_at,
961 &agent.psyche,
962 )
963 {
964 agent.target = None;
965 agent_data.idle(agent, controller, read_data, emitters, rng);
966 } else {
967 let is_time_to_retarget =
968 read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
969
970 if (!agent.psyche.should_stop_pursuing || !in_aggro_range) && is_time_to_retarget {
971 agent_data.choose_target(agent, controller, read_data, AgentData::is_enemy);
972 }
973
974 let target_data = TargetData::new(tgt_pos, target, read_data);
975
976 if aggro_on {
977 agent_data.attack(agent, controller, &target_data, read_data, rng);
983 } else {
984 agent_data.menacing(
985 agent,
986 controller,
987 target,
988 &target_data,
989 read_data,
990 emitters,
991 remembers_fight_with(agent_data.rtsim_entity, read_data, target),
992 );
993 }
997 }
998 }
999 if !agent.psyche.should_stop_pursuing {
1001 bdata.agent.awareness.set_maximally_aware();
1002 }
1003 }
1004 false
1005}
1006
1007fn remembers_fight_with(
1008 _rtsim_entity: Option<&RtSimEntity>,
1009 _read_data: &ReadData,
1010 _other: EcsEntity,
1011) -> bool {
1012 false
1021}
1022
1023