1use crate::{
2 consts::{
3 AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, MAX_PATROL_DIST,
4 SEPARATION_BIAS, SEPARATION_DIST, STD_AWARENESS_DECAY_RATE,
5 },
6 data::{AgentData, AgentEmitters, AttackData, Path, ReadData, Tactic, TargetData},
7 util::{
8 are_our_owners_hostile, entities_have_line_of_sight, get_attacker, get_entity_by_id,
9 is_dead_or_invulnerable, is_dressed_as_cultist, is_dressed_as_pirate, is_dressed_as_witch,
10 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::aim_projectile,
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, utils::StageSection},
38 terrain::Block,
39 time::DayPeriod,
40 util::Dir,
41 vol::ReadVol,
42};
43use itertools::Itertools;
44use rand::{RngExt, 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 pos_difference = tgt_pos - self.pos.0;
106 let pathing_pos = match path {
107 Path::Separate => {
108 let mut sep_vec: Vec3<f32> = Vec3::zero();
109
110 for entity in read_data
111 .cached_spatial_grid
112 .0
113 .in_circle_aabr(self.pos.0.xy(), SEPARATION_DIST)
114 {
115 if let (Some(alignment), Some(other_alignment)) =
116 (self.alignment, read_data.alignments.get(entity))
117 && Alignment::passive_towards(*alignment, *other_alignment)
118 && let (Some(pos), Some(body), Some(other_body)) = (
119 read_data.positions.get(entity),
120 self.body,
121 read_data.bodies.get(entity),
122 )
123 {
124 let dist_xy = self.pos.0.xy().distance(pos.0.xy());
125 let spacing = body.spacing_radius() + other_body.spacing_radius();
126 if dist_xy < spacing {
127 let pos_diff = self.pos.0.xy() - pos.0.xy();
128 sep_vec += pos_diff.try_normalized().unwrap_or_else(Vec2::zero)
129 * ((spacing - dist_xy) / spacing);
130 }
131 }
132 }
133
134 tgt_pos + sep_vec * SEPARATION_BIAS + pos_difference * (1.0 - SEPARATION_BIAS)
135 },
136 Path::AtTarget => tgt_pos,
137 };
138 let speed_multiplier = speed_multiplier.unwrap_or(1.0).min(1.0);
139
140 let in_loaded_chunk = |pos: Vec3<f32>| {
141 read_data
142 .terrain
143 .contains_key(read_data.terrain.pos_key(pos.map(|e| e.floor() as i32)))
144 };
145
146 let is_target_loaded = in_loaded_chunk(pathing_pos);
151
152 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
153 &*read_data.terrain,
154 self.pos.0,
155 self.vel.0,
156 pathing_pos,
157 TraversalConfig {
158 min_tgt_dist: 0.25,
159 is_target_loaded,
160 ..self.traversal_config
161 },
162 &read_data.time,
163 ) {
164 self.unstuck_if(stuck, controller);
165 self.traverse(controller, bearing, speed * speed_multiplier);
166 Some(bearing)
167 } else {
168 None
169 }
170 }
171
172 fn traverse(&self, controller: &mut Controller, bearing: Vec3<f32>, speed: f32) {
173 controller.inputs.move_dir =
174 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
175
176 self.jump_if(
178 (self.physics_state.on_ground.is_some() && bearing.z > 1.5)
179 || self.traversal_config.can_fly,
180 controller,
181 );
182 controller.inputs.move_z = bearing.z;
183 }
184
185 pub fn unstuck_if(&self, condition: bool, controller: &mut Controller) {
186 if condition && rng().random_bool(0.05) {
187 if matches!(self.char_state, CharacterState::Climb(_)) || rng().random_bool(0.5) {
188 controller.push_basic_input(InputKind::Jump);
189 } else {
190 controller.push_basic_input(InputKind::Roll);
191 }
192 } else {
193 if controller.queued_inputs.contains_key(&InputKind::Jump) {
194 controller.push_cancel_input(InputKind::Jump);
195 }
196 if controller.queued_inputs.contains_key(&InputKind::Roll) {
197 controller.push_cancel_input(InputKind::Roll);
198 }
199 }
200 }
201
202 pub fn jump_if(&self, condition: bool, controller: &mut Controller) {
203 if condition {
204 controller.push_basic_input(InputKind::Jump);
205 } else if controller.queued_inputs.contains_key(&InputKind::Jump) {
206 controller.push_cancel_input(InputKind::Jump)
207 }
208 }
209
210 pub fn idle(
211 &self,
212 agent: &mut Agent,
213 controller: &mut Controller,
214 read_data: &ReadData,
215 _emitters: &mut AgentEmitters,
216 rng: &mut impl RngExt,
217 ) {
218 enum ActionTimers {
219 TimerIdle = 0,
220 }
221
222 agent
223 .awareness
224 .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
225
226 let lantern_equipped = self
229 .inventory
230 .equipped(EquipSlot::Lantern)
231 .as_ref()
232 .is_some_and(|item| matches!(&*item.kind(), comp::item::ItemKind::Lantern(_)));
233 let lantern_turned_on = self.light_emitter.is_some();
234 let day_period = DayPeriod::from(read_data.time_of_day.0);
235 if lantern_equipped && rng.random_bool(0.001) {
237 if day_period.is_dark() && !lantern_turned_on {
238 controller.push_event(ControlEvent::EnableLantern)
243 } else if lantern_turned_on && day_period.is_light() {
244 controller.push_event(ControlEvent::DisableLantern)
247 }
248 };
249
250 if let Some(body) = self.body {
251 let attempt_heal = if matches!(body, Body::Humanoid(_)) {
252 self.damage < IDLE_HEALING_ITEM_THRESHOLD
253 } else {
254 true
255 };
256 if attempt_heal && self.heal_self(agent, controller, true) {
257 agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.01;
258 return;
259 }
260 } else {
261 agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.01;
262 return;
263 }
264
265 agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
266
267 'activity: {
268 match agent.rtsim_controller.activity {
269 Some(NpcActivity::Goto(travel_to, speed_factor)) => {
270 self.dismount_uncontrollable(controller, read_data);
271
272 agent.bearing = Vec2::zero();
273
274 if self.traversal_config.can_fly
277 && !read_data
278 .terrain
279 .ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
280 .until(Block::is_solid)
281 .cast()
282 .1
283 .map_or(true, |b| b.is_some())
284 {
285 controller.push_basic_input(InputKind::Fly);
286 } else {
287 controller.push_cancel_input(InputKind::Fly)
288 }
289
290 if let Some(bearing) = self.path_toward_target(
291 agent,
292 controller,
293 travel_to,
294 read_data,
295 Path::AtTarget,
296 Some(speed_factor),
297 ) {
298 let height_offset = bearing.z
299 + if self.traversal_config.can_fly {
300 let obstacle_ahead = read_data
302 .terrain
303 .ray(
304 self.pos.0 + Vec3::unit_z(),
305 self.pos.0
306 + bearing.try_normalized().unwrap_or_else(Vec3::unit_y)
307 * 80.0
308 + Vec3::unit_z(),
309 )
310 .until(Block::is_solid)
311 .cast()
312 .1
313 .map_or(true, |b| b.is_some());
314
315 let mut ground_too_close = self
316 .body
317 .map(|body| {
318 #[cfg(feature = "worldgen")]
319 let height_approx = self.pos.0.z
320 - read_data
321 .world
322 .sim()
323 .get_alt_approx(
324 self.pos.0.xy().map(|x: f32| x as i32),
325 )
326 .unwrap_or(0.0);
327 #[cfg(not(feature = "worldgen"))]
328 let height_approx = self.pos.0.z;
329
330 height_approx < body.flying_height()
331 })
332 .unwrap_or(false);
333
334 const NUM_RAYS: usize = 5;
335
336 for i in 0..=NUM_RAYS {
338 let magnitude = self.body.map_or(20.0, |b| b.flying_height());
339 if let Some(dir) = Lerp::lerp(
344 -Vec3::unit_z(),
345 Vec3::new(bearing.x, bearing.y, 0.0),
346 i as f32 / NUM_RAYS as f32,
347 )
348 .try_normalized()
349 {
350 ground_too_close |= read_data
351 .terrain
352 .ray(self.pos.0, self.pos.0 + magnitude * dir)
353 .until(|b: &Block| b.is_solid() || b.is_liquid())
354 .cast()
355 .1
356 .is_ok_and(|b| b.is_some())
357 }
358 }
359
360 if obstacle_ahead || ground_too_close {
361 5.0 } else {
363 -2.0
364 } } else {
366 0.05 };
368
369 if let Some(mpid) = agent.multi_pid_controllers.as_mut() {
370 if let Some(z_controller) = mpid.z_controller.as_mut() {
371 z_controller.sp = self.pos.0.z + height_offset;
372 controller.inputs.move_z = z_controller.calc_err();
373 z_controller.limit_integral_windup(|z| *z = z.clamp(-10.0, 10.0));
375 } else {
376 controller.inputs.move_z = 0.0;
377 }
378 } else {
379 controller.inputs.move_z = height_offset;
380 }
381 }
382
383 if rng.random_bool(0.1)
385 && matches!(
386 read_data.char_states.get(*self.entity),
387 Some(CharacterState::Wielding(_))
388 )
389 {
390 controller.push_action(ControlAction::Unwield);
391 }
392 break 'activity; },
394
395 Some(NpcActivity::GotoFlying(
396 travel_to,
397 speed_factor,
398 height_offset,
399 direction_override,
400 flight_mode,
401 )) => {
402 self.dismount_uncontrollable(controller, read_data);
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(None));
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.random::<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.rtsim_entity(target)
623 && let Some(target_uid) = read_data.uids.get(target)
624 {
625 controller.push_action(ControlAction::Stand);
627 self.look_toward(controller, read_data, target);
628 controller.push_action(ControlAction::Talk(Some(*target_uid)));
629 break 'activity;
630 }
631 },
632 None => {},
633 }
634
635 let owner_uid = self
636 .alignment
637 .and_then(|alignment| match_some!(alignment, Alignment::Owned(uid) => uid));
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.random_bool(0.0001) {
654 self.dismount_uncontrollable(controller, read_data);
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.random_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.random::<f32>() - 0.5, rng.random::<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.random_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.random::<f32>() < 0.0015 {
790 controller.push_utterance(UtteranceKind::Calm);
791 }
792
793 if rng.random::<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 self.dismount_uncontrollable(controller, read_data);
808
809 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
810 &*read_data.terrain,
811 self.pos.0,
812 self.vel.0,
813 tgt_pos.0,
814 TraversalConfig {
815 min_tgt_dist: AVG_FOLLOW_DIST,
816 ..self.traversal_config
817 },
818 &read_data.time,
819 ) {
820 self.unstuck_if(stuck, controller);
821 let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
822 self.traverse(
823 controller,
824 bearing,
825 speed.min(0.2 + (dist_sqrd - AVG_FOLLOW_DIST.powi(2)) / 8.0),
826 );
827 }
828 }
829
830 pub fn look_toward(
831 &self,
832 controller: &mut Controller,
833 read_data: &ReadData,
834 target: EcsEntity,
835 ) -> bool {
836 if let Some(tgt_pos) = read_data.positions.get(target)
837 && !is_steering(*self.entity, read_data)
838 && let Some(dir) = Dir::look_toward(
839 self.pos,
840 self.body,
841 Some(&comp::Scale(self.scale)),
842 tgt_pos,
843 read_data.bodies.get(target),
844 read_data.scales.get(target),
845 )
846 {
847 controller.inputs.look_dir = dir;
848 true
849 } else {
850 false
851 }
852 }
853
854 pub fn flee(
855 &self,
856 agent: &mut Agent,
857 controller: &mut Controller,
858 read_data: &ReadData,
859 tgt_pos: &Pos,
860 ) {
861 const MAX_FLEE_SPEED: f32 = 0.65;
863
864 self.dismount_uncontrollable(controller, read_data);
865
866 if let Some(body) = self.body
867 && body.can_strafe()
868 && !self.is_gliding
869 {
870 controller.push_action(ControlAction::Unwield);
871 }
872
873 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
874 &*read_data.terrain,
875 self.pos.0,
876 self.vel.0,
877 self.pos.0
879 + (self.pos.0 - tgt_pos.0)
880 .try_normalized()
881 .unwrap_or_else(Vec3::unit_y)
882 * 50.0,
883 TraversalConfig {
884 min_tgt_dist: 1.25,
885 ..self.traversal_config
886 },
887 &read_data.time,
888 ) {
889 self.unstuck_if(stuck, controller);
890 self.traverse(controller, bearing, speed.min(MAX_FLEE_SPEED));
891 }
892 }
893
894 pub fn heal_self(
899 &self,
900 _agent: &mut Agent,
901 controller: &mut Controller,
902 relaxed: bool,
903 ) -> bool {
904 if self.buffs.is_some_and(|buffs| {
906 buffs.iter_active().flatten().any(|buff| {
907 buff.kind.effects(&buff.data, None).iter().any(|effect| {
910 if let comp::BuffEffect::HealthChangeOverTime { rate, .. } = effect
911 && *rate > 0.0
912 {
913 true
914 } else {
915 false
916 }
917 })
918 })
919 }) {
920 return false;
921 }
922
923 let heal_multiplier = self.stats.map_or(1.0, |s| s.item_effect_reduction);
925 if heal_multiplier < 0.5 {
926 return false;
927 }
928 let effect_healing_value = |effect: &Effect| -> (f32, f32) {
930 let mut value = 0.0;
931 let mut heal_reduction = 0.0;
932 match effect {
933 Effect::Health(HealthChange { amount, .. }) => {
934 value += *amount;
935 },
936 Effect::Buff(BuffEffect { kind, data, .. }) => {
937 if let Some(duration) = data.duration {
938 for effect in kind.effects(data, None) {
941 match effect {
942 comp::BuffEffect::HealthChangeOverTime { rate, kind, .. } => {
943 let amount = match kind {
944 comp::ModifierKind::Additive => rate * duration.0 as f32,
945 comp::ModifierKind::Multiplicative => {
946 (1.0 + rate).powf(duration.0 as f32)
947 },
948 };
949
950 value += amount;
951 },
952 comp::BuffEffect::ItemEffectReduction(amount) => {
953 heal_reduction =
954 heal_reduction + amount - heal_reduction * amount;
955 },
956 _ => {},
957 }
958 }
959 value += data.strength * data.duration.map_or(0.0, |d| d.0 as f32);
960 }
961 },
962
963 _ => {},
964 }
965
966 (value, heal_reduction)
967 };
968 let healing_value = |item: &Item| {
969 let mut value = 0.0;
970 let mut heal_multiplier_value = 1.0;
971
972 if let ItemKind::Consumable { kind, effects, .. } = &*item.kind()
973 && (matches!(kind, ConsumableKind::Drink)
974 || (relaxed && matches!(kind, ConsumableKind::Food)))
975 {
976 match effects {
977 Effects::Any(effects) => {
978 for effect in effects.iter() {
980 let (add, red) = effect_healing_value(effect);
981 value += add / effects.len() as f32;
982 heal_multiplier_value *= 1.0 - red / effects.len() as f32;
983 }
984 },
985 Effects::All(_) | Effects::One(_) => {
986 for effect in effects.effects() {
987 let (add, red) = effect_healing_value(effect);
988 value += add;
989 heal_multiplier_value *= 1.0 - red;
990 }
991 },
992 }
993 }
994 if heal_multiplier_value < 1.0 && (heal_multiplier < 1.0 || relaxed) {
997 value *= 0.1;
998 }
999 value as i32
1000 };
1001
1002 let item = self
1003 .inventory
1004 .slots_with_id()
1005 .filter_map(|(id, slot)| match slot {
1006 Some(item) if healing_value(item) > 0 => Some((id, item)),
1007 _ => None,
1008 })
1009 .max_by_key(|(_, item)| {
1010 if relaxed {
1011 -healing_value(item)
1012 } else {
1013 healing_value(item)
1014 }
1015 });
1016
1017 if let Some((id, _)) = item {
1018 use comp::inventory::slot::Slot;
1019 controller.push_action(ControlAction::InventoryAction(InventoryAction::Use(
1020 Slot::Inventory(id),
1021 )));
1022 true
1023 } else {
1024 false
1025 }
1026 }
1027
1028 pub fn choose_target(
1029 &self,
1030 agent: &mut Agent,
1031 controller: &mut Controller,
1032 read_data: &ReadData,
1033 is_enemy: fn(&Self, EcsEntity, &ReadData) -> bool,
1034 ) {
1035 enum ActionStateTimers {
1036 TimerChooseTarget = 0,
1037 }
1038 agent.behavior_state.timers[ActionStateTimers::TimerChooseTarget as usize] = 0.0;
1039 let mut aggro_on = false;
1040
1041 let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
1044
1045 let entities_nearby = grid
1046 .in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist())
1047 .collect_vec();
1048
1049 let get_pos = |entity| read_data.positions.get(entity);
1050 let get_enemy = |(entity, attack_target): (EcsEntity, bool)| {
1051 if attack_target {
1052 if is_enemy(self, entity, read_data) {
1053 Some((entity, true))
1054 } else if self.should_defend(entity, read_data) {
1055 if let Some(attacker) = get_attacker(entity, read_data) {
1056 if !self.passive_towards(attacker, read_data) {
1057 aggro_on = true;
1059 Some((attacker, true))
1060 } else {
1061 None
1062 }
1063 } else {
1064 None
1065 }
1066 } else {
1067 None
1068 }
1069 } else {
1070 Some((entity, false))
1071 }
1072 };
1073 let is_valid_target = |entity: EcsEntity| match read_data.bodies.get(entity) {
1074 Some(Body::Item(item)) => {
1075 if !matches!(item, body::item::Body::Thrown(_)) {
1076 let is_humanoid = matches!(self.body, Some(Body::Humanoid(_)));
1077 let avoids_item_drops = matches!(
1078 self.body,
1079 Some(Body::BipedLarge(biped_large::Body {
1080 species: biped_large::Species::Gigasfrost
1081 | biped_large::Species::Gigasfire,
1082 ..
1083 }))
1084 );
1085 let wants_pickup = !avoids_item_drops
1088 && (is_humanoid || matches!(item, body::item::Body::Consumable));
1089
1090 let attempt_pickup = wants_pickup
1093 && read_data
1094 .loot_owners
1095 .get(entity).is_none_or(|loot_owner| {
1096 !(is_humanoid
1097 && loot_owner.can_pickup(
1098 *self.uid,
1099 read_data.groups.get(entity),
1100 self.alignment,
1101 self.body,
1102 None,
1103 )
1104 && (
1105 !loot_owner.is_soft() ||
1106 loot_owner
1108 .uid()
1109 .and_then(|uid| read_data.id_maps.uid_entity(uid)).is_none_or(|entity| !is_enemy(self, entity, read_data)))
1110 )
1111 });
1112
1113 if attempt_pickup {
1114 Some((entity, false))
1115 } else {
1116 None
1117 }
1118 } else {
1119 None
1120 }
1121 },
1122 _ => {
1123 if read_data
1124 .healths
1125 .get(entity)
1126 .is_some_and(|health| !health.is_dead && !is_invulnerable(entity, read_data))
1127 {
1128 let needs_saving = comp::is_downed(
1129 read_data.healths.get(entity),
1130 read_data.char_states.get(entity),
1131 );
1132
1133 let wants_to_save = match (self.alignment, read_data.alignments.get(entity)) {
1134 (Some(Alignment::Npc), _) if read_data.presences.get(entity).is_some_and(|presence| matches!(presence.kind, PresenceKind::Character(_))) => true,
1137 (Some(Alignment::Npc), Some(Alignment::Npc)) => true,
1138 (Some(Alignment::Enemy), Some(Alignment::Enemy)) => true,
1139 _ => false,
1140 } && agent.allowed_to_speak()
1141 && read_data
1143 .interactors
1144 .get(entity).is_none_or(|interactors| {
1145 !interactors.has_interaction(InteractionKind::HelpDowned)
1146 }) && self.char_state.can_interact();
1147
1148 Some((entity, !(needs_saving && wants_to_save)))
1150 } else {
1151 None
1152 }
1153 },
1154 };
1155
1156 let is_detected = |entity: &EcsEntity, e_pos: &Pos, e_scale: Option<&Scale>| {
1157 self.detects_other(agent, controller, entity, e_pos, e_scale, read_data)
1158 };
1159
1160 let target = entities_nearby
1161 .iter()
1162 .filter_map(|e| is_valid_target(*e))
1163 .filter_map(get_enemy)
1164 .filter_map(|(entity, attack_target)| {
1165 get_pos(entity).map(|pos| (entity, pos, attack_target))
1166 })
1167 .filter(|(entity, e_pos, _)| is_detected(entity, e_pos, read_data.scales.get(*entity)))
1168 .min_by_key(|(_, e_pos, attack_target)| {
1169 (
1170 *attack_target,
1171 (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32,
1172 )
1173 })
1174 .map(|(entity, _, attack_target)| (entity, attack_target));
1175
1176 if agent.target.is_none() && target.is_some() {
1177 if aggro_on {
1178 controller.push_utterance(UtteranceKind::Angry);
1179 } else {
1180 controller.push_utterance(UtteranceKind::Surprised);
1181 }
1182 }
1183 if agent.psyche.should_stop_pursuing || target.is_some() {
1184 agent.target = target.map(|(entity, attack_target)| Target {
1185 target: entity,
1186 hostile: attack_target,
1187 selected_at: read_data.time.0,
1188 aggro_on,
1189 last_known_pos: get_pos(entity).map(|pos| pos.0),
1190 })
1191 }
1192 }
1193
1194 pub fn attack(
1195 &self,
1196 agent: &mut Agent,
1197 controller: &mut Controller,
1198 tgt_data: &TargetData,
1199 read_data: &ReadData,
1200 rng: &mut impl RngExt,
1201 ) {
1202 #[cfg(any(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
1203 let _rng = rng;
1204
1205 #[cfg(not(feature = "use-dyn-lib"))]
1206 {
1207 #[cfg(not(feature = "be-dyn-lib"))]
1208 self.attack_inner(agent, controller, tgt_data, read_data, rng);
1209 #[cfg(feature = "be-dyn-lib")]
1210 self.attack_inner(agent, controller, tgt_data, read_data);
1211 }
1212 #[cfg(feature = "use-dyn-lib")]
1213 {
1214 let lock = LIB.lock().unwrap();
1215 let lib = &lock.as_ref().unwrap().lib;
1216 const ATTACK_FN: &[u8] = b"attack_inner\0";
1217
1218 let attack_fn: common_dynlib::Symbol<
1219 fn(&Self, &mut Agent, &mut Controller, &TargetData, &ReadData),
1220 > = unsafe { lib.get(ATTACK_FN) }.unwrap_or_else(|e| {
1221 panic!(
1222 "Trying to use: {} but had error: {:?}",
1223 CStr::from_bytes_with_nul(ATTACK_FN)
1224 .map(CStr::to_str)
1225 .unwrap()
1226 .unwrap(),
1227 e
1228 )
1229 });
1230 attack_fn(self, agent, controller, tgt_data, read_data);
1231 }
1232 }
1233
1234 #[cfg_attr(feature = "be-dyn-lib", unsafe(export_name = "attack_inner"))]
1235 pub fn attack_inner(
1236 &self,
1237 agent: &mut Agent,
1238 controller: &mut Controller,
1239 tgt_data: &TargetData,
1240 read_data: &ReadData,
1241 #[cfg(not(feature = "be-dyn-lib"))] rng: &mut impl RngExt,
1242 ) {
1243 #[cfg(feature = "be-dyn-lib")]
1244 let rng = &mut rng();
1245
1246 self.dismount_uncontrollable(controller, read_data);
1247
1248 let tool_tactic = |tool_kind| match tool_kind {
1249 ToolKind::Bow => Tactic::Bow,
1250 ToolKind::Staff => Tactic::Staff,
1251 ToolKind::Sceptre => Tactic::Sceptre,
1252 ToolKind::Hammer => Tactic::Hammer,
1253 ToolKind::Sword | ToolKind::Blowgun => Tactic::Sword,
1254 ToolKind::Axe => Tactic::Axe,
1255 _ => Tactic::SimpleMelee,
1256 };
1257
1258 let tactic = self
1259 .inventory
1260 .equipped(EquipSlot::ActiveMainhand)
1261 .as_ref()
1262 .map(|item| {
1263 if let Some(ability_spec) = item.ability_spec() {
1264 match &*ability_spec {
1265 AbilitySpec::Custom(spec) => match spec.as_str() {
1266 "Oni" | "Sword Simple" | "BipedLargeCultistSword" => {
1267 Tactic::SwordSimple
1268 },
1269 "Staff Simple" | "BipedLargeCultistStaff" | "Ogre Staff" => {
1270 Tactic::Staff
1271 },
1272 "BipedLargeCultistHammer" => Tactic::Hammer,
1273 "Simple Flying Melee" => Tactic::SimpleFlyingMelee,
1274 "Bow Simple" | "BipedLargeCultistBow" => Tactic::Bow,
1275 "Stone Golem" | "Coral Golem" => Tactic::StoneGolem,
1276 "Iron Golem" => Tactic::IronGolem,
1277 "Quad Med Quick" => Tactic::CircleCharge {
1278 radius: 5,
1279 circle_time: 2,
1280 },
1281 "Quad Med Jump" | "Darkhound" => Tactic::QuadMedJump,
1282 "Quad Med Charge" => Tactic::CircleCharge {
1283 radius: 6,
1284 circle_time: 1,
1285 },
1286 "Quad Med Basic" => Tactic::QuadMedBasic,
1287 "Quad Med Hoof" => Tactic::QuadMedHoof,
1288 "ClaySteed" => Tactic::ClaySteed,
1289 "Elephant" => Tactic::Elephant,
1290 "Rocksnapper" => Tactic::Rocksnapper,
1291 "Roshwalr" => Tactic::Roshwalr,
1292 "Asp" | "Maneater" => Tactic::QuadLowRanged,
1293 "Quad Low Breathe" | "Quad Low Beam" | "Basilisk" => {
1294 Tactic::QuadLowBeam
1295 },
1296 "Organ" => Tactic::OrganAura,
1297 "Quad Low Tail" | "Husk Brute" => Tactic::TailSlap,
1298 "Quad Low Quick" => Tactic::QuadLowQuick,
1299 "Quad Low Basic" => Tactic::QuadLowBasic,
1300 "Theropod Basic" | "Theropod Bird" | "Theropod Small" => {
1301 Tactic::Theropod
1302 },
1303 "Antlion" => Tactic::ArthropodMelee,
1305 "Tarantula" | "Horn Beetle" => Tactic::ArthropodAmbush,
1306 "Weevil" | "Black Widow" | "Crawler" => Tactic::ArthropodRanged,
1307 "Theropod Charge" => Tactic::CircleCharge {
1308 radius: 6,
1309 circle_time: 1,
1310 },
1311 "Turret" => Tactic::RadialTurret,
1312 "Flamethrower" => Tactic::RadialTurret,
1313 "Haniwa Sentry" => Tactic::RotatingTurret,
1314 "Bird Large Breathe" => Tactic::BirdLargeBreathe,
1315 "Bird Large Fire" => Tactic::BirdLargeFire,
1316 "Bird Large Basic" => Tactic::BirdLargeBasic,
1317 "Flame Wyvern" | "Frost Wyvern" | "Cloud Wyvern" | "Sea Wyvern"
1318 | "Weald Wyvern" => Tactic::Wyvern,
1319 "Bird Medium Basic" => Tactic::BirdMediumBasic,
1320 "Bushly" | "Cactid" | "Irrwurz" | "Driggle" | "Mossy Snail"
1321 | "Strigoi Claws" | "Harlequin" => Tactic::SimpleDouble,
1322 "Clay Golem" => Tactic::ClayGolem,
1323 "Ancient Effigy" => Tactic::AncientEffigy,
1324 "TerracottaStatue" | "Mogwai" => Tactic::TerracottaStatue,
1325 "TerracottaBesieger" => Tactic::Bow,
1326 "TerracottaDemolisher" => Tactic::SimpleDouble,
1327 "TerracottaPunisher" => Tactic::SimpleMelee,
1328 "TerracottaPursuer" => Tactic::SwordSimple,
1329 "Cursekeeper" => Tactic::Cursekeeper,
1330 "CursekeeperFake" => Tactic::CursekeeperFake,
1331 "ShamanicSpirit" => Tactic::ShamanicSpirit,
1332 "Jiangshi" => Tactic::Jiangshi,
1333 "Mindflayer" => Tactic::Mindflayer,
1334 "Flamekeeper" => Tactic::Flamekeeper,
1335 "Forgemaster" => Tactic::Forgemaster,
1336 "Minotaur" => Tactic::Minotaur,
1337 "Cyclops" => Tactic::Cyclops,
1338 "Dullahan" => Tactic::Dullahan,
1339 "Grave Warden" => Tactic::GraveWarden,
1340 "Tidal Warrior" => Tactic::TidalWarrior,
1341 "Karkatha" => Tactic::Karkatha,
1342 "Tidal Totem"
1343 | "Tornado"
1344 | "Gnarling Totem Red"
1345 | "Gnarling Totem Green"
1346 | "Gnarling Totem White" => Tactic::RadialTurret,
1347 "FieryTornado" => Tactic::FieryTornado,
1348 "Yeti" => Tactic::Yeti,
1349 "Harvester" => Tactic::Harvester,
1350 "Cardinal" => Tactic::Cardinal,
1351 "Sea Bishop" => Tactic::SeaBishop,
1352 "Dagon" => Tactic::Dagon,
1353 "Snaretongue" => Tactic::Snaretongue,
1354 "Dagonite" => Tactic::ArthropodAmbush,
1355 "Gnarling Dagger" => Tactic::SimpleBackstab,
1356 "Gnarling Blowgun" => Tactic::ElevatedRanged,
1357 "Deadwood" => Tactic::Deadwood,
1358 "Mandragora" => Tactic::Mandragora,
1359 "Wood Golem" => Tactic::WoodGolem,
1360 "Gnarling Chieftain" => Tactic::GnarlingChieftain,
1361 "Frost Gigas" => Tactic::FrostGigas,
1362 "Boreal Hammer" => Tactic::BorealHammer,
1363 "Boreal Bow" => Tactic::BorealBow,
1364 "Fire Gigas" => Tactic::FireGigas,
1365 "Ashen Axe" => Tactic::AshenAxe,
1366 "Ashen Staff" => Tactic::AshenStaff,
1367 "Adlet Hunter" => Tactic::AdletHunter,
1368 "Adlet Icepicker" => Tactic::AdletIcepicker,
1369 "Adlet Tracker" => Tactic::AdletTracker,
1370 "Hydra" => Tactic::Hydra,
1371 "Ice Drake" => Tactic::IceDrake,
1372 "Frostfang" => Tactic::RandomAbilities {
1373 primary: 1,
1374 secondary: 3,
1375 abilities: [0; BASE_ABILITY_LIMIT],
1376 },
1377 "Tursus Claws" => Tactic::RandomAbilities {
1378 primary: 2,
1379 secondary: 1,
1380 abilities: [4, 0, 0, 0, 0],
1381 },
1382 "Adlet Elder" => Tactic::AdletElder,
1383 "Haniwa Soldier" => Tactic::HaniwaSoldier,
1384 "Haniwa Guard" => Tactic::HaniwaGuard,
1385 "Haniwa Archer" => Tactic::HaniwaArcher,
1386 "Bloodmoon Bat" => Tactic::BloodmoonBat,
1387 "Vampire Bat" => Tactic::VampireBat,
1388 "Bloodmoon Heiress" => Tactic::BloodmoonHeiress,
1389
1390 _ => Tactic::SimpleMelee,
1391 },
1392 AbilitySpec::Tool(tool_kind) => tool_tactic(*tool_kind),
1393 }
1394 } else if let ItemKind::Tool(tool) = &*item.kind() {
1395 tool_tactic(tool.kind)
1396 } else {
1397 Tactic::SimpleMelee
1398 }
1399 })
1400 .unwrap_or(Tactic::SimpleMelee);
1401
1402 controller.push_action(ControlAction::Wield);
1404
1405 let self_radius = self.body.map_or(0.5, |b| b.max_radius()) * self.scale;
1408 let self_attack_range =
1409 (self.body.map_or(0.5, |b| b.front_radius()) + DEFAULT_ATTACK_RANGE) * self.scale;
1410 let tgt_radius =
1411 tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
1412 let min_attack_dist = self_attack_range + tgt_radius;
1413 let body_dist = self_radius + tgt_radius;
1414 let dist_sqrd = self.pos.0.distance_squared(tgt_data.pos.0);
1415 let angle = self
1416 .ori
1417 .look_vec()
1418 .angle_between(tgt_data.pos.0 - self.pos.0)
1419 .to_degrees();
1420 let angle_xy = self
1421 .ori
1422 .look_vec()
1423 .xy()
1424 .angle_between((tgt_data.pos.0 - self.pos.0).xy())
1425 .to_degrees();
1426
1427 let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
1428
1429 let tgt_eye_height = tgt_data
1430 .body
1431 .map_or(0.0, |b| b.eye_height(tgt_data.scale.map_or(1.0, |s| s.0)));
1432 let tgt_eye_offset = tgt_eye_height +
1433 if tactic == Tactic::QuadMedJump {
1438 1.0
1439 } else if matches!(tactic, Tactic::QuadLowRanged) {
1440 -1.0
1441 } else {
1442 0.0
1443 };
1444
1445 if let Some(dir) = match self.char_state {
1460 CharacterState::ChargedRanged(c) if dist_sqrd > 0.0 => {
1461 let offset_z = c.static_data.projectile.agent_aim_z_offset(tgt_eye_offset);
1462 let charge_factor =
1463 c.timer.as_secs_f32() / c.static_data.charge_duration.as_secs_f32();
1464 let projectile_speed = c.static_data.initial_projectile_speed
1465 + charge_factor * c.static_data.scaled_projectile_speed;
1466 aim_projectile(
1467 projectile_speed,
1468 self.pos.0
1469 + self.body.map_or(Vec3::zero(), |body| {
1470 body.projectile_offsets(self.ori.look_vec(), self.scale)
1471 }),
1472 Vec3::new(
1473 tgt_data.pos.0.x,
1474 tgt_data.pos.0.y,
1475 tgt_data.pos.0.z + offset_z,
1476 ),
1477 false,
1478 )
1479 },
1480 CharacterState::BasicRanged(c) => {
1481 let offset_z = c.static_data.projectile.agent_aim_z_offset(tgt_eye_offset);
1482 let projectile_speed = c.static_data.projectile_speed;
1483 aim_projectile(
1484 projectile_speed,
1485 self.pos.0
1486 + self.body.map_or(Vec3::zero(), |body| {
1487 body.projectile_offsets(self.ori.look_vec(), self.scale)
1488 }),
1489 Vec3::new(
1490 tgt_data.pos.0.x,
1491 tgt_data.pos.0.y,
1492 tgt_data.pos.0.z + offset_z,
1493 ),
1494 false,
1495 )
1496 .map(|dir| {
1500 if c.static_data.vertical_angle_offset != 0.0 {
1501 let cross_z = vek::Vec3::unit_z().cross(*dir).normalized();
1502 Dir::from_unnormalized(
1503 vek::Quaternion::rotation_3d(c.static_data.vertical_angle_offset, cross_z)
1504 * *dir,
1505 )
1506 .unwrap_or(dir)
1507 } else {
1508 dir
1509 }
1510 })
1511 },
1512 CharacterState::RapidRanged(c) => {
1513 let offset_z = c.static_data.projectile.agent_aim_z_offset(tgt_eye_offset);
1514 let projectile_speed = c.static_data.projectile_speed;
1515 aim_projectile(
1516 projectile_speed,
1517 self.pos.0
1518 + self.body.map_or(Vec3::zero(), |body| {
1519 body.projectile_offsets(self.ori.look_vec(), self.scale)
1520 }),
1521 Vec3::new(
1522 tgt_data.pos.0.x,
1523 tgt_data.pos.0.y,
1524 tgt_data.pos.0.z + offset_z,
1525 ),
1526 false,
1527 )
1528 },
1529 CharacterState::LeapRanged(c) if matches!(c.stage_section, StageSection::Movement) => {
1530 let offset_z = c.static_data.projectile.agent_aim_z_offset(tgt_eye_offset);
1531 let projectile_speed = c.static_data.projectile_speed;
1532 aim_projectile(
1533 projectile_speed,
1534 self.pos.0
1535 + self.body.map_or(Vec3::zero(), |body| {
1536 body.projectile_offsets(self.ori.look_vec(), self.scale)
1537 }),
1538 Vec3::new(
1539 tgt_data.pos.0.x,
1540 tgt_data.pos.0.y,
1541 tgt_data.pos.0.z + offset_z,
1542 ),
1543 false,
1544 )
1545 },
1546 CharacterState::LeapMelee(_)
1547 if matches!(tactic, Tactic::Hammer | Tactic::BorealHammer | Tactic::Axe) =>
1548 {
1549 let direction_weight = match tactic {
1550 Tactic::Hammer | Tactic::BorealHammer => 0.1,
1551 Tactic::Axe => 0.3,
1552 _ => unreachable!("Direction weight called on incorrect tactic."),
1553 };
1554
1555 let tgt_pos = tgt_data.pos.0;
1556 let self_pos = self.pos.0;
1557
1558 let delta_x = (tgt_pos.x - self_pos.x) * direction_weight;
1559 let delta_y = (tgt_pos.y - self_pos.y) * direction_weight;
1560
1561 Dir::from_unnormalized(Vec3::new(delta_x, delta_y, -1.0))
1562 },
1563 CharacterState::BasicBeam(_) => {
1564 let aim_from = self.body.map_or(self.pos.0, |body| {
1565 self.pos.0
1566 + basic_beam::beam_offsets(
1567 body,
1568 controller.inputs.look_dir,
1569 self.ori.look_vec(),
1570 self.vel.0 - self.physics_state.ground_vel,
1572 self.physics_state.on_ground,
1573 )
1574 });
1575 let aim_to = Vec3::new(
1576 tgt_data.pos.0.x,
1577 tgt_data.pos.0.y,
1578 tgt_data.pos.0.z + tgt_eye_offset,
1579 );
1580 Dir::from_unnormalized(aim_to - aim_from)
1581 },
1582 _ => {
1583 let aim_from = Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset);
1584 let aim_to = Vec3::new(
1585 tgt_data.pos.0.x,
1586 tgt_data.pos.0.y,
1587 tgt_data.pos.0.z + tgt_eye_offset,
1588 );
1589 Dir::from_unnormalized(aim_to - aim_from)
1590 },
1591 } {
1592 controller.inputs.look_dir = dir;
1593 }
1594
1595 let attack_data = AttackData {
1596 body_dist,
1597 min_attack_dist,
1598 dist_sqrd,
1599 angle,
1600 angle_xy,
1601 };
1602
1603 match tactic {
1606 Tactic::SimpleFlyingMelee => self.handle_simple_flying_melee(
1607 agent,
1608 controller,
1609 &attack_data,
1610 tgt_data,
1611 read_data,
1612 rng,
1613 ),
1614 Tactic::SimpleMelee => {
1615 self.handle_simple_melee(agent, controller, &attack_data, tgt_data, read_data, rng)
1616 },
1617 Tactic::Axe => {
1618 self.handle_axe_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1619 },
1620 Tactic::Hammer => {
1621 self.handle_hammer_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1622 },
1623 Tactic::Sword => {
1624 self.handle_sword_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1625 },
1626 Tactic::Bow => {
1627 self.handle_bow_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1628 },
1629 Tactic::Staff => {
1630 self.handle_staff_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1631 },
1632 Tactic::Sceptre => self.handle_sceptre_attack(
1633 agent,
1634 controller,
1635 &attack_data,
1636 tgt_data,
1637 read_data,
1638 rng,
1639 ),
1640 Tactic::StoneGolem => {
1641 self.handle_stone_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1642 },
1643 Tactic::IronGolem => {
1644 self.handle_iron_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1645 },
1646 Tactic::CircleCharge {
1647 radius,
1648 circle_time,
1649 } => self.handle_circle_charge_attack(
1650 agent,
1651 controller,
1652 &attack_data,
1653 tgt_data,
1654 read_data,
1655 radius,
1656 circle_time,
1657 rng,
1658 ),
1659 Tactic::QuadLowRanged => self.handle_quadlow_ranged_attack(
1660 agent,
1661 controller,
1662 &attack_data,
1663 tgt_data,
1664 read_data,
1665 ),
1666 Tactic::TailSlap => {
1667 self.handle_tail_slap_attack(agent, controller, &attack_data, tgt_data, read_data)
1668 },
1669 Tactic::QuadLowQuick => self.handle_quadlow_quick_attack(
1670 agent,
1671 controller,
1672 &attack_data,
1673 tgt_data,
1674 read_data,
1675 ),
1676 Tactic::QuadLowBasic => self.handle_quadlow_basic_attack(
1677 agent,
1678 controller,
1679 &attack_data,
1680 tgt_data,
1681 read_data,
1682 ),
1683 Tactic::QuadMedJump => self.handle_quadmed_jump_attack(
1684 agent,
1685 controller,
1686 &attack_data,
1687 tgt_data,
1688 read_data,
1689 ),
1690 Tactic::QuadMedBasic => self.handle_quadmed_basic_attack(
1691 agent,
1692 controller,
1693 &attack_data,
1694 tgt_data,
1695 read_data,
1696 ),
1697 Tactic::QuadMedHoof => self.handle_quadmed_hoof_attack(
1698 agent,
1699 controller,
1700 &attack_data,
1701 tgt_data,
1702 read_data,
1703 ),
1704 Tactic::QuadLowBeam => self.handle_quadlow_beam_attack(
1705 agent,
1706 controller,
1707 &attack_data,
1708 tgt_data,
1709 read_data,
1710 ),
1711 Tactic::Elephant => self.handle_elephant_attack(
1712 agent,
1713 controller,
1714 &attack_data,
1715 tgt_data,
1716 read_data,
1717 rng,
1718 ),
1719 Tactic::Rocksnapper => {
1720 self.handle_rocksnapper_attack(agent, controller, &attack_data, tgt_data, read_data)
1721 },
1722 Tactic::Roshwalr => {
1723 self.handle_roshwalr_attack(agent, controller, &attack_data, tgt_data, read_data)
1724 },
1725 Tactic::OrganAura => {
1726 self.handle_organ_aura_attack(agent, controller, &attack_data, tgt_data, read_data)
1727 },
1728 Tactic::Theropod => {
1729 self.handle_theropod_attack(agent, controller, &attack_data, tgt_data, read_data)
1730 },
1731 Tactic::ArthropodMelee => self.handle_arthropod_melee_attack(
1732 agent,
1733 controller,
1734 &attack_data,
1735 tgt_data,
1736 read_data,
1737 ),
1738 Tactic::ArthropodAmbush => self.handle_arthropod_ambush_attack(
1739 agent,
1740 controller,
1741 &attack_data,
1742 tgt_data,
1743 read_data,
1744 rng,
1745 ),
1746 Tactic::ArthropodRanged => self.handle_arthropod_ranged_attack(
1747 agent,
1748 controller,
1749 &attack_data,
1750 tgt_data,
1751 read_data,
1752 ),
1753 Tactic::Turret => {
1754 self.handle_turret_attack(agent, controller, &attack_data, tgt_data, read_data)
1755 },
1756 Tactic::FixedTurret => self.handle_fixed_turret_attack(
1757 agent,
1758 controller,
1759 &attack_data,
1760 tgt_data,
1761 read_data,
1762 ),
1763 Tactic::RotatingTurret => {
1764 self.handle_rotating_turret_attack(agent, controller, tgt_data, read_data)
1765 },
1766 Tactic::Mindflayer => self.handle_mindflayer_attack(
1767 agent,
1768 controller,
1769 &attack_data,
1770 tgt_data,
1771 read_data,
1772 rng,
1773 ),
1774 Tactic::Flamekeeper => {
1775 self.handle_flamekeeper_attack(agent, controller, &attack_data, tgt_data, read_data)
1776 },
1777 Tactic::Forgemaster => {
1778 self.handle_forgemaster_attack(agent, controller, &attack_data, tgt_data, read_data)
1779 },
1780 Tactic::BirdLargeFire => self.handle_birdlarge_fire_attack(
1781 agent,
1782 controller,
1783 &attack_data,
1784 tgt_data,
1785 read_data,
1786 rng,
1787 ),
1788 Tactic::BirdLargeBreathe => self.handle_birdlarge_breathe_attack(
1790 agent,
1791 controller,
1792 &attack_data,
1793 tgt_data,
1794 read_data,
1795 rng,
1796 ),
1797 Tactic::BirdLargeBasic => self.handle_birdlarge_basic_attack(
1798 agent,
1799 controller,
1800 &attack_data,
1801 tgt_data,
1802 read_data,
1803 ),
1804 Tactic::Wyvern => {
1805 self.handle_wyvern_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1806 },
1807 Tactic::BirdMediumBasic => {
1808 self.handle_simple_melee(agent, controller, &attack_data, tgt_data, read_data, rng)
1809 },
1810 Tactic::SimpleDouble => self.handle_simple_double_attack(
1811 agent,
1812 controller,
1813 &attack_data,
1814 tgt_data,
1815 read_data,
1816 ),
1817 Tactic::Jiangshi => {
1818 self.handle_jiangshi_attack(agent, controller, &attack_data, tgt_data, read_data)
1819 },
1820 Tactic::ClayGolem => {
1821 self.handle_clay_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1822 },
1823 Tactic::ClaySteed => {
1824 self.handle_clay_steed_attack(agent, controller, &attack_data, tgt_data, read_data)
1825 },
1826 Tactic::AncientEffigy => self.handle_ancient_effigy_attack(
1827 agent,
1828 controller,
1829 &attack_data,
1830 tgt_data,
1831 read_data,
1832 ),
1833 Tactic::TerracottaStatue => {
1834 self.handle_terracotta_statue_attack(agent, controller, &attack_data, read_data)
1835 },
1836 Tactic::Minotaur => {
1837 self.handle_minotaur_attack(agent, controller, &attack_data, tgt_data, read_data)
1838 },
1839 Tactic::Cyclops => {
1840 self.handle_cyclops_attack(agent, controller, &attack_data, tgt_data, read_data)
1841 },
1842 Tactic::Dullahan => {
1843 self.handle_dullahan_attack(agent, controller, &attack_data, tgt_data, read_data)
1844 },
1845 Tactic::GraveWarden => self.handle_grave_warden_attack(
1846 agent,
1847 controller,
1848 &attack_data,
1849 tgt_data,
1850 read_data,
1851 ),
1852 Tactic::TidalWarrior => self.handle_tidal_warrior_attack(
1853 agent,
1854 controller,
1855 &attack_data,
1856 tgt_data,
1857 read_data,
1858 ),
1859 Tactic::Karkatha => self.handle_karkatha_attack(
1860 agent,
1861 controller,
1862 &attack_data,
1863 tgt_data,
1864 read_data,
1865 rng,
1866 ),
1867 Tactic::RadialTurret => self.handle_radial_turret_attack(controller),
1868 Tactic::FieryTornado => self.handle_fiery_tornado_attack(agent, controller),
1869 Tactic::Yeti => {
1870 self.handle_yeti_attack(agent, controller, &attack_data, tgt_data, read_data)
1871 },
1872 Tactic::Harvester => self.handle_harvester_attack(
1873 agent,
1874 controller,
1875 &attack_data,
1876 tgt_data,
1877 read_data,
1878 rng,
1879 ),
1880 Tactic::Cardinal => self.handle_cardinal_attack(
1881 agent,
1882 controller,
1883 &attack_data,
1884 tgt_data,
1885 read_data,
1886 rng,
1887 ),
1888 Tactic::SeaBishop => self.handle_sea_bishop_attack(
1889 agent,
1890 controller,
1891 &attack_data,
1892 tgt_data,
1893 read_data,
1894 rng,
1895 ),
1896 Tactic::Cursekeeper => self.handle_cursekeeper_attack(
1897 agent,
1898 controller,
1899 &attack_data,
1900 tgt_data,
1901 read_data,
1902 rng,
1903 ),
1904 Tactic::CursekeeperFake => {
1905 self.handle_cursekeeper_fake_attack(controller, &attack_data)
1906 },
1907 Tactic::ShamanicSpirit => self.handle_shamanic_spirit_attack(
1908 agent,
1909 controller,
1910 &attack_data,
1911 tgt_data,
1912 read_data,
1913 ),
1914 Tactic::Dagon => {
1915 self.handle_dagon_attack(agent, controller, &attack_data, tgt_data, read_data)
1916 },
1917 Tactic::Snaretongue => {
1918 self.handle_snaretongue_attack(agent, controller, &attack_data, read_data)
1919 },
1920 Tactic::SimpleBackstab => {
1921 self.handle_simple_backstab(agent, controller, &attack_data, tgt_data, read_data)
1922 },
1923 Tactic::ElevatedRanged => {
1924 self.handle_elevated_ranged(agent, controller, &attack_data, tgt_data, read_data)
1925 },
1926 Tactic::Deadwood => {
1927 self.handle_deadwood(agent, controller, &attack_data, tgt_data, read_data)
1928 },
1929 Tactic::Mandragora => {
1930 self.handle_mandragora(agent, controller, &attack_data, tgt_data, read_data)
1931 },
1932 Tactic::WoodGolem => {
1933 self.handle_wood_golem(agent, controller, &attack_data, tgt_data, read_data, rng)
1934 },
1935 Tactic::GnarlingChieftain => self.handle_gnarling_chieftain(
1936 agent,
1937 controller,
1938 &attack_data,
1939 tgt_data,
1940 read_data,
1941 rng,
1942 ),
1943 Tactic::FrostGigas => self.handle_frostgigas_attack(
1944 agent,
1945 controller,
1946 &attack_data,
1947 tgt_data,
1948 read_data,
1949 rng,
1950 ),
1951 Tactic::BorealHammer => self.handle_boreal_hammer_attack(
1952 agent,
1953 controller,
1954 &attack_data,
1955 tgt_data,
1956 read_data,
1957 rng,
1958 ),
1959 Tactic::BorealBow => self.handle_boreal_bow_attack(
1960 agent,
1961 controller,
1962 &attack_data,
1963 tgt_data,
1964 read_data,
1965 rng,
1966 ),
1967 Tactic::FireGigas => self.handle_firegigas_attack(
1968 agent,
1969 controller,
1970 &attack_data,
1971 tgt_data,
1972 read_data,
1973 rng,
1974 ),
1975 Tactic::AshenAxe => self.handle_ashen_axe_attack(
1976 agent,
1977 controller,
1978 &attack_data,
1979 tgt_data,
1980 read_data,
1981 rng,
1982 ),
1983 Tactic::AshenStaff => self.handle_ashen_staff_attack(
1984 agent,
1985 controller,
1986 &attack_data,
1987 tgt_data,
1988 read_data,
1989 rng,
1990 ),
1991 Tactic::SwordSimple => self.handle_sword_simple_attack(
1992 agent,
1993 controller,
1994 &attack_data,
1995 tgt_data,
1996 read_data,
1997 ),
1998 Tactic::AdletHunter => {
1999 self.handle_adlet_hunter(agent, controller, &attack_data, tgt_data, read_data, rng)
2000 },
2001 Tactic::AdletIcepicker => {
2002 self.handle_adlet_icepicker(agent, controller, &attack_data, tgt_data, read_data)
2003 },
2004 Tactic::AdletTracker => {
2005 self.handle_adlet_tracker(agent, controller, &attack_data, tgt_data, read_data)
2006 },
2007 Tactic::IceDrake => {
2008 self.handle_icedrake(agent, controller, &attack_data, tgt_data, read_data, rng)
2009 },
2010 Tactic::Hydra => {
2011 self.handle_hydra(agent, controller, &attack_data, tgt_data, read_data, rng)
2012 },
2013 Tactic::BloodmoonBat => self.handle_bloodmoon_bat_attack(
2014 agent,
2015 controller,
2016 &attack_data,
2017 tgt_data,
2018 read_data,
2019 rng,
2020 ),
2021 Tactic::VampireBat => self.handle_vampire_bat_attack(
2022 agent,
2023 controller,
2024 &attack_data,
2025 tgt_data,
2026 read_data,
2027 rng,
2028 ),
2029 Tactic::BloodmoonHeiress => self.handle_bloodmoon_heiress_attack(
2030 agent,
2031 controller,
2032 &attack_data,
2033 tgt_data,
2034 read_data,
2035 rng,
2036 ),
2037 Tactic::RandomAbilities {
2038 primary,
2039 secondary,
2040 abilities,
2041 } => self.handle_random_abilities(
2042 agent,
2043 controller,
2044 &attack_data,
2045 tgt_data,
2046 read_data,
2047 rng,
2048 primary,
2049 secondary,
2050 abilities,
2051 ),
2052 Tactic::AdletElder => {
2053 self.handle_adlet_elder(agent, controller, &attack_data, tgt_data, read_data, rng)
2054 },
2055 Tactic::HaniwaSoldier => {
2056 self.handle_haniwa_soldier(agent, controller, &attack_data, tgt_data, read_data)
2057 },
2058 Tactic::HaniwaGuard => {
2059 self.handle_haniwa_guard(agent, controller, &attack_data, tgt_data, read_data, rng)
2060 },
2061 Tactic::HaniwaArcher => {
2062 self.handle_haniwa_archer(agent, controller, &attack_data, tgt_data, read_data)
2063 },
2064 }
2065 }
2066
2067 pub fn handle_sounds_heard(
2068 &self,
2069 agent: &mut Agent,
2070 controller: &mut Controller,
2071 read_data: &ReadData,
2072 emitters: &mut AgentEmitters,
2073 rng: &mut impl RngExt,
2074 ) {
2075 agent.forget_old_sounds(read_data.time.0);
2076
2077 if is_invulnerable(*self.entity, read_data) || is_steering(*self.entity, read_data) {
2078 self.idle(agent, controller, read_data, emitters, rng);
2079 return;
2080 }
2081
2082 if let Some(sound) = agent.sounds_heard.last() {
2083 let sound_pos = Pos(sound.pos);
2084 let dist_sqrd = self.pos.0.distance_squared(sound_pos.0);
2085 let is_close = dist_sqrd < 35.0_f32.powi(2);
2090
2091 let sound_was_loud = sound.vol >= 10.0;
2092 let sound_was_threatening = sound_was_loud
2093 || matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _));
2094
2095 let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
2096 let follows_threatening_sounds =
2097 has_enemy_alignment || is_village_guard(*self.entity, read_data);
2098
2099 if sound_was_threatening && is_close {
2100 if !self.below_flee_health(agent) && follows_threatening_sounds {
2101 self.follow(agent, controller, read_data, &sound_pos);
2102 } else if self.below_flee_health(agent) || !follows_threatening_sounds {
2103 self.flee(agent, controller, read_data, &sound_pos);
2104 } else {
2105 self.idle(agent, controller, read_data, emitters, rng);
2106 }
2107 } else {
2108 self.idle(agent, controller, read_data, emitters, rng);
2109 }
2110 } else {
2111 self.idle(agent, controller, read_data, emitters, rng);
2112 }
2113 }
2114
2115 pub fn attack_target_attacker(
2116 &self,
2117 agent: &mut Agent,
2118 read_data: &ReadData,
2119 controller: &mut Controller,
2120 emitters: &mut AgentEmitters,
2121 rng: &mut impl RngExt,
2122 ) {
2123 if let Some(Target { target, .. }) = agent.target
2124 && let Some(tgt_health) = read_data.healths.get(target)
2125 && let Some(by) = tgt_health.last_change.damage_by()
2126 && let Some(attacker) = get_entity_by_id(by.uid(), read_data)
2127 {
2128 if agent.target.is_none() {
2129 controller.push_utterance(UtteranceKind::Angry);
2130 }
2131
2132 let attacker_pos = read_data.positions.get(attacker).map(|pos| pos.0);
2133 agent.target = Some(Target::new(
2134 attacker,
2135 true,
2136 read_data.time.0,
2137 true,
2138 attacker_pos,
2139 ));
2140
2141 if let Some(tgt_pos) = read_data.positions.get(attacker) {
2142 if is_dead_or_invulnerable(attacker, read_data) {
2143 agent.target = Some(Target::new(
2144 target,
2145 false,
2146 read_data.time.0,
2147 false,
2148 Some(tgt_pos.0),
2149 ));
2150
2151 self.idle(agent, controller, read_data, emitters, rng);
2152 } else {
2153 let target_data = TargetData::new(tgt_pos, target, read_data);
2154 self.attack(agent, controller, &target_data, read_data, rng);
2161 }
2162 }
2163 }
2164 }
2165
2166 pub fn chat_npc_if_allowed_to_speak(
2169 &self,
2170 msg: Content,
2171 agent: &Agent,
2172 emitters: &mut AgentEmitters,
2173 ) -> bool {
2174 if agent.allowed_to_speak() {
2175 self.chat_npc(msg, emitters);
2176 true
2177 } else {
2178 false
2179 }
2180 }
2181
2182 pub fn chat_npc(&self, content: Content, emitters: &mut AgentEmitters) {
2183 emitters.emit(ChatEvent {
2184 msg: UnresolvedChatMsg::npc(*self.uid, content),
2185 from_client: false,
2186 });
2187 }
2188
2189 fn emit_scream(&self, time: f64, emitters: &mut AgentEmitters) {
2190 if let Some(body) = self.body {
2191 emitters.emit(SoundEvent {
2192 sound: Sound::new(
2193 SoundKind::Utterance(UtteranceKind::Scream, *body),
2194 self.pos.0,
2195 13.0,
2196 time,
2197 ),
2198 });
2199 }
2200 }
2201
2202 pub fn cry_out(&self, agent: &Agent, emitters: &mut AgentEmitters, read_data: &ReadData) {
2203 let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
2204 let is_below_flee_health = self.below_flee_health(agent);
2205
2206 if has_enemy_alignment && is_below_flee_health {
2207 self.chat_npc_if_allowed_to_speak(
2208 Content::localized("npc-speech-cultist_low_health_fleeing"),
2209 agent,
2210 emitters,
2211 );
2212 } else if is_villager(self.alignment) {
2213 self.chat_npc_if_allowed_to_speak(
2214 Content::localized("npc-speech-villager_under_attack"),
2215 agent,
2216 emitters,
2217 );
2218 self.emit_scream(read_data.time.0, emitters);
2219 }
2220 }
2221
2222 pub fn exclaim_relief_about_enemy_dead(&self, agent: &Agent, emitters: &mut AgentEmitters) {
2223 if is_villager(self.alignment) {
2224 self.chat_npc_if_allowed_to_speak(
2225 Content::localized("npc-speech-villager_enemy_killed"),
2226 agent,
2227 emitters,
2228 );
2229 }
2230 }
2231
2232 pub fn below_flee_health(&self, agent: &Agent) -> bool {
2233 self.damage.min(1.0) < agent.psyche.flee_health
2234 }
2235
2236 pub fn is_more_dangerous_than_target(
2237 &self,
2238 entity: EcsEntity,
2239 target: Target,
2240 read_data: &ReadData,
2241 ) -> bool {
2242 let entity_pos = read_data.positions.get(entity);
2243 let target_pos = read_data.positions.get(target.target);
2244
2245 entity_pos.is_some_and(|entity_pos| {
2246 target_pos.is_none_or(|target_pos| {
2247 const FUZZY_DIST_COMPARISON: f32 = 0.8;
2252
2253 let is_target_further = target_pos.0.distance(entity_pos.0)
2254 < target_pos.0.distance(entity_pos.0) * FUZZY_DIST_COMPARISON;
2255 let is_entity_hostile = read_data
2256 .alignments
2257 .get(entity)
2258 .zip(self.alignment)
2259 .is_some_and(|(entity, me)| me.hostile_towards(*entity));
2260
2261 !target.aggro_on || (is_target_further && is_entity_hostile)
2264 })
2265 })
2266 }
2267
2268 pub fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2269 let other_alignment = read_data.alignments.get(entity);
2270
2271 (entity != *self.entity)
2272 && !self.passive_towards(entity, read_data)
2273 && (are_our_owners_hostile(self.alignment, other_alignment, read_data)
2274 || (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data)
2275 || (is_villager(self.alignment) && is_dressed_as_witch(entity, read_data))
2276 || (is_villager(self.alignment) && is_dressed_as_pirate(entity, read_data))))
2277 }
2278
2279 pub fn is_hunting_animal(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2280 (entity != *self.entity)
2281 && !self.friendly_towards(entity, read_data)
2282 && matches!(read_data.bodies.get(entity), Some(Body::QuadrupedSmall(_)))
2283 }
2284
2285 fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2286 let entity_alignment = read_data.alignments.get(entity);
2287
2288 let we_are_friendly = entity_alignment.is_some_and(|entity_alignment| {
2289 self.alignment
2290 .is_some_and(|alignment| !alignment.hostile_towards(*entity_alignment))
2291 });
2292 let we_share_species = read_data.bodies.get(entity).is_some_and(|entity_body| {
2293 self.body.is_some_and(|body| {
2294 entity_body.is_same_species_as(body)
2295 || (entity_body.is_humanoid() && body.is_humanoid())
2296 })
2297 });
2298 let self_owns_entity =
2299 matches!(entity_alignment, Some(Alignment::Owned(ouid)) if *self.uid == *ouid);
2300
2301 (we_are_friendly && we_share_species)
2302 || (is_village_guard(*self.entity, read_data) && is_villager(entity_alignment))
2303 || self_owns_entity
2304 }
2305
2306 fn passive_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2307 if let (Some(self_alignment), Some(other_alignment)) =
2308 (self.alignment, read_data.alignments.get(entity))
2309 {
2310 self_alignment.passive_towards(*other_alignment)
2311 } else {
2312 false
2313 }
2314 }
2315
2316 fn friendly_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2317 if let (Some(self_alignment), Some(other_alignment)) =
2318 (self.alignment, read_data.alignments.get(entity))
2319 {
2320 self_alignment.friendly_towards(*other_alignment)
2321 } else {
2322 false
2323 }
2324 }
2325
2326 pub fn can_see_entity(
2327 &self,
2328 agent: &Agent,
2329 controller: &Controller,
2330 other: EcsEntity,
2331 other_pos: &Pos,
2332 other_scale: Option<&Scale>,
2333 read_data: &ReadData,
2334 ) -> bool {
2335 let other_stealth_multiplier = {
2336 let other_inventory = read_data.inventories.get(other);
2337 let other_char_state = read_data.char_states.get(other);
2338
2339 perception_dist_multiplier_from_stealth(other_inventory, other_char_state, self.msm)
2340 };
2341
2342 let within_sight_dist = {
2343 let sight_dist = agent.psyche.sight_dist * other_stealth_multiplier;
2344 let dist_sqrd = other_pos.0.distance_squared(self.pos.0);
2345
2346 dist_sqrd < sight_dist.powi(2)
2347 };
2348
2349 let within_fov = (other_pos.0 - self.pos.0)
2350 .try_normalized()
2351 .is_some_and(|v| v.dot(*controller.inputs.look_dir) > 0.15);
2352
2353 let other_body = read_data.bodies.get(other);
2354
2355 (within_sight_dist)
2356 && within_fov
2357 && entities_have_line_of_sight(
2358 self.pos,
2359 self.body,
2360 self.scale,
2361 other_pos,
2362 other_body,
2363 other_scale,
2364 read_data,
2365 )
2366 }
2367
2368 pub fn detects_other(
2369 &self,
2370 agent: &Agent,
2371 controller: &Controller,
2372 other: &EcsEntity,
2373 other_pos: &Pos,
2374 other_scale: Option<&Scale>,
2375 read_data: &ReadData,
2376 ) -> bool {
2377 self.can_sense_directly_near(other_pos)
2378 || self.can_see_entity(agent, controller, *other, other_pos, other_scale, read_data)
2379 }
2380
2381 pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool {
2382 let chance = rng().random_bool(0.3);
2383 e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) && chance
2384 }
2385
2386 pub fn menacing(
2387 &self,
2388 agent: &mut Agent,
2389 controller: &mut Controller,
2390 target: EcsEntity,
2391 tgt_data: &TargetData,
2392 read_data: &ReadData,
2393 emitters: &mut AgentEmitters,
2394 remembers_fight_with_target: bool,
2395 ) {
2396 let max_move = 0.5;
2397 let move_dir = controller.inputs.move_dir;
2398 let move_dir_mag = move_dir.magnitude();
2399 let mut chat = |agent: &mut Agent, content: Content| {
2400 self.chat_npc_if_allowed_to_speak(content, agent, emitters);
2401 };
2402 let mut chat_villager_remembers_fighting = |agent: &mut Agent| {
2403 let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
2404
2405 if let Some(tgt_name) = tgt_name.as_ref().and_then(|name| name.as_plain()) {
2408 chat(
2409 agent,
2410 Content::localized_with_args("npc-speech-remembers-fight", [(
2411 "name", tgt_name,
2412 )]),
2413 )
2414 } else {
2415 chat(
2416 agent,
2417 Content::localized("npc-speech-remembers-fight-no-name"),
2418 );
2419 }
2420 };
2421
2422 self.look_toward(controller, read_data, target);
2423 controller.push_action(ControlAction::Wield);
2424
2425 if move_dir_mag > max_move {
2426 controller.inputs.move_dir = max_move * move_dir / move_dir_mag;
2427 }
2428
2429 match agent
2430 .timer
2431 .timeout_elapsed(read_data.time.0, comp::agent::TimerAction::Warn, 5.0)
2432 {
2433 Some(true) | None => {
2434 self.path_toward_target(
2435 agent,
2436 controller,
2437 tgt_data.pos.0,
2438 read_data,
2439 Path::AtTarget,
2440 Some(0.4),
2441 );
2442 },
2443 Some(false) => {
2444 agent
2445 .timer
2446 .start(read_data.time.0, comp::agent::TimerAction::Warn);
2447 controller.push_utterance(UtteranceKind::Angry);
2448 if is_villager(self.alignment) {
2449 if remembers_fight_with_target {
2450 chat_villager_remembers_fighting(agent);
2451 } else if is_dressed_as_cultist(target, read_data) {
2452 chat(
2453 agent,
2454 Content::localized("npc-speech-villager_cultist_alarm"),
2455 );
2456 } else if is_dressed_as_witch(target, read_data) {
2457 chat(agent, Content::localized("npc-speech-villager_witch_alarm"));
2458 } else if is_dressed_as_pirate(target, read_data) {
2459 chat(
2460 agent,
2461 Content::localized("npc-speech-villager_pirate_alarm"),
2462 );
2463 } else {
2464 chat(agent, Content::localized("npc-speech-menacing"));
2465 }
2466 } else {
2467 chat(agent, Content::localized("npc-speech-menacing"));
2468 }
2469 },
2470 }
2471 }
2472
2473 pub fn dismount_uncontrollable(&self, controller: &mut Controller, read_data: &ReadData) {
2475 if read_data.is_riders.get(*self.entity).is_some_and(|mount| {
2476 read_data
2477 .id_maps
2478 .uid_entity(mount.mount)
2479 .and_then(|e| read_data.bodies.get(e))
2480 .is_none_or(|b| b.has_free_will())
2481 }) || read_data
2482 .is_volume_riders
2483 .get(*self.entity)
2484 .is_some_and(|r| !r.is_steering_entity())
2485 {
2486 controller.push_event(ControlEvent::Unmount);
2487 }
2488 }
2489
2490 pub fn dismount(&self, controller: &mut Controller, read_data: &ReadData) {
2495 if read_data.is_riders.contains(*self.entity)
2496 || read_data
2497 .is_volume_riders
2498 .get(*self.entity)
2499 .is_some_and(|r| !r.is_steering_entity())
2500 {
2501 controller.push_event(ControlEvent::Unmount);
2502 }
2503 }
2504}