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