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