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