veloren_server_agent/
action_nodes.rs

1use crate::{
2    consts::{
3        AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, MAX_PATROL_DIST,
4        PARTIAL_PATH_DIST, SEPARATION_BIAS, SEPARATION_DIST, STD_AWARENESS_DECAY_RATE,
5    },
6    data::{AgentData, AgentEmitters, AttackData, Path, ReadData, Tactic, TargetData},
7    util::{
8        aim_projectile, are_our_owners_hostile, entities_have_line_of_sight, get_attacker,
9        get_entity_by_id, is_dead_or_invulnerable, is_dressed_as_cultist, is_invulnerable,
10        is_steering, is_village_guard, is_villager,
11    },
12};
13use common::{
14    combat::perception_dist_multiplier_from_stealth,
15    comp::{
16        self, Agent, Alignment, Body, CharacterState, Content, ControlAction, ControlEvent,
17        Controller, HealthChange, InputKind, InventoryAction, Pos, PresenceKind, Scale,
18        UnresolvedChatMsg, UtteranceKind,
19        ability::BASE_ABILITY_LIMIT,
20        agent::{FlightMode, PidControllers, Sound, SoundKind, Target},
21        inventory::slot::EquipSlot,
22        item::{
23            ConsumableKind, Effects, Item, ItemDesc, ItemKind,
24            tool::{AbilitySpec, ToolKind},
25        },
26        item_drop,
27        projectile::ProjectileConstructorKind,
28    },
29    consts::MAX_MOUNT_RANGE,
30    effect::{BuffEffect, Effect},
31    event::{ChatEvent, EmitExt, SoundEvent},
32    interaction::InteractionKind,
33    mounting::VolumePos,
34    path::TraversalConfig,
35    rtsim::NpcActivity,
36    states::basic_beam,
37    terrain::Block,
38    time::DayPeriod,
39    util::Dir,
40    vol::ReadVol,
41};
42use itertools::Itertools;
43use rand::{Rng, thread_rng};
44use specs::Entity as EcsEntity;
45use vek::*;
46
47#[cfg(feature = "use-dyn-lib")]
48use {crate::LIB, std::ffi::CStr};
49
50impl AgentData<'_> {
51    ////////////////////////////////////////
52    // Action Nodes
53    ////////////////////////////////////////
54    pub fn glider_equip(&self, controller: &mut Controller, read_data: &ReadData) {
55        self.dismount(controller, read_data);
56        controller.push_action(ControlAction::GlideWield);
57    }
58
59    // TODO: add the ability to follow the target?
60    pub fn glider_flight(&self, controller: &mut Controller, _read_data: &ReadData) {
61        let Some(fluid) = self.physics_state.in_fluid else {
62            return;
63        };
64
65        let vel = self.vel;
66
67        let comp::Vel(rel_flow) = fluid.relative_flow(vel);
68
69        let is_wind_downwards = rel_flow.z.is_sign_negative();
70
71        let look_dir = if is_wind_downwards {
72            Vec3::from(-rel_flow.xy())
73        } else {
74            -rel_flow
75        };
76
77        controller.inputs.look_dir = Dir::from_unnormalized(look_dir).unwrap_or_else(Dir::forward);
78    }
79
80    pub fn fly_upward(&self, controller: &mut Controller, read_data: &ReadData) {
81        self.dismount(controller, read_data);
82
83        controller.push_basic_input(InputKind::Fly);
84        controller.inputs.move_z = 1.0;
85    }
86
87    /// Directs the entity to path and move toward the target
88    /// If path is not Full, the entity will path to a location 50 units along
89    /// the vector between the entity and the target. The speed multiplier
90    /// multiplies the movement speed by a value less than 1.0.
91    /// A `None` value implies a multiplier of 1.0.
92    /// Returns `false` if the pathfinding algorithm fails to return a path
93    pub fn path_toward_target(
94        &self,
95        agent: &mut Agent,
96        controller: &mut Controller,
97        tgt_pos: Vec3<f32>,
98        read_data: &ReadData,
99        path: Path,
100        speed_multiplier: Option<f32>,
101    ) -> Option<Vec3<f32>> {
102        self.dismount(controller, read_data);
103
104        let partial_path_tgt_pos = |pos_difference: Vec3<f32>| {
105            self.pos.0
106                + PARTIAL_PATH_DIST * pos_difference.try_normalized().unwrap_or_else(Vec3::zero)
107        };
108        let pos_difference = tgt_pos - self.pos.0;
109        let pathing_pos = match path {
110            Path::Separate => {
111                let mut sep_vec: Vec3<f32> = Vec3::<f32>::zero();
112
113                for entity in read_data
114                    .cached_spatial_grid
115                    .0
116                    .in_circle_aabr(self.pos.0.xy(), SEPARATION_DIST)
117                {
118                    if let (Some(alignment), Some(other_alignment)) =
119                        (self.alignment, read_data.alignments.get(entity))
120                    {
121                        if Alignment::passive_towards(*alignment, *other_alignment) {
122                            if let (Some(pos), Some(body), Some(other_body)) = (
123                                read_data.positions.get(entity),
124                                self.body,
125                                read_data.bodies.get(entity),
126                            ) {
127                                let dist_xy = self.pos.0.xy().distance(pos.0.xy());
128                                let spacing = body.spacing_radius() + other_body.spacing_radius();
129                                if dist_xy < spacing {
130                                    let pos_diff = self.pos.0.xy() - pos.0.xy();
131                                    sep_vec += pos_diff.try_normalized().unwrap_or_else(Vec2::zero)
132                                        * ((spacing - dist_xy) / spacing);
133                                }
134                            }
135                        }
136                    }
137                }
138                partial_path_tgt_pos(
139                    sep_vec * SEPARATION_BIAS + pos_difference * (1.0 - SEPARATION_BIAS),
140                )
141            },
142            Path::Full => tgt_pos,
143            Path::Partial => partial_path_tgt_pos(pos_difference),
144        };
145        let speed_multiplier = speed_multiplier.unwrap_or(1.0).min(1.0);
146
147        let in_loaded_chunk = |pos: Vec3<f32>| {
148            read_data
149                .terrain
150                .contains_key(read_data.terrain.pos_key(pos.map(|e| e.floor() as i32)))
151        };
152
153        // If current position lies inside a loaded chunk, we need to plan routes using
154        // voxel info. If target happens to be in an unloaded chunk,
155        // we need to make our way to the current chunk border, and
156        // then reroute if needed.
157        let is_target_loaded = in_loaded_chunk(pathing_pos);
158
159        if let Some((bearing, speed)) = agent.chaser.chase(
160            &*read_data.terrain,
161            self.pos.0,
162            self.vel.0,
163            pathing_pos,
164            TraversalConfig {
165                min_tgt_dist: 0.25,
166                is_target_loaded,
167                ..self.traversal_config
168            },
169        ) {
170            self.traverse(controller, bearing, speed * speed_multiplier);
171            Some(bearing)
172        } else {
173            None
174        }
175    }
176
177    fn traverse(&self, controller: &mut Controller, bearing: Vec3<f32>, speed: f32) {
178        controller.inputs.move_dir =
179            bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
180
181        // Only jump if we are grounded and can't blockhop or if we can fly
182        self.jump_if(
183            (self.physics_state.on_ground.is_some() && bearing.z > 1.5)
184                || self.traversal_config.can_fly,
185            controller,
186        );
187        controller.inputs.move_z = bearing.z;
188    }
189
190    pub fn jump_if(&self, condition: bool, controller: &mut Controller) {
191        if condition {
192            controller.push_basic_input(InputKind::Jump);
193        } else {
194            controller.push_cancel_input(InputKind::Jump)
195        }
196    }
197
198    pub fn idle(
199        &self,
200        agent: &mut Agent,
201        controller: &mut Controller,
202        read_data: &ReadData,
203        _emitters: &mut AgentEmitters,
204        rng: &mut impl Rng,
205    ) {
206        enum ActionTimers {
207            TimerIdle = 0,
208        }
209
210        agent
211            .awareness
212            .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
213
214        // Light lanterns at night
215        // TODO Add a method to turn on NPC lanterns underground
216        let lantern_equipped = self
217            .inventory
218            .equipped(EquipSlot::Lantern)
219            .as_ref()
220            .is_some_and(|item| matches!(&*item.kind(), comp::item::ItemKind::Lantern(_)));
221        let lantern_turned_on = self.light_emitter.is_some();
222        let day_period = DayPeriod::from(read_data.time_of_day.0);
223        // Only emit event for agents that have a lantern equipped
224        if lantern_equipped && rng.gen_bool(0.001) {
225            if day_period.is_dark() && !lantern_turned_on {
226                // Agents with turned off lanterns turn them on randomly once it's
227                // nighttime and keep them on.
228                // Only emit event for agents that sill need to
229                // turn on their lantern.
230                controller.push_event(ControlEvent::EnableLantern)
231            } else if lantern_turned_on && day_period.is_light() {
232                // agents with turned on lanterns turn them off randomly once it's
233                // daytime and keep them off.
234                controller.push_event(ControlEvent::DisableLantern)
235            }
236        };
237
238        if let Some(body) = self.body {
239            let attempt_heal = if matches!(body, Body::Humanoid(_)) {
240                self.damage < IDLE_HEALING_ITEM_THRESHOLD
241            } else {
242                true
243            };
244            if attempt_heal && self.heal_self(agent, controller, true) {
245                agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.01;
246                return;
247            }
248        } else {
249            agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.01;
250            return;
251        }
252
253        agent.behavior_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
254
255        'activity: {
256            match agent.rtsim_controller.activity {
257                Some(NpcActivity::Goto(travel_to, speed_factor)) => {
258                    if read_data
259                        .is_volume_riders
260                        .get(*self.entity)
261                        .is_some_and(|r| !r.is_steering_entity())
262                    {
263                        controller.push_event(ControlEvent::Unmount);
264                    }
265
266                    agent.bearing = Vec2::zero();
267
268                    // If it has an rtsim destination and can fly, then it should.
269                    // If it is flying and bumps something above it, then it should move down.
270                    if self.traversal_config.can_fly
271                        && !read_data
272                            .terrain
273                            .ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
274                            .until(Block::is_solid)
275                            .cast()
276                            .1
277                            .map_or(true, |b| b.is_some())
278                    {
279                        controller.push_basic_input(InputKind::Fly);
280                    } else {
281                        controller.push_cancel_input(InputKind::Fly)
282                    }
283
284                    if let Some(bearing) = self.path_toward_target(
285                        agent,
286                        controller,
287                        travel_to,
288                        read_data,
289                        Path::Full,
290                        Some(speed_factor),
291                    ) {
292                        let height_offset = bearing.z
293                            + if self.traversal_config.can_fly {
294                                // NOTE: costs 4 us (imbris)
295                                let obstacle_ahead = read_data
296                                    .terrain
297                                    .ray(
298                                        self.pos.0 + Vec3::unit_z(),
299                                        self.pos.0
300                                            + bearing.try_normalized().unwrap_or_else(Vec3::unit_y)
301                                                * 80.0
302                                            + Vec3::unit_z(),
303                                    )
304                                    .until(Block::is_solid)
305                                    .cast()
306                                    .1
307                                    .map_or(true, |b| b.is_some());
308
309                                let mut ground_too_close = self
310                                    .body
311                                    .map(|body| {
312                                        #[cfg(feature = "worldgen")]
313                                        let height_approx = self.pos.0.z
314                                            - read_data
315                                                .world
316                                                .sim()
317                                                .get_alt_approx(
318                                                    self.pos.0.xy().map(|x: f32| x as i32),
319                                                )
320                                                .unwrap_or(0.0);
321                                        #[cfg(not(feature = "worldgen"))]
322                                        let height_approx = self.pos.0.z;
323
324                                        height_approx < body.flying_height()
325                                    })
326                                    .unwrap_or(false);
327
328                                const NUM_RAYS: usize = 5;
329
330                                // NOTE: costs 15-20 us (imbris)
331                                for i in 0..=NUM_RAYS {
332                                    let magnitude = self.body.map_or(20.0, |b| b.flying_height());
333                                    // Lerp between a line straight ahead and straight down to
334                                    // detect a
335                                    // wedge of obstacles we might fly into (inclusive so that both
336                                    // vectors are sampled)
337                                    if let Some(dir) = Lerp::lerp(
338                                        -Vec3::unit_z(),
339                                        Vec3::new(bearing.x, bearing.y, 0.0),
340                                        i as f32 / NUM_RAYS as f32,
341                                    )
342                                    .try_normalized()
343                                    {
344                                        ground_too_close |= read_data
345                                            .terrain
346                                            .ray(self.pos.0, self.pos.0 + magnitude * dir)
347                                            .until(|b: &Block| b.is_solid() || b.is_liquid())
348                                            .cast()
349                                            .1
350                                            .is_ok_and(|b| b.is_some())
351                                    }
352                                }
353
354                                if obstacle_ahead || ground_too_close {
355                                    5.0 //fly up when approaching obstacles
356                                } else {
357                                    -2.0
358                                } //flying things should slowly come down from the stratosphere
359                            } else {
360                                0.05 //normal land traveller offset
361                            };
362
363                        if let Some(mpid) = agent.multi_pid_controllers.as_mut() {
364                            if let Some(z_controller) = mpid.z_controller.as_mut() {
365                                z_controller.sp = self.pos.0.z + height_offset;
366                                controller.inputs.move_z = z_controller.calc_err();
367                                // when changing setpoints, limit PID windup
368                                z_controller.limit_integral_windup(|z| *z = z.clamp(-10.0, 10.0));
369                            } else {
370                                controller.inputs.move_z = 0.0;
371                            }
372                        } else {
373                            controller.inputs.move_z = height_offset;
374                        }
375                    }
376
377                    // Put away weapon
378                    if rng.gen_bool(0.1)
379                        && matches!(
380                            read_data.char_states.get(*self.entity),
381                            Some(CharacterState::Wielding(_))
382                        )
383                    {
384                        controller.push_action(ControlAction::Unwield);
385                    }
386                    break 'activity; // Don't fall through to idle wandering
387                },
388
389                Some(NpcActivity::GotoFlying(
390                    travel_to,
391                    speed_factor,
392                    height_offset,
393                    direction_override,
394                    flight_mode,
395                )) => {
396                    if read_data
397                        .is_volume_riders
398                        .get(*self.entity)
399                        .is_some_and(|r| !r.is_steering_entity())
400                    {
401                        controller.push_event(ControlEvent::Unmount);
402                    }
403
404                    if self.traversal_config.vectored_propulsion {
405                        // This is the action for Airships.
406
407                        // Note - when the Agent code is run, the entity will be the captain that is
408                        // mounted on the ship and the movement calculations
409                        // must be done relative to the captain's position
410                        // which is offset from the ship's position and apparently scaled.
411                        // When the State system runs to apply the movement accel and velocity, the
412                        // ship entity will be the subject entity.
413
414                        // entities that have vectored propulsion should always be flying
415                        // and do not depend on forward movement or displacement to move.
416                        // E.g., Airships.
417                        controller.push_basic_input(InputKind::Fly);
418
419                        // These entities can either:
420                        // - Move in any direction, following the terrain
421                        // - Move essentially vertically, as in
422                        //   - Hover in place (station-keeping), like at a dock
423                        //   - Move straight up or down, as when taking off or landing
424
425                        // If there is lateral movement, then the entity's direction should be
426                        // aligned with that movement direction. If there is
427                        // no or minimal lateral movement, then the entity
428                        // is either hovering or moving vertically, and the entity's direction
429                        // should not change. This is indicated by the direction_override parameter.
430
431                        // If a direction override is provided, attempt to orient the entity in that
432                        // direction.
433                        if let Some(direction) = direction_override {
434                            controller.inputs.look_dir = direction;
435                        } else {
436                            // else orient the entity in the direction of travel, but keep it level
437                            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                        // the look_dir will be used as the orientation override. Orientation
443                        // override is always enabled for airships, so this
444                        // code must set controller.inputs.look_dir for
445                        // all cases (vertical or lateral movement).
446
447                        // When pid_mode is PureZ, only the z component of movement is is adjusted
448                        // by the PID controller.
449
450                        // If the PID controller is not set or the mode or gain has changed, create
451                        // a new one. PidControllers is a wrapper around one
452                        // or more PID controllers. Each controller acts on
453                        // one axis of movement. There are three controllers for FixedDirection mode
454                        // and one for PureZ mode.
455                        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                            // look ahead some blocks to sample the terrain altitude
470                            #[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                            // check if getting close to terrain
497                            if terrain_alt >= self.pos.0.z - 32.0 {
498                                // It's likely the airship will hit an upslope. Maximize the climb
499                                // rate.
500                                controller.inputs.move_z = 1.0 * speed_factor;
501                                // try to stop forward movement
502                                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                            // PID controllers that change the setpoint suffer from "windup", where
511                            // the integral term accumulates error.
512                            // There are several ways to compensate for this. One way is to limit
513                            // the integral term to a range.
514                            mpid.limit_windup_z(|z| *z = z.clamp(-20.0, 20.0));
515                        } else {
516                            // When doing step-wise movement, the target waypoint changes. Make sure
517                            // the PID controller setpoints keep up with
518                            // the changes.
519                            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                            // If terrain following, get the terrain altitude at the current
527                            // position. Set the z setpoint to the max
528                            // of terrain alt + height offset or the
529                            // target z.
530                            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                            // Limit the integral term to a range to prevent windup.
549                            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; // Don't fall through to idle wandering
555                },
556                Some(NpcActivity::Gather(_resources)) => {
557                    // TODO: Implement
558                    controller.push_action(ControlAction::Dance);
559                    break 'activity; // Don't fall through to idle wandering
560                },
561                Some(NpcActivity::Dance(dir)) => {
562                    // Look at targets specified by rtsim
563                    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; // Don't fall through to idle wandering
574                },
575                Some(NpcActivity::Cheer(dir)) => {
576                    if let Some(look_dir) = dir {
577                        controller.inputs.look_dir = look_dir;
578                        if self.ori.look_dir().dot(look_dir.to_vec()) < 0.95 {
579                            controller.inputs.move_dir = look_dir.to_vec().xy() * 0.01;
580                            break 'activity;
581                        } else {
582                            controller.inputs.move_dir = Vec2::zero();
583                        }
584                    }
585                    controller.push_action(ControlAction::Talk);
586                    break 'activity; // Don't fall through to idle wandering
587                },
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; // Don't fall through to idle wandering
609                },
610                Some(NpcActivity::HuntAnimals) => {
611                    if rng.gen::<f32>() < 0.1 {
612                        self.choose_target(
613                            agent,
614                            controller,
615                            read_data,
616                            AgentData::is_hunting_animal,
617                        );
618                    }
619                },
620                Some(NpcActivity::Talk(target)) => {
621                    if agent.target.is_none()
622                        && let Some(target) = read_data.id_maps.actor_entity(target)
623                    {
624                        // We're always aware of someone we're talking to
625                        controller.push_action(ControlAction::Stand);
626                        self.look_toward(controller, read_data, target);
627                        controller.push_action(ControlAction::Talk);
628                        break 'activity;
629                    }
630                },
631                None => {},
632            }
633
634            let owner_uid = self.alignment.and_then(|alignment| match alignment {
635                Alignment::Owned(owner_uid) => Some(owner_uid),
636                _ => None,
637            });
638
639            let owner = owner_uid.and_then(|owner_uid| get_entity_by_id(*owner_uid, read_data));
640
641            let is_being_pet = read_data
642                .interactors
643                .get(*self.entity)
644                .and_then(|interactors| interactors.get(*owner_uid?))
645                .is_some_and(|interaction| matches!(interaction.kind, InteractionKind::Pet));
646
647            let is_in_range = owner
648                .and_then(|owner| read_data.positions.get(owner))
649                .is_some_and(|pos| pos.0.distance_squared(self.pos.0) < MAX_MOUNT_RANGE.powi(2));
650
651            // Idle NPCs should try to jump on the shoulders of their owner, sometimes.
652            if read_data.is_riders.contains(*self.entity) {
653                if rng.gen_bool(0.0001) {
654                    controller.push_event(ControlEvent::Unmount);
655                } else {
656                    break 'activity;
657                }
658            } else if let Some(owner_uid) = owner_uid
659                && is_in_range
660                && !is_being_pet
661                && rng.gen_bool(0.01)
662            {
663                controller.push_event(ControlEvent::Mount(*owner_uid));
664                break 'activity;
665            }
666
667            // Bats should fly
668            // Use a proportional controller as the bouncing effect mimics bat flight
669            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                // Bats don't like the ground, so make sure they are always flying
694                controller.push_basic_input(InputKind::Fly);
695                // Use a proportional controller with a coefficient of 1.0 to
696                // maintain altitude
697                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 on the ground, jump
707                if self.physics_state.on_ground.is_some() {
708                    controller.push_basic_input(InputKind::Jump);
709                }
710            }
711
712            let diff = Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5);
713            agent.bearing += (diff * 0.1 - agent.bearing * 0.01)
714                * agent.psyche.idle_wander_factor.max(0.0).sqrt()
715                * agent.psyche.aggro_range_multiplier.max(0.0).sqrt();
716            if let Some(patrol_origin) = agent.patrol_origin
717                // Use owner as patrol origin otherwise
718                .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            // Stop if we're too close to a wall
734            // or about to walk off a cliff
735            // NOTE: costs 1 us (imbris) <- before cliff raycast added
736            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            // Put away weapon
780            if rng.gen_bool(0.1)
781                && matches!(
782                    read_data.char_states.get(*self.entity),
783                    Some(CharacterState::Wielding(_))
784                )
785            {
786                controller.push_action(ControlAction::Unwield);
787            }
788
789            if rng.gen::<f32>() < 0.0015 {
790                controller.push_utterance(UtteranceKind::Calm);
791            }
792
793            // Sit
794            if rng.gen::<f32>() < 0.0035 {
795                controller.push_action(ControlAction::Sit);
796            }
797        }
798    }
799
800    pub fn follow(
801        &self,
802        agent: &mut Agent,
803        controller: &mut Controller,
804        read_data: &ReadData,
805        tgt_pos: &Pos,
806    ) {
807        if read_data.is_riders.contains(*self.entity)
808            || read_data
809                .is_volume_riders
810                .get(*self.entity)
811                .is_some_and(|r| !r.is_steering_entity())
812        {
813            controller.push_event(ControlEvent::Unmount);
814        }
815
816        if let Some((bearing, speed)) = agent.chaser.chase(
817            &*read_data.terrain,
818            self.pos.0,
819            self.vel.0,
820            tgt_pos.0,
821            TraversalConfig {
822                min_tgt_dist: AVG_FOLLOW_DIST,
823                ..self.traversal_config
824            },
825        ) {
826            let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
827            self.traverse(
828                controller,
829                bearing,
830                speed.min(0.2 + (dist_sqrd - AVG_FOLLOW_DIST.powi(2)) / 8.0),
831            );
832        }
833    }
834
835    pub fn look_toward(
836        &self,
837        controller: &mut Controller,
838        read_data: &ReadData,
839        target: EcsEntity,
840    ) -> bool {
841        if let Some(tgt_pos) = read_data.positions.get(target)
842            && !is_steering(*self.entity, read_data)
843        {
844            let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
845            let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| {
846                b.eye_height(read_data.scales.get(target).map_or(1.0, |s| s.0))
847            });
848            if let Some(dir) = Dir::from_unnormalized(
849                Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
850                    - Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
851            ) {
852                controller.inputs.look_dir = dir;
853            }
854            true
855        } else {
856            false
857        }
858    }
859
860    pub fn flee(
861        &self,
862        agent: &mut Agent,
863        controller: &mut Controller,
864        read_data: &ReadData,
865        tgt_pos: &Pos,
866    ) {
867        // Proportion of full speed
868        const MAX_FLEE_SPEED: f32 = 0.65;
869
870        if read_data.is_riders.contains(*self.entity)
871            || read_data
872                .is_volume_riders
873                .get(*self.entity)
874                .is_some_and(|r| !r.is_steering_entity())
875        {
876            controller.push_event(ControlEvent::Unmount);
877        }
878
879        if let Some(body) = self.body {
880            if body.can_strafe() && !self.is_gliding {
881                controller.push_action(ControlAction::Unwield);
882            }
883        }
884
885        if let Some((bearing, speed)) = agent.chaser.chase(
886            &*read_data.terrain,
887            self.pos.0,
888            self.vel.0,
889            // Away from the target (ironically)
890            self.pos.0
891                + (self.pos.0 - tgt_pos.0)
892                    .try_normalized()
893                    .unwrap_or_else(Vec3::unit_y)
894                    * 50.0,
895            TraversalConfig {
896                min_tgt_dist: 1.25,
897                ..self.traversal_config
898            },
899        ) {
900            self.traverse(controller, bearing, speed.min(MAX_FLEE_SPEED));
901        }
902    }
903
904    /// Attempt to consume a healing item, and return whether any healing items
905    /// were queued. Callers should use this to implement a delay so that
906    /// the healing isn't interrupted. If `relaxed` is `true`, we allow eating
907    /// food and prioritise healing.
908    pub fn heal_self(
909        &self,
910        _agent: &mut Agent,
911        controller: &mut Controller,
912        relaxed: bool,
913    ) -> bool {
914        // Wait for potion sickness to wear off if potions are less than 50% effective.
915        let heal_multiplier = self.stats.map_or(1.0, |s| s.item_effect_reduction);
916        if heal_multiplier < 0.5 {
917            return false;
918        }
919        // (healing_value, heal_reduction)
920        let effect_healing_value = |effect: &Effect| -> (f32, f32) {
921            let mut value = 0.0;
922            let mut heal_reduction = 0.0;
923            match effect {
924                Effect::Health(HealthChange { amount, .. }) => {
925                    value += *amount;
926                },
927                Effect::Buff(BuffEffect { kind, data, .. }) => {
928                    if let Some(duration) = data.duration {
929                        for effect in kind.effects(data) {
930                            match effect {
931                                comp::BuffEffect::HealthChangeOverTime { rate, kind, .. } => {
932                                    let amount = match kind {
933                                        comp::ModifierKind::Additive => rate * duration.0 as f32,
934                                        comp::ModifierKind::Multiplicative => {
935                                            (1.0 + rate).powf(duration.0 as f32)
936                                        },
937                                    };
938
939                                    value += amount;
940                                },
941                                comp::BuffEffect::ItemEffectReduction(amount) => {
942                                    heal_reduction =
943                                        heal_reduction + amount - heal_reduction * amount;
944                                },
945                                _ => {},
946                            }
947                        }
948                        value += data.strength * data.duration.map_or(0.0, |d| d.0 as f32);
949                    }
950                },
951
952                _ => {},
953            }
954
955            (value, heal_reduction)
956        };
957        let healing_value = |item: &Item| {
958            let mut value = 0.0;
959            let mut heal_multiplier_value = 1.0;
960
961            if let ItemKind::Consumable { kind, effects, .. } = &*item.kind() {
962                if matches!(kind, ConsumableKind::Drink)
963                    || (relaxed && matches!(kind, ConsumableKind::Food))
964                {
965                    match effects {
966                        Effects::Any(effects) => {
967                            // Add the average of all effects.
968                            for effect in effects.iter() {
969                                let (add, red) = effect_healing_value(effect);
970                                value += add / effects.len() as f32;
971                                heal_multiplier_value *= 1.0 - red / effects.len() as f32;
972                            }
973                        },
974                        Effects::All(_) | Effects::One(_) => {
975                            for effect in effects.effects() {
976                                let (add, red) = effect_healing_value(effect);
977                                value += add;
978                                heal_multiplier_value *= 1.0 - red;
979                            }
980                        },
981                    }
982                }
983            }
984            // Prefer non-potion sources of healing when under at least one stack of potion
985            // sickness, or when incurring potion sickness is unnecessary
986            if heal_multiplier_value < 1.0 && (heal_multiplier < 1.0 || relaxed) {
987                value *= 0.1;
988            }
989            value as i32
990        };
991
992        let item = self
993            .inventory
994            .slots_with_id()
995            .filter_map(|(id, slot)| match slot {
996                Some(item) if healing_value(item) > 0 => Some((id, item)),
997                _ => None,
998            })
999            .max_by_key(|(_, item)| {
1000                if relaxed {
1001                    -healing_value(item)
1002                } else {
1003                    healing_value(item)
1004                }
1005            });
1006
1007        if let Some((id, _)) = item {
1008            use comp::inventory::slot::Slot;
1009            controller.push_action(ControlAction::InventoryAction(InventoryAction::Use(
1010                Slot::Inventory(id),
1011            )));
1012            true
1013        } else {
1014            false
1015        }
1016    }
1017
1018    pub fn choose_target(
1019        &self,
1020        agent: &mut Agent,
1021        controller: &mut Controller,
1022        read_data: &ReadData,
1023        is_enemy: fn(&Self, EcsEntity, &ReadData) -> bool,
1024    ) {
1025        enum ActionStateTimers {
1026            TimerChooseTarget = 0,
1027        }
1028        agent.behavior_state.timers[ActionStateTimers::TimerChooseTarget as usize] = 0.0;
1029        let mut aggro_on = false;
1030
1031        // Search the area.
1032        // TODO: choose target by more than just distance
1033        let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
1034
1035        let entities_nearby = grid
1036            .in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist())
1037            .collect_vec();
1038
1039        let get_pos = |entity| read_data.positions.get(entity);
1040        let get_enemy = |(entity, attack_target): (EcsEntity, bool)| {
1041            if attack_target {
1042                if is_enemy(self, entity, read_data) {
1043                    Some((entity, true))
1044                } else if self.should_defend(entity, read_data) {
1045                    if let Some(attacker) = get_attacker(entity, read_data) {
1046                        if !self.passive_towards(attacker, read_data) {
1047                            // aggro_on: attack immediately, do not warn/menace.
1048                            aggro_on = true;
1049                            Some((attacker, true))
1050                        } else {
1051                            None
1052                        }
1053                    } else {
1054                        None
1055                    }
1056                } else {
1057                    None
1058                }
1059            } else {
1060                Some((entity, false))
1061            }
1062        };
1063        let is_valid_target = |entity: EcsEntity| match read_data.bodies.get(entity) {
1064            Some(Body::ItemDrop(item)) => {
1065                let is_humanoid = matches!(self.body, Some(Body::Humanoid(_)));
1066                // If the agent is humanoid, it will pick up all kinds of item drops. If the
1067                // agent isn't humanoid, it will pick up only consumable item drops.
1068                let wants_pickup = is_humanoid || matches!(item, item_drop::Body::Consumable);
1069
1070                // The agent will attempt to pickup the item if it wants to pick it up and
1071                // is allowed to
1072                let attempt_pickup = wants_pickup
1073                    && read_data
1074                        .loot_owners
1075                        .get(entity).is_none_or(|loot_owner| {
1076                            !(is_humanoid
1077                                && loot_owner.is_soft()
1078                                // If we are hostile towards the owner, ignore their wish to not pick up the loot
1079                                && loot_owner
1080                                    .uid()
1081                                    .and_then(|uid| read_data.id_maps.uid_entity(uid)).is_none_or(|entity| !is_enemy(self, entity, read_data)))
1082                                && loot_owner.can_pickup(
1083                                    *self.uid,
1084                                    read_data.groups.get(entity),
1085                                    self.alignment,
1086                                    self.body,
1087                                    None,
1088                                )
1089                        });
1090
1091                if attempt_pickup {
1092                    Some((entity, false))
1093                } else {
1094                    None
1095                }
1096            },
1097            _ => {
1098                if read_data
1099                    .healths
1100                    .get(entity)
1101                    .is_some_and(|health| !health.is_dead && !is_invulnerable(entity, read_data))
1102                {
1103                    let needs_saving = comp::is_downed(
1104                        read_data.healths.get(entity),
1105                        read_data.char_states.get(entity),
1106                    );
1107
1108                    let wants_to_save = match (self.alignment, read_data.alignments.get(entity)) {
1109                        // Npcs generally do want to save players. Could have extra checks for
1110                        // sentiment in the future.
1111                        (Some(Alignment::Npc), _) if read_data.presences.get(entity).is_some_and(|presence| matches!(presence.kind, PresenceKind::Character(_))) => true,
1112                        (Some(Alignment::Npc), Some(Alignment::Npc)) => true,
1113                        (Some(Alignment::Enemy), Some(Alignment::Enemy)) => true,
1114                        _ => false,
1115                    } && agent.allowed_to_speak()
1116                        // Check that anyone else isn't already saving them.
1117                        && read_data
1118                            .interactors
1119                            .get(entity).is_none_or(|interactors| {
1120                                !interactors.has_interaction(InteractionKind::HelpDowned)
1121                            }) && self.char_state.can_interact();
1122
1123                    // TODO: Make targets that need saving have less priority as a target.
1124                    Some((entity, !(needs_saving && wants_to_save)))
1125                } else {
1126                    None
1127                }
1128            },
1129        };
1130
1131        let is_detected = |entity: &EcsEntity, e_pos: &Pos, e_scale: Option<&Scale>| {
1132            self.detects_other(agent, controller, entity, e_pos, e_scale, read_data)
1133        };
1134
1135        let target = entities_nearby
1136            .iter()
1137            .filter_map(|e| is_valid_target(*e))
1138            .filter_map(get_enemy)
1139            .filter_map(|(entity, attack_target)| {
1140                get_pos(entity).map(|pos| (entity, pos, attack_target))
1141            })
1142            .filter(|(entity, e_pos, _)| is_detected(entity, e_pos, read_data.scales.get(*entity)))
1143            .min_by_key(|(_, e_pos, attack_target)| {
1144                (
1145                    *attack_target,
1146                    (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32,
1147                )
1148            })
1149            .map(|(entity, _, attack_target)| (entity, attack_target));
1150
1151        if agent.target.is_none() && target.is_some() {
1152            if aggro_on {
1153                controller.push_utterance(UtteranceKind::Angry);
1154            } else {
1155                controller.push_utterance(UtteranceKind::Surprised);
1156            }
1157        }
1158        if agent.psyche.should_stop_pursuing || target.is_some() {
1159            agent.target = target.map(|(entity, attack_target)| Target {
1160                target: entity,
1161                hostile: attack_target,
1162                selected_at: read_data.time.0,
1163                aggro_on,
1164                last_known_pos: get_pos(entity).map(|pos| pos.0),
1165            })
1166        }
1167    }
1168
1169    pub fn attack(
1170        &self,
1171        agent: &mut Agent,
1172        controller: &mut Controller,
1173        tgt_data: &TargetData,
1174        read_data: &ReadData,
1175        rng: &mut impl Rng,
1176    ) {
1177        #[cfg(any(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
1178        let _rng = rng;
1179
1180        #[cfg(not(feature = "use-dyn-lib"))]
1181        {
1182            #[cfg(not(feature = "be-dyn-lib"))]
1183            self.attack_inner(agent, controller, tgt_data, read_data, rng);
1184            #[cfg(feature = "be-dyn-lib")]
1185            self.attack_inner(agent, controller, tgt_data, read_data);
1186        }
1187        #[cfg(feature = "use-dyn-lib")]
1188        {
1189            let lock = LIB.lock().unwrap();
1190            let lib = &lock.as_ref().unwrap().lib;
1191            const ATTACK_FN: &[u8] = b"attack_inner\0";
1192
1193            let attack_fn: common_dynlib::Symbol<
1194                fn(&Self, &mut Agent, &mut Controller, &TargetData, &ReadData),
1195            > = unsafe { lib.get(ATTACK_FN) }.unwrap_or_else(|e| {
1196                panic!(
1197                    "Trying to use: {} but had error: {:?}",
1198                    CStr::from_bytes_with_nul(ATTACK_FN)
1199                        .map(CStr::to_str)
1200                        .unwrap()
1201                        .unwrap(),
1202                    e
1203                )
1204            });
1205            attack_fn(self, agent, controller, tgt_data, read_data);
1206        }
1207    }
1208
1209    #[cfg_attr(feature = "be-dyn-lib", unsafe(export_name = "attack_inner"))]
1210    pub fn attack_inner(
1211        &self,
1212        agent: &mut Agent,
1213        controller: &mut Controller,
1214        tgt_data: &TargetData,
1215        read_data: &ReadData,
1216        #[cfg(not(feature = "be-dyn-lib"))] rng: &mut impl Rng,
1217    ) {
1218        #[cfg(feature = "be-dyn-lib")]
1219        let rng = &mut thread_rng();
1220
1221        self.dismount(controller, read_data);
1222
1223        let tool_tactic = |tool_kind| match tool_kind {
1224            ToolKind::Bow => Tactic::Bow,
1225            ToolKind::Staff => Tactic::Staff,
1226            ToolKind::Sceptre => Tactic::Sceptre,
1227            ToolKind::Hammer => Tactic::Hammer,
1228            ToolKind::Sword | ToolKind::Blowgun => Tactic::Sword,
1229            ToolKind::Axe => Tactic::Axe,
1230            _ => Tactic::SimpleMelee,
1231        };
1232
1233        let tactic = self
1234            .inventory
1235            .equipped(EquipSlot::ActiveMainhand)
1236            .as_ref()
1237            .map(|item| {
1238                if let Some(ability_spec) = item.ability_spec() {
1239                    match &*ability_spec {
1240                        AbilitySpec::Custom(spec) => match spec.as_str() {
1241                            "Oni" | "Sword Simple" | "BipedLargeCultistSword" => {
1242                                Tactic::SwordSimple
1243                            },
1244                            "Staff Simple" | "BipedLargeCultistStaff" => Tactic::Staff,
1245                            "BipedLargeCultistHammer" => Tactic::Hammer,
1246                            "Simple Flying Melee" => Tactic::SimpleFlyingMelee,
1247                            "Bow Simple" | "BipedLargeCultistBow" => Tactic::Bow,
1248                            "Stone Golem" | "Coral Golem" => Tactic::StoneGolem,
1249                            "Iron Golem" => Tactic::IronGolem,
1250                            "Quad Med Quick" => Tactic::CircleCharge {
1251                                radius: 3,
1252                                circle_time: 2,
1253                            },
1254                            "Quad Med Jump" | "Darkhound" => Tactic::QuadMedJump,
1255                            "Quad Med Charge" => Tactic::CircleCharge {
1256                                radius: 6,
1257                                circle_time: 1,
1258                            },
1259                            "Quad Med Basic" => Tactic::QuadMedBasic,
1260                            "Quad Med Hoof" => Tactic::QuadMedHoof,
1261                            "ClaySteed" => Tactic::ClaySteed,
1262                            "Rocksnapper" => Tactic::Rocksnapper,
1263                            "Roshwalr" => Tactic::Roshwalr,
1264                            "Asp" | "Maneater" => Tactic::QuadLowRanged,
1265                            "Quad Low Breathe" | "Quad Low Beam" | "Basilisk" => {
1266                                Tactic::QuadLowBeam
1267                            },
1268                            "Organ" => Tactic::OrganAura,
1269                            "Quad Low Tail" | "Husk Brute" => Tactic::TailSlap,
1270                            "Quad Low Quick" => Tactic::QuadLowQuick,
1271                            "Quad Low Basic" => Tactic::QuadLowBasic,
1272                            "Theropod Basic" | "Theropod Bird" | "Theropod Small" => {
1273                                Tactic::Theropod
1274                            },
1275                            // Arthropods
1276                            "Antlion" => Tactic::ArthropodMelee,
1277                            "Tarantula" | "Horn Beetle" => Tactic::ArthropodAmbush,
1278                            "Weevil" | "Black Widow" | "Crawler" => Tactic::ArthropodRanged,
1279                            "Theropod Charge" => Tactic::CircleCharge {
1280                                radius: 6,
1281                                circle_time: 1,
1282                            },
1283                            "Turret" => Tactic::RadialTurret,
1284                            "Flamethrower" => Tactic::RadialTurret,
1285                            "Haniwa Sentry" => Tactic::RotatingTurret,
1286                            "Bird Large Breathe" => Tactic::BirdLargeBreathe,
1287                            "Bird Large Fire" => Tactic::BirdLargeFire,
1288                            "Bird Large Basic" => Tactic::BirdLargeBasic,
1289                            "Flame Wyvern" | "Frost Wyvern" | "Cloud Wyvern" | "Sea Wyvern"
1290                            | "Weald Wyvern" => Tactic::Wyvern,
1291                            "Bird Medium Basic" => Tactic::BirdMediumBasic,
1292                            "Bushly" | "Cactid" | "Irrwurz" | "Driggle" | "Mossy Snail"
1293                            | "Strigoi Claws" | "Harlequin" => Tactic::SimpleDouble,
1294                            "Clay Golem" => Tactic::ClayGolem,
1295                            "Ancient Effigy" => Tactic::AncientEffigy,
1296                            "TerracottaStatue" | "Mogwai" => Tactic::TerracottaStatue,
1297                            "TerracottaBesieger" => Tactic::Bow,
1298                            "TerracottaDemolisher" => Tactic::SimpleDouble,
1299                            "TerracottaPunisher" => Tactic::SimpleMelee,
1300                            "TerracottaPursuer" => Tactic::SwordSimple,
1301                            "Cursekeeper" => Tactic::Cursekeeper,
1302                            "CursekeeperFake" => Tactic::CursekeeperFake,
1303                            "ShamanicSpirit" => Tactic::ShamanicSpirit,
1304                            "Jiangshi" => Tactic::Jiangshi,
1305                            "Mindflayer" => Tactic::Mindflayer,
1306                            "Flamekeeper" => Tactic::Flamekeeper,
1307                            "Forgemaster" => Tactic::Forgemaster,
1308                            "Minotaur" => Tactic::Minotaur,
1309                            "Cyclops" => Tactic::Cyclops,
1310                            "Dullahan" => Tactic::Dullahan,
1311                            "Grave Warden" => Tactic::GraveWarden,
1312                            "Tidal Warrior" => Tactic::TidalWarrior,
1313                            "Karkatha" => Tactic::Karkatha,
1314                            "Tidal Totem"
1315                            | "Tornado"
1316                            | "Gnarling Totem Red"
1317                            | "Gnarling Totem Green"
1318                            | "Gnarling Totem White" => Tactic::RadialTurret,
1319                            "FieryTornado" => Tactic::FieryTornado,
1320                            "Yeti" => Tactic::Yeti,
1321                            "Harvester" => Tactic::Harvester,
1322                            "Cardinal" => Tactic::Cardinal,
1323                            "Sea Bishop" => Tactic::SeaBishop,
1324                            "Dagon" => Tactic::Dagon,
1325                            "Snaretongue" => Tactic::Snaretongue,
1326                            "Dagonite" => Tactic::ArthropodAmbush,
1327                            "Gnarling Dagger" => Tactic::SimpleBackstab,
1328                            "Gnarling Blowgun" => Tactic::ElevatedRanged,
1329                            "Deadwood" => Tactic::Deadwood,
1330                            "Mandragora" => Tactic::Mandragora,
1331                            "Wood Golem" => Tactic::WoodGolem,
1332                            "Gnarling Chieftain" => Tactic::GnarlingChieftain,
1333                            "Frost Gigas" => Tactic::FrostGigas,
1334                            "Boreal Hammer" => Tactic::BorealHammer,
1335                            "Boreal Bow" => Tactic::BorealBow,
1336                            "Adlet Hunter" => Tactic::AdletHunter,
1337                            "Adlet Icepicker" => Tactic::AdletIcepicker,
1338                            "Adlet Tracker" => Tactic::AdletTracker,
1339                            "Hydra" => Tactic::Hydra,
1340                            "Ice Drake" => Tactic::IceDrake,
1341                            "Frostfang" => Tactic::RandomAbilities {
1342                                primary: 1,
1343                                secondary: 3,
1344                                abilities: [0; BASE_ABILITY_LIMIT],
1345                            },
1346                            "Tursus Claws" => Tactic::RandomAbilities {
1347                                primary: 2,
1348                                secondary: 1,
1349                                abilities: [4, 0, 0, 0, 0],
1350                            },
1351                            "Adlet Elder" => Tactic::AdletElder,
1352                            "Haniwa Soldier" => Tactic::HaniwaSoldier,
1353                            "Haniwa Guard" => Tactic::HaniwaGuard,
1354                            "Haniwa Archer" => Tactic::HaniwaArcher,
1355                            "Bloodmoon Bat" => Tactic::BloodmoonBat,
1356                            "Vampire Bat" => Tactic::VampireBat,
1357                            "Bloodmoon Heiress" => Tactic::BloodmoonHeiress,
1358
1359                            _ => Tactic::SimpleMelee,
1360                        },
1361                        AbilitySpec::Tool(tool_kind) => tool_tactic(*tool_kind),
1362                    }
1363                } else if let ItemKind::Tool(tool) = &*item.kind() {
1364                    tool_tactic(tool.kind)
1365                } else {
1366                    Tactic::SimpleMelee
1367                }
1368            })
1369            .unwrap_or(Tactic::SimpleMelee);
1370
1371        // Wield the weapon as running towards the target
1372        controller.push_action(ControlAction::Wield);
1373
1374        // Information for attack checks
1375        // 'min_attack_dist' uses DEFAULT_ATTACK_RANGE, while 'body_dist' does not
1376        let self_radius = self.body.map_or(0.5, |b| b.max_radius()) * self.scale;
1377        let self_attack_range =
1378            (self.body.map_or(0.5, |b| b.front_radius()) + DEFAULT_ATTACK_RANGE) * self.scale;
1379        let tgt_radius =
1380            tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0);
1381        let min_attack_dist = self_attack_range + tgt_radius;
1382        let body_dist = self_radius + tgt_radius;
1383        let dist_sqrd = self.pos.0.distance_squared(tgt_data.pos.0);
1384        let angle = self
1385            .ori
1386            .look_vec()
1387            .angle_between(tgt_data.pos.0 - self.pos.0)
1388            .to_degrees();
1389        let angle_xy = self
1390            .ori
1391            .look_vec()
1392            .xy()
1393            .angle_between((tgt_data.pos.0 - self.pos.0).xy())
1394            .to_degrees();
1395
1396        let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale));
1397
1398        let tgt_eye_height = tgt_data
1399            .body
1400            .map_or(0.0, |b| b.eye_height(tgt_data.scale.map_or(1.0, |s| s.0)));
1401        let tgt_eye_offset = tgt_eye_height +
1402                   // Special case for jumping attacks to jump at the body
1403                   // of the target and not the ground around the target
1404                   // For the ranged it is to shoot at the feet and not
1405                   // the head to get splash damage
1406                   if tactic == Tactic::QuadMedJump {
1407                       1.0
1408                   } else if matches!(tactic, Tactic::QuadLowRanged) {
1409                       -1.0
1410                   } else {
1411                       0.0
1412                   };
1413
1414        // FIXME:
1415        // 1) Retrieve actual projectile speed!
1416        // We have to assume projectiles are faster than base speed because there are
1417        // skills that increase it, and in most cases this will cause agents to
1418        // overshoot
1419        //
1420        // 2) We use eye_offset-s which isn't actually ideal.
1421        // Some attacks (beam for example) may use different offsets,
1422        // we should probably use offsets from corresponding states.
1423        //
1424        // 3) Should we even have this big switch?
1425        // Not all attacks may want their direction overwritten.
1426        // And this is quite hard to debug when you don't see it in actual
1427        // attack handler.
1428        if let Some(dir) = match self.char_state {
1429            CharacterState::ChargedRanged(c) if dist_sqrd > 0.0 => {
1430                let charge_factor =
1431                    c.timer.as_secs_f32() / c.static_data.charge_duration.as_secs_f32();
1432                let projectile_speed = c.static_data.initial_projectile_speed
1433                    + charge_factor * c.static_data.scaled_projectile_speed;
1434                aim_projectile(
1435                    projectile_speed,
1436                    self.pos.0
1437                        + self.body.map_or(Vec3::zero(), |body| {
1438                            body.projectile_offsets(self.ori.look_vec(), self.scale)
1439                        }),
1440                    Vec3::new(
1441                        tgt_data.pos.0.x,
1442                        tgt_data.pos.0.y,
1443                        tgt_data.pos.0.z + tgt_eye_offset,
1444                    ),
1445                )
1446            },
1447            CharacterState::BasicRanged(c) => {
1448                let offset_z = match c.static_data.projectile.kind {
1449                    // Aim explosives and hazards at feet instead of eyes for splash damage
1450                    ProjectileConstructorKind::Explosive { .. }
1451                    | ProjectileConstructorKind::ExplosiveHazard { .. }
1452                    | ProjectileConstructorKind::Hazard { .. } => 0.0,
1453                    _ => tgt_eye_offset,
1454                };
1455                let projectile_speed = c.static_data.projectile_speed;
1456                aim_projectile(
1457                    projectile_speed,
1458                    self.pos.0
1459                        + self.body.map_or(Vec3::zero(), |body| {
1460                            body.projectile_offsets(self.ori.look_vec(), self.scale)
1461                        }),
1462                    Vec3::new(
1463                        tgt_data.pos.0.x,
1464                        tgt_data.pos.0.y,
1465                        tgt_data.pos.0.z + offset_z,
1466                    ),
1467                )
1468            },
1469            CharacterState::RepeaterRanged(c) => {
1470                let projectile_speed = c.static_data.projectile_speed;
1471                aim_projectile(
1472                    projectile_speed,
1473                    self.pos.0
1474                        + self.body.map_or(Vec3::zero(), |body| {
1475                            body.projectile_offsets(self.ori.look_vec(), self.scale)
1476                        }),
1477                    Vec3::new(
1478                        tgt_data.pos.0.x,
1479                        tgt_data.pos.0.y,
1480                        tgt_data.pos.0.z + tgt_eye_offset,
1481                    ),
1482                )
1483            },
1484            CharacterState::LeapMelee(_)
1485                if matches!(tactic, Tactic::Hammer | Tactic::BorealHammer | Tactic::Axe) =>
1486            {
1487                let direction_weight = match tactic {
1488                    Tactic::Hammer | Tactic::BorealHammer => 0.1,
1489                    Tactic::Axe => 0.3,
1490                    _ => unreachable!("Direction weight called on incorrect tactic."),
1491                };
1492
1493                let tgt_pos = tgt_data.pos.0;
1494                let self_pos = self.pos.0;
1495
1496                let delta_x = (tgt_pos.x - self_pos.x) * direction_weight;
1497                let delta_y = (tgt_pos.y - self_pos.y) * direction_weight;
1498
1499                Dir::from_unnormalized(Vec3::new(delta_x, delta_y, -1.0))
1500            },
1501            CharacterState::BasicBeam(_) => {
1502                let aim_from = self.body.map_or(self.pos.0, |body| {
1503                    self.pos.0
1504                        + basic_beam::beam_offsets(
1505                            body,
1506                            controller.inputs.look_dir,
1507                            self.ori.look_vec(),
1508                            // Try to match animation by getting some context
1509                            self.vel.0 - self.physics_state.ground_vel,
1510                            self.physics_state.on_ground,
1511                        )
1512                });
1513                let aim_to = Vec3::new(
1514                    tgt_data.pos.0.x,
1515                    tgt_data.pos.0.y,
1516                    tgt_data.pos.0.z + tgt_eye_offset,
1517                );
1518                Dir::from_unnormalized(aim_to - aim_from)
1519            },
1520            _ => {
1521                let aim_from = Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset);
1522                let aim_to = Vec3::new(
1523                    tgt_data.pos.0.x,
1524                    tgt_data.pos.0.y,
1525                    tgt_data.pos.0.z + tgt_eye_offset,
1526                );
1527                Dir::from_unnormalized(aim_to - aim_from)
1528            },
1529        } {
1530            controller.inputs.look_dir = dir;
1531        }
1532
1533        let attack_data = AttackData {
1534            body_dist,
1535            min_attack_dist,
1536            dist_sqrd,
1537            angle,
1538            angle_xy,
1539        };
1540
1541        // Match on tactic. Each tactic has different controls depending on the distance
1542        // from the agent to the target.
1543        match tactic {
1544            Tactic::SimpleFlyingMelee => self.handle_simple_flying_melee(
1545                agent,
1546                controller,
1547                &attack_data,
1548                tgt_data,
1549                read_data,
1550                rng,
1551            ),
1552            Tactic::SimpleMelee => {
1553                self.handle_simple_melee(agent, controller, &attack_data, tgt_data, read_data, rng)
1554            },
1555            Tactic::Axe => {
1556                self.handle_axe_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1557            },
1558            Tactic::Hammer => {
1559                self.handle_hammer_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1560            },
1561            Tactic::Sword => {
1562                self.handle_sword_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1563            },
1564            Tactic::Bow => {
1565                self.handle_bow_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1566            },
1567            Tactic::Staff => {
1568                self.handle_staff_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1569            },
1570            Tactic::Sceptre => self.handle_sceptre_attack(
1571                agent,
1572                controller,
1573                &attack_data,
1574                tgt_data,
1575                read_data,
1576                rng,
1577            ),
1578            Tactic::StoneGolem => {
1579                self.handle_stone_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1580            },
1581            Tactic::IronGolem => {
1582                self.handle_iron_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1583            },
1584            Tactic::CircleCharge {
1585                radius,
1586                circle_time,
1587            } => self.handle_circle_charge_attack(
1588                agent,
1589                controller,
1590                &attack_data,
1591                tgt_data,
1592                read_data,
1593                radius,
1594                circle_time,
1595                rng,
1596            ),
1597            Tactic::QuadLowRanged => self.handle_quadlow_ranged_attack(
1598                agent,
1599                controller,
1600                &attack_data,
1601                tgt_data,
1602                read_data,
1603            ),
1604            Tactic::TailSlap => {
1605                self.handle_tail_slap_attack(agent, controller, &attack_data, tgt_data, read_data)
1606            },
1607            Tactic::QuadLowQuick => self.handle_quadlow_quick_attack(
1608                agent,
1609                controller,
1610                &attack_data,
1611                tgt_data,
1612                read_data,
1613            ),
1614            Tactic::QuadLowBasic => self.handle_quadlow_basic_attack(
1615                agent,
1616                controller,
1617                &attack_data,
1618                tgt_data,
1619                read_data,
1620            ),
1621            Tactic::QuadMedJump => self.handle_quadmed_jump_attack(
1622                agent,
1623                controller,
1624                &attack_data,
1625                tgt_data,
1626                read_data,
1627            ),
1628            Tactic::QuadMedBasic => self.handle_quadmed_basic_attack(
1629                agent,
1630                controller,
1631                &attack_data,
1632                tgt_data,
1633                read_data,
1634            ),
1635            Tactic::QuadMedHoof => self.handle_quadmed_hoof_attack(
1636                agent,
1637                controller,
1638                &attack_data,
1639                tgt_data,
1640                read_data,
1641            ),
1642            Tactic::QuadLowBeam => self.handle_quadlow_beam_attack(
1643                agent,
1644                controller,
1645                &attack_data,
1646                tgt_data,
1647                read_data,
1648            ),
1649            Tactic::Rocksnapper => {
1650                self.handle_rocksnapper_attack(agent, controller, &attack_data, tgt_data, read_data)
1651            },
1652            Tactic::Roshwalr => {
1653                self.handle_roshwalr_attack(agent, controller, &attack_data, tgt_data, read_data)
1654            },
1655            Tactic::OrganAura => {
1656                self.handle_organ_aura_attack(agent, controller, &attack_data, tgt_data, read_data)
1657            },
1658            Tactic::Theropod => {
1659                self.handle_theropod_attack(agent, controller, &attack_data, tgt_data, read_data)
1660            },
1661            Tactic::ArthropodMelee => self.handle_arthropod_melee_attack(
1662                agent,
1663                controller,
1664                &attack_data,
1665                tgt_data,
1666                read_data,
1667            ),
1668            Tactic::ArthropodAmbush => self.handle_arthropod_ambush_attack(
1669                agent,
1670                controller,
1671                &attack_data,
1672                tgt_data,
1673                read_data,
1674                rng,
1675            ),
1676            Tactic::ArthropodRanged => self.handle_arthropod_ranged_attack(
1677                agent,
1678                controller,
1679                &attack_data,
1680                tgt_data,
1681                read_data,
1682            ),
1683            Tactic::Turret => {
1684                self.handle_turret_attack(agent, controller, &attack_data, tgt_data, read_data)
1685            },
1686            Tactic::FixedTurret => self.handle_fixed_turret_attack(
1687                agent,
1688                controller,
1689                &attack_data,
1690                tgt_data,
1691                read_data,
1692            ),
1693            Tactic::RotatingTurret => {
1694                self.handle_rotating_turret_attack(agent, controller, tgt_data, read_data)
1695            },
1696            Tactic::Mindflayer => self.handle_mindflayer_attack(
1697                agent,
1698                controller,
1699                &attack_data,
1700                tgt_data,
1701                read_data,
1702                rng,
1703            ),
1704            Tactic::Flamekeeper => {
1705                self.handle_flamekeeper_attack(agent, controller, &attack_data, tgt_data, read_data)
1706            },
1707            Tactic::Forgemaster => {
1708                self.handle_forgemaster_attack(agent, controller, &attack_data, tgt_data, read_data)
1709            },
1710            Tactic::BirdLargeFire => self.handle_birdlarge_fire_attack(
1711                agent,
1712                controller,
1713                &attack_data,
1714                tgt_data,
1715                read_data,
1716                rng,
1717            ),
1718            // Mostly identical to BirdLargeFire but tweaked for flamethrower instead of shockwave
1719            Tactic::BirdLargeBreathe => self.handle_birdlarge_breathe_attack(
1720                agent,
1721                controller,
1722                &attack_data,
1723                tgt_data,
1724                read_data,
1725                rng,
1726            ),
1727            Tactic::BirdLargeBasic => self.handle_birdlarge_basic_attack(
1728                agent,
1729                controller,
1730                &attack_data,
1731                tgt_data,
1732                read_data,
1733            ),
1734            Tactic::Wyvern => {
1735                self.handle_wyvern_attack(agent, controller, &attack_data, tgt_data, read_data, rng)
1736            },
1737            Tactic::BirdMediumBasic => {
1738                self.handle_simple_melee(agent, controller, &attack_data, tgt_data, read_data, rng)
1739            },
1740            Tactic::SimpleDouble => self.handle_simple_double_attack(
1741                agent,
1742                controller,
1743                &attack_data,
1744                tgt_data,
1745                read_data,
1746            ),
1747            Tactic::Jiangshi => {
1748                self.handle_jiangshi_attack(agent, controller, &attack_data, tgt_data, read_data)
1749            },
1750            Tactic::ClayGolem => {
1751                self.handle_clay_golem_attack(agent, controller, &attack_data, tgt_data, read_data)
1752            },
1753            Tactic::ClaySteed => {
1754                self.handle_clay_steed_attack(agent, controller, &attack_data, tgt_data, read_data)
1755            },
1756            Tactic::AncientEffigy => self.handle_ancient_effigy_attack(
1757                agent,
1758                controller,
1759                &attack_data,
1760                tgt_data,
1761                read_data,
1762            ),
1763            Tactic::TerracottaStatue => {
1764                self.handle_terracotta_statue_attack(agent, controller, &attack_data, read_data)
1765            },
1766            Tactic::Minotaur => {
1767                self.handle_minotaur_attack(agent, controller, &attack_data, tgt_data, read_data)
1768            },
1769            Tactic::Cyclops => {
1770                self.handle_cyclops_attack(agent, controller, &attack_data, tgt_data, read_data)
1771            },
1772            Tactic::Dullahan => {
1773                self.handle_dullahan_attack(agent, controller, &attack_data, tgt_data, read_data)
1774            },
1775            Tactic::GraveWarden => self.handle_grave_warden_attack(
1776                agent,
1777                controller,
1778                &attack_data,
1779                tgt_data,
1780                read_data,
1781            ),
1782            Tactic::TidalWarrior => self.handle_tidal_warrior_attack(
1783                agent,
1784                controller,
1785                &attack_data,
1786                tgt_data,
1787                read_data,
1788            ),
1789            Tactic::Karkatha => self.handle_karkatha_attack(
1790                agent,
1791                controller,
1792                &attack_data,
1793                tgt_data,
1794                read_data,
1795                rng,
1796            ),
1797            Tactic::RadialTurret => self.handle_radial_turret_attack(controller),
1798            Tactic::FieryTornado => self.handle_fiery_tornado_attack(agent, controller),
1799            Tactic::Yeti => {
1800                self.handle_yeti_attack(agent, controller, &attack_data, tgt_data, read_data)
1801            },
1802            Tactic::Harvester => self.handle_harvester_attack(
1803                agent,
1804                controller,
1805                &attack_data,
1806                tgt_data,
1807                read_data,
1808                rng,
1809            ),
1810            Tactic::Cardinal => self.handle_cardinal_attack(
1811                agent,
1812                controller,
1813                &attack_data,
1814                tgt_data,
1815                read_data,
1816                rng,
1817            ),
1818            Tactic::SeaBishop => self.handle_sea_bishop_attack(
1819                agent,
1820                controller,
1821                &attack_data,
1822                tgt_data,
1823                read_data,
1824                rng,
1825            ),
1826            Tactic::Cursekeeper => self.handle_cursekeeper_attack(
1827                agent,
1828                controller,
1829                &attack_data,
1830                tgt_data,
1831                read_data,
1832                rng,
1833            ),
1834            Tactic::CursekeeperFake => {
1835                self.handle_cursekeeper_fake_attack(controller, &attack_data)
1836            },
1837            Tactic::ShamanicSpirit => self.handle_shamanic_spirit_attack(
1838                agent,
1839                controller,
1840                &attack_data,
1841                tgt_data,
1842                read_data,
1843            ),
1844            Tactic::Dagon => {
1845                self.handle_dagon_attack(agent, controller, &attack_data, tgt_data, read_data)
1846            },
1847            Tactic::Snaretongue => {
1848                self.handle_snaretongue_attack(agent, controller, &attack_data, read_data)
1849            },
1850            Tactic::SimpleBackstab => {
1851                self.handle_simple_backstab(agent, controller, &attack_data, tgt_data, read_data)
1852            },
1853            Tactic::ElevatedRanged => {
1854                self.handle_elevated_ranged(agent, controller, &attack_data, tgt_data, read_data)
1855            },
1856            Tactic::Deadwood => {
1857                self.handle_deadwood(agent, controller, &attack_data, tgt_data, read_data)
1858            },
1859            Tactic::Mandragora => {
1860                self.handle_mandragora(agent, controller, &attack_data, tgt_data, read_data)
1861            },
1862            Tactic::WoodGolem => {
1863                self.handle_wood_golem(agent, controller, &attack_data, tgt_data, read_data, rng)
1864            },
1865            Tactic::GnarlingChieftain => self.handle_gnarling_chieftain(
1866                agent,
1867                controller,
1868                &attack_data,
1869                tgt_data,
1870                read_data,
1871                rng,
1872            ),
1873            Tactic::FrostGigas => self.handle_frostgigas_attack(
1874                agent,
1875                controller,
1876                &attack_data,
1877                tgt_data,
1878                read_data,
1879                rng,
1880            ),
1881            Tactic::BorealHammer => self.handle_boreal_hammer_attack(
1882                agent,
1883                controller,
1884                &attack_data,
1885                tgt_data,
1886                read_data,
1887                rng,
1888            ),
1889            Tactic::BorealBow => self.handle_boreal_bow_attack(
1890                agent,
1891                controller,
1892                &attack_data,
1893                tgt_data,
1894                read_data,
1895                rng,
1896            ),
1897            Tactic::SwordSimple => self.handle_sword_simple_attack(
1898                agent,
1899                controller,
1900                &attack_data,
1901                tgt_data,
1902                read_data,
1903            ),
1904            Tactic::AdletHunter => {
1905                self.handle_adlet_hunter(agent, controller, &attack_data, tgt_data, read_data, rng)
1906            },
1907            Tactic::AdletIcepicker => {
1908                self.handle_adlet_icepicker(agent, controller, &attack_data, tgt_data, read_data)
1909            },
1910            Tactic::AdletTracker => {
1911                self.handle_adlet_tracker(agent, controller, &attack_data, tgt_data, read_data)
1912            },
1913            Tactic::IceDrake => {
1914                self.handle_icedrake(agent, controller, &attack_data, tgt_data, read_data, rng)
1915            },
1916            Tactic::Hydra => {
1917                self.handle_hydra(agent, controller, &attack_data, tgt_data, read_data, rng)
1918            },
1919            Tactic::BloodmoonBat => self.handle_bloodmoon_bat_attack(
1920                agent,
1921                controller,
1922                &attack_data,
1923                tgt_data,
1924                read_data,
1925                rng,
1926            ),
1927            Tactic::VampireBat => self.handle_vampire_bat_attack(
1928                agent,
1929                controller,
1930                &attack_data,
1931                tgt_data,
1932                read_data,
1933                rng,
1934            ),
1935            Tactic::BloodmoonHeiress => self.handle_bloodmoon_heiress_attack(
1936                agent,
1937                controller,
1938                &attack_data,
1939                tgt_data,
1940                read_data,
1941                rng,
1942            ),
1943            Tactic::RandomAbilities {
1944                primary,
1945                secondary,
1946                abilities,
1947            } => self.handle_random_abilities(
1948                agent,
1949                controller,
1950                &attack_data,
1951                tgt_data,
1952                read_data,
1953                rng,
1954                primary,
1955                secondary,
1956                abilities,
1957            ),
1958            Tactic::AdletElder => {
1959                self.handle_adlet_elder(agent, controller, &attack_data, tgt_data, read_data, rng)
1960            },
1961            Tactic::HaniwaSoldier => {
1962                self.handle_haniwa_soldier(agent, controller, &attack_data, tgt_data, read_data)
1963            },
1964            Tactic::HaniwaGuard => {
1965                self.handle_haniwa_guard(agent, controller, &attack_data, tgt_data, read_data, rng)
1966            },
1967            Tactic::HaniwaArcher => {
1968                self.handle_haniwa_archer(agent, controller, &attack_data, tgt_data, read_data)
1969            },
1970        }
1971    }
1972
1973    pub fn handle_sounds_heard(
1974        &self,
1975        agent: &mut Agent,
1976        controller: &mut Controller,
1977        read_data: &ReadData,
1978        emitters: &mut AgentEmitters,
1979        rng: &mut impl Rng,
1980    ) {
1981        agent.forget_old_sounds(read_data.time.0);
1982
1983        if is_invulnerable(*self.entity, read_data) || is_steering(*self.entity, read_data) {
1984            self.idle(agent, controller, read_data, emitters, rng);
1985            return;
1986        }
1987
1988        if let Some(sound) = agent.sounds_heard.last() {
1989            let sound_pos = Pos(sound.pos);
1990            let dist_sqrd = self.pos.0.distance_squared(sound_pos.0);
1991            // NOTE: There is an implicit distance requirement given that sound volume
1992            // dissipates as it travels, but we will not want to flee if a sound is super
1993            // loud but heard from a great distance, regardless of how loud it was.
1994            // `is_close` is this limiter.
1995            let is_close = dist_sqrd < 35.0_f32.powi(2);
1996
1997            let sound_was_loud = sound.vol >= 10.0;
1998            let sound_was_threatening = sound_was_loud
1999                || matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _));
2000
2001            let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
2002            // FIXME: We need to be able to change the name of a guard without breaking this
2003            // logic. The `Mark` enum from common::agent could be used to match with
2004            // `agent::Mark::Guard`
2005            let is_village_guard = read_data
2006                .stats
2007                .get(*self.entity)
2008                .is_some_and(|stats| stats.name == *"Guard".to_string());
2009            let follows_threatening_sounds = has_enemy_alignment || is_village_guard;
2010
2011            if sound_was_threatening && is_close {
2012                if !self.below_flee_health(agent) && follows_threatening_sounds {
2013                    self.follow(agent, controller, read_data, &sound_pos);
2014                } else if self.below_flee_health(agent) || !follows_threatening_sounds {
2015                    self.flee(agent, controller, read_data, &sound_pos);
2016                } else {
2017                    self.idle(agent, controller, read_data, emitters, rng);
2018                }
2019            } else {
2020                self.idle(agent, controller, read_data, emitters, rng);
2021            }
2022        } else {
2023            self.idle(agent, controller, read_data, emitters, rng);
2024        }
2025    }
2026
2027    pub fn attack_target_attacker(
2028        &self,
2029        agent: &mut Agent,
2030        read_data: &ReadData,
2031        controller: &mut Controller,
2032        emitters: &mut AgentEmitters,
2033        rng: &mut impl Rng,
2034    ) {
2035        if let Some(Target { target, .. }) = agent.target {
2036            if let Some(tgt_health) = read_data.healths.get(target) {
2037                if let Some(by) = tgt_health.last_change.damage_by() {
2038                    if let Some(attacker) = get_entity_by_id(by.uid(), read_data) {
2039                        if agent.target.is_none() {
2040                            controller.push_utterance(UtteranceKind::Angry);
2041                        }
2042
2043                        let attacker_pos = read_data.positions.get(attacker).map(|pos| pos.0);
2044                        agent.target = Some(Target::new(
2045                            attacker,
2046                            true,
2047                            read_data.time.0,
2048                            true,
2049                            attacker_pos,
2050                        ));
2051
2052                        if let Some(tgt_pos) = read_data.positions.get(attacker) {
2053                            if is_dead_or_invulnerable(attacker, read_data) {
2054                                agent.target = Some(Target::new(
2055                                    target,
2056                                    false,
2057                                    read_data.time.0,
2058                                    false,
2059                                    Some(tgt_pos.0),
2060                                ));
2061
2062                                self.idle(agent, controller, read_data, emitters, rng);
2063                            } else {
2064                                let target_data = TargetData::new(tgt_pos, target, read_data);
2065                                // TODO: Reimplement this in rtsim
2066                                // if let Some(tgt_name) =
2067                                //     read_data.stats.get(target).map(|stats| stats.name.clone())
2068                                // {
2069                                //     agent.add_fight_to_memory(&tgt_name, read_data.time.0)
2070                                // }
2071                                self.attack(agent, controller, &target_data, read_data, rng);
2072                            }
2073                        }
2074                    }
2075                }
2076            }
2077        }
2078    }
2079
2080    // TODO: Pass a localisation key instead of `Content` to avoid allocating if
2081    // we're not permitted to speak.
2082    pub fn chat_npc_if_allowed_to_speak(
2083        &self,
2084        msg: Content,
2085        agent: &Agent,
2086        emitters: &mut AgentEmitters,
2087    ) -> bool {
2088        if agent.allowed_to_speak() {
2089            self.chat_npc(msg, emitters);
2090            true
2091        } else {
2092            false
2093        }
2094    }
2095
2096    pub fn chat_npc(&self, content: Content, emitters: &mut AgentEmitters) {
2097        emitters.emit(ChatEvent {
2098            msg: UnresolvedChatMsg::npc(*self.uid, content),
2099            from_client: false,
2100        });
2101    }
2102
2103    fn emit_scream(&self, time: f64, emitters: &mut AgentEmitters) {
2104        if let Some(body) = self.body {
2105            emitters.emit(SoundEvent {
2106                sound: Sound::new(
2107                    SoundKind::Utterance(UtteranceKind::Scream, *body),
2108                    self.pos.0,
2109                    13.0,
2110                    time,
2111                ),
2112            });
2113        }
2114    }
2115
2116    pub fn cry_out(&self, agent: &Agent, emitters: &mut AgentEmitters, read_data: &ReadData) {
2117        let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
2118
2119        if has_enemy_alignment {
2120            // FIXME: If going to use "cultist + low health + fleeing" string, make sure
2121            // they are each true.
2122            self.chat_npc_if_allowed_to_speak(
2123                Content::localized("npc-speech-cultist_low_health_fleeing"),
2124                agent,
2125                emitters,
2126            );
2127        } else if is_villager(self.alignment) {
2128            self.chat_npc_if_allowed_to_speak(
2129                Content::localized("npc-speech-villager_under_attack"),
2130                agent,
2131                emitters,
2132            );
2133            self.emit_scream(read_data.time.0, emitters);
2134        }
2135    }
2136
2137    pub fn exclaim_relief_about_enemy_dead(&self, agent: &Agent, emitters: &mut AgentEmitters) {
2138        if is_villager(self.alignment) {
2139            self.chat_npc_if_allowed_to_speak(
2140                Content::localized("npc-speech-villager_enemy_killed"),
2141                agent,
2142                emitters,
2143            );
2144        }
2145    }
2146
2147    pub fn below_flee_health(&self, agent: &Agent) -> bool {
2148        self.damage.min(1.0) < agent.psyche.flee_health
2149    }
2150
2151    pub fn is_more_dangerous_than_target(
2152        &self,
2153        entity: EcsEntity,
2154        target: Target,
2155        read_data: &ReadData,
2156    ) -> bool {
2157        let entity_pos = read_data.positions.get(entity);
2158        let target_pos = read_data.positions.get(target.target);
2159
2160        entity_pos.is_some_and(|entity_pos| {
2161            target_pos.is_none_or(|target_pos| {
2162                // Fuzzy factor that makes it harder for players to cheese enemies by making
2163                // them quickly flip aggro between two players.
2164                // It does this by only switching aggro if the entity is closer to the enemy by
2165                // a specific proportional threshold.
2166                const FUZZY_DIST_COMPARISON: f32 = 0.8;
2167
2168                let is_target_further = target_pos.0.distance(entity_pos.0)
2169                    < target_pos.0.distance(entity_pos.0) * FUZZY_DIST_COMPARISON;
2170                let is_entity_hostile = read_data
2171                    .alignments
2172                    .get(entity)
2173                    .zip(self.alignment)
2174                    .is_some_and(|(entity, me)| me.hostile_towards(*entity));
2175
2176                // Consider entity more dangerous than target if entity is closer or if target
2177                // had not triggered aggro.
2178                !target.aggro_on || (is_target_further && is_entity_hostile)
2179            })
2180        })
2181    }
2182
2183    pub fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2184        let other_alignment = read_data.alignments.get(entity);
2185
2186        (entity != *self.entity)
2187            && !self.passive_towards(entity, read_data)
2188            && (are_our_owners_hostile(self.alignment, other_alignment, read_data)
2189                || (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data)))
2190    }
2191
2192    pub fn is_hunting_animal(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2193        (entity != *self.entity)
2194            && !self.friendly_towards(entity, read_data)
2195            && matches!(read_data.bodies.get(entity), Some(Body::QuadrupedSmall(_)))
2196    }
2197
2198    fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2199        let entity_alignment = read_data.alignments.get(entity);
2200
2201        let we_are_friendly = entity_alignment.is_some_and(|entity_alignment| {
2202            self.alignment
2203                .is_some_and(|alignment| !alignment.hostile_towards(*entity_alignment))
2204        });
2205        let we_share_species = read_data.bodies.get(entity).is_some_and(|entity_body| {
2206            self.body.is_some_and(|body| {
2207                entity_body.is_same_species_as(body)
2208                    || (entity_body.is_humanoid() && body.is_humanoid())
2209            })
2210        });
2211        let self_owns_entity =
2212            matches!(entity_alignment, Some(Alignment::Owned(ouid)) if *self.uid == *ouid);
2213
2214        (we_are_friendly && we_share_species)
2215            || (is_village_guard(*self.entity, read_data) && is_villager(entity_alignment))
2216            || self_owns_entity
2217    }
2218
2219    fn passive_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2220        if let (Some(self_alignment), Some(other_alignment)) =
2221            (self.alignment, read_data.alignments.get(entity))
2222        {
2223            self_alignment.passive_towards(*other_alignment)
2224        } else {
2225            false
2226        }
2227    }
2228
2229    fn friendly_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
2230        if let (Some(self_alignment), Some(other_alignment)) =
2231            (self.alignment, read_data.alignments.get(entity))
2232        {
2233            self_alignment.friendly_towards(*other_alignment)
2234        } else {
2235            false
2236        }
2237    }
2238
2239    pub fn can_see_entity(
2240        &self,
2241        agent: &Agent,
2242        controller: &Controller,
2243        other: EcsEntity,
2244        other_pos: &Pos,
2245        other_scale: Option<&Scale>,
2246        read_data: &ReadData,
2247    ) -> bool {
2248        let other_stealth_multiplier = {
2249            let other_inventory = read_data.inventories.get(other);
2250            let other_char_state = read_data.char_states.get(other);
2251
2252            perception_dist_multiplier_from_stealth(other_inventory, other_char_state, self.msm)
2253        };
2254
2255        let within_sight_dist = {
2256            let sight_dist = agent.psyche.sight_dist * other_stealth_multiplier;
2257            let dist_sqrd = other_pos.0.distance_squared(self.pos.0);
2258
2259            dist_sqrd < sight_dist.powi(2)
2260        };
2261
2262        let within_fov = (other_pos.0 - self.pos.0)
2263            .try_normalized()
2264            .is_some_and(|v| v.dot(*controller.inputs.look_dir) > 0.15);
2265
2266        let other_body = read_data.bodies.get(other);
2267
2268        (within_sight_dist)
2269            && within_fov
2270            && entities_have_line_of_sight(
2271                self.pos,
2272                self.body,
2273                self.scale,
2274                other_pos,
2275                other_body,
2276                other_scale,
2277                read_data,
2278            )
2279    }
2280
2281    pub fn detects_other(
2282        &self,
2283        agent: &Agent,
2284        controller: &Controller,
2285        other: &EcsEntity,
2286        other_pos: &Pos,
2287        other_scale: Option<&Scale>,
2288        read_data: &ReadData,
2289    ) -> bool {
2290        self.can_sense_directly_near(other_pos)
2291            || self.can_see_entity(agent, controller, *other, other_pos, other_scale, read_data)
2292    }
2293
2294    pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool {
2295        let chance = thread_rng().gen_bool(0.3);
2296        e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) && chance
2297    }
2298
2299    pub fn menacing(
2300        &self,
2301        agent: &mut Agent,
2302        controller: &mut Controller,
2303        target: EcsEntity,
2304        read_data: &ReadData,
2305        emitters: &mut AgentEmitters,
2306        rng: &mut impl Rng,
2307        remembers_fight_with_target: bool,
2308    ) {
2309        let max_move = 0.5;
2310        let move_dir = controller.inputs.move_dir;
2311        let move_dir_mag = move_dir.magnitude();
2312        let small_chance = rng.gen::<f32>() < read_data.dt.0 * 0.25;
2313        let mut chat = |content: Content| {
2314            self.chat_npc_if_allowed_to_speak(content, agent, emitters);
2315        };
2316        let mut chat_villager_remembers_fighting = || {
2317            let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
2318
2319            // TODO: Localise
2320            if let Some(tgt_name) = tgt_name {
2321                chat(Content::Plain(format!(
2322                    "{}! How dare you cross me again!",
2323                    &tgt_name
2324                )));
2325            } else {
2326                chat(Content::Plain(
2327                    "You! How dare you cross me again!".to_string(),
2328                ));
2329            }
2330        };
2331
2332        self.look_toward(controller, read_data, target);
2333        controller.push_action(ControlAction::Wield);
2334
2335        if move_dir_mag > max_move {
2336            controller.inputs.move_dir = max_move * move_dir / move_dir_mag;
2337        }
2338
2339        if small_chance {
2340            controller.push_utterance(UtteranceKind::Angry);
2341            if is_villager(self.alignment) {
2342                if remembers_fight_with_target {
2343                    chat_villager_remembers_fighting();
2344                } else if is_dressed_as_cultist(target, read_data) {
2345                    chat(Content::localized("npc-speech-villager_cultist_alarm"));
2346                } else {
2347                    chat(Content::localized("npc-speech-menacing"));
2348                }
2349            } else {
2350                chat(Content::localized("npc-speech-menacing"));
2351            }
2352        }
2353    }
2354
2355    pub fn dismount(&self, controller: &mut Controller, read_data: &ReadData) {
2356        if read_data.is_riders.contains(*self.entity)
2357            || read_data
2358                .is_volume_riders
2359                .get(*self.entity)
2360                .is_some_and(|r| !r.is_steering_entity())
2361        {
2362            controller.push_event(ControlEvent::Unmount);
2363        }
2364    }
2365}