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