Skip to main content

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