1use crate::{
2 consts::{
3 AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, MAX_PATROL_DIST,
4 PARTIAL_PATH_DIST, SEPARATION_BIAS, SEPARATION_DIST, STD_AWARENESS_DECAY_RATE,
5 },
6 data::{AgentData, AgentEmitters, AttackData, Path, ReadData, Tactic, TargetData},
7 util::{
8 aim_projectile, are_our_owners_hostile, entities_have_line_of_sight, get_attacker,
9 get_entity_by_id, is_dead_or_invulnerable, is_dressed_as_cultist, is_invulnerable,
10 is_steering, is_village_guard, is_villager,
11 },
12};
13use common::{
14 combat::perception_dist_multiplier_from_stealth,
15 comp::{
16 self, Agent, Alignment, Body, CharacterState, Content, ControlAction, ControlEvent,
17 Controller, HealthChange, InputKind, InventoryAction, Pos, PresenceKind, Scale,
18 UnresolvedChatMsg, UtteranceKind,
19 ability::BASE_ABILITY_LIMIT,
20 agent::{FlightMode, PidControllers, Sound, SoundKind, Target},
21 body,
22 inventory::slot::EquipSlot,
23 item::{
24 ConsumableKind, Effects, Item, ItemDesc, ItemKind,
25 tool::{AbilitySpec, ToolKind},
26 },
27 projectile::ProjectileConstructorKind,
28 },
29 consts::MAX_MOUNT_RANGE,
30 effect::{BuffEffect, Effect},
31 event::{ChatEvent, EmitExt, SoundEvent},
32 interaction::InteractionKind,
33 mounting::VolumePos,
34 path::TraversalConfig,
35 rtsim::NpcActivity,
36 states::basic_beam,
37 terrain::Block,
38 time::DayPeriod,
39 util::Dir,
40 vol::ReadVol,
41};
42use itertools::Itertools;
43use rand::{Rng, thread_rng};
44use specs::Entity as EcsEntity;
45use vek::*;
46
47#[cfg(feature = "use-dyn-lib")]
48use {crate::LIB, std::ffi::CStr};
49
50impl AgentData<'_> {
51 pub fn glider_equip(&self, controller: &mut Controller, read_data: &ReadData) {
55 self.dismount(controller, read_data);
56 controller.push_action(ControlAction::GlideWield);
57 }
58
59 pub fn glider_flight(&self, controller: &mut Controller, _read_data: &ReadData) {
61 let Some(fluid) = self.physics_state.in_fluid else {
62 return;
63 };
64
65 let vel = self.vel;
66
67 let comp::Vel(rel_flow) = fluid.relative_flow(vel);
68
69 let is_wind_downwards = rel_flow.z.is_sign_negative();
70
71 let look_dir = if is_wind_downwards {
72 Vec3::from(-rel_flow.xy())
73 } else {
74 -rel_flow
75 };
76
77 controller.inputs.look_dir = Dir::from_unnormalized(look_dir).unwrap_or_else(Dir::forward);
78 }
79
80 pub fn fly_upward(&self, controller: &mut Controller, read_data: &ReadData) {
81 self.dismount(controller, read_data);
82
83 controller.push_basic_input(InputKind::Fly);
84 controller.inputs.move_z = 1.0;
85 }
86
87 pub fn path_toward_target(
94 &self,
95 agent: &mut Agent,
96 controller: &mut Controller,
97 tgt_pos: Vec3<f32>,
98 read_data: &ReadData,
99 path: Path,
100 speed_multiplier: Option<f32>,
101 ) -> Option<Vec3<f32>> {
102 self.dismount_uncontrollable(controller, read_data);
103
104 let partial_path_tgt_pos = |pos_difference: Vec3<f32>| {
105 self.pos.0
106 + PARTIAL_PATH_DIST * pos_difference.try_normalized().unwrap_or_else(Vec3::zero)
107 };
108 let pos_difference = tgt_pos - self.pos.0;
109 let pathing_pos = match path {
110 Path::Separate => {
111 let mut sep_vec: Vec3<f32> = Vec3::<f32>::zero();
112
113 for entity in read_data
114 .cached_spatial_grid
115 .0
116 .in_circle_aabr(self.pos.0.xy(), SEPARATION_DIST)
117 {
118 if let (Some(alignment), Some(other_alignment)) =
119 (self.alignment, read_data.alignments.get(entity))
120 {
121 if Alignment::passive_towards(*alignment, *other_alignment) {
122 if let (Some(pos), Some(body), Some(other_body)) = (
123 read_data.positions.get(entity),
124 self.body,
125 read_data.bodies.get(entity),
126 ) {
127 let dist_xy = self.pos.0.xy().distance(pos.0.xy());
128 let spacing = body.spacing_radius() + other_body.spacing_radius();
129 if dist_xy < spacing {
130 let pos_diff = self.pos.0.xy() - pos.0.xy();
131 sep_vec += pos_diff.try_normalized().unwrap_or_else(Vec2::zero)
132 * ((spacing - dist_xy) / spacing);
133 }
134 }
135 }
136 }
137 }
138 partial_path_tgt_pos(
139 sep_vec * SEPARATION_BIAS + pos_difference * (1.0 - SEPARATION_BIAS),
140 )
141 },
142 Path::Full => tgt_pos,
143 Path::Partial => partial_path_tgt_pos(pos_difference),
144 };
145 let speed_multiplier = speed_multiplier.unwrap_or(1.0).min(1.0);
146
147 let in_loaded_chunk = |pos: Vec3<f32>| {
148 read_data
149 .terrain
150 .contains_key(read_data.terrain.pos_key(pos.map(|e| e.floor() as i32)))
151 };
152
153 let is_target_loaded = in_loaded_chunk(pathing_pos);
158
159 if let Some((bearing, speed)) = agent.chaser.chase(
160 &*read_data.terrain,
161 self.pos.0,
162 self.vel.0,
163 pathing_pos,
164 TraversalConfig {
165 min_tgt_dist: 0.25,
166 is_target_loaded,
167 ..self.traversal_config
168 },
169 ) {
170 self.traverse(controller, bearing, speed * speed_multiplier);
171 Some(bearing)
172 } else {
173 None
174 }
175 }
176
177 fn traverse(&self, controller: &mut Controller, bearing: Vec3<f32>, speed: f32) {
178 controller.inputs.move_dir =
179 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
180
181 self.jump_if(
183 (self.physics_state.on_ground.is_some() && bearing.z > 1.5)
184 || self.traversal_config.can_fly,
185 controller,
186 );
187 controller.inputs.move_z = bearing.z;
188 }
189
190 pub fn jump_if(&self, condition: bool, controller: &mut Controller) {
191 if condition {
192 controller.push_basic_input(InputKind::Jump);
193 } else {
194 controller.push_cancel_input(InputKind::Jump)
195 }
196 }
197
198 pub fn idle(
199 &self,
200 agent: &mut Agent,
201 controller: &mut Controller,
202 read_data: &ReadData,
203 _emitters: &mut AgentEmitters,
204 rng: &mut impl Rng,
205 ) {
206 enum ActionTimers {
207 TimerIdle = 0,
208 }
209
210 agent
211 .awareness
212 .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
213
214 let lantern_equipped = self
217 .inventory
218 .equipped(EquipSlot::Lantern)
219 .as_ref()
220 .is_some_and(|item| matches!(&*item.kind(), comp::item::ItemKind::Lantern(_)));
221 let lantern_turned_on = self.light_emitter.is_some();
222 let day_period = DayPeriod::from(read_data.time_of_day.0);
223 if lantern_equipped && rng.gen_bool(0.001) {
225 if day_period.is_dark() && !lantern_turned_on {
226 controller.push_event(ControlEvent::EnableLantern)
231 } else if lantern_turned_on && day_period.is_light() {
232 controller.push_event(ControlEvent::DisableLantern)
235 }
236 };
237
238 if let Some(body) = self.body {
239 let attempt_heal = if matches!(body, Body::Humanoid(_)) {
240 self.damage < IDLE_HEALING_ITEM_THRESHOLD
241 } else {
242 true
243 };
244 if attempt_heal && self.heal_self(agent, controller, true) {
245 agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.01;
246 return;
247 }
248 } else {
249 agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.01;
250 return;
251 }
252
253 agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
254
255 'activity: {
256 match agent.rtsim_controller.activity {
257 Some(NpcActivity::Goto(travel_to, speed_factor)) => {
258 self.dismount_uncontrollable(controller, read_data);
259
260 agent.bearing = Vec2::zero();
261
262 if self.traversal_config.can_fly
265 && !read_data
266 .terrain
267 .ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
268 .until(Block::is_solid)
269 .cast()
270 .1
271 .map_or(true, |b| b.is_some())
272 {
273 controller.push_basic_input(InputKind::Fly);
274 } else {
275 controller.push_cancel_input(InputKind::Fly)
276 }
277
278 if let Some(bearing) = self.path_toward_target(
279 agent,
280 controller,
281 travel_to,
282 read_data,
283 Path::Full,
284 Some(speed_factor),
285 ) {
286 let height_offset = bearing.z
287 + if self.traversal_config.can_fly {
288 let obstacle_ahead = read_data
290 .terrain
291 .ray(
292 self.pos.0 + Vec3::unit_z(),
293 self.pos.0
294 + bearing.try_normalized().unwrap_or_else(Vec3::unit_y)
295 * 80.0
296 + Vec3::unit_z(),
297 )
298 .until(Block::is_solid)
299 .cast()
300 .1
301 .map_or(true, |b| b.is_some());
302
303 let mut ground_too_close = self
304 .body
305 .map(|body| {
306 #[cfg(feature = "worldgen")]
307 let height_approx = self.pos.0.z
308 - read_data
309 .world
310 .sim()
311 .get_alt_approx(
312 self.pos.0.xy().map(|x: f32| x as i32),
313 )
314 .unwrap_or(0.0);
315 #[cfg(not(feature = "worldgen"))]
316 let height_approx = self.pos.0.z;
317
318 height_approx < body.flying_height()
319 })
320 .unwrap_or(false);
321
322 const NUM_RAYS: usize = 5;
323
324 for i in 0..=NUM_RAYS {
326 let magnitude = self.body.map_or(20.0, |b| b.flying_height());
327 if let Some(dir) = Lerp::lerp(
332 -Vec3::unit_z(),
333 Vec3::new(bearing.x, bearing.y, 0.0),
334 i as f32 / NUM_RAYS as f32,
335 )
336 .try_normalized()
337 {
338 ground_too_close |= read_data
339 .terrain
340 .ray(self.pos.0, self.pos.0 + magnitude * dir)
341 .until(|b: &Block| b.is_solid() || b.is_liquid())
342 .cast()
343 .1
344 .is_ok_and(|b| b.is_some())
345 }
346 }
347
348 if obstacle_ahead || ground_too_close {
349 5.0 } else {
351 -2.0
352 } } else {
354 0.05 };
356
357 if let Some(mpid) = agent.multi_pid_controllers.as_mut() {
358 if let Some(z_controller) = mpid.z_controller.as_mut() {
359 z_controller.sp = self.pos.0.z + height_offset;
360 controller.inputs.move_z = z_controller.calc_err();
361 z_controller.limit_integral_windup(|z| *z = z.clamp(-10.0, 10.0));
363 } else {
364 controller.inputs.move_z = 0.0;
365 }
366 } else {
367 controller.inputs.move_z = height_offset;
368 }
369 }
370
371 if rng.gen_bool(0.1)
373 && matches!(
374 read_data.char_states.get(*self.entity),
375 Some(CharacterState::Wielding(_))
376 )
377 {
378 controller.push_action(ControlAction::Unwield);
379 }
380 break 'activity; },
382
383 Some(NpcActivity::GotoFlying(
384 travel_to,
385 speed_factor,
386 height_offset,
387 direction_override,
388 flight_mode,
389 )) => {
390 self.dismount_uncontrollable(controller, read_data);
391
392 if self.traversal_config.vectored_propulsion {
393 controller.push_basic_input(InputKind::Fly);
406
407 if let Some(direction) = direction_override {
422 controller.inputs.look_dir = direction;
423 } else {
424 controller.inputs.look_dir =
426 Dir::from_unnormalized((travel_to - self.pos.0).xy().with_z(0.0))
427 .unwrap_or_default();
428 }
429
430 if agent
444 .multi_pid_controllers
445 .as_ref()
446 .is_some_and(|mpid| mpid.mode != flight_mode)
447 {
448 agent.multi_pid_controllers = None;
449 }
450 let mpid = agent.multi_pid_controllers.get_or_insert_with(|| {
451 PidControllers::<16>::new_multi_pid_controllers(flight_mode, travel_to)
452 });
453 let sample_time = read_data.time.0;
454
455 #[allow(unused_variables)]
456 let terrain_alt_with_lookahead = |dist: f32| -> f32 {
457 #[cfg(feature = "worldgen")]
459 let terrain_alt = read_data
460 .world
461 .sim()
462 .get_alt_approx(
463 (self.pos.0.xy()
464 + controller.inputs.look_dir.to_vec().xy() * dist)
465 .map(|x: f32| x as i32),
466 )
467 .unwrap_or(0.0);
468 #[cfg(not(feature = "worldgen"))]
469 let terrain_alt = 0.0;
470 terrain_alt
471 };
472
473 if flight_mode == FlightMode::FlyThrough {
474 let travel_vec = travel_to - self.pos.0;
475 let bearing =
476 travel_vec.xy().try_normalized().unwrap_or_else(Vec2::zero);
477 controller.inputs.move_dir = bearing * speed_factor;
478 let terrain_alt = terrain_alt_with_lookahead(32.0);
479 let height = height_offset.unwrap_or(100.0);
480 if let Some(z_controller) = mpid.z_controller.as_mut() {
481 z_controller.sp = terrain_alt + height;
482 }
483 mpid.add_measurement(sample_time, self.pos.0);
484 if terrain_alt >= self.pos.0.z - 32.0 {
486 controller.inputs.move_z = 1.0 * speed_factor;
489 controller.inputs.move_dir =
491 self.vel.0.xy().try_normalized().unwrap_or_else(Vec2::zero)
492 * -1.0
493 * speed_factor;
494 } else {
495 controller.inputs.move_z =
496 mpid.calc_err_z().unwrap_or(0.0).min(1.0) * speed_factor;
497 }
498 mpid.limit_windup_z(|z| *z = z.clamp(-20.0, 20.0));
503 } else {
504 if let Some(x_controller) = mpid.x_controller.as_mut() {
508 x_controller.sp = travel_to.x;
509 }
510 if let Some(y_controller) = mpid.y_controller.as_mut() {
511 y_controller.sp = travel_to.y;
512 }
513
514 let z_setpoint = if let Some(height) = height_offset {
519 let clearance_alt = terrain_alt_with_lookahead(16.0) + height;
520 clearance_alt.max(travel_to.z)
521 } else {
522 travel_to.z
523 };
524 if let Some(z_controller) = mpid.z_controller.as_mut() {
525 z_controller.sp = z_setpoint;
526 }
527
528 mpid.add_measurement(sample_time, self.pos.0);
529 controller.inputs.move_dir.x =
530 mpid.calc_err_x().unwrap_or(0.0).min(1.0) * speed_factor;
531 controller.inputs.move_dir.y =
532 mpid.calc_err_y().unwrap_or(0.0).min(1.0) * speed_factor;
533 controller.inputs.move_z =
534 mpid.calc_err_z().unwrap_or(0.0).min(1.0) * speed_factor;
535
536 mpid.limit_windup_x(|x| *x = x.clamp(-1.0, 1.0));
538 mpid.limit_windup_y(|y| *y = y.clamp(-1.0, 1.0));
539 mpid.limit_windup_z(|z| *z = z.clamp(-1.0, 1.0));
540 }
541 }
542 break 'activity; },
544 Some(NpcActivity::Gather(_resources)) => {
545 controller.push_action(ControlAction::Dance);
547 break 'activity; },
549 Some(NpcActivity::Dance(dir)) => {
550 if let Some(look_dir) = dir {
552 controller.inputs.look_dir = look_dir;
553 if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 {
554 controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01;
555 break 'activity;
556 } else {
557 controller.inputs.move_dir = Vec2::zero();
558 }
559 }
560 controller.push_action(ControlAction::Dance);
561 break 'activity; },
563 Some(NpcActivity::Cheer(dir)) => {
564 if let Some(look_dir) = dir {
565 controller.inputs.look_dir = look_dir;
566 if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 {
567 controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01;
568 break 'activity;
569 } else {
570 controller.inputs.move_dir = Vec2::zero();
571 }
572 }
573 controller.push_action(ControlAction::Talk);
574 break 'activity; },
576 Some(NpcActivity::Sit(dir, pos)) => {
577 if let Some(pos) =
578 pos.filter(|p| read_data.terrain.get(*p).is_ok_and(|b| b.is_mountable()))
579 {
580 if !read_data.is_volume_riders.contains(*self.entity) {
581 controller
582 .push_event(ControlEvent::MountVolume(VolumePos::terrain(pos)));
583 }
584 } else {
585 if let Some(look_dir) = dir {
586 controller.inputs.look_dir = look_dir;
587 if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 {
588 controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01;
589 break 'activity;
590 } else {
591 controller.inputs.move_dir = Vec2::zero();
592 }
593 }
594 controller.push_action(ControlAction::Sit);
595 }
596 break 'activity; },
598 Some(NpcActivity::HuntAnimals) => {
599 if rng.gen::<f32>() < 0.1 {
600 self.choose_target(
601 agent,
602 controller,
603 read_data,
604 AgentData::is_hunting_animal,
605 );
606 }
607 },
608 Some(NpcActivity::Talk(target)) => {
609 if agent.target.is_none()
610 && let Some(target) = read_data.id_maps.actor_entity(target)
611 {
612 controller.push_action(ControlAction::Stand);
614 self.look_toward(controller, read_data, target);
615 controller.push_action(ControlAction::Talk);
616 break 'activity;
617 }
618 },
619 None => {},
620 }
621
622 let owner_uid = self.alignment.and_then(|alignment| match alignment {
623 Alignment::Owned(owner_uid) => Some(owner_uid),
624 _ => None,
625 });
626
627 let owner = owner_uid.and_then(|owner_uid| get_entity_by_id(*owner_uid, read_data));
628
629 let is_being_pet = read_data
630 .interactors
631 .get(*self.entity)
632 .and_then(|interactors| interactors.get(*owner_uid?))
633 .is_some_and(|interaction| matches!(interaction.kind, InteractionKind::Pet));
634
635 let is_in_range = owner
636 .and_then(|owner| read_data.positions.get(owner))
637 .is_some_and(|pos| pos.0.distance_squared(self.pos.0) < MAX_MOUNT_RANGE.powi(2));
638
639 if read_data.is_riders.contains(*self.entity) {
641 if rng.gen_bool(0.0001) {
642 self.dismount_uncontrollable(controller, read_data);
643 } else {
644 break 'activity;
645 }
646 } else if let Some(owner_uid) = owner_uid
647 && is_in_range
648 && !is_being_pet
649 && rng.gen_bool(0.01)
650 {
651 controller.push_event(ControlEvent::Mount(*owner_uid));
652 break 'activity;
653 }
654
655 if self.traversal_config.can_fly
658 && self
659 .inventory
660 .equipped(EquipSlot::ActiveMainhand)
661 .as_ref()
662 .is_some_and(|item| {
663 item.ability_spec().is_some_and(|a_s| match &*a_s {
664 AbilitySpec::Custom(spec) => {
665 matches!(
666 spec.as_str(),
667 "Simple Flying Melee"
668 | "Bloodmoon Bat"
669 | "Vampire Bat"
670 | "Flame Wyvern"
671 | "Frost Wyvern"
672 | "Cloud Wyvern"
673 | "Sea Wyvern"
674 | "Weald Wyvern"
675 )
676 },
677 _ => false,
678 })
679 })
680 {
681 controller.push_basic_input(InputKind::Fly);
683 let alt = read_data
686 .terrain
687 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
688 .until(Block::is_solid)
689 .cast()
690 .0;
691 let set_point = 5.0;
692 let error = set_point - alt;
693 controller.inputs.move_z = error;
694 if self.physics_state.on_ground.is_some() {
696 controller.push_basic_input(InputKind::Jump);
697 }
698 }
699
700 let diff = Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5);
701 agent.bearing += (diff * 0.1 - agent.bearing * 0.01)
702 * agent.psyche.idle_wander_factor.max(0.0).sqrt()
703 * agent.psyche.aggro_range_multiplier.max(0.0).sqrt();
704 if let Some(patrol_origin) = agent.patrol_origin
705 .or_else(|| if let Some(Alignment::Owned(owner_uid)) = self.alignment
707 && let Some(owner) = get_entity_by_id(*owner_uid, read_data)
708 && let Some(pos) = read_data.positions.get(owner)
709 {
710 Some(pos.0)
711 } else {
712 None
713 })
714 {
715 agent.bearing += ((patrol_origin.xy() - self.pos.0.xy())
716 / (0.01 + MAX_PATROL_DIST * agent.psyche.idle_wander_factor))
717 * 0.015
718 * agent.psyche.idle_wander_factor;
719 }
720
721 agent.bearing *= 0.1
725 + if read_data
726 .terrain
727 .ray(
728 self.pos.0 + Vec3::unit_z(),
729 self.pos.0
730 + Vec3::from(agent.bearing)
731 .try_normalized()
732 .unwrap_or_else(Vec3::unit_y)
733 * 5.0
734 + Vec3::unit_z(),
735 )
736 .until(Block::is_solid)
737 .cast()
738 .1
739 .map_or(true, |b| b.is_none())
740 && read_data
741 .terrain
742 .ray(
743 self.pos.0
744 + Vec3::from(agent.bearing)
745 .try_normalized()
746 .unwrap_or_else(Vec3::unit_y),
747 self.pos.0
748 + Vec3::from(agent.bearing)
749 .try_normalized()
750 .unwrap_or_else(Vec3::unit_y)
751 - Vec3::unit_z() * 4.0,
752 )
753 .until(Block::is_solid)
754 .cast()
755 .0
756 < 3.0
757 {
758 0.9
759 } else {
760 0.0
761 };
762
763 if agent.bearing.magnitude_squared() > 0.5f32.powi(2) {
764 controller.inputs.move_dir = agent.bearing;
765 }
766
767 if rng.gen_bool(0.1)
769 && matches!(
770 read_data.char_states.get(*self.entity),
771 Some(CharacterState::Wielding(_))
772 )
773 {
774 controller.push_action(ControlAction::Unwield);
775 }
776
777 if rng.gen::<f32>() < 0.0015 {
778 controller.push_utterance(UtteranceKind::Calm);
779 }
780
781 if rng.gen::<f32>() < 0.0035 {
783 controller.push_action(ControlAction::Sit);
784 }
785 }
786 }
787
788 pub fn follow(
789 &self,
790 agent: &mut Agent,
791 controller: &mut Controller,
792 read_data: &ReadData,
793 tgt_pos: &Pos,
794 ) {
795 self.dismount_uncontrollable(controller, read_data);
796
797 if let Some((bearing, speed)) = agent.chaser.chase(
798 &*read_data.terrain,
799 self.pos.0,
800 self.vel.0,
801 tgt_pos.0,
802 TraversalConfig {
803 min_tgt_dist: AVG_FOLLOW_DIST,
804 ..self.traversal_config
805 },
806 ) {
807 let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
808 self.traverse(
809 controller,
810 bearing,
811 speed.min(0.2 + (dist_sqrd - AVG_FOLLOW_DIST.powi(2)) / 8.0),
812 );
813 }
814 }
815
816 pub fn look_toward(
817 &self,
818 controller: &mut Controller,
819 read_data: &ReadData,
820 target: EcsEntity,
821 ) -> bool {
822 if let Some(tgt_pos) = read_data.positions.get(target)
823 && !is_steering(*self.entity, read_data)
824 {
825 let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
826 let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| {
827 b.eye_height(read_data.scales.get(target).map_or(1.0, |s| s.0))
828 });
829 if let Some(dir) = Dir::from_unnormalized(
830 Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
831 - Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
832 ) {
833 controller.inputs.look_dir = dir;
834 }
835 true
836 } else {
837 false
838 }
839 }
840
841 pub fn flee(
842 &self,
843 agent: &mut Agent,
844 controller: &mut Controller,
845 read_data: &ReadData,
846 tgt_pos: &Pos,
847 ) {
848 const MAX_FLEE_SPEED: f32 = 0.65;
850
851 self.dismount_uncontrollable(controller, read_data);
852
853 if let Some(body) = self.body {
854 if body.can_strafe() && !self.is_gliding {
855 controller.push_action(ControlAction::Unwield);
856 }
857 }
858
859 if let Some((bearing, speed)) = agent.chaser.chase(
860 &*read_data.terrain,
861 self.pos.0,
862 self.vel.0,
863 self.pos.0
865 + (self.pos.0 - tgt_pos.0)
866 .try_normalized()
867 .unwrap_or_else(Vec3::unit_y)
868 * 50.0,
869 TraversalConfig {
870 min_tgt_dist: 1.25,
871 ..self.traversal_config
872 },
873 ) {
874 self.traverse(controller, bearing, speed.min(MAX_FLEE_SPEED));
875 }
876 }
877
878 pub fn heal_self(
883 &self,
884 _agent: &mut Agent,
885 controller: &mut Controller,
886 relaxed: bool,
887 ) -> bool {
888 let heal_multiplier = self.stats.map_or(1.0, |s| s.item_effect_reduction);
890 if heal_multiplier < 0.5 {
891 return false;
892 }
893 let effect_healing_value = |effect: &Effect| -> (f32, f32) {
895 let mut value = 0.0;
896 let mut heal_reduction = 0.0;
897 match effect {
898 Effect::Health(HealthChange { amount, .. }) => {
899 value += *amount;
900 },
901 Effect::Buff(BuffEffect { kind, data, .. }) => {
902 if let Some(duration) = data.duration {
903 for effect in kind.effects(data) {
904 match effect {
905 comp::BuffEffect::HealthChangeOverTime { rate, kind, .. } => {
906 let amount = match kind {
907 comp::ModifierKind::Additive => rate * duration.0 as f32,
908 comp::ModifierKind::Multiplicative => {
909 (1.0 + rate).powf(duration.0 as f32)
910 },
911 };
912
913 value += amount;
914 },
915 comp::BuffEffect::ItemEffectReduction(amount) => {
916 heal_reduction =
917 heal_reduction + amount - heal_reduction * amount;
918 },
919 _ => {},
920 }
921 }
922 value += data.strength * data.duration.map_or(0.0, |d| d.0 as f32);
923 }
924 },
925
926 _ => {},
927 }
928
929 (value, heal_reduction)
930 };
931 let healing_value = |item: &Item| {
932 let mut value = 0.0;
933 let mut heal_multiplier_value = 1.0;
934
935 if let ItemKind::Consumable { kind, effects, .. } = &*item.kind() {
936 if matches!(kind, ConsumableKind::Drink)
937 || (relaxed && matches!(kind, ConsumableKind::Food))
938 {
939 match effects {
940 Effects::Any(effects) => {
941 for effect in effects.iter() {
943 let (add, red) = effect_healing_value(effect);
944 value += add / effects.len() as f32;
945 heal_multiplier_value *= 1.0 - red / effects.len() as f32;
946 }
947 },
948 Effects::All(_) | Effects::One(_) => {
949 for effect in effects.effects() {
950 let (add, red) = effect_healing_value(effect);
951 value += add;
952 heal_multiplier_value *= 1.0 - red;
953 }
954 },
955 }
956 }
957 }
958 if heal_multiplier_value < 1.0 && (heal_multiplier < 1.0 || relaxed) {
961 value *= 0.1;
962 }
963 value as i32
964 };
965
966 let item = self
967 .inventory
968 .slots_with_id()
969 .filter_map(|(id, slot)| match slot {
970 Some(item) if healing_value(item) > 0 => Some((id, item)),
971 _ => None,
972 })
973 .max_by_key(|(_, item)| {
974 if relaxed {
975 -healing_value(item)
976 } else {
977 healing_value(item)
978 }
979 });
980
981 if let Some((id, _)) = item {
982 use comp::inventory::slot::Slot;
983 controller.push_action(ControlAction::InventoryAction(InventoryAction::Use(
984 Slot::Inventory(id),
985 )));
986 true
987 } else {
988 false
989 }
990 }
991
992 pub fn choose_target(
993 &self,
994 agent: &mut Agent,
995 controller: &mut Controller,
996 read_data: &ReadData,
997 is_enemy: fn(&Self, EcsEntity, &ReadData) -> bool,
998 ) {
999 enum ActionStateTimers {
1000 TimerChooseTarget = 0,
1001 }
1002 agent.behavior_state.timers[ActionStateTimers::TimerChooseTarget as usize] = 0.0;
1003 let mut aggro_on = false;
1004
1005 let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
1008
1009 let entities_nearby = grid
1010 .in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist())
1011 .collect_vec();
1012
1013 let get_pos = |entity| read_data.positions.get(entity);
1014 let get_enemy = |(entity, attack_target): (EcsEntity, bool)| {
1015 if attack_target {
1016 if is_enemy(self, entity, read_data) {
1017 Some((entity, true))
1018 } else if self.should_defend(entity, read_data) {
1019 if let Some(attacker) = get_attacker(entity, read_data) {
1020 if !self.passive_towards(attacker, read_data) {
1021 aggro_on = true;
1023 Some((attacker, true))
1024 } else {
1025 None
1026 }
1027 } else {
1028 None
1029 }
1030 } else {
1031 None
1032 }
1033 } else {
1034 Some((entity, false))
1035 }
1036 };
1037 let is_valid_target = |entity: EcsEntity| match read_data.bodies.get(entity) {
1038 Some(Body::Item(item)) => {
1039 if !matches!(item, body::item::Body::Thrown(_)) {
1040 let is_humanoid = matches!(self.body, Some(Body::Humanoid(_)));
1041 let wants_pickup = is_humanoid || matches!(item, body::item::Body::Consumable);
1044
1045 let attempt_pickup = wants_pickup
1048 && read_data
1049 .loot_owners
1050 .get(entity).is_none_or(|loot_owner| {
1051 !(is_humanoid
1052 && loot_owner.is_soft()
1053 && loot_owner
1055 .uid()
1056 .and_then(|uid| read_data.id_maps.uid_entity(uid)).is_none_or(|entity| !is_enemy(self, entity, read_data)))
1057 && loot_owner.can_pickup(
1058 *self.uid,
1059 read_data.groups.get(entity),
1060 self.alignment,
1061 self.body,
1062 None,
1063 )
1064 });
1065
1066 if attempt_pickup {
1067 Some((entity, false))
1068 } else {
1069 None
1070 }
1071 } else {
1072 None
1073 }
1074 },
1075 _ => {
1076 if read_data
1077 .healths
1078 .get(entity)
1079 .is_some_and(|health| !health.is_dead && !is_invulnerable(entity, read_data))
1080 {
1081 let needs_saving = comp::is_downed(
1082 read_data.healths.get(entity),
1083 read_data.char_states.get(entity),
1084 );
1085
1086 let wants_to_save = match (self.alignment, read_data.alignments.get(entity)) {
1087 (Some(Alignment::Npc), _) if read_data.presences.get(entity).is_some_and(|presence| matches!(presence.kind, PresenceKind::Character(_))) => true,
1090 (Some(Alignment::Npc), Some(Alignment::Npc)) => true,
1091 (Some(Alignment::Enemy), Some(Alignment::Enemy)) => true,
1092 _ => false,
1093 } && agent.allowed_to_speak()
1094 && read_data
1096 .interactors
1097 .get(entity).is_none_or(|interactors| {
1098 !interactors.has_interaction(InteractionKind::HelpDowned)
1099 }) && self.char_state.can_interact();
1100
1101 Some((entity, !(needs_saving && wants_to_save)))
1103 } else {
1104 None
1105 }
1106 },
1107 };
1108
1109 let is_detected = |entity: &EcsEntity, e_pos: &Pos, e_scale: Option<&Scale>| {
1110 self.detects_other(agent, controller, entity, e_pos, e_scale, read_data)
1111 };
1112
1113 let target = entities_nearby
1114 .iter()
1115 .filter_map(|e| is_valid_target(*e))
1116 .filter_map(get_enemy)
1117 .filter_map(|(entity, attack_target)| {
1118 get_pos(entity).map(|pos| (entity, pos, attack_target))
1119 })
1120 .filter(|(entity, e_pos, _)| is_detected(entity, e_pos, read_data.scales.get(*entity)))
1121 .min_by_key(|(_, e_pos, attack_target)| {
1122 (
1123 *attack_target,
1124 (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32,
1125 )
1126 })
1127 .map(|(entity, _, attack_target)| (entity, attack_target));
1128
1129 if agent.target.is_none() && target.is_some() {
1130 if aggro_on {
1131 controller.push_utterance(UtteranceKind::Angry);
1132 } else {
1133 controller.push_utterance(UtteranceKind::Surprised);
1134 }
1135 }
1136 if agent.psyche.should_stop_pursuing || target.is_some() {
1137 agent.target = target.map(|(entity, attack_target)| Target {
1138 target: entity,
1139 hostile: attack_target,
1140 selected_at: read_data.time.0,
1141 aggro_on,
1142 last_known_pos: get_pos(entity).map(|pos| pos.0),
1143 })
1144 }
1145 }
1146
1147 pub fn attack(
1148 &self,
1149 agent: &mut Agent,
1150 controller: &mut Controller,
1151 tgt_data: &TargetData,
1152 read_data: &ReadData,
1153 rng: &mut impl Rng,
1154 ) {
1155 #[cfg(any(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
1156 let _rng = rng;
1157
1158 #[cfg(not(feature = "use-dyn-lib"))]
1159 {
1160 #[cfg(not(feature = "be-dyn-lib"))]
1161 self.attack_inner(agent, controller, tgt_data, read_data, rng);
1162 #[cfg(feature = "be-dyn-lib")]
1163 self.attack_inner(agent, controller, tgt_data, read_data);
1164 }
1165 #[cfg(feature = "use-dyn-lib")]
1166 {
1167 let lock = LIB.lock().unwrap();
1168 let lib = &lock.as_ref().unwrap().lib;
1169 const ATTACK_FN: &[u8] = b"attack_inner\0";
1170
1171 let attack_fn: common_dynlib::Symbol<
1172 fn(&Self, &mut Agent, &mut Controller, &TargetData, &ReadData),
1173 > = unsafe { lib.get(ATTACK_FN) }.unwrap_or_else(|e| {
1174 panic!(
1175 "Trying to use: {} but had error: {:?}",
1176 CStr::from_bytes_with_nul(ATTACK_FN)
1177 .map(CStr::to_str)
1178 .unwrap()
1179 .unwrap(),
1180 e
1181 )
1182 });
1183 attack_fn(self, agent, controller, tgt_data, read_data);
1184 }
1185 }
1186
1187 #[cfg_attr(feature = "be-dyn-lib", unsafe(export_name = "attack_inner"))]
1188 pub fn attack_inner(
1189 &self,
1190 agent: &mut Agent,
1191 controller: &mut Controller,
1192 tgt_data: &TargetData,
1193 read_data: &ReadData,
1194 #[cfg(not(feature = "be-dyn-lib"))] rng: &mut impl Rng,
1195 ) {
1196 #[cfg(feature = "be-dyn-lib")]
1197 let rng = &mut thread_rng();
1198
1199 self.dismount_uncontrollable(controller, read_data);
1200
1201 let tool_tactic = |tool_kind| match tool_kind {
1202 ToolKind::Bow => Tactic::Bow,
1203 ToolKind::Staff => Tactic::Staff,
1204 ToolKind::Sceptre => Tactic::Sceptre,
1205 ToolKind::Hammer => Tactic::Hammer,
1206 ToolKind::Sword | ToolKind::Blowgun => Tactic::Sword,
1207 ToolKind::Axe => Tactic::Axe,
1208 _ => Tactic::SimpleMelee,
1209 };
1210
1211 let tactic = self
1212 .inventory
1213 .equipped(EquipSlot::ActiveMainhand)
1214 .as_ref()
1215 .map(|item| {
1216 if let Some(ability_spec) = item.ability_spec() {
1217 match &*ability_spec {
1218 AbilitySpec::Custom(spec) => match spec.as_str() {
1219 "Oni" | "Sword Simple" | "BipedLargeCultistSword" => {
1220 Tactic::SwordSimple
1221 },
1222 "Staff Simple" | "BipedLargeCultistStaff" => Tactic::Staff,
1223 "BipedLargeCultistHammer" => Tactic::Hammer,
1224 "Simple Flying Melee" => Tactic::SimpleFlyingMelee,
1225 "Bow Simple" | "BipedLargeCultistBow" => Tactic::Bow,
1226 "Stone Golem" | "Coral Golem" => Tactic::StoneGolem,
1227 "Iron Golem" => Tactic::IronGolem,
1228 "Quad Med Quick" => Tactic::CircleCharge {
1229 radius: 5,
1230 circle_time: 2,
1231 },
1232 "Quad Med Jump" | "Darkhound" => Tactic::QuadMedJump,
1233 "Quad Med Charge" => Tactic::CircleCharge {
1234 radius: 6,
1235 circle_time: 1,
1236 },
1237 "Quad Med Basic" => Tactic::QuadMedBasic,
1238 "Quad Med Hoof" => Tactic::QuadMedHoof,
1239 "ClaySteed" => Tactic::ClaySteed,
1240 "Rocksnapper" => Tactic::Rocksnapper,
1241 "Roshwalr" => Tactic::Roshwalr,
1242 "Asp" | "Maneater" => Tactic::QuadLowRanged,
1243 "Quad Low Breathe" | "Quad Low Beam" | "Basilisk" => {
1244 Tactic::QuadLowBeam
1245 },
1246 "Organ" => Tactic::OrganAura,
1247 "Quad Low Tail" | "Husk Brute" => Tactic::TailSlap,
1248 "Quad Low Quick" => Tactic::QuadLowQuick,
1249 "Quad Low Basic" => Tactic::QuadLowBasic,
1250 "Theropod Basic" | "Theropod Bird" | "Theropod Small" => {
1251 Tactic::Theropod
1252 },
1253 "Antlion" => Tactic::ArthropodMelee,
1255 "Tarantula" | "Horn Beetle" => Tactic::ArthropodAmbush,
1256 "Weevil" | "Black Widow" | "Crawler" => Tactic::ArthropodRanged,
1257 "Theropod Charge" => Tactic::CircleCharge {
1258 radius: 6,
1259 circle_time: 1,
1260 },
1261 "Turret" => Tactic::RadialTurret,
1262 "Flamethrower" => Tactic::RadialTurret,
1263 "Haniwa Sentry" => Tactic::RotatingTurret,
1264 "Bird Large Breathe" => Tactic::BirdLargeBreathe,
1265 "Bird Large Fire" => Tactic::BirdLargeFire,
1266 "Bird Large Basic" => Tactic::BirdLargeBasic,
1267 "Flame Wyvern" | "Frost Wyvern" | "Cloud Wyvern" | "Sea Wyvern"
1268 | "Weald Wyvern" => Tactic::Wyvern,
1269 "Bird Medium Basic" => Tactic::BirdMediumBasic,
1270 "Bushly" | "Cactid" | "Irrwurz" | "Driggle" | "Mossy Snail"
1271 | "Strigoi Claws" | "Harlequin" => Tactic::SimpleDouble,
1272 "Clay Golem" => Tactic::ClayGolem,
1273 "Ancient Effigy" => Tactic::AncientEffigy,
1274 "TerracottaStatue" | "Mogwai" => Tactic::TerracottaStatue,
1275 "TerracottaBesieger" => Tactic::Bow,
1276 "TerracottaDemolisher" => Tactic::SimpleDouble,
1277 "TerracottaPunisher" => Tactic::SimpleMelee,
1278 "TerracottaPursuer" => Tactic::SwordSimple,
1279 "Cursekeeper" => Tactic::Cursekeeper,
1280 "CursekeeperFake" => Tactic::CursekeeperFake,
1281 "ShamanicSpirit" => Tactic::ShamanicSpirit,
1282 "Jiangshi" => Tactic::Jiangshi,
1283 "Mindflayer" => Tactic::Mindflayer,
1284 "Flamekeeper" => Tactic::Flamekeeper,
1285 "Forgemaster" => Tactic::Forgemaster,
1286 "Minotaur" => Tactic::Minotaur,
1287 "Cyclops" => Tactic::Cyclops,
1288 "Dullahan" => Tactic::Dullahan,
1289 "Grave Warden" => Tactic::GraveWarden,
1290 "Tidal Warrior" => Tactic::TidalWarrior,
1291 "Karkatha" => Tactic::Karkatha,
1292 "Tidal Totem"
1293 | "Tornado"
1294 | "Gnarling Totem Red"
1295 | "Gnarling Totem Green"
1296 | "Gnarling Totem White" => Tactic::RadialTurret,
1297 "FieryTornado" => Tactic::FieryTornado,
1298 "Yeti" => Tactic::Yeti,
1299 "Harvester" => Tactic::Harvester,
1300 "Cardinal" => Tactic::Cardinal,
1301 "Sea Bishop" => Tactic::SeaBishop,
1302 "Dagon" => Tactic::Dagon,
1303 "Snaretongue" => Tactic::Snaretongue,
1304 "Dagonite" => Tactic::ArthropodAmbush,
1305 "Gnarling Dagger" => Tactic::SimpleBackstab,
1306 "Gnarling Blowgun" => Tactic::ElevatedRanged,
1307 "Deadwood" => Tactic::Deadwood,
1308 "Mandragora" => Tactic::Mandragora,
1309 "Wood Golem" => Tactic::WoodGolem,
1310 "Gnarling Chieftain" => Tactic::GnarlingChieftain,
1311 "Frost Gigas" => Tactic::FrostGigas,
1312 "Boreal Hammer" => Tactic::BorealHammer,
1313 "Boreal Bow" => Tactic::BorealBow,
1314 "Adlet Hunter" => Tactic::AdletHunter,
1315 "Adlet Icepicker" => Tactic::AdletIcepicker,
1316 "Adlet Tracker" => Tactic::AdletTracker,
1317 "Hydra" => Tactic::Hydra,
1318 "Ice Drake" => Tactic::IceDrake,
1319 "Frostfang" => Tactic::RandomAbilities {
1320 primary: 1,
1321 secondary: 3,
1322 abilities: [0; BASE_ABILITY_LIMIT],
1323 },
1324 "Tursus Claws" => Tactic::RandomAbilities {
1325 primary: 2,
1326 secondary: 1,
1327 abilities: [4, 0, 0, 0, 0],
1328 },
1329 "Adlet Elder" => Tactic::AdletElder,
1330 "Haniwa Soldier" => Tactic::HaniwaSoldier,
1331 "Haniwa Guard" => Tactic::HaniwaGuard,
1332 "Haniwa Archer" => Tactic::HaniwaArcher,
1333 "Bloodmoon Bat" => Tactic::BloodmoonBat,
1334 "Vampire Bat" => Tactic::VampireBat,
1335 "Bloodmoon Heiress" => Tactic::BloodmoonHeiress,
1336
1337 _ => Tactic::SimpleMelee,
1338 },
1339 AbilitySpec::Tool(tool_kind) => tool_tactic(*tool_kind),
1340 }
1341 } else if let ItemKind::Tool(tool) = &*item.kind() {
1342 tool_tactic(tool.kind)
1343 } else {
1344 Tactic::SimpleMelee
1345 }
1346 })
1347 .unwrap_or(Tactic::SimpleMelee);
1348
1349 controller.push_action(ControlAction::Wield);
1351
1352 let self_radius = self.body.map_or(0.5, |b| b.max_radius()) * self.scale;
1355 let self_attack_range =
1356 (self.body.map_or(0.5, |b| b.front_radius()) + DEFAULT_ATTACK_RANGE) * self.scale;
1357 let tgt_radius =
1358 tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
1359 let min_attack_dist = self_attack_range + tgt_radius;
1360 let body_dist = self_radius + tgt_radius;
1361 let dist_sqrd = self.pos.0.distance_squared(tgt_data.pos.0);
1362 let angle = self
1363 .ori
1364 .look_vec()
1365 .angle_between(tgt_data.pos.0 - self.pos.0)
1366 .to_degrees();
1367 let angle_xy = self
1368 .ori
1369 .look_vec()
1370 .xy()
1371 .angle_between((tgt_data.pos.0 - self.pos.0).xy())
1372 .to_degrees();
1373
1374 let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
1375
1376 let tgt_eye_height = tgt_data
1377 .body
1378 .map_or(0.0, |b| b.eye_height(tgt_data.scale.map_or(1.0, |s| s.0)));
1379 let tgt_eye_offset = tgt_eye_height +
1380 if tactic == Tactic::QuadMedJump {
1385 1.0
1386 } else if matches!(tactic, Tactic::QuadLowRanged) {
1387 -1.0
1388 } else {
1389 0.0
1390 };
1391
1392 if let Some(dir) = match self.char_state {
1407 CharacterState::ChargedRanged(c) if dist_sqrd > 0.0 => {
1408 let charge_factor =
1409 c.timer.as_secs_f32() / c.static_data.charge_duration.as_secs_f32();
1410 let projectile_speed = c.static_data.initial_projectile_speed
1411 + charge_factor * c.static_data.scaled_projectile_speed;
1412 aim_projectile(
1413 projectile_speed,
1414 self.pos.0
1415 + self.body.map_or(Vec3::zero(), |body| {
1416 body.projectile_offsets(self.ori.look_vec(), self.scale)
1417 }),
1418 Vec3::new(
1419 tgt_data.pos.0.x,
1420 tgt_data.pos.0.y,
1421 tgt_data.pos.0.z + tgt_eye_offset,
1422 ),
1423 )
1424 },
1425 CharacterState::BasicRanged(c) => {
1426 let offset_z = match c.static_data.projectile.kind {
1427 ProjectileConstructorKind::Explosive { .. }
1429 | ProjectileConstructorKind::ExplosiveHazard { .. }
1430 | ProjectileConstructorKind::Hazard { .. } => 0.0,
1431 _ => tgt_eye_offset,
1432 };
1433 let projectile_speed = c.static_data.projectile_speed;
1434 aim_projectile(
1435 projectile_speed,
1436 self.pos.0
1437 + self.body.map_or(Vec3::zero(), |body| {
1438 body.projectile_offsets(self.ori.look_vec(), self.scale)
1439 }),
1440 Vec3::new(
1441 tgt_data.pos.0.x,
1442 tgt_data.pos.0.y,
1443 tgt_data.pos.0.z + offset_z,
1444 ),
1445 )
1446 },
1447 CharacterState::RepeaterRanged(c) => {
1448 let projectile_speed = c.static_data.projectile_speed;
1449 aim_projectile(
1450 projectile_speed,
1451 self.pos.0
1452 + self.body.map_or(Vec3::zero(), |body| {
1453 body.projectile_offsets(self.ori.look_vec(), self.scale)
1454 }),
1455 Vec3::new(
1456 tgt_data.pos.0.x,
1457 tgt_data.pos.0.y,
1458 tgt_data.pos.0.z + tgt_eye_offset,
1459 ),
1460 )
1461 },
1462 CharacterState::LeapMelee(_)
1463 if matches!(tactic, Tactic::Hammer | Tactic::BorealHammer | Tactic::Axe) =>
1464 {
1465 let direction_weight = match tactic {
1466 Tactic::Hammer | Tactic::BorealHammer => 0.1,
1467 Tactic::Axe => 0.3,
1468 _ => unreachable!("Direction weight called on incorrect tactic."),
1469 };
1470
1471 let tgt_pos = tgt_data.pos.0;
1472 let self_pos = self.pos.0;
1473
1474 let delta_x = (tgt_pos.x - self_pos.x) * direction_weight;
1475 let delta_y = (tgt_pos.y - self_pos.y) * direction_weight;
1476
1477 Dir::from_unnormalized(Vec3::new(delta_x, delta_y, -1.0))
1478 },
1479 CharacterState::BasicBeam(_) => {
1480 let aim_from = self.body.map_or(self.pos.0, |body| {
1481 self.pos.0
1482 + basic_beam::beam_offsets(
1483 body,
1484 controller.inputs.look_dir,
1485 self.ori.look_vec(),
1486 self.vel.0 - self.physics_state.ground_vel,
1488 self.physics_state.on_ground,
1489 )
1490 });
1491 let aim_to = Vec3::new(
1492 tgt_data.pos.0.x,
1493 tgt_data.pos.0.y,
1494 tgt_data.pos.0.z + tgt_eye_offset,
1495 );
1496 Dir::from_unnormalized(aim_to - aim_from)
1497 },
1498 _ => {
1499 let aim_from = Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset);
1500 let aim_to = Vec3::new(
1501 tgt_data.pos.0.x,
1502 tgt_data.pos.0.y,
1503 tgt_data.pos.0.z + tgt_eye_offset,
1504 );
1505 Dir::from_unnormalized(aim_to - aim_from)
1506 },
1507 } {
1508 controller.inputs.look_dir = dir;
1509 }
1510
1511 let attack_data = AttackData {
1512 body_dist,
1513 min_attack_dist,
1514 dist_sqrd,
1515 angle,
1516 angle_xy,
1517 };
1518
1519 match tactic {
1522 Tactic::SimpleFlyingMelee => self.handle_simple_flying_melee(
1523 agent,
1524 controller,
1525 &attack_data,
1526 tgt_data,
1527 read_data,
1528 rng,
1529 ),
1530 Tactic::SimpleMelee => {
1531 self.handle_simple_melee(agent, controller, &attack_data, tgt_data, read_data, rng)
1532 },
1533 Tactic::Axe => {
1534 self.handle_axe_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1535 },
1536 Tactic::Hammer => {
1537 self.handle_hammer_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1538 },
1539 Tactic::Sword => {
1540 self.handle_sword_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1541 },
1542 Tactic::Bow => {
1543 self.handle_bow_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1544 },
1545 Tactic::Staff => {
1546 self.handle_staff_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1547 },
1548 Tactic::Sceptre => self.handle_sceptre_attack(
1549 agent,
1550 controller,
1551 &attack_data,
1552 tgt_data,
1553 read_data,
1554 rng,
1555 ),
1556 Tactic::StoneGolem => {
1557 self.handle_stone_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1558 },
1559 Tactic::IronGolem => {
1560 self.handle_iron_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1561 },
1562 Tactic::CircleCharge {
1563 radius,
1564 circle_time,
1565 } => self.handle_circle_charge_attack(
1566 agent,
1567 controller,
1568 &attack_data,
1569 tgt_data,
1570 read_data,
1571 radius,
1572 circle_time,
1573 rng,
1574 ),
1575 Tactic::QuadLowRanged => self.handle_quadlow_ranged_attack(
1576 agent,
1577 controller,
1578 &attack_data,
1579 tgt_data,
1580 read_data,
1581 ),
1582 Tactic::TailSlap => {
1583 self.handle_tail_slap_attack(agent, controller, &attack_data, tgt_data, read_data)
1584 },
1585 Tactic::QuadLowQuick => self.handle_quadlow_quick_attack(
1586 agent,
1587 controller,
1588 &attack_data,
1589 tgt_data,
1590 read_data,
1591 ),
1592 Tactic::QuadLowBasic => self.handle_quadlow_basic_attack(
1593 agent,
1594 controller,
1595 &attack_data,
1596 tgt_data,
1597 read_data,
1598 ),
1599 Tactic::QuadMedJump => self.handle_quadmed_jump_attack(
1600 agent,
1601 controller,
1602 &attack_data,
1603 tgt_data,
1604 read_data,
1605 ),
1606 Tactic::QuadMedBasic => self.handle_quadmed_basic_attack(
1607 agent,
1608 controller,
1609 &attack_data,
1610 tgt_data,
1611 read_data,
1612 ),
1613 Tactic::QuadMedHoof => self.handle_quadmed_hoof_attack(
1614 agent,
1615 controller,
1616 &attack_data,
1617 tgt_data,
1618 read_data,
1619 ),
1620 Tactic::QuadLowBeam => self.handle_quadlow_beam_attack(
1621 agent,
1622 controller,
1623 &attack_data,
1624 tgt_data,
1625 read_data,
1626 ),
1627 Tactic::Rocksnapper => {
1628 self.handle_rocksnapper_attack(agent, controller, &attack_data, tgt_data, read_data)
1629 },
1630 Tactic::Roshwalr => {
1631 self.handle_roshwalr_attack(agent, controller, &attack_data, tgt_data, read_data)
1632 },
1633 Tactic::OrganAura => {
1634 self.handle_organ_aura_attack(agent, controller, &attack_data, tgt_data, read_data)
1635 },
1636 Tactic::Theropod => {
1637 self.handle_theropod_attack(agent, controller, &attack_data, tgt_data, read_data)
1638 },
1639 Tactic::ArthropodMelee => self.handle_arthropod_melee_attack(
1640 agent,
1641 controller,
1642 &attack_data,
1643 tgt_data,
1644 read_data,
1645 ),
1646 Tactic::ArthropodAmbush => self.handle_arthropod_ambush_attack(
1647 agent,
1648 controller,
1649 &attack_data,
1650 tgt_data,
1651 read_data,
1652 rng,
1653 ),
1654 Tactic::ArthropodRanged => self.handle_arthropod_ranged_attack(
1655 agent,
1656 controller,
1657 &attack_data,
1658 tgt_data,
1659 read_data,
1660 ),
1661 Tactic::Turret => {
1662 self.handle_turret_attack(agent, controller, &attack_data, tgt_data, read_data)
1663 },
1664 Tactic::FixedTurret => self.handle_fixed_turret_attack(
1665 agent,
1666 controller,
1667 &attack_data,
1668 tgt_data,
1669 read_data,
1670 ),
1671 Tactic::RotatingTurret => {
1672 self.handle_rotating_turret_attack(agent, controller, tgt_data, read_data)
1673 },
1674 Tactic::Mindflayer => self.handle_mindflayer_attack(
1675 agent,
1676 controller,
1677 &attack_data,
1678 tgt_data,
1679 read_data,
1680 rng,
1681 ),
1682 Tactic::Flamekeeper => {
1683 self.handle_flamekeeper_attack(agent, controller, &attack_data, tgt_data, read_data)
1684 },
1685 Tactic::Forgemaster => {
1686 self.handle_forgemaster_attack(agent, controller, &attack_data, tgt_data, read_data)
1687 },
1688 Tactic::BirdLargeFire => self.handle_birdlarge_fire_attack(
1689 agent,
1690 controller,
1691 &attack_data,
1692 tgt_data,
1693 read_data,
1694 rng,
1695 ),
1696 Tactic::BirdLargeBreathe => self.handle_birdlarge_breathe_attack(
1698 agent,
1699 controller,
1700 &attack_data,
1701 tgt_data,
1702 read_data,
1703 rng,
1704 ),
1705 Tactic::BirdLargeBasic => self.handle_birdlarge_basic_attack(
1706 agent,
1707 controller,
1708 &attack_data,
1709 tgt_data,
1710 read_data,
1711 ),
1712 Tactic::Wyvern => {
1713 self.handle_wyvern_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1714 },
1715 Tactic::BirdMediumBasic => {
1716 self.handle_simple_melee(agent, controller, &attack_data, tgt_data, read_data, rng)
1717 },
1718 Tactic::SimpleDouble => self.handle_simple_double_attack(
1719 agent,
1720 controller,
1721 &attack_data,
1722 tgt_data,
1723 read_data,
1724 ),
1725 Tactic::Jiangshi => {
1726 self.handle_jiangshi_attack(agent, controller, &attack_data, tgt_data, read_data)
1727 },
1728 Tactic::ClayGolem => {
1729 self.handle_clay_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1730 },
1731 Tactic::ClaySteed => {
1732 self.handle_clay_steed_attack(agent, controller, &attack_data, tgt_data, read_data)
1733 },
1734 Tactic::AncientEffigy => self.handle_ancient_effigy_attack(
1735 agent,
1736 controller,
1737 &attack_data,
1738 tgt_data,
1739 read_data,
1740 ),
1741 Tactic::TerracottaStatue => {
1742 self.handle_terracotta_statue_attack(agent, controller, &attack_data, read_data)
1743 },
1744 Tactic::Minotaur => {
1745 self.handle_minotaur_attack(agent, controller, &attack_data, tgt_data, read_data)
1746 },
1747 Tactic::Cyclops => {
1748 self.handle_cyclops_attack(agent, controller, &attack_data, tgt_data, read_data)
1749 },
1750 Tactic::Dullahan => {
1751 self.handle_dullahan_attack(agent, controller, &attack_data, tgt_data, read_data)
1752 },
1753 Tactic::GraveWarden => self.handle_grave_warden_attack(
1754 agent,
1755 controller,
1756 &attack_data,
1757 tgt_data,
1758 read_data,
1759 ),
1760 Tactic::TidalWarrior => self.handle_tidal_warrior_attack(
1761 agent,
1762 controller,
1763 &attack_data,
1764 tgt_data,
1765 read_data,
1766 ),
1767 Tactic::Karkatha => self.handle_karkatha_attack(
1768 agent,
1769 controller,
1770 &attack_data,
1771 tgt_data,
1772 read_data,
1773 rng,
1774 ),
1775 Tactic::RadialTurret => self.handle_radial_turret_attack(controller),
1776 Tactic::FieryTornado => self.handle_fiery_tornado_attack(agent, controller),
1777 Tactic::Yeti => {
1778 self.handle_yeti_attack(agent, controller, &attack_data, tgt_data, read_data)
1779 },
1780 Tactic::Harvester => self.handle_harvester_attack(
1781 agent,
1782 controller,
1783 &attack_data,
1784 tgt_data,
1785 read_data,
1786 rng,
1787 ),
1788 Tactic::Cardinal => self.handle_cardinal_attack(
1789 agent,
1790 controller,
1791 &attack_data,
1792 tgt_data,
1793 read_data,
1794 rng,
1795 ),
1796 Tactic::SeaBishop => self.handle_sea_bishop_attack(
1797 agent,
1798 controller,
1799 &attack_data,
1800 tgt_data,
1801 read_data,
1802 rng,
1803 ),
1804 Tactic::Cursekeeper => self.handle_cursekeeper_attack(
1805 agent,
1806 controller,
1807 &attack_data,
1808 tgt_data,
1809 read_data,
1810 rng,
1811 ),
1812 Tactic::CursekeeperFake => {
1813 self.handle_cursekeeper_fake_attack(controller, &attack_data)
1814 },
1815 Tactic::ShamanicSpirit => self.handle_shamanic_spirit_attack(
1816 agent,
1817 controller,
1818 &attack_data,
1819 tgt_data,
1820 read_data,
1821 ),
1822 Tactic::Dagon => {
1823 self.handle_dagon_attack(agent, controller, &attack_data, tgt_data, read_data)
1824 },
1825 Tactic::Snaretongue => {
1826 self.handle_snaretongue_attack(agent, controller, &attack_data, read_data)
1827 },
1828 Tactic::SimpleBackstab => {
1829 self.handle_simple_backstab(agent, controller, &attack_data, tgt_data, read_data)
1830 },
1831 Tactic::ElevatedRanged => {
1832 self.handle_elevated_ranged(agent, controller, &attack_data, tgt_data, read_data)
1833 },
1834 Tactic::Deadwood => {
1835 self.handle_deadwood(agent, controller, &attack_data, tgt_data, read_data)
1836 },
1837 Tactic::Mandragora => {
1838 self.handle_mandragora(agent, controller, &attack_data, tgt_data, read_data)
1839 },
1840 Tactic::WoodGolem => {
1841 self.handle_wood_golem(agent, controller, &attack_data, tgt_data, read_data, rng)
1842 },
1843 Tactic::GnarlingChieftain => self.handle_gnarling_chieftain(
1844 agent,
1845 controller,
1846 &attack_data,
1847 tgt_data,
1848 read_data,
1849 rng,
1850 ),
1851 Tactic::FrostGigas => self.handle_frostgigas_attack(
1852 agent,
1853 controller,
1854 &attack_data,
1855 tgt_data,
1856 read_data,
1857 rng,
1858 ),
1859 Tactic::BorealHammer => self.handle_boreal_hammer_attack(
1860 agent,
1861 controller,
1862 &attack_data,
1863 tgt_data,
1864 read_data,
1865 rng,
1866 ),
1867 Tactic::BorealBow => self.handle_boreal_bow_attack(
1868 agent,
1869 controller,
1870 &attack_data,
1871 tgt_data,
1872 read_data,
1873 rng,
1874 ),
1875 Tactic::SwordSimple => self.handle_sword_simple_attack(
1876 agent,
1877 controller,
1878 &attack_data,
1879 tgt_data,
1880 read_data,
1881 ),
1882 Tactic::AdletHunter => {
1883 self.handle_adlet_hunter(agent, controller, &attack_data, tgt_data, read_data, rng)
1884 },
1885 Tactic::AdletIcepicker => {
1886 self.handle_adlet_icepicker(agent, controller, &attack_data, tgt_data, read_data)
1887 },
1888 Tactic::AdletTracker => {
1889 self.handle_adlet_tracker(agent, controller, &attack_data, tgt_data, read_data)
1890 },
1891 Tactic::IceDrake => {
1892 self.handle_icedrake(agent, controller, &attack_data, tgt_data, read_data, rng)
1893 },
1894 Tactic::Hydra => {
1895 self.handle_hydra(agent, controller, &attack_data, tgt_data, read_data, rng)
1896 },
1897 Tactic::BloodmoonBat => self.handle_bloodmoon_bat_attack(
1898 agent,
1899 controller,
1900 &attack_data,
1901 tgt_data,
1902 read_data,
1903 rng,
1904 ),
1905 Tactic::VampireBat => self.handle_vampire_bat_attack(
1906 agent,
1907 controller,
1908 &attack_data,
1909 tgt_data,
1910 read_data,
1911 rng,
1912 ),
1913 Tactic::BloodmoonHeiress => self.handle_bloodmoon_heiress_attack(
1914 agent,
1915 controller,
1916 &attack_data,
1917 tgt_data,
1918 read_data,
1919 rng,
1920 ),
1921 Tactic::RandomAbilities {
1922 primary,
1923 secondary,
1924 abilities,
1925 } => self.handle_random_abilities(
1926 agent,
1927 controller,
1928 &attack_data,
1929 tgt_data,
1930 read_data,
1931 rng,
1932 primary,
1933 secondary,
1934 abilities,
1935 ),
1936 Tactic::AdletElder => {
1937 self.handle_adlet_elder(agent, controller, &attack_data, tgt_data, read_data, rng)
1938 },
1939 Tactic::HaniwaSoldier => {
1940 self.handle_haniwa_soldier(agent, controller, &attack_data, tgt_data, read_data)
1941 },
1942 Tactic::HaniwaGuard => {
1943 self.handle_haniwa_guard(agent, controller, &attack_data, tgt_data, read_data, rng)
1944 },
1945 Tactic::HaniwaArcher => {
1946 self.handle_haniwa_archer(agent, controller, &attack_data, tgt_data, read_data)
1947 },
1948 }
1949 }
1950
1951 pub fn handle_sounds_heard(
1952 &self,
1953 agent: &mut Agent,
1954 controller: &mut Controller,
1955 read_data: &ReadData,
1956 emitters: &mut AgentEmitters,
1957 rng: &mut impl Rng,
1958 ) {
1959 agent.forget_old_sounds(read_data.time.0);
1960
1961 if is_invulnerable(*self.entity, read_data) || is_steering(*self.entity, read_data) {
1962 self.idle(agent, controller, read_data, emitters, rng);
1963 return;
1964 }
1965
1966 if let Some(sound) = agent.sounds_heard.last() {
1967 let sound_pos = Pos(sound.pos);
1968 let dist_sqrd = self.pos.0.distance_squared(sound_pos.0);
1969 let is_close = dist_sqrd < 35.0_f32.powi(2);
1974
1975 let sound_was_loud = sound.vol >= 10.0;
1976 let sound_was_threatening = sound_was_loud
1977 || matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _));
1978
1979 let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
1980 let follows_threatening_sounds =
1981 has_enemy_alignment || is_village_guard(*self.entity, read_data);
1982
1983 if sound_was_threatening && is_close {
1984 if !self.below_flee_health(agent) && follows_threatening_sounds {
1985 self.follow(agent, controller, read_data, &sound_pos);
1986 } else if self.below_flee_health(agent) || !follows_threatening_sounds {
1987 self.flee(agent, controller, read_data, &sound_pos);
1988 } else {
1989 self.idle(agent, controller, read_data, emitters, rng);
1990 }
1991 } else {
1992 self.idle(agent, controller, read_data, emitters, rng);
1993 }
1994 } else {
1995 self.idle(agent, controller, read_data, emitters, rng);
1996 }
1997 }
1998
1999 pub fn attack_target_attacker(
2000 &self,
2001 agent: &mut Agent,
2002 read_data: &ReadData,
2003 controller: &mut Controller,
2004 emitters: &mut AgentEmitters,
2005 rng: &mut impl Rng,
2006 ) {
2007 if let Some(Target { target, .. }) = agent.target {
2008 if let Some(tgt_health) = read_data.healths.get(target) {
2009 if let Some(by) = tgt_health.last_change.damage_by() {
2010 if let Some(attacker) = get_entity_by_id(by.uid(), read_data) {
2011 if agent.target.is_none() {
2012 controller.push_utterance(UtteranceKind::Angry);
2013 }
2014
2015 let attacker_pos = read_data.positions.get(attacker).map(|pos| pos.0);
2016 agent.target = Some(Target::new(
2017 attacker,
2018 true,
2019 read_data.time.0,
2020 true,
2021 attacker_pos,
2022 ));
2023
2024 if let Some(tgt_pos) = read_data.positions.get(attacker) {
2025 if is_dead_or_invulnerable(attacker, read_data) {
2026 agent.target = Some(Target::new(
2027 target,
2028 false,
2029 read_data.time.0,
2030 false,
2031 Some(tgt_pos.0),
2032 ));
2033
2034 self.idle(agent, controller, read_data, emitters, rng);
2035 } else {
2036 let target_data = TargetData::new(tgt_pos, target, read_data);
2037 self.attack(agent, controller, &target_data, read_data, rng);
2044 }
2045 }
2046 }
2047 }
2048 }
2049 }
2050 }
2051
2052 pub fn chat_npc_if_allowed_to_speak(
2055 &self,
2056 msg: Content,
2057 agent: &Agent,
2058 emitters: &mut AgentEmitters,
2059 ) -> bool {
2060 if agent.allowed_to_speak() {
2061 self.chat_npc(msg, emitters);
2062 true
2063 } else {
2064 false
2065 }
2066 }
2067
2068 pub fn chat_npc(&self, content: Content, emitters: &mut AgentEmitters) {
2069 emitters.emit(ChatEvent {
2070 msg: UnresolvedChatMsg::npc(*self.uid, content),
2071 from_client: false,
2072 });
2073 }
2074
2075 fn emit_scream(&self, time: f64, emitters: &mut AgentEmitters) {
2076 if let Some(body) = self.body {
2077 emitters.emit(SoundEvent {
2078 sound: Sound::new(
2079 SoundKind::Utterance(UtteranceKind::Scream, *body),
2080 self.pos.0,
2081 13.0,
2082 time,
2083 ),
2084 });
2085 }
2086 }
2087
2088 pub fn cry_out(&self, agent: &Agent, emitters: &mut AgentEmitters, read_data: &ReadData) {
2089 let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
2090 let is_below_flee_health = self.below_flee_health(agent);
2091
2092 if has_enemy_alignment && is_below_flee_health {
2093 self.chat_npc_if_allowed_to_speak(
2094 Content::localized("npc-speech-cultist_low_health_fleeing"),
2095 agent,
2096 emitters,
2097 );
2098 } else if is_villager(self.alignment) {
2099 self.chat_npc_if_allowed_to_speak(
2100 Content::localized("npc-speech-villager_under_attack"),
2101 agent,
2102 emitters,
2103 );
2104 self.emit_scream(read_data.time.0, emitters);
2105 }
2106 }
2107
2108 pub fn exclaim_relief_about_enemy_dead(&self, agent: &Agent, emitters: &mut AgentEmitters) {
2109 if is_villager(self.alignment) {
2110 self.chat_npc_if_allowed_to_speak(
2111 Content::localized("npc-speech-villager_enemy_killed"),
2112 agent,
2113 emitters,
2114 );
2115 }
2116 }
2117
2118 pub fn below_flee_health(&self, agent: &Agent) -> bool {
2119 self.damage.min(1.0) < agent.psyche.flee_health
2120 }
2121
2122 pub fn is_more_dangerous_than_target(
2123 &self,
2124 entity: EcsEntity,
2125 target: Target,
2126 read_data: &ReadData,
2127 ) -> bool {
2128 let entity_pos = read_data.positions.get(entity);
2129 let target_pos = read_data.positions.get(target.target);
2130
2131 entity_pos.is_some_and(|entity_pos| {
2132 target_pos.is_none_or(|target_pos| {
2133 const FUZZY_DIST_COMPARISON: f32 = 0.8;
2138
2139 let is_target_further = target_pos.0.distance(entity_pos.0)
2140 < target_pos.0.distance(entity_pos.0) * FUZZY_DIST_COMPARISON;
2141 let is_entity_hostile = read_data
2142 .alignments
2143 .get(entity)
2144 .zip(self.alignment)
2145 .is_some_and(|(entity, me)| me.hostile_towards(*entity));
2146
2147 !target.aggro_on || (is_target_further && is_entity_hostile)
2150 })
2151 })
2152 }
2153
2154 pub fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2155 let other_alignment = read_data.alignments.get(entity);
2156
2157 (entity != *self.entity)
2158 && !self.passive_towards(entity, read_data)
2159 && (are_our_owners_hostile(self.alignment, other_alignment, read_data)
2160 || (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data)))
2161 }
2162
2163 pub fn is_hunting_animal(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2164 (entity != *self.entity)
2165 && !self.friendly_towards(entity, read_data)
2166 && matches!(read_data.bodies.get(entity), Some(Body::QuadrupedSmall(_)))
2167 }
2168
2169 fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2170 let entity_alignment = read_data.alignments.get(entity);
2171
2172 let we_are_friendly = entity_alignment.is_some_and(|entity_alignment| {
2173 self.alignment
2174 .is_some_and(|alignment| !alignment.hostile_towards(*entity_alignment))
2175 });
2176 let we_share_species = read_data.bodies.get(entity).is_some_and(|entity_body| {
2177 self.body.is_some_and(|body| {
2178 entity_body.is_same_species_as(body)
2179 || (entity_body.is_humanoid() && body.is_humanoid())
2180 })
2181 });
2182 let self_owns_entity =
2183 matches!(entity_alignment, Some(Alignment::Owned(ouid)) if *self.uid == *ouid);
2184
2185 (we_are_friendly && we_share_species)
2186 || (is_village_guard(*self.entity, read_data) && is_villager(entity_alignment))
2187 || self_owns_entity
2188 }
2189
2190 fn passive_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2191 if let (Some(self_alignment), Some(other_alignment)) =
2192 (self.alignment, read_data.alignments.get(entity))
2193 {
2194 self_alignment.passive_towards(*other_alignment)
2195 } else {
2196 false
2197 }
2198 }
2199
2200 fn friendly_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2201 if let (Some(self_alignment), Some(other_alignment)) =
2202 (self.alignment, read_data.alignments.get(entity))
2203 {
2204 self_alignment.friendly_towards(*other_alignment)
2205 } else {
2206 false
2207 }
2208 }
2209
2210 pub fn can_see_entity(
2211 &self,
2212 agent: &Agent,
2213 controller: &Controller,
2214 other: EcsEntity,
2215 other_pos: &Pos,
2216 other_scale: Option<&Scale>,
2217 read_data: &ReadData,
2218 ) -> bool {
2219 let other_stealth_multiplier = {
2220 let other_inventory = read_data.inventories.get(other);
2221 let other_char_state = read_data.char_states.get(other);
2222
2223 perception_dist_multiplier_from_stealth(other_inventory, other_char_state, self.msm)
2224 };
2225
2226 let within_sight_dist = {
2227 let sight_dist = agent.psyche.sight_dist * other_stealth_multiplier;
2228 let dist_sqrd = other_pos.0.distance_squared(self.pos.0);
2229
2230 dist_sqrd < sight_dist.powi(2)
2231 };
2232
2233 let within_fov = (other_pos.0 - self.pos.0)
2234 .try_normalized()
2235 .is_some_and(|v| v.dot(*controller.inputs.look_dir) > 0.15);
2236
2237 let other_body = read_data.bodies.get(other);
2238
2239 (within_sight_dist)
2240 && within_fov
2241 && entities_have_line_of_sight(
2242 self.pos,
2243 self.body,
2244 self.scale,
2245 other_pos,
2246 other_body,
2247 other_scale,
2248 read_data,
2249 )
2250 }
2251
2252 pub fn detects_other(
2253 &self,
2254 agent: &Agent,
2255 controller: &Controller,
2256 other: &EcsEntity,
2257 other_pos: &Pos,
2258 other_scale: Option<&Scale>,
2259 read_data: &ReadData,
2260 ) -> bool {
2261 self.can_sense_directly_near(other_pos)
2262 || self.can_see_entity(agent, controller, *other, other_pos, other_scale, read_data)
2263 }
2264
2265 pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool {
2266 let chance = thread_rng().gen_bool(0.3);
2267 e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) && chance
2268 }
2269
2270 pub fn menacing(
2271 &self,
2272 agent: &mut Agent,
2273 controller: &mut Controller,
2274 target: EcsEntity,
2275 read_data: &ReadData,
2276 emitters: &mut AgentEmitters,
2277 rng: &mut impl Rng,
2278 remembers_fight_with_target: bool,
2279 ) {
2280 let max_move = 0.5;
2281 let move_dir = controller.inputs.move_dir;
2282 let move_dir_mag = move_dir.magnitude();
2283 let small_chance = rng.gen::<f32>() < read_data.dt.0 * 0.25;
2284 let mut chat = |content: Content| {
2285 self.chat_npc_if_allowed_to_speak(content, agent, emitters);
2286 };
2287 let mut chat_villager_remembers_fighting = || {
2288 let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
2289
2290 if let Some(tgt_name) = tgt_name.as_ref().and_then(|name| name.as_plain()) {
2293 chat(Content::localized_with_args(
2294 "npc-speech-remembers-fight",
2295 [("name", tgt_name)],
2296 ))
2297 } else {
2298 chat(Content::localized("npc-speech-remembers-fight-no-name"));
2299 }
2300 };
2301
2302 self.look_toward(controller, read_data, target);
2303 controller.push_action(ControlAction::Wield);
2304
2305 if move_dir_mag > max_move {
2306 controller.inputs.move_dir = max_move * move_dir / move_dir_mag;
2307 }
2308
2309 if small_chance {
2310 controller.push_utterance(UtteranceKind::Angry);
2311 if is_villager(self.alignment) {
2312 if remembers_fight_with_target {
2313 chat_villager_remembers_fighting();
2314 } else if is_dressed_as_cultist(target, read_data) {
2315 chat(Content::localized("npc-speech-villager_cultist_alarm"));
2316 } else {
2317 chat(Content::localized("npc-speech-menacing"));
2318 }
2319 } else {
2320 chat(Content::localized("npc-speech-menacing"));
2321 }
2322 }
2323 }
2324
2325 pub fn dismount_uncontrollable(&self, controller: &mut Controller, read_data: &ReadData) {
2327 if read_data.is_riders.get(*self.entity).is_some_and(|mount| {
2328 read_data
2329 .id_maps
2330 .uid_entity(mount.mount)
2331 .and_then(|e| read_data.bodies.get(e))
2332 .is_none_or(|b| matches!(b, Body::Humanoid(_)))
2335 }) || read_data
2336 .is_volume_riders
2337 .get(*self.entity)
2338 .is_some_and(|r| !r.is_steering_entity())
2339 {
2340 controller.push_event(ControlEvent::Unmount);
2341 }
2342 }
2343
2344 pub fn dismount(&self, controller: &mut Controller, read_data: &ReadData) {
2349 if read_data.is_riders.contains(*self.entity)
2350 || read_data
2351 .is_volume_riders
2352 .get(*self.entity)
2353 .is_some_and(|r| !r.is_steering_entity())
2354 {
2355 controller.push_event(ControlEvent::Unmount);
2356 }
2357 }
2358}