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