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