veloren_server_agent/
attack.rs

1use crate::{
2    consts::MAX_PATH_DIST,
3    data::*,
4    util::{entities_have_line_of_sight, handle_attack_aggression},
5};
6use common::{
7    combat::{self, AttackSource},
8    comp::{
9        Ability, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction,
10        ControlEvent, Controller, Fluid, InputKind,
11        ability::{
12            AbilityReqItem, ActiveAbilities, AuxiliaryAbility, BASE_ABILITY_LIMIT, BowStance,
13            Stance, SwordStance,
14        },
15        buff::BuffKind,
16        fluid_dynamics::LiquidKind,
17        item::tool::AbilityContext,
18        skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
19    },
20    consts::GRAVITY,
21    path::TraversalConfig,
22    states::{
23        self_buff,
24        sprite_summon::{self, SpriteSummonAnchor},
25        utils::StageSection,
26    },
27    terrain::Block,
28    util::Dir,
29    vol::ReadVol,
30};
31use rand::{Rng, seq::IndexedRandom};
32use std::{f32::consts::PI, time::Duration};
33use vek::*;
34use world::util::CARDINALS;
35
36// ground-level max range from projectile speed and launch height
37fn projectile_flat_range(speed: f32, height: f32) -> f32 {
38    let w = speed.powi(2);
39    let u = 0.5 * 2_f32.sqrt() * speed;
40    (0.5 * w + u * (0.5 * w + 2.0 * GRAVITY * height).sqrt()) / GRAVITY
41}
42
43// multi-projectile spread (in degrees) based on maximum of linear increase
44fn projectile_multi_angle(projectile_spread: f32, num_projectiles: u32) -> f32 {
45    (180.0 / PI) * projectile_spread * (num_projectiles - 1) as f32
46}
47
48fn rng_from_span(rng: &mut impl Rng, span: [f32; 2]) -> f32 { rng.random_range(span[0]..=span[1]) }
49
50impl AgentData<'_> {
51    // Intended for any agent that has one attack, that attack is a melee attack,
52    // and the agent is able to freely walk around
53    pub fn handle_simple_melee(
54        &self,
55        agent: &mut Agent,
56        controller: &mut Controller,
57        attack_data: &AttackData,
58        tgt_data: &TargetData,
59        read_data: &ReadData,
60        rng: &mut impl Rng,
61    ) {
62        if attack_data.in_min_range() && attack_data.angle < 30.0 {
63            controller.push_basic_input(InputKind::Primary);
64            controller.inputs.move_dir = Vec2::zero();
65        } else {
66            self.path_toward_target(
67                agent,
68                controller,
69                tgt_data.pos.0,
70                read_data,
71                Path::AtTarget,
72                None,
73            );
74            if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
75                && attack_data.dist_sqrd < 16.0f32.powi(2)
76                && rng.random::<f32>() < 0.02
77            {
78                controller.push_basic_input(InputKind::Roll);
79            }
80        }
81    }
82
83    // Intended for any agent that has one attack, that attack is a melee attack,
84    // and the agent is able to freely fly around
85    pub fn handle_simple_flying_melee(
86        &self,
87        _agent: &mut Agent,
88        controller: &mut Controller,
89        attack_data: &AttackData,
90        tgt_data: &TargetData,
91        read_data: &ReadData,
92        _rng: &mut impl Rng,
93    ) {
94        // Fly to target
95        let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
96            .try_normalized()
97            .unwrap_or_else(Vec3::zero);
98        let speed = 1.0;
99        controller.inputs.move_dir = dir_to_target.xy() * speed;
100
101        // Always fly! If the floor can't touch you, it can't hurt you...
102        controller.push_basic_input(InputKind::Fly);
103        // Flee from the ground! The internet told me it was lava!
104        // If on the ground, jump with every last ounce of energy, holding onto
105        // all that is dear in life and straining for the wide open skies.
106        if self.physics_state.on_ground.is_some() {
107            controller.push_basic_input(InputKind::Jump);
108        } else {
109            // Use a proportional controller with a coefficient of 1.0 to
110            // maintain altidude at the the provided set point
111            let mut maintain_altitude = |set_point| {
112                let alt = read_data
113                    .terrain
114                    .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
115                    .until(Block::is_solid)
116                    .cast()
117                    .0;
118                let error = set_point - alt;
119                controller.inputs.move_z = error;
120            };
121            if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
122                maintain_altitude(5.0);
123            } else {
124                maintain_altitude(2.0);
125
126                // Attack if in range
127                if attack_data.dist_sqrd < 3.5_f32.powi(2) && attack_data.angle < 150.0 {
128                    controller.push_basic_input(InputKind::Primary);
129                }
130            }
131        }
132    }
133
134    pub fn handle_bloodmoon_bat_attack(
135        &self,
136        agent: &mut Agent,
137        controller: &mut Controller,
138        attack_data: &AttackData,
139        tgt_data: &TargetData,
140        read_data: &ReadData,
141        _rng: &mut impl Rng,
142    ) {
143        enum ActionStateTimers {
144            AttackTimer,
145        }
146
147        let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
148
149        agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
150        if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
151            // Reset timer
152            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
153        }
154
155        let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
156            .try_normalized()
157            .unwrap_or_else(Vec3::zero);
158        let speed = 1.0;
159        controller.inputs.move_dir = dir_to_target.xy() * speed;
160
161        // Always fly
162        controller.push_basic_input(InputKind::Fly);
163        if self.physics_state.on_ground.is_some() {
164            controller.push_basic_input(InputKind::Jump);
165        } else {
166            // Use a proportional controller with a coefficient of 1.0 to
167            // maintain altidude at the the provided set point
168            let mut maintain_altitude = |set_point| {
169                let alt = read_data
170                    .terrain
171                    .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
172                    .until(Block::is_solid)
173                    .cast()
174                    .0;
175                let error = set_point - alt;
176                controller.inputs.move_z = error;
177            };
178            if !(-20.6..20.6).contains(&(tgt_data.pos.0.y - home.y))
179                || !(-26.6..26.6).contains(&(tgt_data.pos.0.x - home.x))
180            {
181                if (home - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
182                    controller.push_action(ControlAction::StartInput {
183                        input: InputKind::Ability(0),
184                        target_entity: None,
185                        select_pos: Some(home),
186                    });
187                } else {
188                    controller.push_basic_input(InputKind::Ability(1));
189                }
190            } else if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
191                maintain_altitude(5.0);
192            } else {
193                maintain_altitude(2.0);
194                if tgt_data.pos.0.z < home.z + 5.0 && self.pos.0.z < home.z + 25.0 {
195                    if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 {
196                        controller.push_basic_input(InputKind::Secondary);
197                    } else {
198                        controller.push_basic_input(InputKind::Ability(1));
199                    }
200                } else if attack_data.dist_sqrd < 6.0_f32.powi(2) {
201                    // use shockwave or singlestrike when close
202                    if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 2.0 {
203                        controller.push_basic_input(InputKind::Ability(2));
204                    } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize]
205                        < 4.0
206                    {
207                        controller.push_basic_input(InputKind::Ability(3));
208                    } else {
209                        controller.push_basic_input(InputKind::Primary);
210                    }
211                } else if tgt_data.pos.0.z < home.z + 30.0
212                    && agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0
213                {
214                    controller.push_action(ControlAction::StartInput {
215                        input: InputKind::Ability(0),
216                        target_entity: agent
217                            .target
218                            .as_ref()
219                            .and_then(|t| read_data.uids.get(t.target))
220                            .copied(),
221                        select_pos: None,
222                    });
223                }
224            }
225        }
226    }
227
228    pub fn handle_vampire_bat_attack(
229        &self,
230        agent: &mut Agent,
231        controller: &mut Controller,
232        _attack_data: &AttackData,
233        _tgt_data: &TargetData,
234        read_data: &ReadData,
235        _rng: &mut impl Rng,
236    ) {
237        enum ActionStateTimers {
238            AttackTimer,
239        }
240
241        agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
242        if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 9.0 {
243            // Reset timer
244            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
245        }
246
247        // stay centered
248        let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
249        self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
250        // teleport home if straying too far
251        if (home - self.pos.0).xy().magnitude_squared() > (10.0_f32).powi(2) {
252            controller.push_action(ControlAction::StartInput {
253                input: InputKind::Ability(1),
254                target_entity: None,
255                select_pos: Some(home),
256            });
257        }
258        // Always fly! If the floor can't touch you, it can't hurt you...
259        controller.push_basic_input(InputKind::Fly);
260        if self.pos.0.z < home.z + 4.0
261            && agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 6.0
262        {
263            controller.push_basic_input(InputKind::Secondary);
264        } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0
265            && (self.pos.0.z - home.z) < 110.0
266        {
267            controller.push_basic_input(InputKind::Primary);
268        } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 6.0 {
269            controller.push_basic_input(InputKind::Ability(0));
270        }
271    }
272
273    pub fn handle_bloodmoon_heiress_attack(
274        &self,
275        agent: &mut Agent,
276        controller: &mut Controller,
277        attack_data: &AttackData,
278        tgt_data: &TargetData,
279        read_data: &ReadData,
280        rng: &mut impl Rng,
281    ) {
282        const DASH_TIMER: usize = 0;
283        const SUMMON_THRESHOLD: f32 = 0.20;
284        enum ActionStateFCounters {
285            FCounterHealthThreshold = 0,
286        }
287        enum ActionStateConditions {
288            ConditionCounterInit = 0,
289        }
290        agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
291        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
292        let line_of_sight_with_target = || {
293            entities_have_line_of_sight(
294                self.pos,
295                self.body,
296                self.scale,
297                tgt_data.pos,
298                tgt_data.body,
299                tgt_data.scale,
300                read_data,
301            )
302        };
303        // Sets counter at start of combat, using `condition` to keep track of whether
304        // it was already initialized
305        if !agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] {
306            agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
307                1.0 - SUMMON_THRESHOLD;
308            agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] =
309                true;
310        }
311
312        if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
313            > health_fraction
314        {
315            // Summon minions at particular thresholds of health
316            controller.push_basic_input(InputKind::Ability(2));
317
318            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
319            {
320                agent.combat_state.counters
321                    [ActionStateFCounters::FCounterHealthThreshold as usize] -= SUMMON_THRESHOLD;
322            }
323        }
324        // teleport to target when it can't be pathed to
325        else if self
326            .path_toward_target(
327                agent,
328                controller,
329                tgt_data.pos.0,
330                read_data,
331                Path::Separate,
332                None,
333            )
334            .is_none()
335            || !(-3.0..3.0).contains(&(tgt_data.pos.0.z - self.pos.0.z))
336        {
337            controller.push_action(ControlAction::StartInput {
338                input: InputKind::Ability(0),
339                target_entity: agent
340                    .target
341                    .as_ref()
342                    .and_then(|t| read_data.uids.get(t.target))
343                    .copied(),
344                select_pos: None,
345            });
346        } else if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
347        {
348            controller.push_basic_input(InputKind::Secondary);
349        } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
350            if agent.combat_state.timers[DASH_TIMER] > 2.0 {
351                agent.combat_state.timers[DASH_TIMER] = 0.0;
352            }
353            match rng.random_range(0..2) {
354                0 => controller.push_basic_input(InputKind::Primary),
355                _ => controller.push_basic_input(InputKind::Ability(3)),
356            };
357        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
358            && self
359                .path_toward_target(
360                    agent,
361                    controller,
362                    tgt_data.pos.0,
363                    read_data,
364                    Path::Separate,
365                    None,
366                )
367                .is_some()
368            && line_of_sight_with_target()
369            && agent.combat_state.timers[DASH_TIMER] > 4.0
370            && attack_data.angle < 45.0
371        {
372            match rng.random_range(0..2) {
373                0 => controller.push_basic_input(InputKind::Secondary),
374                _ => controller.push_basic_input(InputKind::Ability(1)),
375            };
376            agent.combat_state.timers[DASH_TIMER] = 0.0;
377        } else {
378            self.path_toward_target(
379                agent,
380                controller,
381                tgt_data.pos.0,
382                read_data,
383                Path::AtTarget,
384                None,
385            );
386        }
387    }
388
389    // Intended for any agent that has one attack, that attack is a melee attack,
390    // the agent is able to freely walk around, and the agent is trying to attack
391    // from behind its target
392    pub fn handle_simple_backstab(
393        &self,
394        agent: &mut Agent,
395        controller: &mut Controller,
396        attack_data: &AttackData,
397        tgt_data: &TargetData,
398        read_data: &ReadData,
399    ) {
400        // Behaviour parameters
401        const STRAFE_DIST: f32 = 4.5;
402        const STRAFE_SPEED_MULT: f32 = 0.75;
403        const STRAFE_SPIRAL_MULT: f32 = 0.8; // how quickly they close gap while strafing
404        const BACKSTAB_SPEED_MULT: f32 = 0.3;
405
406        // Handle movement of agent
407        let target_ori = agent
408            .target
409            .and_then(|t| read_data.orientations.get(t.target))
410            .map(|ori| ori.look_vec())
411            .unwrap_or_default();
412        let dist = attack_data.dist_sqrd.sqrt();
413        let in_front_of_target = target_ori.dot(self.pos.0 - tgt_data.pos.0) > 0.0;
414
415        // Handle attacking of agent
416        if attack_data.in_min_range() && attack_data.angle < 30.0 {
417            controller.push_basic_input(InputKind::Primary);
418            controller.inputs.move_dir = Vec2::zero();
419        }
420
421        if attack_data.dist_sqrd < STRAFE_DIST.powi(2) {
422            // If in front of the target, circle to try and get behind, else just make a
423            // beeline for the back of the agent
424            let vec_to_target = (tgt_data.pos.0 - self.pos.0).xy();
425            if in_front_of_target {
426                let theta = (PI / 2. - dist * 0.1).max(0.0);
427                // Checks both CW and CCW rotation
428                let potential_move_dirs = [
429                    vec_to_target
430                        .rotated_z(theta)
431                        .try_normalized()
432                        .unwrap_or_default(),
433                    vec_to_target
434                        .rotated_z(-theta)
435                        .try_normalized()
436                        .unwrap_or_default(),
437                ];
438                // Finds shortest path to get behind
439                if let Some(move_dir) = potential_move_dirs
440                    .iter()
441                    .find(|move_dir| target_ori.xy().dot(**move_dir) < 0.0)
442                {
443                    controller.inputs.move_dir =
444                        STRAFE_SPEED_MULT * (*move_dir - STRAFE_SPIRAL_MULT * target_ori.xy());
445                }
446            } else {
447                // Aim for a point a given distance behind the target to prevent sideways
448                // movement
449                let move_target = tgt_data.pos.0.xy() - dist / 2. * target_ori.xy();
450                controller.inputs.move_dir = ((move_target - self.pos.0) * BACKSTAB_SPEED_MULT)
451                    .try_normalized()
452                    .unwrap_or_default();
453            }
454        } else {
455            self.path_toward_target(
456                agent,
457                controller,
458                tgt_data.pos.0,
459                read_data,
460                Path::AtTarget,
461                None,
462            );
463        }
464    }
465
466    pub fn handle_elevated_ranged(
467        &self,
468        agent: &mut Agent,
469        controller: &mut Controller,
470        attack_data: &AttackData,
471        tgt_data: &TargetData,
472        read_data: &ReadData,
473    ) {
474        // Behaviour parameters
475        const PREF_DIST: f32 = 30.0;
476        const RETREAT_DIST: f32 = 8.0;
477
478        let line_of_sight_with_target = || {
479            entities_have_line_of_sight(
480                self.pos,
481                self.body,
482                self.scale,
483                tgt_data.pos,
484                tgt_data.body,
485                tgt_data.scale,
486                read_data,
487            )
488        };
489        let elevation = self.pos.0.z - tgt_data.pos.0.z;
490
491        if attack_data.angle_xy < 30.0
492            && (elevation > 10.0 || attack_data.dist_sqrd > PREF_DIST.powi(2))
493            && line_of_sight_with_target()
494        {
495            controller.push_basic_input(InputKind::Primary);
496        } else if attack_data.dist_sqrd < RETREAT_DIST.powi(2) {
497            // Attempt to move quickly away from target if too close
498            if let Some((bearing, _, stuck)) = agent.chaser.chase(
499                &*read_data.terrain,
500                self.pos.0,
501                self.vel.0,
502                tgt_data.pos.0,
503                TraversalConfig {
504                    min_tgt_dist: 1.25,
505                    ..self.traversal_config
506                },
507                &read_data.time,
508            ) {
509                let flee_dir = -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero);
510                let pos = self.pos.0.xy().with_z(self.pos.0.z + 1.5);
511                if read_data
512                    .terrain
513                    .ray(pos, pos + flee_dir * 2.0)
514                    .until(|b| b.is_solid() || b.get_sprite().is_none())
515                    .cast()
516                    .0
517                    > 1.0
518                {
519                    // If able to flee, flee
520                    controller.inputs.move_dir = flee_dir;
521                    if !self.char_state.is_attack() {
522                        self.unstuck_if(stuck, controller);
523                        controller.inputs.look_dir = -controller.inputs.look_dir;
524                    }
525                } else {
526                    // Otherwise, fight to the death
527                    controller.push_basic_input(InputKind::Primary);
528                }
529            }
530        } else if attack_data.dist_sqrd < PREF_DIST.powi(2) {
531            // Attempt to move away from target if too close, while still attacking
532            if let Some((bearing, _, stuck)) = agent.chaser.chase(
533                &*read_data.terrain,
534                self.pos.0,
535                self.vel.0,
536                tgt_data.pos.0,
537                TraversalConfig {
538                    min_tgt_dist: 1.25,
539                    ..self.traversal_config
540                },
541                &read_data.time,
542            ) {
543                if line_of_sight_with_target() {
544                    controller.push_basic_input(InputKind::Primary);
545                }
546                self.unstuck_if(stuck, controller);
547                controller.inputs.move_dir =
548                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero);
549            }
550        } else {
551            self.path_toward_target(
552                agent,
553                controller,
554                tgt_data.pos.0,
555                read_data,
556                Path::AtTarget,
557                None,
558            );
559        }
560    }
561
562    pub fn handle_hammer_attack(
563        &self,
564        agent: &mut Agent,
565        controller: &mut Controller,
566        attack_data: &AttackData,
567        tgt_data: &TargetData,
568        read_data: &ReadData,
569        rng: &mut impl Rng,
570    ) {
571        if !agent.combat_state.initialized {
572            agent.combat_state.initialized = true;
573            let available_tactics = {
574                let mut tactics = Vec::new();
575                let try_tactic = |skill, tactic, tactics: &mut Vec<HammerTactics>| {
576                    if self.skill_set.has_skill(Skill::Hammer(skill)) {
577                        tactics.push(tactic);
578                    }
579                };
580                try_tactic(
581                    HammerSkill::Thunderclap,
582                    HammerTactics::AttackExpert,
583                    &mut tactics,
584                );
585                try_tactic(
586                    HammerSkill::Judgement,
587                    HammerTactics::SupportExpert,
588                    &mut tactics,
589                );
590                if tactics.is_empty() {
591                    try_tactic(
592                        HammerSkill::IronTempest,
593                        HammerTactics::AttackAdvanced,
594                        &mut tactics,
595                    );
596                    try_tactic(
597                        HammerSkill::Rampart,
598                        HammerTactics::SupportAdvanced,
599                        &mut tactics,
600                    );
601                }
602                if tactics.is_empty() {
603                    try_tactic(
604                        HammerSkill::Retaliate,
605                        HammerTactics::AttackIntermediate,
606                        &mut tactics,
607                    );
608                    try_tactic(
609                        HammerSkill::PileDriver,
610                        HammerTactics::SupportIntermediate,
611                        &mut tactics,
612                    );
613                }
614                if tactics.is_empty() {
615                    try_tactic(
616                        HammerSkill::Tremor,
617                        HammerTactics::AttackSimple,
618                        &mut tactics,
619                    );
620                    try_tactic(
621                        HammerSkill::HeavyWhorl,
622                        HammerTactics::SupportSimple,
623                        &mut tactics,
624                    );
625                }
626                if tactics.is_empty() {
627                    try_tactic(
628                        HammerSkill::ScornfulSwipe,
629                        HammerTactics::Simple,
630                        &mut tactics,
631                    );
632                }
633                if tactics.is_empty() {
634                    tactics.push(HammerTactics::Unskilled);
635                }
636                tactics
637            };
638
639            let tactic = available_tactics
640                .choose(rng)
641                .copied()
642                .unwrap_or(HammerTactics::Unskilled);
643
644            agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
645
646            let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
647            let set_ability = |controller: &mut Controller, slot, skill| {
648                controller.push_event(ControlEvent::ChangeAbility {
649                    slot,
650                    auxiliary_key,
651                    new_ability: AuxiliaryAbility::MainWeapon(skill),
652                });
653            };
654            let mut set_random = |controller: &mut Controller, slot, options: &mut Vec<usize>| {
655                if options.is_empty() {
656                    return;
657                }
658                let i = rng.random_range(0..options.len());
659                set_ability(controller, slot, options.swap_remove(i));
660            };
661
662            match tactic {
663                HammerTactics::Unskilled => {},
664                HammerTactics::Simple => {
665                    // Scornful swipe
666                    set_ability(controller, 0, 0);
667                },
668                HammerTactics::AttackSimple => {
669                    // Scornful swipe
670                    set_ability(controller, 0, 0);
671                    // Tremor or vigorous bash
672                    set_ability(controller, 1, rng.random_range(1..3));
673                },
674                HammerTactics::AttackIntermediate => {
675                    // Scornful swipe
676                    set_ability(controller, 0, 0);
677                    // Tremor or vigorous bash
678                    set_ability(controller, 1, rng.random_range(1..3));
679                    // Retaliate, spine cracker, or breach
680                    set_ability(controller, 2, rng.random_range(3..6));
681                },
682                HammerTactics::AttackAdvanced => {
683                    // Scornful swipe, tremor, vigorous bash, retaliate, spine cracker, or breach
684                    let mut options = vec![0, 1, 2, 3, 4, 5];
685                    set_random(controller, 0, &mut options);
686                    set_random(controller, 1, &mut options);
687                    set_random(controller, 2, &mut options);
688                    set_ability(controller, 3, rng.random_range(6..8));
689                },
690                HammerTactics::AttackExpert => {
691                    // Scornful swipe, tremor, vigorous bash, retaliate, spine cracker, breach, iron
692                    // tempest, or upheaval
693                    let mut options = vec![0, 1, 2, 3, 4, 5, 6, 7];
694                    set_random(controller, 0, &mut options);
695                    set_random(controller, 1, &mut options);
696                    set_random(controller, 2, &mut options);
697                    set_random(controller, 3, &mut options);
698                    set_ability(controller, 4, rng.random_range(8..10));
699                },
700                HammerTactics::SupportSimple => {
701                    // Scornful swipe
702                    set_ability(controller, 0, 0);
703                    // Heavy whorl or intercept
704                    set_ability(controller, 1, rng.random_range(10..12));
705                },
706                HammerTactics::SupportIntermediate => {
707                    // Scornful swipe
708                    set_ability(controller, 0, 0);
709                    // Heavy whorl or intercept
710                    set_ability(controller, 1, rng.random_range(10..12));
711                    // Retaliate, spine cracker, or breach
712                    set_ability(controller, 2, rng.random_range(12..15));
713                },
714                HammerTactics::SupportAdvanced => {
715                    // Scornful swipe, heavy whorl, intercept, pile driver, lung pummel, or helm
716                    // crusher
717                    let mut options = vec![0, 10, 11, 12, 13, 14];
718                    set_random(controller, 0, &mut options);
719                    set_random(controller, 1, &mut options);
720                    set_random(controller, 2, &mut options);
721                    set_ability(controller, 3, rng.random_range(15..17));
722                },
723                HammerTactics::SupportExpert => {
724                    // Scornful swipe, heavy whorl, intercept, pile driver, lung pummel, helm
725                    // crusher, rampart, or tenacity
726                    let mut options = vec![0, 10, 11, 12, 13, 14, 15, 16];
727                    set_random(controller, 0, &mut options);
728                    set_random(controller, 1, &mut options);
729                    set_random(controller, 2, &mut options);
730                    set_random(controller, 3, &mut options);
731                    set_ability(controller, 4, rng.random_range(17..19));
732                },
733            }
734
735            agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
736                ActionMode::Reckless as u8;
737        }
738
739        enum IntCounters {
740            Tactic = 0,
741            ActionMode = 1,
742        }
743
744        enum Timers {
745            GuardedCycle = 0,
746            PosTimeOut = 1,
747        }
748
749        enum Conditions {
750            GuardedDefend = 0,
751            RollingBreakThrough = 1,
752        }
753
754        enum FloatCounters {
755            GuardedTimer = 0,
756        }
757
758        enum Positions {
759            GuardedCover = 0,
760            Flee = 1,
761        }
762
763        let attempt_attack = handle_attack_aggression(
764            self,
765            agent,
766            controller,
767            attack_data,
768            tgt_data,
769            read_data,
770            rng,
771            Timers::PosTimeOut as usize,
772            Timers::GuardedCycle as usize,
773            FloatCounters::GuardedTimer as usize,
774            IntCounters::ActionMode as usize,
775            Conditions::GuardedDefend as usize,
776            Conditions::RollingBreakThrough as usize,
777            Positions::GuardedCover as usize,
778            Positions::Flee as usize,
779        );
780
781        let attack_failed = if attempt_attack {
782            let primary = self.extract_ability(AbilityInput::Primary);
783            let secondary = self.extract_ability(AbilityInput::Secondary);
784            let abilities = [
785                self.extract_ability(AbilityInput::Auxiliary(0)),
786                self.extract_ability(AbilityInput::Auxiliary(1)),
787                self.extract_ability(AbilityInput::Auxiliary(2)),
788                self.extract_ability(AbilityInput::Auxiliary(3)),
789                self.extract_ability(AbilityInput::Auxiliary(4)),
790            ];
791            let could_use_input = |input, desired_energy| match input {
792                InputKind::Primary => primary.as_ref().is_some_and(|p| {
793                    p.could_use(attack_data, self, tgt_data, read_data, desired_energy)
794                }),
795                InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
796                    s.could_use(attack_data, self, tgt_data, read_data, desired_energy)
797                }),
798                InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
799                    let ability = self.active_abilities.get_ability(
800                        AbilityInput::Auxiliary(x),
801                        Some(self.inventory),
802                        Some(self.skill_set),
803                        self.stats,
804                    );
805                    let additional_conditions = match ability {
806                        Ability::MainWeaponAux(0) => self
807                            .buffs
808                            .is_some_and(|buffs| !buffs.contains(BuffKind::ScornfulTaunt)),
809                        Ability::MainWeaponAux(2) => {
810                            tgt_data.char_state.is_some_and(|cs| cs.is_stunned())
811                        },
812                        Ability::MainWeaponAux(4) => tgt_data.ori.is_some_and(|ori| {
813                            ori.look_vec().angle_between(tgt_data.pos.0 - self.pos.0)
814                                < combat::BEHIND_TARGET_ANGLE
815                        }),
816                        Ability::MainWeaponAux(5) => tgt_data.char_state.is_some_and(|cs| {
817                            cs.is_block(AttackSource::Melee) || cs.is_parry(AttackSource::Melee)
818                        }),
819                        Ability::MainWeaponAux(7) => tgt_data
820                            .buffs
821                            .is_some_and(|buffs| !buffs.contains(BuffKind::OffBalance)),
822                        Ability::MainWeaponAux(12) => tgt_data
823                            .buffs
824                            .is_some_and(|buffs| !buffs.contains(BuffKind::Rooted)),
825                        Ability::MainWeaponAux(13) => tgt_data
826                            .buffs
827                            .is_some_and(|buffs| !buffs.contains(BuffKind::Winded)),
828                        Ability::MainWeaponAux(14) => tgt_data
829                            .buffs
830                            .is_some_and(|buffs| !buffs.contains(BuffKind::Amnesia)),
831                        Ability::MainWeaponAux(15) => self
832                            .buffs
833                            .is_some_and(|buffs| !buffs.contains(BuffKind::ProtectingWard)),
834                        _ => true,
835                    };
836                    a.could_use(attack_data, self, tgt_data, read_data, desired_energy)
837                        && additional_conditions
838                }),
839                _ => false,
840            };
841            let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
842                if matches!(current_input, InputKind::Secondary) {
843                    let charging =
844                        matches!(self.char_state.stage_section(), Some(StageSection::Charge));
845                    let charged = self
846                        .char_state
847                        .durations()
848                        .and_then(|durs| durs.charge)
849                        .zip(self.char_state.timer())
850                        .is_some_and(|(dur, timer)| timer > dur);
851                    if !(charging && charged) {
852                        *next_input = Some(InputKind::Secondary);
853                    }
854                } else {
855                    *next_input = Some(current_input);
856                }
857            };
858            let current_input = self.char_state.ability_info().map(|ai| ai.input);
859            let ability_preferences = AbilityPreferences {
860                desired_energy: 40.0,
861                combo_scaling_buildup: 0,
862            };
863            let mut next_input = None;
864            if let Some(input) = current_input {
865                continue_current_input(input, &mut next_input);
866            } else {
867                match HammerTactics::from_u8(
868                    agent.combat_state.int_counters[IntCounters::Tactic as usize],
869                ) {
870                    HammerTactics::Unskilled => {
871                        if rng.random_bool(0.5) {
872                            next_input = Some(InputKind::Primary);
873                        } else {
874                            next_input = Some(InputKind::Secondary);
875                        }
876                    },
877                    HammerTactics::Simple => {
878                        if rng.random_bool(0.5) {
879                            next_input = Some(InputKind::Primary);
880                        } else {
881                            next_input = Some(InputKind::Secondary);
882                        }
883                    },
884                    HammerTactics::AttackSimple | HammerTactics::SupportSimple => {
885                        if could_use_input(InputKind::Ability(0), ability_preferences) {
886                            next_input = Some(InputKind::Ability(0));
887                        } else if rng.random_bool(0.5) {
888                            next_input = Some(InputKind::Primary);
889                        } else {
890                            next_input = Some(InputKind::Secondary);
891                        }
892                    },
893                    HammerTactics::AttackIntermediate | HammerTactics::SupportIntermediate => {
894                        let random_ability = InputKind::Ability(rng.random_range(0..3));
895                        if could_use_input(random_ability, ability_preferences) {
896                            next_input = Some(random_ability);
897                        } else if rng.random_bool(0.5) {
898                            next_input = Some(InputKind::Primary);
899                        } else {
900                            next_input = Some(InputKind::Secondary);
901                        }
902                    },
903                    HammerTactics::AttackAdvanced | HammerTactics::SupportAdvanced => {
904                        let random_ability = InputKind::Ability(rng.random_range(0..5));
905                        if could_use_input(random_ability, ability_preferences) {
906                            next_input = Some(random_ability);
907                        } else if rng.random_bool(0.5) {
908                            next_input = Some(InputKind::Primary);
909                        } else {
910                            next_input = Some(InputKind::Secondary);
911                        }
912                    },
913                    HammerTactics::AttackExpert | HammerTactics::SupportExpert => {
914                        let random_ability = InputKind::Ability(rng.random_range(0..5));
915                        if could_use_input(random_ability, ability_preferences) {
916                            next_input = Some(random_ability);
917                        } else if rng.random_bool(0.5) {
918                            next_input = Some(InputKind::Primary);
919                        } else {
920                            next_input = Some(InputKind::Secondary);
921                        }
922                    },
923                }
924            }
925            if let Some(input) = next_input {
926                if could_use_input(input, ability_preferences) {
927                    controller.push_basic_input(input);
928                    false
929                } else {
930                    true
931                }
932            } else {
933                true
934            }
935        } else {
936            false
937        };
938
939        if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
940            self.path_toward_target(
941                agent,
942                controller,
943                tgt_data.pos.0,
944                read_data,
945                Path::Separate,
946                None,
947            );
948        }
949    }
950
951    pub fn handle_sword_attack(
952        &self,
953        agent: &mut Agent,
954        controller: &mut Controller,
955        attack_data: &AttackData,
956        tgt_data: &TargetData,
957        read_data: &ReadData,
958        rng: &mut impl Rng,
959    ) {
960        if !agent.combat_state.initialized {
961            agent.combat_state.initialized = true;
962            let available_tactics = {
963                let mut tactics = Vec::new();
964                let try_tactic = |skill, tactic, tactics: &mut Vec<SwordTactics>| {
965                    if self.skill_set.has_skill(Skill::Sword(skill)) {
966                        tactics.push(tactic);
967                    }
968                };
969                try_tactic(
970                    SwordSkill::HeavyFortitude,
971                    SwordTactics::HeavyAdvanced,
972                    &mut tactics,
973                );
974                try_tactic(
975                    SwordSkill::AgileDancingEdge,
976                    SwordTactics::AgileAdvanced,
977                    &mut tactics,
978                );
979                try_tactic(
980                    SwordSkill::DefensiveStalwartSword,
981                    SwordTactics::DefensiveAdvanced,
982                    &mut tactics,
983                );
984                try_tactic(
985                    SwordSkill::CripplingEviscerate,
986                    SwordTactics::CripplingAdvanced,
987                    &mut tactics,
988                );
989                try_tactic(
990                    SwordSkill::CleavingBladeFever,
991                    SwordTactics::CleavingAdvanced,
992                    &mut tactics,
993                );
994                if tactics.is_empty() {
995                    try_tactic(
996                        SwordSkill::HeavySweep,
997                        SwordTactics::HeavySimple,
998                        &mut tactics,
999                    );
1000                    try_tactic(
1001                        SwordSkill::AgileQuickDraw,
1002                        SwordTactics::AgileSimple,
1003                        &mut tactics,
1004                    );
1005                    try_tactic(
1006                        SwordSkill::DefensiveDisengage,
1007                        SwordTactics::DefensiveSimple,
1008                        &mut tactics,
1009                    );
1010                    try_tactic(
1011                        SwordSkill::CripplingGouge,
1012                        SwordTactics::CripplingSimple,
1013                        &mut tactics,
1014                    );
1015                    try_tactic(
1016                        SwordSkill::CleavingWhirlwindSlice,
1017                        SwordTactics::CleavingSimple,
1018                        &mut tactics,
1019                    );
1020                }
1021                if tactics.is_empty() {
1022                    try_tactic(SwordSkill::CrescentSlash, SwordTactics::Basic, &mut tactics);
1023                }
1024                if tactics.is_empty() {
1025                    tactics.push(SwordTactics::Unskilled);
1026                }
1027                tactics
1028            };
1029
1030            let tactic = available_tactics
1031                .choose(rng)
1032                .copied()
1033                .unwrap_or(SwordTactics::Unskilled);
1034
1035            agent.combat_state.int_counters[IntCounters::Tactics as usize] = tactic as u8;
1036
1037            let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
1038            let set_sword_ability = |controller: &mut Controller, slot, skill| {
1039                controller.push_event(ControlEvent::ChangeAbility {
1040                    slot,
1041                    auxiliary_key,
1042                    new_ability: AuxiliaryAbility::MainWeapon(skill),
1043                });
1044            };
1045
1046            match tactic {
1047                SwordTactics::Unskilled => {},
1048                SwordTactics::Basic => {
1049                    // Crescent slash
1050                    set_sword_ability(controller, 0, 0);
1051                    // Fell strike
1052                    set_sword_ability(controller, 1, 1);
1053                    // Skewer
1054                    set_sword_ability(controller, 2, 2);
1055                    // Cascade
1056                    set_sword_ability(controller, 3, 3);
1057                    // Cross cut
1058                    set_sword_ability(controller, 4, 4);
1059                },
1060                SwordTactics::HeavySimple => {
1061                    // Finisher
1062                    set_sword_ability(controller, 0, 5);
1063                    // Crescent slash
1064                    set_sword_ability(controller, 1, 0);
1065                    // Cascade
1066                    set_sword_ability(controller, 2, 3);
1067                    // Windmill slash
1068                    set_sword_ability(controller, 3, 6);
1069                    // Pommel strike
1070                    set_sword_ability(controller, 4, 7);
1071                },
1072                SwordTactics::AgileSimple => {
1073                    // Finisher
1074                    set_sword_ability(controller, 0, 5);
1075                    // Skewer
1076                    set_sword_ability(controller, 1, 2);
1077                    // Cross cut
1078                    set_sword_ability(controller, 2, 4);
1079                    // Quick draw
1080                    set_sword_ability(controller, 3, 8);
1081                    // Feint
1082                    set_sword_ability(controller, 4, 9);
1083                },
1084                SwordTactics::DefensiveSimple => {
1085                    // Finisher
1086                    set_sword_ability(controller, 0, 5);
1087                    // Crescent slash
1088                    set_sword_ability(controller, 1, 0);
1089                    // Fell strike
1090                    set_sword_ability(controller, 2, 1);
1091                    // Riposte
1092                    set_sword_ability(controller, 3, 10);
1093                    // Disengage
1094                    set_sword_ability(controller, 4, 11);
1095                },
1096                SwordTactics::CripplingSimple => {
1097                    // Finisher
1098                    set_sword_ability(controller, 0, 5);
1099                    // Fell strike
1100                    set_sword_ability(controller, 1, 1);
1101                    // Skewer
1102                    set_sword_ability(controller, 2, 2);
1103                    // Gouge
1104                    set_sword_ability(controller, 3, 12);
1105                    // Hamstring
1106                    set_sword_ability(controller, 4, 13);
1107                },
1108                SwordTactics::CleavingSimple => {
1109                    // Finisher
1110                    set_sword_ability(controller, 0, 5);
1111                    // Cascade
1112                    set_sword_ability(controller, 1, 3);
1113                    // Cross cut
1114                    set_sword_ability(controller, 2, 4);
1115                    // Whirlwind slice
1116                    set_sword_ability(controller, 3, 14);
1117                    // Earth splitter
1118                    set_sword_ability(controller, 4, 15);
1119                },
1120                SwordTactics::HeavyAdvanced => {
1121                    // Finisher
1122                    set_sword_ability(controller, 0, 5);
1123                    // Windmill slash
1124                    set_sword_ability(controller, 1, 6);
1125                    // Pommel strike
1126                    set_sword_ability(controller, 2, 7);
1127                    // Fortitude
1128                    set_sword_ability(controller, 3, 16);
1129                    // Pillar Thrust
1130                    set_sword_ability(controller, 4, 17);
1131                },
1132                SwordTactics::AgileAdvanced => {
1133                    // Finisher
1134                    set_sword_ability(controller, 0, 5);
1135                    // Quick draw
1136                    set_sword_ability(controller, 1, 8);
1137                    // Feint
1138                    set_sword_ability(controller, 2, 9);
1139                    // Dancing edge
1140                    set_sword_ability(controller, 3, 18);
1141                    // Flurry
1142                    set_sword_ability(controller, 4, 19);
1143                },
1144                SwordTactics::DefensiveAdvanced => {
1145                    // Finisher
1146                    set_sword_ability(controller, 0, 5);
1147                    // Riposte
1148                    set_sword_ability(controller, 1, 10);
1149                    // Disengage
1150                    set_sword_ability(controller, 2, 11);
1151                    // Stalwart sword
1152                    set_sword_ability(controller, 3, 20);
1153                    // Deflect
1154                    set_sword_ability(controller, 4, 21);
1155                },
1156                SwordTactics::CripplingAdvanced => {
1157                    // Finisher
1158                    set_sword_ability(controller, 0, 5);
1159                    // Gouge
1160                    set_sword_ability(controller, 1, 12);
1161                    // Hamstring
1162                    set_sword_ability(controller, 2, 13);
1163                    // Eviscerate
1164                    set_sword_ability(controller, 3, 22);
1165                    // Bloody gash
1166                    set_sword_ability(controller, 4, 23);
1167                },
1168                SwordTactics::CleavingAdvanced => {
1169                    // Finisher
1170                    set_sword_ability(controller, 0, 5);
1171                    // Whirlwind slice
1172                    set_sword_ability(controller, 1, 14);
1173                    // Earth splitter
1174                    set_sword_ability(controller, 2, 15);
1175                    // Blade fever
1176                    set_sword_ability(controller, 3, 24);
1177                    // Sky splitter
1178                    set_sword_ability(controller, 4, 25);
1179                },
1180            }
1181
1182            agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
1183                ActionMode::Reckless as u8;
1184        }
1185
1186        enum IntCounters {
1187            Tactics = 0,
1188            ActionMode = 1,
1189        }
1190
1191        enum Timers {
1192            GuardedCycle = 0,
1193            PosTimeOut = 1,
1194        }
1195
1196        enum Conditions {
1197            GuardedDefend = 0,
1198            RollingBreakThrough = 1,
1199        }
1200
1201        enum FloatCounters {
1202            GuardedTimer = 0,
1203        }
1204
1205        enum Positions {
1206            GuardedCover = 0,
1207            Flee = 1,
1208        }
1209
1210        let attempt_attack = handle_attack_aggression(
1211            self,
1212            agent,
1213            controller,
1214            attack_data,
1215            tgt_data,
1216            read_data,
1217            rng,
1218            Timers::PosTimeOut as usize,
1219            Timers::GuardedCycle as usize,
1220            FloatCounters::GuardedTimer as usize,
1221            IntCounters::ActionMode as usize,
1222            Conditions::GuardedDefend as usize,
1223            Conditions::RollingBreakThrough as usize,
1224            Positions::GuardedCover as usize,
1225            Positions::Flee as usize,
1226        );
1227
1228        let attack_failed = if attempt_attack {
1229            let primary = self.extract_ability(AbilityInput::Primary);
1230            let secondary = self.extract_ability(AbilityInput::Secondary);
1231            let abilities = [
1232                self.extract_ability(AbilityInput::Auxiliary(0)),
1233                self.extract_ability(AbilityInput::Auxiliary(1)),
1234                self.extract_ability(AbilityInput::Auxiliary(2)),
1235                self.extract_ability(AbilityInput::Auxiliary(3)),
1236                self.extract_ability(AbilityInput::Auxiliary(4)),
1237            ];
1238            let could_use_input = |input, desired_energy| match input {
1239                InputKind::Primary => primary.as_ref().is_some_and(|p| {
1240                    p.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1241                }),
1242                InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
1243                    s.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1244                }),
1245                InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
1246                    a.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1247                }),
1248                _ => false,
1249            };
1250            let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
1251                if matches!(current_input, InputKind::Secondary) {
1252                    let charging =
1253                        matches!(self.char_state.stage_section(), Some(StageSection::Charge));
1254                    let charged = self
1255                        .char_state
1256                        .durations()
1257                        .and_then(|durs| durs.charge)
1258                        .zip(self.char_state.timer())
1259                        .is_some_and(|(dur, timer)| timer > dur);
1260                    if !(charging && charged) {
1261                        *next_input = Some(InputKind::Secondary);
1262                    }
1263                } else {
1264                    *next_input = Some(current_input);
1265                }
1266            };
1267            match SwordTactics::from_u8(
1268                agent.combat_state.int_counters[IntCounters::Tactics as usize],
1269            ) {
1270                SwordTactics::Unskilled => {
1271                    let ability_preferences = AbilityPreferences {
1272                        desired_energy: 15.0,
1273                        combo_scaling_buildup: 0,
1274                    };
1275                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1276                    let mut next_input = None;
1277                    if let Some(input) = current_input {
1278                        continue_current_input(input, &mut next_input);
1279                    } else if rng.random_bool(0.5) {
1280                        next_input = Some(InputKind::Primary);
1281                    } else {
1282                        next_input = Some(InputKind::Secondary);
1283                    };
1284                    if let Some(input) = next_input {
1285                        if could_use_input(input, ability_preferences) {
1286                            controller.push_basic_input(input);
1287                            false
1288                        } else {
1289                            true
1290                        }
1291                    } else {
1292                        true
1293                    }
1294                },
1295                SwordTactics::Basic => {
1296                    let ability_preferences = AbilityPreferences {
1297                        desired_energy: 25.0,
1298                        combo_scaling_buildup: 0,
1299                    };
1300                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1301                    let mut next_input = None;
1302                    if let Some(input) = current_input {
1303                        continue_current_input(input, &mut next_input);
1304                    } else {
1305                        let attempt_ability = InputKind::Ability(rng.random_range(0..5));
1306                        if could_use_input(attempt_ability, ability_preferences) {
1307                            next_input = Some(attempt_ability);
1308                        } else if rng.random_bool(0.5) {
1309                            next_input = Some(InputKind::Primary);
1310                        } else {
1311                            next_input = Some(InputKind::Secondary);
1312                        }
1313                    };
1314                    if let Some(input) = next_input {
1315                        if could_use_input(input, ability_preferences) {
1316                            controller.push_basic_input(input);
1317                            false
1318                        } else {
1319                            true
1320                        }
1321                    } else {
1322                        true
1323                    }
1324                },
1325                SwordTactics::HeavySimple => {
1326                    let ability_preferences = AbilityPreferences {
1327                        desired_energy: 35.0,
1328                        combo_scaling_buildup: 0,
1329                    };
1330                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1331                    let mut next_input = None;
1332                    if let Some(input) = current_input {
1333                        continue_current_input(input, &mut next_input);
1334                    } else {
1335                        let stance_ability = InputKind::Ability(rng.random_range(3..5));
1336                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1337                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Heavy))) {
1338                            if could_use_input(stance_ability, ability_preferences) {
1339                                next_input = Some(stance_ability);
1340                            } else if rng.random_bool(0.5) {
1341                                next_input = Some(InputKind::Primary);
1342                            } else {
1343                                next_input = Some(InputKind::Secondary);
1344                            }
1345                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1346                            next_input = Some(InputKind::Ability(0));
1347                        } else if could_use_input(random_ability, ability_preferences) {
1348                            next_input = Some(random_ability);
1349                        } else if rng.random_bool(0.5) {
1350                            next_input = Some(InputKind::Primary);
1351                        } else {
1352                            next_input = Some(InputKind::Secondary);
1353                        }
1354                    };
1355                    if let Some(input) = next_input {
1356                        if could_use_input(input, ability_preferences) {
1357                            controller.push_basic_input(input);
1358                            false
1359                        } else {
1360                            true
1361                        }
1362                    } else {
1363                        true
1364                    }
1365                },
1366                SwordTactics::AgileSimple => {
1367                    let ability_preferences = AbilityPreferences {
1368                        desired_energy: 35.0,
1369                        combo_scaling_buildup: 0,
1370                    };
1371                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1372                    let mut next_input = None;
1373                    if let Some(input) = current_input {
1374                        continue_current_input(input, &mut next_input);
1375                    } else {
1376                        let stance_ability = InputKind::Ability(rng.random_range(3..5));
1377                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1378                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Agile))) {
1379                            if could_use_input(stance_ability, ability_preferences) {
1380                                next_input = Some(stance_ability);
1381                            } else if rng.random_bool(0.5) {
1382                                next_input = Some(InputKind::Primary);
1383                            } else {
1384                                next_input = Some(InputKind::Secondary);
1385                            }
1386                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1387                            next_input = Some(InputKind::Ability(0));
1388                        } else if could_use_input(random_ability, ability_preferences) {
1389                            next_input = Some(random_ability);
1390                        } else if rng.random_bool(0.5) {
1391                            next_input = Some(InputKind::Primary);
1392                        } else {
1393                            next_input = Some(InputKind::Secondary);
1394                        }
1395                    };
1396                    if let Some(input) = next_input {
1397                        if could_use_input(input, ability_preferences) {
1398                            controller.push_basic_input(input);
1399                            false
1400                        } else {
1401                            true
1402                        }
1403                    } else {
1404                        true
1405                    }
1406                },
1407                SwordTactics::DefensiveSimple => {
1408                    let ability_preferences = AbilityPreferences {
1409                        desired_energy: 35.0,
1410                        combo_scaling_buildup: 0,
1411                    };
1412                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1413                    let mut next_input = None;
1414                    if let Some(input) = current_input {
1415                        continue_current_input(input, &mut next_input);
1416                    } else {
1417                        let stance_ability = InputKind::Ability(rng.random_range(3..5));
1418                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1419                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Defensive))) {
1420                            if could_use_input(stance_ability, ability_preferences) {
1421                                next_input = Some(stance_ability);
1422                            } else if rng.random_bool(0.5) {
1423                                next_input = Some(InputKind::Primary);
1424                            } else {
1425                                next_input = Some(InputKind::Secondary);
1426                            }
1427                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1428                            next_input = Some(InputKind::Ability(0));
1429                        } else if could_use_input(InputKind::Ability(3), ability_preferences) {
1430                            next_input = Some(InputKind::Ability(3));
1431                        } else if could_use_input(random_ability, ability_preferences) {
1432                            next_input = Some(random_ability);
1433                        } else if rng.random_bool(0.5) {
1434                            next_input = Some(InputKind::Primary);
1435                        } else {
1436                            next_input = Some(InputKind::Secondary);
1437                        }
1438                    };
1439                    if let Some(input) = next_input {
1440                        if could_use_input(input, ability_preferences) {
1441                            controller.push_basic_input(input);
1442                            false
1443                        } else {
1444                            true
1445                        }
1446                    } else {
1447                        true
1448                    }
1449                },
1450                SwordTactics::CripplingSimple => {
1451                    let ability_preferences = AbilityPreferences {
1452                        desired_energy: 35.0,
1453                        combo_scaling_buildup: 0,
1454                    };
1455                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1456                    let mut next_input = None;
1457                    if let Some(input) = current_input {
1458                        continue_current_input(input, &mut next_input);
1459                    } else {
1460                        let stance_ability = InputKind::Ability(rng.random_range(3..5));
1461                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1462                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Crippling))) {
1463                            if could_use_input(stance_ability, ability_preferences) {
1464                                next_input = Some(stance_ability);
1465                            } else if rng.random_bool(0.5) {
1466                                next_input = Some(InputKind::Primary);
1467                            } else {
1468                                next_input = Some(InputKind::Secondary);
1469                            }
1470                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1471                            next_input = Some(InputKind::Ability(0));
1472                        } else if could_use_input(random_ability, ability_preferences) {
1473                            next_input = Some(random_ability);
1474                        } else if rng.random_bool(0.5) {
1475                            next_input = Some(InputKind::Primary);
1476                        } else {
1477                            next_input = Some(InputKind::Secondary);
1478                        }
1479                    };
1480                    if let Some(input) = next_input {
1481                        if could_use_input(input, ability_preferences) {
1482                            controller.push_basic_input(input);
1483                            false
1484                        } else {
1485                            true
1486                        }
1487                    } else {
1488                        true
1489                    }
1490                },
1491                SwordTactics::CleavingSimple => {
1492                    let ability_preferences = AbilityPreferences {
1493                        desired_energy: 35.0,
1494                        combo_scaling_buildup: 0,
1495                    };
1496                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1497                    let mut next_input = None;
1498                    if let Some(input) = current_input {
1499                        continue_current_input(input, &mut next_input);
1500                    } else {
1501                        let stance_ability = InputKind::Ability(rng.random_range(3..5));
1502                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1503                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Cleaving))) {
1504                            if could_use_input(stance_ability, ability_preferences) {
1505                                next_input = Some(stance_ability);
1506                            } else if rng.random_bool(0.5) {
1507                                next_input = Some(InputKind::Primary);
1508                            } else {
1509                                next_input = Some(InputKind::Secondary);
1510                            }
1511                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1512                            next_input = Some(InputKind::Ability(0));
1513                        } else if could_use_input(random_ability, ability_preferences) {
1514                            next_input = Some(random_ability);
1515                        } else if rng.random_bool(0.5) {
1516                            next_input = Some(InputKind::Primary);
1517                        } else {
1518                            next_input = Some(InputKind::Secondary);
1519                        }
1520                    };
1521                    if let Some(input) = next_input {
1522                        if could_use_input(input, ability_preferences) {
1523                            controller.push_basic_input(input);
1524                            false
1525                        } else {
1526                            true
1527                        }
1528                    } else {
1529                        true
1530                    }
1531                },
1532                SwordTactics::HeavyAdvanced => {
1533                    let ability_preferences = AbilityPreferences {
1534                        desired_energy: 50.0,
1535                        combo_scaling_buildup: 0,
1536                    };
1537                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1538                    let mut next_input = None;
1539                    if let Some(input) = current_input {
1540                        continue_current_input(input, &mut next_input);
1541                    } else {
1542                        let stance_ability = InputKind::Ability(rng.random_range(1..3));
1543                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1544                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Heavy))) {
1545                            if could_use_input(stance_ability, ability_preferences) {
1546                                next_input = Some(stance_ability);
1547                            } else if rng.random_bool(0.5) {
1548                                next_input = Some(InputKind::Primary);
1549                            } else {
1550                                next_input = Some(InputKind::Secondary);
1551                            }
1552                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1553                            next_input = Some(InputKind::Ability(0));
1554                        } else if could_use_input(random_ability, ability_preferences) {
1555                            next_input = Some(random_ability);
1556                        } else if rng.random_bool(0.5) {
1557                            next_input = Some(InputKind::Primary);
1558                        } else {
1559                            next_input = Some(InputKind::Secondary);
1560                        }
1561                    };
1562                    if let Some(input) = next_input {
1563                        if could_use_input(input, ability_preferences) {
1564                            controller.push_basic_input(input);
1565                            false
1566                        } else {
1567                            true
1568                        }
1569                    } else {
1570                        true
1571                    }
1572                },
1573                SwordTactics::AgileAdvanced => {
1574                    let ability_preferences = AbilityPreferences {
1575                        desired_energy: 50.0,
1576                        combo_scaling_buildup: 0,
1577                    };
1578                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1579                    let mut next_input = None;
1580                    if let Some(input) = current_input {
1581                        continue_current_input(input, &mut next_input);
1582                    } else {
1583                        let stance_ability = InputKind::Ability(rng.random_range(1..3));
1584                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1585                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Agile))) {
1586                            if could_use_input(stance_ability, ability_preferences) {
1587                                next_input = Some(stance_ability);
1588                            } else if rng.random_bool(0.5) {
1589                                next_input = Some(InputKind::Primary);
1590                            } else {
1591                                next_input = Some(InputKind::Secondary);
1592                            }
1593                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1594                            next_input = Some(InputKind::Ability(0));
1595                        } else if could_use_input(random_ability, ability_preferences) {
1596                            next_input = Some(random_ability);
1597                        } else if rng.random_bool(0.5) {
1598                            next_input = Some(InputKind::Primary);
1599                        } else {
1600                            next_input = Some(InputKind::Secondary);
1601                        }
1602                    };
1603                    if let Some(input) = next_input {
1604                        if could_use_input(input, ability_preferences) {
1605                            controller.push_basic_input(input);
1606                            false
1607                        } else {
1608                            true
1609                        }
1610                    } else {
1611                        true
1612                    }
1613                },
1614                SwordTactics::DefensiveAdvanced => {
1615                    let ability_preferences = AbilityPreferences {
1616                        desired_energy: 50.0,
1617                        combo_scaling_buildup: 0,
1618                    };
1619                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1620                    let mut next_input = None;
1621                    if let Some(input) = current_input {
1622                        continue_current_input(input, &mut next_input);
1623                    } else {
1624                        let stance_ability = InputKind::Ability(rng.random_range(1..3));
1625                        let random_ability = InputKind::Ability(rng.random_range(1..4));
1626                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Defensive))) {
1627                            if could_use_input(stance_ability, ability_preferences) {
1628                                next_input = Some(stance_ability);
1629                            } else if rng.random_bool(0.5) {
1630                                next_input = Some(InputKind::Primary);
1631                            } else {
1632                                next_input = Some(InputKind::Secondary);
1633                            }
1634                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1635                            next_input = Some(InputKind::Ability(0));
1636                        } else if could_use_input(random_ability, ability_preferences) {
1637                            next_input = Some(random_ability);
1638                        } else if could_use_input(InputKind::Ability(4), ability_preferences)
1639                            && rng.random_bool(2.0 * read_data.dt.0 as f64)
1640                        {
1641                            next_input = Some(InputKind::Ability(4));
1642                        } else if rng.random_bool(0.5) {
1643                            next_input = Some(InputKind::Primary);
1644                        } else {
1645                            next_input = Some(InputKind::Secondary);
1646                        }
1647                    };
1648                    if let Some(input) = next_input {
1649                        if could_use_input(input, ability_preferences) {
1650                            controller.push_basic_input(input);
1651                            false
1652                        } else {
1653                            true
1654                        }
1655                    } else {
1656                        true
1657                    }
1658                },
1659                SwordTactics::CripplingAdvanced => {
1660                    let ability_preferences = AbilityPreferences {
1661                        desired_energy: 50.0,
1662                        combo_scaling_buildup: 0,
1663                    };
1664                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1665                    let mut next_input = None;
1666                    if let Some(input) = current_input {
1667                        continue_current_input(input, &mut next_input);
1668                    } else {
1669                        let stance_ability = InputKind::Ability(rng.random_range(1..3));
1670                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1671                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Crippling))) {
1672                            if could_use_input(stance_ability, ability_preferences) {
1673                                next_input = Some(stance_ability);
1674                            } else if rng.random_bool(0.5) {
1675                                next_input = Some(InputKind::Primary);
1676                            } else {
1677                                next_input = Some(InputKind::Secondary);
1678                            }
1679                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1680                            next_input = Some(InputKind::Ability(0));
1681                        } else if could_use_input(random_ability, ability_preferences) {
1682                            next_input = Some(random_ability);
1683                        } else if rng.random_bool(0.5) {
1684                            next_input = Some(InputKind::Primary);
1685                        } else {
1686                            next_input = Some(InputKind::Secondary);
1687                        }
1688                    };
1689                    if let Some(input) = next_input {
1690                        if could_use_input(input, ability_preferences) {
1691                            controller.push_basic_input(input);
1692                            false
1693                        } else {
1694                            true
1695                        }
1696                    } else {
1697                        true
1698                    }
1699                },
1700                SwordTactics::CleavingAdvanced => {
1701                    let ability_preferences = AbilityPreferences {
1702                        desired_energy: 50.0,
1703                        combo_scaling_buildup: 0,
1704                    };
1705                    let current_input = self.char_state.ability_info().map(|ai| ai.input);
1706                    let mut next_input = None;
1707                    if let Some(input) = current_input {
1708                        continue_current_input(input, &mut next_input);
1709                    } else {
1710                        let stance_ability = InputKind::Ability(rng.random_range(1..3));
1711                        let random_ability = InputKind::Ability(rng.random_range(1..5));
1712                        if !matches!(self.stance, Some(Stance::Sword(SwordStance::Cleaving))) {
1713                            if could_use_input(stance_ability, ability_preferences) {
1714                                next_input = Some(stance_ability);
1715                            } else if rng.random_bool(0.5) {
1716                                next_input = Some(InputKind::Primary);
1717                            } else {
1718                                next_input = Some(InputKind::Secondary);
1719                            }
1720                        } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1721                            next_input = Some(InputKind::Ability(0));
1722                        } else if could_use_input(random_ability, ability_preferences) {
1723                            next_input = Some(random_ability);
1724                        } else if rng.random_bool(0.5) {
1725                            next_input = Some(InputKind::Primary);
1726                        } else {
1727                            next_input = Some(InputKind::Secondary);
1728                        }
1729                    };
1730                    if let Some(input) = next_input {
1731                        if could_use_input(input, ability_preferences) {
1732                            controller.push_basic_input(input);
1733                            false
1734                        } else {
1735                            true
1736                        }
1737                    } else {
1738                        true
1739                    }
1740                },
1741            }
1742        } else {
1743            false
1744        };
1745
1746        if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
1747            self.path_toward_target(
1748                agent,
1749                controller,
1750                tgt_data.pos.0,
1751                read_data,
1752                Path::Separate,
1753                None,
1754            );
1755        }
1756    }
1757
1758    pub fn handle_axe_attack(
1759        &self,
1760        agent: &mut Agent,
1761        controller: &mut Controller,
1762        attack_data: &AttackData,
1763        tgt_data: &TargetData,
1764        read_data: &ReadData,
1765        rng: &mut impl Rng,
1766    ) {
1767        if !agent.combat_state.initialized {
1768            agent.combat_state.initialized = true;
1769            let available_tactics = {
1770                let mut tactics = Vec::new();
1771                let try_tactic = |skill, tactic, tactics: &mut Vec<AxeTactics>| {
1772                    if self.skill_set.has_skill(Skill::Axe(skill)) {
1773                        tactics.push(tactic);
1774                    }
1775                };
1776                try_tactic(AxeSkill::Execute, AxeTactics::SavageAdvanced, &mut tactics);
1777                try_tactic(
1778                    AxeSkill::Lacerate,
1779                    AxeTactics::MercilessAdvanced,
1780                    &mut tactics,
1781                );
1782                try_tactic(AxeSkill::Bulkhead, AxeTactics::RivingAdvanced, &mut tactics);
1783                if tactics.is_empty() {
1784                    try_tactic(
1785                        AxeSkill::RisingTide,
1786                        AxeTactics::SavageIntermediate,
1787                        &mut tactics,
1788                    );
1789                    try_tactic(
1790                        AxeSkill::FierceRaze,
1791                        AxeTactics::MercilessIntermediate,
1792                        &mut tactics,
1793                    );
1794                    try_tactic(
1795                        AxeSkill::Plunder,
1796                        AxeTactics::RivingIntermediate,
1797                        &mut tactics,
1798                    );
1799                }
1800                if tactics.is_empty() {
1801                    try_tactic(
1802                        AxeSkill::BrutalSwing,
1803                        AxeTactics::SavageSimple,
1804                        &mut tactics,
1805                    );
1806                    try_tactic(AxeSkill::Rake, AxeTactics::MercilessSimple, &mut tactics);
1807                    try_tactic(AxeSkill::SkullBash, AxeTactics::RivingSimple, &mut tactics);
1808                }
1809                if tactics.is_empty() {
1810                    tactics.push(AxeTactics::Unskilled);
1811                }
1812                tactics
1813            };
1814
1815            let tactic = available_tactics
1816                .choose(rng)
1817                .copied()
1818                .unwrap_or(AxeTactics::Unskilled);
1819
1820            agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
1821
1822            let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
1823            let set_axe_ability = |controller: &mut Controller, slot, skill| {
1824                controller.push_event(ControlEvent::ChangeAbility {
1825                    slot,
1826                    auxiliary_key,
1827                    new_ability: AuxiliaryAbility::MainWeapon(skill),
1828                });
1829            };
1830
1831            match tactic {
1832                AxeTactics::Unskilled => {},
1833                AxeTactics::SavageSimple => {
1834                    // Brutal swing
1835                    set_axe_ability(controller, 0, 0);
1836                },
1837                AxeTactics::MercilessSimple => {
1838                    // Rake
1839                    set_axe_ability(controller, 0, 6);
1840                },
1841                AxeTactics::RivingSimple => {
1842                    // Skull bash
1843                    set_axe_ability(controller, 0, 12);
1844                },
1845                AxeTactics::SavageIntermediate => {
1846                    // Brutal swing
1847                    set_axe_ability(controller, 0, 0);
1848                    // Berserk
1849                    set_axe_ability(controller, 1, 1);
1850                    // Rising tide
1851                    set_axe_ability(controller, 2, 2);
1852                },
1853                AxeTactics::MercilessIntermediate => {
1854                    // Rake
1855                    set_axe_ability(controller, 0, 6);
1856                    // Bloodfeast
1857                    set_axe_ability(controller, 1, 7);
1858                    // Fierce raze
1859                    set_axe_ability(controller, 2, 8);
1860                },
1861                AxeTactics::RivingIntermediate => {
1862                    // Skull bash
1863                    set_axe_ability(controller, 0, 12);
1864                    // Sunder
1865                    set_axe_ability(controller, 1, 13);
1866                    // Plunder
1867                    set_axe_ability(controller, 2, 14);
1868                },
1869                AxeTactics::SavageAdvanced => {
1870                    // Berserk
1871                    set_axe_ability(controller, 0, 1);
1872                    // Rising tide
1873                    set_axe_ability(controller, 1, 2);
1874                    // Savage sense
1875                    set_axe_ability(controller, 2, 3);
1876                    // Adrenaline rush
1877                    set_axe_ability(controller, 3, 4);
1878                    // Execute/maelstrom
1879                    set_axe_ability(controller, 4, 5);
1880                },
1881                AxeTactics::MercilessAdvanced => {
1882                    // Bloodfeast
1883                    set_axe_ability(controller, 0, 7);
1884                    // Fierce raze
1885                    set_axe_ability(controller, 1, 8);
1886                    // Furor
1887                    set_axe_ability(controller, 2, 9);
1888                    // Fracture
1889                    set_axe_ability(controller, 3, 10);
1890                    // Lacerate/riptide
1891                    set_axe_ability(controller, 4, 11);
1892                },
1893                AxeTactics::RivingAdvanced => {
1894                    // Sunder
1895                    set_axe_ability(controller, 0, 13);
1896                    // Plunder
1897                    set_axe_ability(controller, 1, 14);
1898                    // Defiance
1899                    set_axe_ability(controller, 2, 15);
1900                    // Keelhaul
1901                    set_axe_ability(controller, 3, 16);
1902                    // Bulkhead/capsize
1903                    set_axe_ability(controller, 4, 17);
1904                },
1905            }
1906
1907            agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
1908                ActionMode::Reckless as u8;
1909        }
1910
1911        enum IntCounters {
1912            Tactic = 0,
1913            ActionMode = 1,
1914        }
1915
1916        enum Timers {
1917            GuardedCycle = 0,
1918            PosTimeOut = 1,
1919        }
1920
1921        enum Conditions {
1922            GuardedDefend = 0,
1923            RollingBreakThrough = 1,
1924        }
1925
1926        enum FloatCounters {
1927            GuardedTimer = 0,
1928        }
1929
1930        enum Positions {
1931            GuardedCover = 0,
1932            Flee = 1,
1933        }
1934
1935        let attempt_attack = handle_attack_aggression(
1936            self,
1937            agent,
1938            controller,
1939            attack_data,
1940            tgt_data,
1941            read_data,
1942            rng,
1943            Timers::PosTimeOut as usize,
1944            Timers::GuardedCycle as usize,
1945            FloatCounters::GuardedTimer as usize,
1946            IntCounters::ActionMode as usize,
1947            Conditions::GuardedDefend as usize,
1948            Conditions::RollingBreakThrough as usize,
1949            Positions::GuardedCover as usize,
1950            Positions::Flee as usize,
1951        );
1952
1953        let attack_failed = if attempt_attack {
1954            let primary = self.extract_ability(AbilityInput::Primary);
1955            let secondary = self.extract_ability(AbilityInput::Secondary);
1956            let abilities = [
1957                self.extract_ability(AbilityInput::Auxiliary(0)),
1958                self.extract_ability(AbilityInput::Auxiliary(1)),
1959                self.extract_ability(AbilityInput::Auxiliary(2)),
1960                self.extract_ability(AbilityInput::Auxiliary(3)),
1961                self.extract_ability(AbilityInput::Auxiliary(4)),
1962            ];
1963            let could_use_input = |input, ability_preferences| match input {
1964                InputKind::Primary => primary.as_ref().is_some_and(|p| {
1965                    p.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
1966                }),
1967                InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
1968                    s.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
1969                }),
1970                InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
1971                    a.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
1972                }),
1973                _ => false,
1974            };
1975            let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
1976                if matches!(current_input, InputKind::Secondary) {
1977                    let charging =
1978                        matches!(self.char_state.stage_section(), Some(StageSection::Charge));
1979                    let charged = self
1980                        .char_state
1981                        .durations()
1982                        .and_then(|durs| durs.charge)
1983                        .zip(self.char_state.timer())
1984                        .is_some_and(|(dur, timer)| timer > dur);
1985                    if !(charging && charged) {
1986                        *next_input = Some(InputKind::Secondary);
1987                    }
1988                } else {
1989                    *next_input = Some(current_input);
1990                }
1991            };
1992            let current_input = self.char_state.ability_info().map(|ai| ai.input);
1993            let ability_preferences = AbilityPreferences {
1994                desired_energy: 40.0,
1995                combo_scaling_buildup: 15,
1996            };
1997            let mut next_input = None;
1998            if let Some(input) = current_input {
1999                continue_current_input(input, &mut next_input);
2000            } else {
2001                match AxeTactics::from_u8(
2002                    agent.combat_state.int_counters[IntCounters::Tactic as usize],
2003                ) {
2004                    AxeTactics::Unskilled => {
2005                        if rng.random_bool(0.5) {
2006                            next_input = Some(InputKind::Primary);
2007                        } else {
2008                            next_input = Some(InputKind::Secondary);
2009                        }
2010                    },
2011                    AxeTactics::SavageSimple
2012                    | AxeTactics::MercilessSimple
2013                    | AxeTactics::RivingSimple => {
2014                        if could_use_input(InputKind::Ability(0), ability_preferences) {
2015                            next_input = Some(InputKind::Ability(0));
2016                        } else if rng.random_bool(0.5) {
2017                            next_input = Some(InputKind::Primary);
2018                        } else {
2019                            next_input = Some(InputKind::Secondary);
2020                        }
2021                    },
2022                    AxeTactics::SavageIntermediate
2023                    | AxeTactics::MercilessIntermediate
2024                    | AxeTactics::RivingIntermediate => {
2025                        let random_ability = InputKind::Ability(rng.random_range(0..3));
2026                        if could_use_input(random_ability, ability_preferences) {
2027                            next_input = Some(random_ability);
2028                        } else if rng.random_bool(0.5) {
2029                            next_input = Some(InputKind::Primary);
2030                        } else {
2031                            next_input = Some(InputKind::Secondary);
2032                        }
2033                    },
2034                    AxeTactics::SavageAdvanced
2035                    | AxeTactics::MercilessAdvanced
2036                    | AxeTactics::RivingAdvanced => {
2037                        let random_ability = InputKind::Ability(rng.random_range(0..5));
2038                        if could_use_input(random_ability, ability_preferences) {
2039                            next_input = Some(random_ability);
2040                        } else if rng.random_bool(0.5) {
2041                            next_input = Some(InputKind::Primary);
2042                        } else {
2043                            next_input = Some(InputKind::Secondary);
2044                        }
2045                    },
2046                }
2047            }
2048            if let Some(input) = next_input {
2049                if could_use_input(input, ability_preferences) {
2050                    controller.push_basic_input(input);
2051                    false
2052                } else {
2053                    true
2054                }
2055            } else {
2056                true
2057            }
2058        } else {
2059            false
2060        };
2061
2062        if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
2063            self.path_toward_target(
2064                agent,
2065                controller,
2066                tgt_data.pos.0,
2067                read_data,
2068                Path::Separate,
2069                None,
2070            );
2071        }
2072    }
2073
2074    pub fn handle_bow_attack(
2075        &self,
2076        agent: &mut Agent,
2077        controller: &mut Controller,
2078        attack_data: &AttackData,
2079        tgt_data: &TargetData,
2080        read_data: &ReadData,
2081        rng: &mut impl Rng,
2082    ) {
2083        if !agent.combat_state.initialized {
2084            agent.combat_state.initialized = true;
2085            let available_tactics = {
2086                let mut tactics = Vec::new();
2087                let try_tactic = |skill, tactic, tactics: &mut Vec<BowTactics>| {
2088                    if self.skill_set.has_skill(Skill::Bow(skill)) {
2089                        tactics.push(tactic);
2090                    }
2091                };
2092                try_tactic(
2093                    BowSkill::Heartseeker,
2094                    BowTactics::HunterAdvanced,
2095                    &mut tactics,
2096                );
2097                try_tactic(
2098                    BowSkill::FreezeArrow,
2099                    BowTactics::TricksterAdvanced,
2100                    &mut tactics,
2101                );
2102                try_tactic(
2103                    BowSkill::Fusillade,
2104                    BowTactics::ArtilleryAdvanced,
2105                    &mut tactics,
2106                );
2107                if tactics.is_empty() {
2108                    try_tactic(
2109                        BowSkill::OwlTalon,
2110                        BowTactics::HunterIntermediate,
2111                        &mut tactics,
2112                    );
2113                    try_tactic(
2114                        BowSkill::IgniteArrow,
2115                        BowTactics::TricksterIntermediate,
2116                        &mut tactics,
2117                    );
2118                    try_tactic(
2119                        BowSkill::PiercingGale,
2120                        BowTactics::ArtilleryIntermediate,
2121                        &mut tactics,
2122                    );
2123                }
2124                if tactics.is_empty() {
2125                    try_tactic(BowSkill::ArdentHunt, BowTactics::HunterSimple, &mut tactics);
2126                    try_tactic(
2127                        BowSkill::SepticShot,
2128                        BowTactics::TricksterSimple,
2129                        &mut tactics,
2130                    );
2131                    try_tactic(BowSkill::Barrage, BowTactics::ArtillerySimple, &mut tactics);
2132                }
2133                if tactics.is_empty() {
2134                    try_tactic(BowSkill::HeavyNock, BowTactics::Simple, &mut tactics);
2135                }
2136                if tactics.is_empty() {
2137                    tactics.push(BowTactics::Unskilled);
2138                }
2139                tactics
2140            };
2141
2142            let tactic = available_tactics
2143                .choose(rng)
2144                .copied()
2145                .unwrap_or(BowTactics::Unskilled);
2146
2147            agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
2148
2149            let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
2150            let set_ability = |controller: &mut Controller, slot, skill| {
2151                controller.push_event(ControlEvent::ChangeAbility {
2152                    slot,
2153                    auxiliary_key,
2154                    new_ability: AuxiliaryAbility::MainWeapon(skill),
2155                });
2156            };
2157            let mut set_random = |controller: &mut Controller, slot, options: &mut Vec<usize>| {
2158                if options.is_empty() {
2159                    return;
2160                }
2161                let i = rng.random_range(0..options.len());
2162                set_ability(controller, slot, options.swap_remove(i));
2163            };
2164
2165            match tactic {
2166                BowTactics::Unskilled => {},
2167                BowTactics::Simple => {
2168                    // foothold or heavy nock
2169                    set_ability(controller, 0, rng.random_range(0..2));
2170                },
2171                BowTactics::HunterSimple => {
2172                    // foothold
2173                    set_ability(controller, 0, 0);
2174                    // heavy nock
2175                    set_ability(controller, 1, 1);
2176                    // ardent hunt
2177                    set_ability(controller, 2, 2);
2178                },
2179                BowTactics::HunterIntermediate => {
2180                    // ardent hunt
2181                    set_ability(controller, 0, 2);
2182                    // foothold, heavy nock, owl talon, or eagle eye
2183                    let mut options = vec![0, 1, 3, 4];
2184                    set_random(controller, 1, &mut options);
2185                    set_random(controller, 2, &mut options);
2186                    set_random(controller, 3, &mut options);
2187                },
2188                BowTactics::HunterAdvanced => {
2189                    // ardent hunt, owl talon, eagle eye, heartseeker, or hawkstrike
2190                    let mut options = vec![2, 3, 4, 5, 6];
2191                    set_random(controller, 1, &mut options);
2192                    set_random(controller, 2, &mut options);
2193                    set_random(controller, 3, &mut options);
2194                    set_random(controller, 4, &mut options);
2195                    // foothold or heavy nock
2196                    set_ability(controller, 0, rng.random_range(0..2));
2197                },
2198                BowTactics::TricksterSimple => {
2199                    // foothold
2200                    set_ability(controller, 0, 0);
2201                    // heavy nock
2202                    set_ability(controller, 1, 1);
2203                    // septic shot
2204                    set_ability(controller, 2, 7);
2205                },
2206                BowTactics::TricksterIntermediate => {
2207                    // septic shot
2208                    set_ability(controller, 0, 7);
2209                    // foothold, heavy nock, ignite arrow, or drench arrow
2210                    let mut options = vec![0, 1, 8, 9];
2211                    set_random(controller, 1, &mut options);
2212                    set_random(controller, 2, &mut options);
2213                    set_random(controller, 3, &mut options);
2214                },
2215                BowTactics::TricksterAdvanced => {
2216                    // septic shot, ignite arrow, drench arrow, freeze arrow, jolt arrow
2217                    let mut options = vec![7, 8, 9, 10, 11];
2218                    set_random(controller, 1, &mut options);
2219                    set_random(controller, 2, &mut options);
2220                    set_random(controller, 3, &mut options);
2221                    set_random(controller, 4, &mut options);
2222                    // foothold or heavy nock
2223                    set_ability(controller, 0, rng.random_range(0..2));
2224                },
2225                BowTactics::ArtillerySimple => {
2226                    // foothold
2227                    set_ability(controller, 0, 0);
2228                    // heavy nock
2229                    set_ability(controller, 1, 1);
2230                    // barrage
2231                    set_ability(controller, 2, 12);
2232                },
2233                BowTactics::ArtilleryIntermediate => {
2234                    // barrage
2235                    set_ability(controller, 0, 12);
2236                    // foothold, heavy nock, piercing gale, or scatterburst
2237                    let mut options = vec![0, 1, 13, 14];
2238                    set_random(controller, 1, &mut options);
2239                    set_random(controller, 2, &mut options);
2240                    set_random(controller, 3, &mut options);
2241                },
2242                BowTactics::ArtilleryAdvanced => {
2243                    // barrage, piercing gale, scatterburst, fusillade, death volley
2244                    let mut options = vec![12, 13, 14, 15, 16];
2245                    set_random(controller, 1, &mut options);
2246                    set_random(controller, 2, &mut options);
2247                    set_random(controller, 3, &mut options);
2248                    set_random(controller, 4, &mut options);
2249                    // foothold or heavy nock
2250                    set_ability(controller, 0, rng.random_range(0..2));
2251                },
2252            }
2253        }
2254
2255        enum IntCounters {
2256            Tactic = 0,
2257            ActionMode = 1,
2258        }
2259
2260        enum Conditions {
2261            RollingBreakThrough = 0,
2262            MaintainDist = 1,
2263            CreateDist = 2,
2264        }
2265
2266        enum Positions {
2267            Flee = 0,
2268            Maintain = 1,
2269        }
2270
2271        enum Timers {
2272            GuardedCycle = 0,
2273        }
2274
2275        enum FloatCounters {
2276            GuardedCycle = 0,
2277        }
2278
2279        // TODO: Abstract this with `handle_attack_aggression` later (probably with
2280        // staff rework?) once the effectiveness of this strategy has been evaluated
2281        // more
2282        let attempt_attack = {
2283            if let Some(health) = self.health {
2284                agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
2285                    if health.fraction() < 0.2 {
2286                        ActionMode::Fleeing as u8
2287                    } else if health.fraction() < 0.95 {
2288                        ActionMode::Guarded as u8
2289                    } else {
2290                        ActionMode::Reckless as u8
2291                    };
2292            }
2293
2294            let range1 = rng.random_range(6.0..10.0);
2295            let range2 = rng.random_range(8.0..15.0);
2296
2297            let mut flee_handler = |agent: &mut Agent| {
2298                if agent.combat_state.conditions[Conditions::RollingBreakThrough as usize] {
2299                    controller.push_basic_input(InputKind::Roll);
2300                    agent.combat_state.conditions[Conditions::RollingBreakThrough as usize] = false;
2301                }
2302                if let Some(pos) = agent.combat_state.positions[Positions::Flee as usize] {
2303                    if let Some(dir) = Dir::from_unnormalized(pos - self.pos.0) {
2304                        controller.inputs.look_dir = dir;
2305                    }
2306                    if pos.distance_squared(self.pos.0) < 5_f32.powi(2) {
2307                        agent.combat_state.positions[Positions::Flee as usize] = None;
2308                    }
2309                    self.path_toward_target(
2310                        agent,
2311                        controller,
2312                        pos,
2313                        read_data,
2314                        Path::Separate,
2315                        None,
2316                    );
2317                } else {
2318                    agent.combat_state.positions[Positions::Flee as usize] = {
2319                        let rand_dir = {
2320                            let dir = (self.pos.0 - tgt_data.pos.0)
2321                                .try_normalized()
2322                                .unwrap_or(Vec3::unit_x())
2323                                .xy();
2324                            dir.rotated_z(rng.random_range(-0.75..0.75))
2325                        };
2326                        let attempted_dist = rng.random_range(16.0..26.0);
2327                        let actual_dist = read_data
2328                            .terrain
2329                            .ray(
2330                                self.pos.0 + Vec3::unit_z() * 0.5,
2331                                self.pos.0 + Vec3::unit_z() * 0.5 + rand_dir * attempted_dist,
2332                            )
2333                            .until(Block::is_solid)
2334                            .cast()
2335                            .0
2336                            - 1.0;
2337                        if actual_dist < 10.0 {
2338                            let dist = read_data
2339                                .terrain
2340                                .ray(
2341                                    self.pos.0 + Vec3::unit_z() * 0.5,
2342                                    self.pos.0 + Vec3::unit_z() * 0.5 - rand_dir * attempted_dist,
2343                                )
2344                                .until(Block::is_solid)
2345                                .cast()
2346                                .0
2347                                - 1.0;
2348                            agent.combat_state.conditions
2349                                [Conditions::RollingBreakThrough as usize] = true;
2350                            Some(self.pos.0 - rand_dir * dist)
2351                        } else {
2352                            Some(self.pos.0 + rand_dir * actual_dist)
2353                        }
2354                    };
2355                }
2356            };
2357
2358            match ActionMode::from_u8(
2359                agent.combat_state.int_counters[IntCounters::ActionMode as usize],
2360            ) {
2361                ActionMode::Reckless => true,
2362                ActionMode::Guarded => {
2363                    agent.combat_state.timers[Timers::GuardedCycle as usize] += read_data.dt.0;
2364                    if agent.combat_state.timers[Timers::GuardedCycle as usize]
2365                        > agent.combat_state.counters[FloatCounters::GuardedCycle as usize]
2366                    {
2367                        agent.combat_state.timers[Timers::GuardedCycle as usize] = 0.0;
2368                        agent.combat_state.conditions[Conditions::MaintainDist as usize] ^= true;
2369                        agent.combat_state.counters[FloatCounters::GuardedCycle as usize] =
2370                            if agent.combat_state.conditions[Conditions::MaintainDist as usize] {
2371                                range1
2372                            } else {
2373                                range2
2374                            };
2375                    }
2376                    if let Some(pos) = agent.combat_state.positions[Positions::Maintain as usize]
2377                        && pos.distance_squared(self.pos.0) < 5_f32.powi(2)
2378                    {
2379                        agent.combat_state.positions[Positions::Maintain as usize] = None;
2380                    }
2381                    let circle = if agent.combat_state.conditions[Conditions::MaintainDist as usize]
2382                    {
2383                        if attack_data.dist_sqrd < 7_f32.powi(2) {
2384                            agent.combat_state.conditions[Conditions::CreateDist as usize] = true;
2385                        }
2386                        if attack_data.dist_sqrd > 12_f32.powi(2) {
2387                            agent.combat_state.conditions[Conditions::CreateDist as usize] = false;
2388                        }
2389                        if agent.combat_state.conditions[Conditions::CreateDist as usize] {
2390                            flee_handler(agent);
2391                            false
2392                        } else {
2393                            true
2394                        }
2395                    } else {
2396                        true
2397                    };
2398                    if circle {
2399                        if let Some(pos) =
2400                            agent.combat_state.positions[Positions::Maintain as usize]
2401                        {
2402                            self.path_toward_target(
2403                                agent,
2404                                controller,
2405                                pos,
2406                                read_data,
2407                                Path::Separate,
2408                                None,
2409                            );
2410                        } else {
2411                            agent.combat_state.positions[Positions::Maintain as usize] = {
2412                                let rand_dir = {
2413                                    let dir = (tgt_data.pos.0 - self.pos.0)
2414                                        .try_normalized()
2415                                        .unwrap_or(Vec3::unit_x())
2416                                        .xy();
2417                                    if rng.random_bool(0.5) {
2418                                        dir.rotated_z(PI / 2.0 + rng.random_range(0.0..0.75))
2419                                    } else {
2420                                        dir.rotated_z(-PI / 2.0 - rng.random_range(0.0..0.75))
2421                                    }
2422                                };
2423                                let attempted_dist = rng.random_range(12.0..20.0);
2424                                let actual_dist = read_data
2425                                    .terrain
2426                                    .ray(
2427                                        self.pos.0 + Vec3::unit_z() * 0.5,
2428                                        self.pos.0
2429                                            + Vec3::unit_z() * 0.5
2430                                            + rand_dir * attempted_dist,
2431                                    )
2432                                    .until(Block::is_solid)
2433                                    .cast()
2434                                    .0
2435                                    - 1.0;
2436                                Some(self.pos.0 + rand_dir * actual_dist)
2437                            };
2438                        }
2439                        true
2440                    } else {
2441                        false
2442                    }
2443                },
2444                ActionMode::Fleeing => {
2445                    flee_handler(agent);
2446                    false
2447                },
2448            }
2449        };
2450
2451        let attack_failed = if attempt_attack {
2452            let primary = self.extract_ability(AbilityInput::Primary);
2453            let secondary = self.extract_ability(AbilityInput::Secondary);
2454            let abilities = [
2455                self.extract_ability(AbilityInput::Auxiliary(0)),
2456                self.extract_ability(AbilityInput::Auxiliary(1)),
2457                self.extract_ability(AbilityInput::Auxiliary(2)),
2458                self.extract_ability(AbilityInput::Auxiliary(3)),
2459                self.extract_ability(AbilityInput::Auxiliary(4)),
2460            ];
2461            let could_use_input = |input, ability_preferences| match input {
2462                InputKind::Primary => primary.as_ref().is_some_and(|p| {
2463                    p.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
2464                }),
2465                InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
2466                    s.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
2467                }),
2468                InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
2469                    let ability = self.active_abilities.get_ability(
2470                        AbilityInput::Auxiliary(x),
2471                        Some(self.inventory),
2472                        Some(self.skill_set),
2473                        self.stats,
2474                    );
2475                    let additional_conditions = match ability {
2476                        Ability::MainWeaponAux(8) => self
2477                            .inventory
2478                            .get_slot_of_item_by_def_id(&AbilityReqItem::item_def_id(
2479                                &AbilityReqItem::Firedrop,
2480                            ))
2481                            .is_some(),
2482                        Ability::MainWeaponAux(9) => self
2483                            .inventory
2484                            .get_slot_of_item_by_def_id(&AbilityReqItem::item_def_id(
2485                                &AbilityReqItem::PoisonClot,
2486                            ))
2487                            .is_some(),
2488                        Ability::MainWeaponAux(10) => self
2489                            .inventory
2490                            .get_slot_of_item_by_def_id(&AbilityReqItem::item_def_id(
2491                                &AbilityReqItem::GelidGel,
2492                            ))
2493                            .is_some(),
2494                        Ability::MainWeaponAux(11) => self
2495                            .inventory
2496                            .get_slot_of_item_by_def_id(&AbilityReqItem::item_def_id(
2497                                &AbilityReqItem::LevinDust,
2498                            ))
2499                            .is_some(),
2500                        _ => true,
2501                    };
2502                    a.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
2503                        && additional_conditions
2504                }),
2505                _ => false,
2506            };
2507            let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
2508                let charging =
2509                    matches!(self.char_state.stage_section(), Some(StageSection::Charge));
2510                let charged = self
2511                    .char_state
2512                    .durations()
2513                    .and_then(|durs| durs.charge)
2514                    .zip(self.char_state.timer())
2515                    .is_some_and(|(dur, timer)| timer > dur);
2516                let recover =
2517                    matches!(self.char_state.stage_section(), Some(StageSection::Recover));
2518
2519                if !(recover || (charging && charged)) {
2520                    *next_input = Some(current_input);
2521                }
2522            };
2523            let prefer_m1m2 = matches!(
2524                self.stance,
2525                Some(Stance::Bow(
2526                    BowStance::Scatterburst
2527                        | BowStance::IgniteArrow
2528                        | BowStance::DrenchArrow
2529                        | BowStance::FreezeArrow
2530                        | BowStance::JoltArrow
2531                ))
2532            );
2533            let prefer_m2 = matches!(
2534                self.stance,
2535                Some(Stance::Bow(
2536                    BowStance::Barrage
2537                        | BowStance::PiercingGale
2538                        | BowStance::Hawkstrike
2539                        | BowStance::Fusillade
2540                        | BowStance::DeathVolley
2541                ))
2542            );
2543            let current_input = self.char_state.ability_info().map(|ai| ai.input);
2544            let ability_preferences = AbilityPreferences {
2545                desired_energy: 40.0,
2546                combo_scaling_buildup: 0,
2547            };
2548            let mut next_input = None;
2549            if let Some(input) = current_input {
2550                continue_current_input(input, &mut next_input);
2551            } else if prefer_m1m2 {
2552                if rng.random_bool(0.3) {
2553                    next_input = Some(InputKind::Primary);
2554                } else {
2555                    next_input = Some(InputKind::Secondary);
2556                }
2557            } else if prefer_m2 {
2558                if could_use_input(InputKind::Secondary, ability_preferences) {
2559                    next_input = Some(InputKind::Secondary);
2560                } else {
2561                    next_input = Some(InputKind::Primary);
2562                }
2563            } else {
2564                match BowTactics::from_u8(
2565                    agent.combat_state.int_counters[IntCounters::Tactic as usize],
2566                ) {
2567                    BowTactics::Unskilled => {
2568                        if rng.random_bool(0.5) {
2569                            next_input = Some(InputKind::Primary);
2570                        } else {
2571                            next_input = Some(InputKind::Secondary);
2572                        }
2573                    },
2574                    BowTactics::Simple => {
2575                        if could_use_input(InputKind::Ability(0), ability_preferences) {
2576                            next_input = Some(InputKind::Ability(0));
2577                        } else if rng.random_bool(0.5) {
2578                            next_input = Some(InputKind::Primary);
2579                        } else {
2580                            next_input = Some(InputKind::Secondary);
2581                        }
2582                    },
2583                    BowTactics::HunterSimple
2584                    | BowTactics::TricksterSimple
2585                    | BowTactics::ArtillerySimple => {
2586                        let random_ability = InputKind::Ability(rng.random_range(0..3));
2587                        if could_use_input(random_ability, ability_preferences) {
2588                            next_input = Some(random_ability);
2589                        } else if rng.random_bool(0.5) {
2590                            next_input = Some(InputKind::Primary);
2591                        } else {
2592                            next_input = Some(InputKind::Secondary);
2593                        }
2594                    },
2595                    BowTactics::HunterIntermediate
2596                    | BowTactics::TricksterIntermediate
2597                    | BowTactics::ArtilleryIntermediate => {
2598                        let random_ability = InputKind::Ability(rng.random_range(0..4));
2599                        if could_use_input(random_ability, ability_preferences) {
2600                            next_input = Some(random_ability);
2601                        } else if rng.random_bool(0.5) {
2602                            next_input = Some(InputKind::Primary);
2603                        } else {
2604                            next_input = Some(InputKind::Secondary);
2605                        }
2606                    },
2607                    BowTactics::HunterAdvanced
2608                    | BowTactics::TricksterAdvanced
2609                    | BowTactics::ArtilleryAdvanced => {
2610                        let random_ability = InputKind::Ability(rng.random_range(0..5));
2611                        if could_use_input(random_ability, ability_preferences) {
2612                            next_input = Some(random_ability);
2613                        } else if rng.random_bool(0.5) {
2614                            next_input = Some(InputKind::Primary);
2615                        } else {
2616                            next_input = Some(InputKind::Secondary);
2617                        }
2618                    },
2619                }
2620            }
2621            if let Some(input) = next_input {
2622                if could_use_input(input, ability_preferences) {
2623                    if matches!(input, InputKind::Secondary)
2624                        && matches!(self.stance, Some(Stance::Bow(BowStance::DeathVolley)))
2625                    {
2626                        controller.push_action(ControlAction::StartInput {
2627                            input: InputKind::Secondary,
2628                            target_entity: None,
2629                            select_pos: Some(tgt_data.pos.0),
2630                        });
2631                    } else {
2632                        controller.push_basic_input(input);
2633                    }
2634                    false
2635                } else {
2636                    true
2637                }
2638            } else {
2639                true
2640            }
2641        } else {
2642            false
2643        };
2644
2645        if attack_failed
2646            && (attack_data.dist_sqrd > 25_f32.powi(2)
2647                || !entities_have_line_of_sight(
2648                    self.pos,
2649                    self.body,
2650                    self.scale,
2651                    tgt_data.pos,
2652                    tgt_data.body,
2653                    tgt_data.scale,
2654                    read_data,
2655                ))
2656        {
2657            self.path_toward_target(
2658                agent,
2659                controller,
2660                tgt_data.pos.0,
2661                read_data,
2662                Path::Separate,
2663                None,
2664            );
2665        }
2666    }
2667
2668    pub fn handle_staff_attack(
2669        &self,
2670        agent: &mut Agent,
2671        controller: &mut Controller,
2672        attack_data: &AttackData,
2673        tgt_data: &TargetData,
2674        read_data: &ReadData,
2675        rng: &mut impl Rng,
2676    ) {
2677        enum ActionStateConditions {
2678            ConditionStaffCanShockwave = 0,
2679        }
2680        let context = AbilityContext::from(self.stance, Some(self.inventory), self.combo);
2681        let extract_ability = |input: AbilityInput| {
2682            self.active_abilities
2683                .activate_ability(
2684                    input,
2685                    Some(self.inventory),
2686                    self.skill_set,
2687                    self.body,
2688                    Some(self.char_state),
2689                    &context,
2690                    self.stats,
2691                )
2692                .map_or(Default::default(), |a| a.0)
2693        };
2694        let (flamethrower, shockwave) = (
2695            extract_ability(AbilityInput::Secondary),
2696            extract_ability(AbilityInput::Auxiliary(0)),
2697        );
2698        let flamethrower_range = match flamethrower {
2699            CharacterAbility::BasicBeam { range, .. } => range,
2700            _ => 20.0_f32,
2701        };
2702        let shockwave_cost = shockwave.energy_cost();
2703        if self.body.is_some_and(|b| b.is_humanoid())
2704            && attack_data.in_min_range()
2705            && self.energy.current()
2706                > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2707            && !matches!(self.char_state, CharacterState::Shockwave(_))
2708        {
2709            // if a humanoid, have enough stamina, not in shockwave, and in melee range,
2710            // emergency roll
2711            controller.push_basic_input(InputKind::Roll);
2712        } else if matches!(self.char_state, CharacterState::Shockwave(_)) {
2713            agent.combat_state.conditions
2714                [ActionStateConditions::ConditionStaffCanShockwave as usize] = false;
2715        } else if agent.combat_state.conditions
2716            [ActionStateConditions::ConditionStaffCanShockwave as usize]
2717            && matches!(self.char_state, CharacterState::Wielding(_))
2718        {
2719            controller.push_basic_input(InputKind::Ability(0));
2720        } else if !matches!(self.char_state, CharacterState::Shockwave(c) if !matches!(c.stage_section, StageSection::Recover))
2721        {
2722            // only try to use another ability unless in shockwave or recover
2723            let target_approaching_speed = -agent
2724                .target
2725                .as_ref()
2726                .map(|t| t.target)
2727                .and_then(|e| read_data.velocities.get(e))
2728                .map_or(0.0, |v| v.0.dot(self.ori.look_vec()));
2729            if self
2730                .skill_set
2731                .has_skill(Skill::Staff(StaffSkill::UnlockShockwave))
2732                && target_approaching_speed > 12.0
2733                && self.energy.current() > shockwave_cost
2734            {
2735                // if enemy is closing distance quickly, use shockwave to knock back
2736                if matches!(self.char_state, CharacterState::Wielding(_)) {
2737                    controller.push_basic_input(InputKind::Ability(0));
2738                } else {
2739                    agent.combat_state.conditions
2740                        [ActionStateConditions::ConditionStaffCanShockwave as usize] = true;
2741                }
2742            } else if self.energy.current()
2743                > shockwave_cost
2744                    + CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2745                && attack_data.dist_sqrd < flamethrower_range.powi(2)
2746            {
2747                controller.push_basic_input(InputKind::Secondary);
2748            } else {
2749                controller.push_basic_input(InputKind::Primary);
2750            }
2751        }
2752        // Logic to move. Intentionally kept separate from ability logic so duplicated
2753        // work is less necessary.
2754        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2755            // Attempt to move away from target if too close
2756            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
2757                &*read_data.terrain,
2758                self.pos.0,
2759                self.vel.0,
2760                tgt_data.pos.0,
2761                TraversalConfig {
2762                    min_tgt_dist: 1.25,
2763                    ..self.traversal_config
2764                },
2765                &read_data.time,
2766            ) {
2767                self.unstuck_if(stuck, controller);
2768                controller.inputs.move_dir =
2769                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2770            }
2771        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2772            // Else attempt to circle target if neither too close nor too far
2773            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
2774                &*read_data.terrain,
2775                self.pos.0,
2776                self.vel.0,
2777                tgt_data.pos.0,
2778                TraversalConfig {
2779                    min_tgt_dist: 1.25,
2780                    ..self.traversal_config
2781                },
2782                &read_data.time,
2783            ) {
2784                self.unstuck_if(stuck, controller);
2785                if entities_have_line_of_sight(
2786                    self.pos,
2787                    self.body,
2788                    self.scale,
2789                    tgt_data.pos,
2790                    tgt_data.body,
2791                    tgt_data.scale,
2792                    read_data,
2793                ) && attack_data.angle < 45.0
2794                {
2795                    controller.inputs.move_dir = bearing
2796                        .xy()
2797                        .rotated_z(rng.random_range(-1.57..-0.5))
2798                        .try_normalized()
2799                        .unwrap_or_else(Vec2::zero)
2800                        * speed;
2801                } else {
2802                    // Unless cannot see target, then move towards them
2803                    controller.inputs.move_dir =
2804                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2805                    self.jump_if(bearing.z > 1.5, controller);
2806                    controller.inputs.move_z = bearing.z;
2807                }
2808            }
2809            // Sometimes try to roll
2810            if self.body.is_some_and(|b| b.is_humanoid())
2811                && attack_data.dist_sqrd < 16.0f32.powi(2)
2812                && !matches!(self.char_state, CharacterState::Shockwave(_))
2813                && rng.random::<f32>() < 0.02
2814            {
2815                controller.push_basic_input(InputKind::Roll);
2816            }
2817        } else {
2818            // If too far, move towards target
2819            self.path_toward_target(
2820                agent,
2821                controller,
2822                tgt_data.pos.0,
2823                read_data,
2824                Path::AtTarget,
2825                None,
2826            );
2827        }
2828    }
2829
2830    pub fn handle_sceptre_attack(
2831        &self,
2832        agent: &mut Agent,
2833        controller: &mut Controller,
2834        attack_data: &AttackData,
2835        tgt_data: &TargetData,
2836        read_data: &ReadData,
2837        rng: &mut impl Rng,
2838    ) {
2839        const DESIRED_ENERGY_LEVEL: f32 = 50.0;
2840        const DESIRED_COMBO_LEVEL: u32 = 8;
2841
2842        let line_of_sight_with_target = || {
2843            entities_have_line_of_sight(
2844                self.pos,
2845                self.body,
2846                self.scale,
2847                tgt_data.pos,
2848                tgt_data.body,
2849                tgt_data.scale,
2850                read_data,
2851            )
2852        };
2853
2854        // Logic to use abilities
2855        if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
2856            && line_of_sight_with_target()
2857        {
2858            // If far enough away, and can see target, check which skill is appropriate to
2859            // use
2860            if self.energy.current() > DESIRED_ENERGY_LEVEL
2861                && read_data
2862                    .combos
2863                    .get(*self.entity)
2864                    .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
2865                && !read_data.buffs.get(*self.entity).iter().any(|buff| {
2866                    buff.iter_kind(BuffKind::Regeneration)
2867                        .peekable()
2868                        .peek()
2869                        .is_some()
2870                })
2871            {
2872                // If have enough energy and combo to use healing aura, do so
2873                controller.push_basic_input(InputKind::Secondary);
2874            } else if self
2875                .skill_set
2876                .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
2877                && self.energy.current() > DESIRED_ENERGY_LEVEL
2878                && !read_data.buffs.get(*self.entity).iter().any(|buff| {
2879                    buff.iter_kind(BuffKind::ProtectingWard)
2880                        .peekable()
2881                        .peek()
2882                        .is_some()
2883                })
2884            {
2885                // Use ward if target is far enough away, self is not buffed, and have
2886                // sufficient energy
2887                controller.push_basic_input(InputKind::Ability(0));
2888            } else {
2889                // If low on energy, use primary to attempt to regen energy
2890                // Or if at desired energy level but not able/willing to ward, just attack
2891                controller.push_basic_input(InputKind::Primary);
2892            }
2893        } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2894            if self.body.is_some_and(|b| b.is_humanoid())
2895                && self.energy.current()
2896                    > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2897                && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
2898            {
2899                // Else roll away if can roll and have enough energy, and not using aura or in
2900                // recover
2901                controller.push_basic_input(InputKind::Roll);
2902            } else if attack_data.angle < 15.0 {
2903                controller.push_basic_input(InputKind::Primary);
2904            }
2905        }
2906        // Logic to move. Intentionally kept separate from ability logic where possible
2907        // so duplicated work is less necessary.
2908        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2909            // Attempt to move away from target if too close
2910            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
2911                &*read_data.terrain,
2912                self.pos.0,
2913                self.vel.0,
2914                tgt_data.pos.0,
2915                TraversalConfig {
2916                    min_tgt_dist: 1.25,
2917                    ..self.traversal_config
2918                },
2919                &read_data.time,
2920            ) {
2921                self.unstuck_if(stuck, controller);
2922                controller.inputs.move_dir =
2923                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2924            }
2925        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2926            // Else attempt to circle target if neither too close nor too far
2927            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
2928                &*read_data.terrain,
2929                self.pos.0,
2930                self.vel.0,
2931                tgt_data.pos.0,
2932                TraversalConfig {
2933                    min_tgt_dist: 1.25,
2934                    ..self.traversal_config
2935                },
2936                &read_data.time,
2937            ) {
2938                self.unstuck_if(stuck, controller);
2939                if line_of_sight_with_target() && attack_data.angle < 45.0 {
2940                    controller.inputs.move_dir = bearing
2941                        .xy()
2942                        .rotated_z(rng.random_range(0.5..1.57))
2943                        .try_normalized()
2944                        .unwrap_or_else(Vec2::zero)
2945                        * speed;
2946                } else {
2947                    // Unless cannot see target, then move towards them
2948                    controller.inputs.move_dir =
2949                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2950                    self.jump_if(bearing.z > 1.5, controller);
2951                    controller.inputs.move_z = bearing.z;
2952                }
2953            }
2954            // Sometimes try to roll
2955            if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
2956                && !matches!(self.char_state, CharacterState::BasicAura(_))
2957                && attack_data.dist_sqrd < 16.0f32.powi(2)
2958                && rng.random::<f32>() < 0.01
2959            {
2960                controller.push_basic_input(InputKind::Roll);
2961            }
2962        } else {
2963            // If too far, move towards target
2964            self.path_toward_target(
2965                agent,
2966                controller,
2967                tgt_data.pos.0,
2968                read_data,
2969                Path::AtTarget,
2970                None,
2971            );
2972        }
2973    }
2974
2975    pub fn handle_stone_golem_attack(
2976        &self,
2977        agent: &mut Agent,
2978        controller: &mut Controller,
2979        attack_data: &AttackData,
2980        tgt_data: &TargetData,
2981        read_data: &ReadData,
2982    ) {
2983        enum ActionStateTimers {
2984            TimerHandleStoneGolemAttack = 0, //Timer 0
2985        }
2986
2987        if attack_data.in_min_range() && attack_data.angle < 90.0 {
2988            controller.inputs.move_dir = Vec2::zero();
2989            controller.push_basic_input(InputKind::Primary);
2990            //controller.inputs.primary.set_state(true);
2991        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2992            if self.vel.0.is_approx_zero() {
2993                controller.push_basic_input(InputKind::Ability(0));
2994            }
2995            if self
2996                .path_toward_target(
2997                    agent,
2998                    controller,
2999                    tgt_data.pos.0,
3000                    read_data,
3001                    Path::Separate,
3002                    None,
3003                )
3004                .is_some()
3005                && entities_have_line_of_sight(
3006                    self.pos,
3007                    self.body,
3008                    self.scale,
3009                    tgt_data.pos,
3010                    tgt_data.body,
3011                    tgt_data.scale,
3012                    read_data,
3013                )
3014                && attack_data.angle < 90.0
3015            {
3016                if agent.combat_state.timers
3017                    [ActionStateTimers::TimerHandleStoneGolemAttack as usize]
3018                    > 5.0
3019                {
3020                    controller.push_basic_input(InputKind::Secondary);
3021                    agent.combat_state.timers
3022                        [ActionStateTimers::TimerHandleStoneGolemAttack as usize] = 0.0;
3023                } else {
3024                    agent.combat_state.timers
3025                        [ActionStateTimers::TimerHandleStoneGolemAttack as usize] += read_data.dt.0;
3026                }
3027            }
3028        } else {
3029            self.path_toward_target(
3030                agent,
3031                controller,
3032                tgt_data.pos.0,
3033                read_data,
3034                Path::AtTarget,
3035                None,
3036            );
3037        }
3038    }
3039
3040    pub fn handle_iron_golem_attack(
3041        &self,
3042        agent: &mut Agent,
3043        controller: &mut Controller,
3044        attack_data: &AttackData,
3045        tgt_data: &TargetData,
3046        read_data: &ReadData,
3047    ) {
3048        enum ActionStateTimers {
3049            AttackTimer = 0,
3050        }
3051
3052        let home = agent.patrol_origin.unwrap_or(self.pos.0);
3053
3054        let attack_select =
3055            if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 {
3056                0
3057            } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.5 {
3058                1
3059            } else {
3060                2
3061            };
3062        // stay centered
3063        if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
3064            self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
3065        // shoot at targets above
3066        } else if tgt_data.pos.0.z > home.z + 5.0 {
3067            controller.push_basic_input(InputKind::Ability(0))
3068        } else if attack_data.in_min_range() {
3069            controller.inputs.move_dir = Vec2::zero();
3070            controller.push_basic_input(InputKind::Primary);
3071        } else {
3072            match attack_select {
3073                0 => {
3074                    // firebolt
3075                    controller.push_basic_input(InputKind::Ability(0))
3076                },
3077                1 => {
3078                    // spin
3079                    controller.push_basic_input(InputKind::Ability(1))
3080                },
3081                _ => {
3082                    // shockwave
3083                    controller.push_basic_input(InputKind::Secondary)
3084                },
3085            };
3086        };
3087        agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
3088        if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 7.5 {
3089            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
3090        };
3091    }
3092
3093    pub fn handle_circle_charge_attack(
3094        &self,
3095        agent: &mut Agent,
3096        controller: &mut Controller,
3097        attack_data: &AttackData,
3098        tgt_data: &TargetData,
3099        read_data: &ReadData,
3100        radius: u32,
3101        circle_time: u32,
3102        rng: &mut impl Rng,
3103    ) {
3104        enum ActionStateCountersF {
3105            CounterFHandleCircleChargeAttack = 0,
3106        }
3107
3108        enum ActionStateCountersI {
3109            CounterIHandleCircleChargeAttack = 0,
3110        }
3111
3112        if agent.combat_state.counters
3113            [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
3114            >= circle_time as f32
3115        {
3116            // if circle charge is in progress and time hasn't expired, continue charging
3117            controller.push_basic_input(InputKind::Secondary);
3118        }
3119        if attack_data.in_min_range() {
3120            if agent.combat_state.counters
3121                [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
3122                > 0.0
3123            {
3124                // set timer and rotation counter to zero if in minimum range
3125                agent.combat_state.counters
3126                    [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] = 0.0;
3127                agent.combat_state.int_counters
3128                    [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] = 0;
3129            } else {
3130                // melee attack
3131                controller.push_basic_input(InputKind::Primary);
3132                controller.inputs.move_dir = Vec2::zero();
3133            }
3134        } else if attack_data.dist_sqrd < (radius as f32 + attack_data.min_attack_dist).powi(2) {
3135            // if in range to charge, circle, then charge
3136            if agent.combat_state.int_counters
3137                [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize]
3138                == 0
3139            {
3140                // if you haven't chosen a direction to go in, choose now
3141                agent.combat_state.int_counters
3142                    [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] =
3143                    1 + rng.random_bool(0.5) as u8;
3144            }
3145            if agent.combat_state.counters
3146                [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
3147                < circle_time as f32
3148            {
3149                // circle if circle timer not ready
3150                let move_dir = match agent.combat_state.int_counters
3151                    [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize]
3152                {
3153                    1 =>
3154                    // circle left if counter is 1
3155                    {
3156                        (tgt_data.pos.0 - self.pos.0)
3157                            .xy()
3158                            .rotated_z(0.47 * PI)
3159                            .try_normalized()
3160                            .unwrap_or_else(Vec2::unit_y)
3161                    },
3162                    2 =>
3163                    // circle right if counter is 2
3164                    {
3165                        (tgt_data.pos.0 - self.pos.0)
3166                            .xy()
3167                            .rotated_z(-0.47 * PI)
3168                            .try_normalized()
3169                            .unwrap_or_else(Vec2::unit_y)
3170                    },
3171                    _ =>
3172                    // if some illegal value slipped in, get zero vector
3173                    {
3174                        Vec2::zero()
3175                    },
3176                };
3177                let obstacle = read_data
3178                    .terrain
3179                    .ray(
3180                        self.pos.0 + Vec3::unit_z(),
3181                        self.pos.0 + move_dir.with_z(0.0) * 2.0 + Vec3::unit_z(),
3182                    )
3183                    .until(Block::is_solid)
3184                    .cast()
3185                    .1
3186                    .map_or(true, |b| b.is_some());
3187                if obstacle {
3188                    // if obstacle detected, stop circling
3189                    agent.combat_state.counters
3190                        [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] =
3191                        circle_time as f32;
3192                }
3193                controller.inputs.move_dir = move_dir;
3194                // use counter as timer since timer may be modified in other parts of the code
3195                agent.combat_state.counters
3196                    [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] +=
3197                    read_data.dt.0;
3198            }
3199            // activating charge once circle timer expires is handled above
3200        } else {
3201            let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3202                // if too far away from target, move towards them
3203                Path::Separate
3204            } else {
3205                Path::AtTarget
3206            };
3207            self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
3208        }
3209    }
3210
3211    pub fn handle_quadlow_ranged_attack(
3212        &self,
3213        agent: &mut Agent,
3214        controller: &mut Controller,
3215        attack_data: &AttackData,
3216        tgt_data: &TargetData,
3217        read_data: &ReadData,
3218    ) {
3219        enum ActionStateTimers {
3220            TimerHandleQuadLowRanged = 0,
3221        }
3222
3223        if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2)
3224            && attack_data.angle < 90.0
3225        {
3226            controller.inputs.move_dir = if !attack_data.in_min_range() {
3227                (tgt_data.pos.0 - self.pos.0)
3228                    .xy()
3229                    .try_normalized()
3230                    .unwrap_or_else(Vec2::unit_y)
3231            } else {
3232                Vec2::zero()
3233            };
3234
3235            controller.push_basic_input(InputKind::Primary);
3236        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3237            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
3238                &*read_data.terrain,
3239                self.pos.0,
3240                self.vel.0,
3241                tgt_data.pos.0,
3242                TraversalConfig {
3243                    min_tgt_dist: 1.25,
3244                    ..self.traversal_config
3245                },
3246                &read_data.time,
3247            ) {
3248                self.unstuck_if(stuck, controller);
3249                if attack_data.angle < 15.0
3250                    && entities_have_line_of_sight(
3251                        self.pos,
3252                        self.body,
3253                        self.scale,
3254                        tgt_data.pos,
3255                        tgt_data.body,
3256                        tgt_data.scale,
3257                        read_data,
3258                    )
3259                {
3260                    if agent.combat_state.timers
3261                        [ActionStateTimers::TimerHandleQuadLowRanged as usize]
3262                        > 5.0
3263                    {
3264                        agent.combat_state.timers
3265                            [ActionStateTimers::TimerHandleQuadLowRanged as usize] = 0.0;
3266                    } else if agent.combat_state.timers
3267                        [ActionStateTimers::TimerHandleQuadLowRanged as usize]
3268                        > 2.5
3269                    {
3270                        controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3271                            .xy()
3272                            .rotated_z(1.75 * PI)
3273                            .try_normalized()
3274                            .unwrap_or_else(Vec2::zero)
3275                            * speed;
3276                        agent.combat_state.timers
3277                            [ActionStateTimers::TimerHandleQuadLowRanged as usize] +=
3278                            read_data.dt.0;
3279                    } else {
3280                        controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3281                            .xy()
3282                            .rotated_z(0.25 * PI)
3283                            .try_normalized()
3284                            .unwrap_or_else(Vec2::zero)
3285                            * speed;
3286                        agent.combat_state.timers
3287                            [ActionStateTimers::TimerHandleQuadLowRanged as usize] +=
3288                            read_data.dt.0;
3289                    }
3290                    controller.push_basic_input(InputKind::Secondary);
3291                    self.jump_if(bearing.z > 1.5, controller);
3292                    controller.inputs.move_z = bearing.z;
3293                } else {
3294                    controller.inputs.move_dir =
3295                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
3296                    self.jump_if(bearing.z > 1.5, controller);
3297                    controller.inputs.move_z = bearing.z;
3298                }
3299            } else {
3300                agent.target = None;
3301            }
3302        } else {
3303            self.path_toward_target(
3304                agent,
3305                controller,
3306                tgt_data.pos.0,
3307                read_data,
3308                Path::AtTarget,
3309                None,
3310            );
3311        }
3312    }
3313
3314    pub fn handle_tail_slap_attack(
3315        &self,
3316        agent: &mut Agent,
3317        controller: &mut Controller,
3318        attack_data: &AttackData,
3319        tgt_data: &TargetData,
3320        read_data: &ReadData,
3321    ) {
3322        enum ActionStateTimers {
3323            TimerTailSlap = 0,
3324        }
3325
3326        if attack_data.angle < 90.0
3327            && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
3328        {
3329            if agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] > 4.0 {
3330                controller.push_cancel_input(InputKind::Primary);
3331                agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] = 0.0;
3332            } else if agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] > 1.0 {
3333                controller.push_basic_input(InputKind::Primary);
3334                agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] +=
3335                    read_data.dt.0;
3336            } else {
3337                controller.push_basic_input(InputKind::Secondary);
3338                agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] +=
3339                    read_data.dt.0;
3340            }
3341            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3342                .xy()
3343                .try_normalized()
3344                .unwrap_or_else(Vec2::unit_y)
3345                * 0.1;
3346        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3347            self.path_toward_target(
3348                agent,
3349                controller,
3350                tgt_data.pos.0,
3351                read_data,
3352                Path::Separate,
3353                None,
3354            );
3355        } else {
3356            self.path_toward_target(
3357                agent,
3358                controller,
3359                tgt_data.pos.0,
3360                read_data,
3361                Path::AtTarget,
3362                None,
3363            );
3364        }
3365    }
3366
3367    pub fn handle_quadlow_quick_attack(
3368        &self,
3369        agent: &mut Agent,
3370        controller: &mut Controller,
3371        attack_data: &AttackData,
3372        tgt_data: &TargetData,
3373        read_data: &ReadData,
3374    ) {
3375        if attack_data.angle < 90.0
3376            && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
3377        {
3378            controller.inputs.move_dir = Vec2::zero();
3379            controller.push_basic_input(InputKind::Secondary);
3380        } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2)
3381            && attack_data.dist_sqrd > (2.0 * attack_data.min_attack_dist).powi(2)
3382            && attack_data.angle < 90.0
3383        {
3384            controller.push_basic_input(InputKind::Primary);
3385            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3386                .xy()
3387                .rotated_z(-0.47 * PI)
3388                .try_normalized()
3389                .unwrap_or_else(Vec2::unit_y);
3390        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3391            self.path_toward_target(
3392                agent,
3393                controller,
3394                tgt_data.pos.0,
3395                read_data,
3396                Path::Separate,
3397                None,
3398            );
3399        } else {
3400            self.path_toward_target(
3401                agent,
3402                controller,
3403                tgt_data.pos.0,
3404                read_data,
3405                Path::AtTarget,
3406                None,
3407            );
3408        }
3409    }
3410
3411    pub fn handle_quadlow_basic_attack(
3412        &self,
3413        agent: &mut Agent,
3414        controller: &mut Controller,
3415        attack_data: &AttackData,
3416        tgt_data: &TargetData,
3417        read_data: &ReadData,
3418    ) {
3419        enum ActionStateTimers {
3420            TimerQuadLowBasic = 0,
3421        }
3422
3423        if attack_data.angle < 70.0
3424            && attack_data.dist_sqrd < (1.3 * attack_data.min_attack_dist).powi(2)
3425        {
3426            controller.inputs.move_dir = Vec2::zero();
3427            if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] > 5.0 {
3428                agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] = 0.0;
3429            } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] > 2.0
3430            {
3431                controller.push_basic_input(InputKind::Secondary);
3432                agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] +=
3433                    read_data.dt.0;
3434            } else {
3435                controller.push_basic_input(InputKind::Primary);
3436                agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] +=
3437                    read_data.dt.0;
3438            }
3439        } else {
3440            let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3441                Path::Separate
3442            } else {
3443                Path::AtTarget
3444            };
3445            self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
3446        }
3447    }
3448
3449    pub fn handle_quadmed_jump_attack(
3450        &self,
3451        agent: &mut Agent,
3452        controller: &mut Controller,
3453        attack_data: &AttackData,
3454        tgt_data: &TargetData,
3455        read_data: &ReadData,
3456    ) {
3457        if attack_data.angle < 90.0
3458            && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
3459        {
3460            controller.inputs.move_dir = Vec2::zero();
3461            controller.push_basic_input(InputKind::Secondary);
3462        } else if attack_data.angle < 15.0
3463            && attack_data.dist_sqrd < (5.0 * attack_data.min_attack_dist).powi(2)
3464        {
3465            controller.push_basic_input(InputKind::Ability(0));
3466        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3467            if self
3468                .path_toward_target(
3469                    agent,
3470                    controller,
3471                    tgt_data.pos.0,
3472                    read_data,
3473                    Path::Separate,
3474                    None,
3475                )
3476                .is_some()
3477                && attack_data.angle < 15.0
3478                && entities_have_line_of_sight(
3479                    self.pos,
3480                    self.body,
3481                    self.scale,
3482                    tgt_data.pos,
3483                    tgt_data.body,
3484                    tgt_data.scale,
3485                    read_data,
3486                )
3487            {
3488                controller.push_basic_input(InputKind::Primary);
3489            }
3490        } else {
3491            self.path_toward_target(
3492                agent,
3493                controller,
3494                tgt_data.pos.0,
3495                read_data,
3496                Path::AtTarget,
3497                None,
3498            );
3499        }
3500    }
3501
3502    pub fn handle_quadmed_basic_attack(
3503        &self,
3504        agent: &mut Agent,
3505        controller: &mut Controller,
3506        attack_data: &AttackData,
3507        tgt_data: &TargetData,
3508        read_data: &ReadData,
3509    ) {
3510        enum ActionStateTimers {
3511            TimerQuadMedBasic = 0,
3512        }
3513
3514        if attack_data.angle < 90.0 && attack_data.in_min_range() {
3515            controller.inputs.move_dir = Vec2::zero();
3516            if agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] < 2.0 {
3517                controller.push_basic_input(InputKind::Secondary);
3518                agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] +=
3519                    read_data.dt.0;
3520            } else if agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] < 3.0
3521            {
3522                controller.push_basic_input(InputKind::Primary);
3523                agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] +=
3524                    read_data.dt.0;
3525            } else {
3526                agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] = 0.0;
3527            }
3528        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3529            self.path_toward_target(
3530                agent,
3531                controller,
3532                tgt_data.pos.0,
3533                read_data,
3534                Path::Separate,
3535                None,
3536            );
3537        } else {
3538            self.path_toward_target(
3539                agent,
3540                controller,
3541                tgt_data.pos.0,
3542                read_data,
3543                Path::AtTarget,
3544                None,
3545            );
3546        }
3547    }
3548
3549    pub fn handle_quadmed_hoof_attack(
3550        &self,
3551        agent: &mut Agent,
3552        controller: &mut Controller,
3553        attack_data: &AttackData,
3554        tgt_data: &TargetData,
3555        read_data: &ReadData,
3556    ) {
3557        const HOOF_ATTACK_RANGE: f32 = 1.0;
3558        const HOOF_ATTACK_ANGLE: f32 = 50.0;
3559
3560        if attack_data.angle < HOOF_ATTACK_ANGLE
3561            && attack_data.dist_sqrd
3562                < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.front_radius())).powi(2)
3563        {
3564            controller.inputs.move_dir = Vec2::zero();
3565            controller.push_basic_input(InputKind::Primary);
3566        } else {
3567            self.path_toward_target(
3568                agent,
3569                controller,
3570                tgt_data.pos.0,
3571                read_data,
3572                Path::AtTarget,
3573                None,
3574            );
3575        }
3576    }
3577
3578    pub fn handle_quadlow_beam_attack(
3579        &self,
3580        agent: &mut Agent,
3581        controller: &mut Controller,
3582        attack_data: &AttackData,
3583        tgt_data: &TargetData,
3584        read_data: &ReadData,
3585    ) {
3586        enum ActionStateTimers {
3587            TimerQuadLowBeam = 0,
3588        }
3589        if attack_data.angle < 90.0
3590            && attack_data.dist_sqrd < (2.5 * attack_data.min_attack_dist).powi(2)
3591        {
3592            controller.inputs.move_dir = Vec2::zero();
3593            controller.push_basic_input(InputKind::Secondary);
3594        } else if attack_data.dist_sqrd < (7.0 * attack_data.min_attack_dist).powi(2)
3595            && attack_data.angle < 15.0
3596        {
3597            if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 2.0 {
3598                controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3599                    .xy()
3600                    .rotated_z(0.47 * PI)
3601                    .try_normalized()
3602                    .unwrap_or_else(Vec2::unit_y);
3603                controller.push_basic_input(InputKind::Primary);
3604                agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3605                    read_data.dt.0;
3606            } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 4.0
3607                && attack_data.angle < 15.0
3608            {
3609                controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3610                    .xy()
3611                    .rotated_z(-0.47 * PI)
3612                    .try_normalized()
3613                    .unwrap_or_else(Vec2::unit_y);
3614                controller.push_basic_input(InputKind::Primary);
3615                agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3616                    read_data.dt.0;
3617            } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 6.0
3618                && attack_data.angle < 15.0
3619            {
3620                controller.push_basic_input(InputKind::Ability(0));
3621                agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3622                    read_data.dt.0;
3623            } else {
3624                agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] = 0.0;
3625            }
3626        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3627            self.path_toward_target(
3628                agent,
3629                controller,
3630                tgt_data.pos.0,
3631                read_data,
3632                Path::Separate,
3633                None,
3634            );
3635        } else {
3636            self.path_toward_target(
3637                agent,
3638                controller,
3639                tgt_data.pos.0,
3640                read_data,
3641                Path::AtTarget,
3642                None,
3643            );
3644        }
3645    }
3646
3647    pub fn handle_organ_aura_attack(
3648        &self,
3649        agent: &mut Agent,
3650        controller: &mut Controller,
3651        attack_data: &AttackData,
3652        _tgt_data: &TargetData,
3653        read_data: &ReadData,
3654    ) {
3655        enum ActionStateTimers {
3656            TimerOrganAura = 0,
3657        }
3658
3659        const ORGAN_AURA_DURATION: f32 = 34.75;
3660        if attack_data.dist_sqrd < (7.0 * attack_data.min_attack_dist).powi(2) {
3661            if agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize]
3662                > ORGAN_AURA_DURATION
3663            {
3664                agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] = 0.0;
3665            } else if agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] < 1.0 {
3666                controller.push_basic_input(InputKind::Primary);
3667                agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] +=
3668                    read_data.dt.0;
3669            } else {
3670                agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] +=
3671                    read_data.dt.0;
3672            }
3673        } else {
3674            agent.target = None;
3675        }
3676    }
3677
3678    pub fn handle_theropod_attack(
3679        &self,
3680        agent: &mut Agent,
3681        controller: &mut Controller,
3682        attack_data: &AttackData,
3683        tgt_data: &TargetData,
3684        read_data: &ReadData,
3685    ) {
3686        if attack_data.angle < 90.0 && attack_data.in_min_range() {
3687            controller.inputs.move_dir = Vec2::zero();
3688            controller.push_basic_input(InputKind::Primary);
3689        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3690            self.path_toward_target(
3691                agent,
3692                controller,
3693                tgt_data.pos.0,
3694                read_data,
3695                Path::Separate,
3696                None,
3697            );
3698        } else {
3699            self.path_toward_target(
3700                agent,
3701                controller,
3702                tgt_data.pos.0,
3703                read_data,
3704                Path::AtTarget,
3705                None,
3706            );
3707        }
3708    }
3709
3710    pub fn handle_turret_attack(
3711        &self,
3712        agent: &mut Agent,
3713        controller: &mut Controller,
3714        attack_data: &AttackData,
3715        tgt_data: &TargetData,
3716        read_data: &ReadData,
3717    ) {
3718        if entities_have_line_of_sight(
3719            self.pos,
3720            self.body,
3721            self.scale,
3722            tgt_data.pos,
3723            tgt_data.body,
3724            tgt_data.scale,
3725            read_data,
3726        ) && attack_data.angle < 15.0
3727        {
3728            controller.push_basic_input(InputKind::Primary);
3729        } else {
3730            agent.target = None;
3731        }
3732    }
3733
3734    pub fn handle_fixed_turret_attack(
3735        &self,
3736        agent: &mut Agent,
3737        controller: &mut Controller,
3738        attack_data: &AttackData,
3739        tgt_data: &TargetData,
3740        read_data: &ReadData,
3741    ) {
3742        controller.inputs.look_dir = self.ori.look_dir();
3743        if entities_have_line_of_sight(
3744            self.pos,
3745            self.body,
3746            self.scale,
3747            tgt_data.pos,
3748            tgt_data.body,
3749            tgt_data.scale,
3750            read_data,
3751        ) && attack_data.angle < 15.0
3752        {
3753            controller.push_basic_input(InputKind::Primary);
3754        } else {
3755            agent.target = None;
3756        }
3757    }
3758
3759    pub fn handle_rotating_turret_attack(
3760        &self,
3761        agent: &mut Agent,
3762        controller: &mut Controller,
3763        tgt_data: &TargetData,
3764        read_data: &ReadData,
3765    ) {
3766        controller.inputs.look_dir = Dir::new(
3767            Quaternion::from_xyzw(self.ori.look_dir().x, self.ori.look_dir().y, 0.0, 0.0)
3768                .rotated_z(6.0 * read_data.dt.0)
3769                .into_vec3()
3770                .try_normalized()
3771                .unwrap_or_default(),
3772        );
3773        if entities_have_line_of_sight(
3774            self.pos,
3775            self.body,
3776            self.scale,
3777            tgt_data.pos,
3778            tgt_data.body,
3779            tgt_data.scale,
3780            read_data,
3781        ) {
3782            controller.push_basic_input(InputKind::Primary);
3783        } else {
3784            agent.target = None;
3785        }
3786    }
3787
3788    pub fn handle_radial_turret_attack(&self, controller: &mut Controller) {
3789        controller.push_basic_input(InputKind::Primary);
3790    }
3791
3792    pub fn handle_fiery_tornado_attack(&self, agent: &mut Agent, controller: &mut Controller) {
3793        enum Conditions {
3794            AuraEmited = 0,
3795        }
3796        if matches!(self.char_state, CharacterState::BasicAura(c) if matches!(c.stage_section, StageSection::Recover))
3797        {
3798            agent.combat_state.conditions[Conditions::AuraEmited as usize] = true;
3799        }
3800        // 1 time use of aura
3801        if !agent.combat_state.conditions[Conditions::AuraEmited as usize] {
3802            controller.push_basic_input(InputKind::Secondary);
3803        } else {
3804            // Spin
3805            controller.push_basic_input(InputKind::Primary);
3806        }
3807    }
3808
3809    pub fn handle_mindflayer_attack(
3810        &self,
3811        agent: &mut Agent,
3812        controller: &mut Controller,
3813        _attack_data: &AttackData,
3814        tgt_data: &TargetData,
3815        read_data: &ReadData,
3816        _rng: &mut impl Rng,
3817    ) {
3818        enum FCounters {
3819            SummonThreshold = 0,
3820        }
3821        enum Timers {
3822            PositionTimer,
3823            AttackTimer1,
3824            AttackTimer2,
3825        }
3826        enum Conditions {
3827            AttackToggle1,
3828        }
3829        const SUMMON_THRESHOLD: f32 = 0.20;
3830        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
3831        agent.combat_state.timers[Timers::PositionTimer as usize] += read_data.dt.0;
3832        agent.combat_state.timers[Timers::AttackTimer1 as usize] += read_data.dt.0;
3833        agent.combat_state.timers[Timers::AttackTimer2 as usize] += read_data.dt.0;
3834        if agent.combat_state.timers[Timers::AttackTimer1 as usize] > 10.0 {
3835            agent.combat_state.timers[Timers::AttackTimer1 as usize] = 0.0
3836        }
3837        agent.combat_state.conditions[Conditions::AttackToggle1 as usize] =
3838            agent.combat_state.timers[Timers::AttackTimer1 as usize] < 5.0;
3839        if matches!(self.char_state, CharacterState::Blink(c) if matches!(c.stage_section, StageSection::Recover))
3840        {
3841            agent.combat_state.timers[Timers::AttackTimer2 as usize] = 0.0
3842        }
3843
3844        let position_timer = agent.combat_state.timers[Timers::PositionTimer as usize];
3845        if position_timer > 60.0 {
3846            agent.combat_state.timers[Timers::PositionTimer as usize] = 0.0;
3847        }
3848        let home = agent.patrol_origin.unwrap_or(self.pos.0);
3849        let p = match position_timer as i32 {
3850            0_i32..=6_i32 => 0,
3851            7_i32..=13_i32 => 2,
3852            14_i32..=20_i32 => 3,
3853            21_i32..=27_i32 => 1,
3854            28_i32..=34_i32 => 4,
3855            35_i32..=47_i32 => 5,
3856            _ => 6,
3857        };
3858        let pos = if p > 5 {
3859            tgt_data.pos.0
3860        } else if p > 3 {
3861            home
3862        } else {
3863            Vec3::new(
3864                home.x + (CARDINALS[p].x * 15) as f32,
3865                home.y + (CARDINALS[p].y * 15) as f32,
3866                home.z,
3867            )
3868        };
3869        if !agent.combat_state.initialized {
3870            // Sets counter at start of combat, using `condition` to keep track of whether
3871            // it was already initialized
3872            agent.combat_state.counters[FCounters::SummonThreshold as usize] =
3873                1.0 - SUMMON_THRESHOLD;
3874            agent.combat_state.initialized = true;
3875        }
3876
3877        if position_timer > 55.0
3878            && health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
3879        {
3880            // Summon Husks at particular thresholds of health
3881            controller.push_basic_input(InputKind::Ability(2));
3882
3883            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
3884            {
3885                agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
3886                    SUMMON_THRESHOLD;
3887            }
3888        } else if p > 5 {
3889            if pos.distance_squared(self.pos.0) > 20.0_f32.powi(2) {
3890                // teleport chase attack mode
3891                controller.push_action(ControlAction::StartInput {
3892                    input: InputKind::Ability(0),
3893                    target_entity: None,
3894                    select_pos: Some(pos),
3895                });
3896            } else {
3897                controller.push_basic_input(InputKind::Ability(4))
3898            }
3899        } else if p > 4 {
3900            // chase attack mode
3901            self.path_toward_target(
3902                agent,
3903                controller,
3904                tgt_data.pos.0,
3905                read_data,
3906                Path::AtTarget,
3907                None,
3908            );
3909
3910            if agent.combat_state.conditions[Conditions::AttackToggle1 as usize] {
3911                controller.push_basic_input(InputKind::Primary);
3912            } else {
3913                controller.push_basic_input(InputKind::Ability(1))
3914            }
3915        } else {
3916            // positioned attack mode
3917            if pos.distance_squared(self.pos.0) > 5.0_f32.powi(2) {
3918                controller.push_action(ControlAction::StartInput {
3919                    input: InputKind::Ability(0),
3920                    target_entity: None,
3921                    select_pos: Some(pos),
3922                });
3923            } else if agent.combat_state.timers[Timers::AttackTimer2 as usize] < 4.0 {
3924                controller.push_basic_input(InputKind::Secondary);
3925            } else {
3926                controller.push_basic_input(InputKind::Ability(3))
3927            }
3928        }
3929    }
3930
3931    pub fn handle_forgemaster_attack(
3932        &self,
3933        agent: &mut Agent,
3934        controller: &mut Controller,
3935        attack_data: &AttackData,
3936        tgt_data: &TargetData,
3937        read_data: &ReadData,
3938    ) {
3939        const MELEE_RANGE: f32 = 6.0;
3940        const MID_RANGE: f32 = 25.0;
3941        const SUMMON_THRESHOLD: f32 = 0.2;
3942
3943        enum FCounters {
3944            SummonThreshold = 0,
3945        }
3946        enum Timers {
3947            AttackRand = 0,
3948        }
3949        if agent.combat_state.timers[Timers::AttackRand as usize] > 10.0 {
3950            agent.combat_state.timers[Timers::AttackRand as usize] = 0.0;
3951        }
3952
3953        let line_of_sight_with_target = || {
3954            entities_have_line_of_sight(
3955                self.pos,
3956                self.body,
3957                self.scale,
3958                tgt_data.pos,
3959                tgt_data.body,
3960                tgt_data.scale,
3961                read_data,
3962            )
3963        };
3964        let home = agent.patrol_origin.unwrap_or(self.pos.0);
3965        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
3966        // Teleport back to home position if we're too far from our home position but in
3967        // range of the blink ability
3968        if (5f32.powi(2)..100f32.powi(2)).contains(&home.distance_squared(self.pos.0)) {
3969            controller.push_action(ControlAction::StartInput {
3970                input: InputKind::Ability(5),
3971                target_entity: None,
3972                select_pos: Some(home),
3973            });
3974        } else if !agent.combat_state.initialized {
3975            // Sets counter at start of combat, using `condition` to keep track of whether
3976            // it was already initialized
3977            agent.combat_state.counters[FCounters::SummonThreshold as usize] =
3978                1.0 - SUMMON_THRESHOLD;
3979            agent.combat_state.initialized = true;
3980        } else if health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
3981        {
3982            // Summon IronDwarfs at particular thresholds of health
3983            controller.push_basic_input(InputKind::Ability(0));
3984
3985            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
3986            {
3987                agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
3988                    SUMMON_THRESHOLD;
3989            }
3990        } else {
3991            // If target is in melee range use flamecrush and lawawave
3992            if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
3993                if agent.combat_state.timers[Timers::AttackRand as usize] < 3.5 {
3994                    // flamecrush
3995                    controller.push_basic_input(InputKind::Secondary);
3996                } else {
3997                    // lavawave
3998                    controller.push_basic_input(InputKind::Ability(3));
3999                }
4000                // If target is in mid range use lavawave, flamethrower and
4001                // groundislava1
4002            } else if attack_data.dist_sqrd < MID_RANGE.powi(2) && line_of_sight_with_target() {
4003                if agent.combat_state.timers[Timers::AttackRand as usize] > 6.5 {
4004                    controller.push_basic_input(InputKind::Ability(1));
4005                } else if agent.combat_state.timers[Timers::AttackRand as usize] > 3.5 {
4006                    // lavawave
4007                    controller.push_basic_input(InputKind::Ability(3));
4008                } else if agent.combat_state.timers[Timers::AttackRand as usize] > 2.5 {
4009                    // lavamortar
4010                    controller.push_basic_input(InputKind::Primary);
4011                } else {
4012                    // flamethrower
4013                    controller.push_basic_input(InputKind::Ability(2));
4014                }
4015                // If target is beyond mid range use lavamortar and
4016                // groundislava2
4017            } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
4018                if agent.combat_state.timers[Timers::AttackRand as usize] > 6.5 {
4019                    controller.push_basic_input(InputKind::Ability(4));
4020                } else {
4021                    // lavamortar
4022                    controller.push_basic_input(InputKind::Primary);
4023                }
4024            }
4025            agent.combat_state.timers[Timers::AttackRand as usize] += read_data.dt.0;
4026        }
4027        self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
4028    }
4029
4030    pub fn handle_flamekeeper_attack(
4031        &self,
4032        agent: &mut Agent,
4033        controller: &mut Controller,
4034        attack_data: &AttackData,
4035        tgt_data: &TargetData,
4036        read_data: &ReadData,
4037    ) {
4038        const MELEE_RANGE: f32 = 6.0;
4039        const MID_RANGE: f32 = 25.0;
4040        const SUMMON_THRESHOLD: f32 = 0.2;
4041
4042        enum FCounters {
4043            SummonThreshold = 0,
4044        }
4045        enum Timers {
4046            AttackRand = 0,
4047        }
4048        if agent.combat_state.timers[Timers::AttackRand as usize] > 5.0 {
4049            agent.combat_state.timers[Timers::AttackRand as usize] = 0.0;
4050        }
4051
4052        let line_of_sight_with_target = || {
4053            entities_have_line_of_sight(
4054                self.pos,
4055                self.body,
4056                self.scale,
4057                tgt_data.pos,
4058                tgt_data.body,
4059                tgt_data.scale,
4060                read_data,
4061            )
4062        };
4063        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4064        // Sets counter at start of combat, using `condition` to keep track of whether
4065        // it was already initialized
4066        if !agent.combat_state.initialized {
4067            agent.combat_state.counters[FCounters::SummonThreshold as usize] =
4068                1.0 - SUMMON_THRESHOLD;
4069            agent.combat_state.initialized = true;
4070        } else if health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
4071        {
4072            // Summon Flamethrowers at particular thresholds of health
4073            controller.push_basic_input(InputKind::Ability(0));
4074            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
4075            {
4076                agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
4077                    SUMMON_THRESHOLD;
4078            }
4079        } else {
4080            // If target is in melee range use flamecrush
4081            if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
4082                if agent.combat_state.timers[Timers::AttackRand as usize] < 3.5 {
4083                    // flamecrush
4084                    controller.push_basic_input(InputKind::Secondary);
4085                } else {
4086                    // lavawave
4087                    controller.push_basic_input(InputKind::Ability(2));
4088                }
4089                // If target is in mid range use mines, lavawave, flamethrower
4090            } else if attack_data.dist_sqrd < MID_RANGE.powi(2) && line_of_sight_with_target() {
4091                if agent.combat_state.timers[Timers::AttackRand as usize] > 3.5 {
4092                    // lavawave
4093                    controller.push_basic_input(InputKind::Ability(2));
4094                } else if agent.combat_state.timers[Timers::AttackRand as usize] > 2.5 {
4095                    // mines
4096                    controller.push_basic_input(InputKind::Ability(3));
4097                } else {
4098                    // flamethrower
4099                    controller.push_basic_input(InputKind::Ability(1));
4100                }
4101                // If target is beyond mid range use lavamortar
4102            } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
4103                // lavamortar
4104                controller.push_basic_input(InputKind::Primary);
4105            }
4106            self.path_toward_target(
4107                agent,
4108                controller,
4109                tgt_data.pos.0,
4110                read_data,
4111                Path::AtTarget,
4112                None,
4113            );
4114            agent.combat_state.timers[Timers::AttackRand as usize] += read_data.dt.0;
4115        }
4116    }
4117
4118    pub fn handle_birdlarge_fire_attack(
4119        &self,
4120        agent: &mut Agent,
4121        controller: &mut Controller,
4122        attack_data: &AttackData,
4123        tgt_data: &TargetData,
4124        read_data: &ReadData,
4125        _rng: &mut impl Rng,
4126    ) {
4127        const PHOENIX_HEAL_THRESHOLD: f32 = 0.20;
4128
4129        enum Conditions {
4130            Healed = 0,
4131        }
4132        enum ActionStateTimers {
4133            AttackTimer1,
4134            AttackTimer2,
4135            WaterTimer,
4136        }
4137
4138        let attack_timer_1 =
4139            if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 2.0 {
4140                0
4141            } else if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 4.0 {
4142                1
4143            } else if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 6.0 {
4144                2
4145            } else {
4146                3
4147            };
4148        agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] += read_data.dt.0;
4149        if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] > 8.0 {
4150            // Reset timer
4151            agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] = 0.0;
4152        }
4153        let (attack_timer_2, speed) =
4154            if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 3.0 {
4155                // fly high
4156                (0, 2.0)
4157            } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 6.0 {
4158                // attack_mid_1
4159                (1, 2.0)
4160            } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 9.0 {
4161                // fly high
4162                (0, 3.0)
4163            } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 16.0 {
4164                // attack_mid_2
4165                (2, 1.0)
4166            } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 20.0 {
4167                // fly low
4168                (5, 20.0)
4169            } else {
4170                // attack_close
4171                (3, 1.0)
4172            };
4173        agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] += read_data.dt.0;
4174        if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] > 28.0 {
4175            // Reset timer
4176            agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] = 0.0;
4177        }
4178        // Fly to target
4179        let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
4180            .try_normalized()
4181            .unwrap_or_else(Vec3::zero);
4182        controller.inputs.move_dir = dir_to_target.xy() * speed;
4183
4184        // Always fly! If the floor can't touch you, it can't hurt you...
4185        controller.push_basic_input(InputKind::Fly);
4186        // Flee from the ground! The internet told me it was lava!
4187        // If on the ground, jump with every last ounce of energy, holding onto
4188        // all that is dear in life and straining for the wide open skies.
4189
4190        // Don't stay in water
4191        if matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { .. })) {
4192            agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] = 2.0;
4193        };
4194        if agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] > 0.0 {
4195            agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] -= read_data.dt.0;
4196            if agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] > 1.0 {
4197                controller.inputs.move_z = 1.0
4198            } else {
4199                // heat laser
4200                controller.push_basic_input(InputKind::Ability(3))
4201            }
4202        } else if self.physics_state.on_ground.is_some() {
4203            controller.push_basic_input(InputKind::Jump);
4204        } else {
4205            // Use a proportional controller with a coefficient of 1.0 to
4206            // maintain altidude at the the provided set point
4207            let mut maintain_altitude = |set_point| {
4208                let alt = read_data
4209                    .terrain
4210                    .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
4211                    .until(Block::is_solid)
4212                    .cast()
4213                    .0;
4214                let error = set_point - alt;
4215                controller.inputs.move_z = error;
4216            };
4217            // heal once - from_the_ashes
4218            let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4219            if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
4220            {
4221                agent.combat_state.conditions[Conditions::Healed as usize] = true;
4222            }
4223            if !agent.combat_state.conditions[Conditions::Healed as usize]
4224                && PHOENIX_HEAL_THRESHOLD > health_fraction
4225            {
4226                controller.push_basic_input(InputKind::Ability(4));
4227            } else if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (35.0_f32).powi(2) {
4228                // heat laser
4229                maintain_altitude(2.0);
4230                controller.push_basic_input(InputKind::Ability(3))
4231            } else {
4232                match attack_timer_2 {
4233                    0 => maintain_altitude(3.0),
4234                    1 => {
4235                        //summontornados
4236                        controller.push_basic_input(InputKind::Ability(1));
4237                    },
4238                    2 => {
4239                        // firerain
4240                        controller.push_basic_input(InputKind::Ability(2));
4241                    },
4242                    3 => {
4243                        if attack_data.dist_sqrd < 4.0_f32.powi(2) && attack_data.angle < 150.0 {
4244                            // close range attack
4245                            match attack_timer_1 {
4246                                1 => {
4247                                    // short strike
4248                                    controller.push_basic_input(InputKind::Primary);
4249                                },
4250                                3 => {
4251                                    // long strike
4252                                    controller.push_basic_input(InputKind::Secondary)
4253                                },
4254                                _ => {
4255                                    // leg strike
4256                                    controller.push_basic_input(InputKind::Ability(0))
4257                                },
4258                            }
4259                        } else {
4260                            match attack_timer_1 {
4261                                0 | 2 => {
4262                                    maintain_altitude(2.0);
4263                                },
4264                                _ => {
4265                                    // heat laser
4266                                    controller.push_basic_input(InputKind::Ability(3))
4267                                },
4268                            }
4269                        }
4270                    },
4271                    _ => {
4272                        maintain_altitude(2.0);
4273                    },
4274                }
4275            }
4276        }
4277    }
4278
4279    pub fn handle_wyvern_attack(
4280        &self,
4281        agent: &mut Agent,
4282        controller: &mut Controller,
4283        attack_data: &AttackData,
4284        tgt_data: &TargetData,
4285        read_data: &ReadData,
4286        _rng: &mut impl Rng,
4287    ) {
4288        enum ActionStateTimers {
4289            AttackTimer = 0,
4290        }
4291        // Set fly to false
4292        controller.push_cancel_input(InputKind::Fly);
4293        if attack_data.dist_sqrd > 30.0_f32.powi(2) {
4294            if entities_have_line_of_sight(
4295                self.pos,
4296                self.body,
4297                self.scale,
4298                tgt_data.pos,
4299                tgt_data.body,
4300                tgt_data.scale,
4301                read_data,
4302            ) && attack_data.angle < 15.0
4303            {
4304                controller.push_basic_input(InputKind::Primary);
4305            }
4306            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
4307                &*read_data.terrain,
4308                self.pos.0,
4309                self.vel.0,
4310                tgt_data.pos.0,
4311                TraversalConfig {
4312                    min_tgt_dist: 1.25,
4313                    ..self.traversal_config
4314                },
4315                &read_data.time,
4316            ) {
4317                self.unstuck_if(stuck, controller);
4318                controller.inputs.move_dir =
4319                    bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
4320                if (self.pos.0.z - tgt_data.pos.0.z) < 35.0 {
4321                    controller.push_basic_input(InputKind::Fly);
4322                    controller.inputs.move_z = 0.2;
4323                }
4324            }
4325        } else if !read_data
4326            .terrain
4327            .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 2.0))
4328            .until(Block::is_solid)
4329            .cast()
4330            .1
4331            .map_or(true, |b| b.is_some())
4332        {
4333            // Do not increment the timer during this movement
4334            // The next stage shouldn't trigger until the entity
4335            // is on the ground
4336            controller.push_basic_input(InputKind::Fly);
4337            let move_dir = tgt_data.pos.0 - self.pos.0;
4338            controller.inputs.move_dir =
4339                move_dir.xy().try_normalized().unwrap_or_else(Vec2::zero) * 2.0;
4340            controller.inputs.move_z = move_dir.z - 0.5;
4341            if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4342                && attack_data.angle < 15.0
4343            {
4344                controller.push_basic_input(InputKind::Primary);
4345            }
4346        } else if attack_data.dist_sqrd > (3.0 * attack_data.min_attack_dist).powi(2) {
4347            self.path_toward_target(
4348                agent,
4349                controller,
4350                tgt_data.pos.0,
4351                read_data,
4352                Path::Separate,
4353                None,
4354            );
4355        } else if attack_data.angle < 15.0 {
4356            if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
4357                // beam
4358                controller.push_basic_input(InputKind::Ability(1));
4359            } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 9.0 {
4360                // shockwave
4361                controller.push_basic_input(InputKind::Ability(0));
4362            } else {
4363                agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
4364            }
4365            // Move towards the target slowly
4366            self.path_toward_target(
4367                agent,
4368                controller,
4369                tgt_data.pos.0,
4370                read_data,
4371                Path::Separate,
4372                Some(0.5),
4373            );
4374            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
4375        } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 9.0
4376            && attack_data.angle < 90.0
4377            && attack_data.in_min_range()
4378        {
4379            // Triple strike
4380            controller.push_basic_input(InputKind::Secondary);
4381            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
4382        } else {
4383            // Reset timer
4384            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
4385            // Target is behind us or the timer needs to be reset. Chase target
4386            self.path_toward_target(
4387                agent,
4388                controller,
4389                tgt_data.pos.0,
4390                read_data,
4391                Path::Separate,
4392                None,
4393            );
4394        }
4395    }
4396
4397    pub fn handle_birdlarge_breathe_attack(
4398        &self,
4399        agent: &mut Agent,
4400        controller: &mut Controller,
4401        attack_data: &AttackData,
4402        tgt_data: &TargetData,
4403        read_data: &ReadData,
4404        rng: &mut impl Rng,
4405    ) {
4406        enum ActionStateTimers {
4407            TimerBirdLargeBreathe = 0,
4408        }
4409
4410        // Set fly to false
4411        controller.push_cancel_input(InputKind::Fly);
4412        if attack_data.dist_sqrd > 30.0_f32.powi(2) {
4413            if rng.random_bool(0.05)
4414                && entities_have_line_of_sight(
4415                    self.pos,
4416                    self.body,
4417                    self.scale,
4418                    tgt_data.pos,
4419                    tgt_data.body,
4420                    tgt_data.scale,
4421                    read_data,
4422                )
4423                && attack_data.angle < 15.0
4424            {
4425                controller.push_basic_input(InputKind::Primary);
4426            }
4427            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
4428                &*read_data.terrain,
4429                self.pos.0,
4430                self.vel.0,
4431                tgt_data.pos.0,
4432                TraversalConfig {
4433                    min_tgt_dist: 1.25,
4434                    ..self.traversal_config
4435                },
4436                &read_data.time,
4437            ) {
4438                self.unstuck_if(stuck, controller);
4439                controller.inputs.move_dir =
4440                    bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
4441                if (self.pos.0.z - tgt_data.pos.0.z) < 20.0 {
4442                    controller.push_basic_input(InputKind::Fly);
4443                    controller.inputs.move_z = 1.0;
4444                }
4445            }
4446        } else if !read_data
4447            .terrain
4448            .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 2.0))
4449            .until(Block::is_solid)
4450            .cast()
4451            .1
4452            .map_or(true, |b| b.is_some())
4453        {
4454            // Do not increment the timer during this movement
4455            // The next stage shouldn't trigger until the entity
4456            // is on the ground
4457            controller.push_basic_input(InputKind::Fly);
4458            let move_dir = tgt_data.pos.0 - self.pos.0;
4459            controller.inputs.move_dir =
4460                move_dir.xy().try_normalized().unwrap_or_else(Vec2::zero) * 2.0;
4461            controller.inputs.move_z = move_dir.z - 0.5;
4462            if rng.random_bool(0.05)
4463                && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4464                && attack_data.angle < 15.0
4465            {
4466                controller.push_basic_input(InputKind::Primary);
4467            }
4468        } else if rng.random_bool(0.05)
4469            && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4470            && attack_data.angle < 15.0
4471        {
4472            controller.push_basic_input(InputKind::Primary);
4473        } else if rng.random_bool(0.5)
4474            && (self.pos.0.z - tgt_data.pos.0.z) < 15.0
4475            && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4476        {
4477            controller.push_basic_input(InputKind::Fly);
4478            controller.inputs.move_z = 1.0;
4479        } else if attack_data.dist_sqrd > (3.0 * attack_data.min_attack_dist).powi(2) {
4480            self.path_toward_target(
4481                agent,
4482                controller,
4483                tgt_data.pos.0,
4484                read_data,
4485                Path::Separate,
4486                None,
4487            );
4488        } else if self.energy.current() > 60.0
4489            && agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] < 3.0
4490            && attack_data.angle < 15.0
4491        {
4492            // Fire breath attack
4493            controller.push_basic_input(InputKind::Ability(0));
4494            // Move towards the target slowly
4495            self.path_toward_target(
4496                agent,
4497                controller,
4498                tgt_data.pos.0,
4499                read_data,
4500                Path::Separate,
4501                Some(0.5),
4502            );
4503            agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] +=
4504                read_data.dt.0;
4505        } else if agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] < 6.0
4506            && attack_data.angle < 90.0
4507            && attack_data.in_min_range()
4508        {
4509            // Triple strike
4510            controller.push_basic_input(InputKind::Secondary);
4511            agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] +=
4512                read_data.dt.0;
4513        } else {
4514            // Reset timer
4515            agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] = 0.0;
4516            // Target is behind us or the timer needs to be reset. Chase target
4517            self.path_toward_target(
4518                agent,
4519                controller,
4520                tgt_data.pos.0,
4521                read_data,
4522                Path::Separate,
4523                None,
4524            );
4525        }
4526    }
4527
4528    pub fn handle_birdlarge_basic_attack(
4529        &self,
4530        agent: &mut Agent,
4531        controller: &mut Controller,
4532        attack_data: &AttackData,
4533        tgt_data: &TargetData,
4534        read_data: &ReadData,
4535    ) {
4536        enum ActionStateTimers {
4537            TimerBirdLargeBasic = 0,
4538        }
4539
4540        enum ActionStateConditions {
4541            ConditionBirdLargeBasic = 0, /* FIXME: Not sure what this represents. This name
4542                                          * should be reflective of the condition... */
4543        }
4544
4545        const BIRD_ATTACK_RANGE: f32 = 4.0;
4546        const BIRD_CHARGE_DISTANCE: f32 = 15.0;
4547        let bird_attack_distance = self.body.map_or(0.0, |b| b.max_radius()) + BIRD_ATTACK_RANGE;
4548        // Increase action timer
4549        agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] +=
4550            read_data.dt.0;
4551        if agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] > 8.0 {
4552            // If action timer higher than 8, make bird summon tornadoes
4553            controller.push_basic_input(InputKind::Secondary);
4554            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
4555            {
4556                // Reset timer
4557                agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] = 0.0;
4558            }
4559        } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4560        {
4561            // If already in dash, keep dashing if not in recover
4562            controller.push_basic_input(InputKind::Ability(0));
4563        } else if matches!(self.char_state, CharacterState::ComboMelee2(c) if matches!(c.stage_section, StageSection::Recover))
4564        {
4565            // If already in combo keep comboing if not in recover
4566            controller.push_basic_input(InputKind::Primary);
4567        } else if attack_data.dist_sqrd > BIRD_CHARGE_DISTANCE.powi(2) {
4568            // Charges at target if they are far enough away
4569            if attack_data.angle < 60.0 {
4570                controller.push_basic_input(InputKind::Ability(0));
4571            }
4572        } else if attack_data.dist_sqrd < bird_attack_distance.powi(2) {
4573            // Combo melee target
4574            controller.push_basic_input(InputKind::Primary);
4575            agent.combat_state.conditions
4576                [ActionStateConditions::ConditionBirdLargeBasic as usize] = true;
4577        }
4578        // Make bird move towards target
4579        self.path_toward_target(
4580            agent,
4581            controller,
4582            tgt_data.pos.0,
4583            read_data,
4584            Path::Separate,
4585            None,
4586        );
4587    }
4588
4589    pub fn handle_arthropod_ranged_attack(
4590        &self,
4591        agent: &mut Agent,
4592        controller: &mut Controller,
4593        attack_data: &AttackData,
4594        tgt_data: &TargetData,
4595        read_data: &ReadData,
4596    ) {
4597        enum ActionStateTimers {
4598            TimerArthropodRanged = 0,
4599        }
4600
4601        agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] +=
4602            read_data.dt.0;
4603        if agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] > 6.0
4604            && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
4605        {
4606            controller.inputs.move_dir = Vec2::zero();
4607            controller.push_basic_input(InputKind::Secondary);
4608            // Reset timer
4609            if matches!(self.char_state,
4610            CharacterState::SpriteSummon(sprite_summon::Data { stage_section, .. })
4611            | CharacterState::SelfBuff(self_buff::Data { stage_section, .. })
4612            if matches!(stage_section, StageSection::Recover))
4613            {
4614                agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] = 0.0;
4615            }
4616        } else if attack_data.dist_sqrd < (2.5 * attack_data.min_attack_dist).powi(2)
4617            && attack_data.angle < 90.0
4618        {
4619            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4620                .xy()
4621                .try_normalized()
4622                .unwrap_or_else(Vec2::unit_y)
4623                // Slow down if very close to the target
4624                * if attack_data.in_min_range() { 0.3 } else { 1.0 };
4625            controller.push_basic_input(InputKind::Primary);
4626        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
4627            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
4628                &*read_data.terrain,
4629                self.pos.0,
4630                self.vel.0,
4631                tgt_data.pos.0,
4632                TraversalConfig {
4633                    min_tgt_dist: 1.25,
4634                    ..self.traversal_config
4635                },
4636                &read_data.time,
4637            ) {
4638                self.unstuck_if(stuck, controller);
4639                if attack_data.angle < 15.0
4640                    && entities_have_line_of_sight(
4641                        self.pos,
4642                        self.body,
4643                        self.scale,
4644                        tgt_data.pos,
4645                        tgt_data.body,
4646                        tgt_data.scale,
4647                        read_data,
4648                    )
4649                {
4650                    if agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize]
4651                        > 5.0
4652                    {
4653                        agent.combat_state.timers
4654                            [ActionStateTimers::TimerArthropodRanged as usize] = 0.0;
4655                    } else if agent.combat_state.timers
4656                        [ActionStateTimers::TimerArthropodRanged as usize]
4657                        > 2.5
4658                    {
4659                        controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4660                            .xy()
4661                            .rotated_z(1.75 * PI)
4662                            .try_normalized()
4663                            .unwrap_or_else(Vec2::zero)
4664                            * speed;
4665                        agent.combat_state.timers
4666                            [ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0;
4667                    } else {
4668                        controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4669                            .xy()
4670                            .rotated_z(0.25 * PI)
4671                            .try_normalized()
4672                            .unwrap_or_else(Vec2::zero)
4673                            * speed;
4674                        agent.combat_state.timers
4675                            [ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0;
4676                    }
4677                    controller.push_basic_input(InputKind::Ability(0));
4678                    self.jump_if(bearing.z > 1.5, controller);
4679                    controller.inputs.move_z = bearing.z;
4680                } else {
4681                    controller.inputs.move_dir =
4682                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
4683                    self.jump_if(bearing.z > 1.5, controller);
4684                    controller.inputs.move_z = bearing.z;
4685                }
4686            } else {
4687                agent.target = None;
4688            }
4689        } else {
4690            self.path_toward_target(
4691                agent,
4692                controller,
4693                tgt_data.pos.0,
4694                read_data,
4695                Path::AtTarget,
4696                None,
4697            );
4698        }
4699    }
4700
4701    pub fn handle_arthropod_ambush_attack(
4702        &self,
4703        agent: &mut Agent,
4704        controller: &mut Controller,
4705        attack_data: &AttackData,
4706        tgt_data: &TargetData,
4707        read_data: &ReadData,
4708        rng: &mut impl Rng,
4709    ) {
4710        enum ActionStateTimers {
4711            TimersArthropodAmbush = 0,
4712        }
4713
4714        agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] +=
4715            read_data.dt.0;
4716        if agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] > 12.0
4717            && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
4718        {
4719            controller.inputs.move_dir = Vec2::zero();
4720            controller.push_basic_input(InputKind::Secondary);
4721            // Reset timer
4722            if matches!(self.char_state,
4723            CharacterState::SpriteSummon(sprite_summon::Data { stage_section, .. })
4724            | CharacterState::SelfBuff(self_buff::Data { stage_section, .. })
4725            if matches!(stage_section, StageSection::Recover))
4726            {
4727                agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] = 0.0;
4728            }
4729        } else if attack_data.angle < 90.0
4730            && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2)
4731        {
4732            controller.inputs.move_dir = Vec2::zero();
4733            controller.push_basic_input(InputKind::Primary);
4734        } else if rng.random_bool(0.01)
4735            && attack_data.angle < 60.0
4736            && attack_data.dist_sqrd > (2.0 * attack_data.min_attack_dist).powi(2)
4737        {
4738            controller.push_basic_input(InputKind::Ability(0));
4739        } else {
4740            self.path_toward_target(
4741                agent,
4742                controller,
4743                tgt_data.pos.0,
4744                read_data,
4745                Path::AtTarget,
4746                None,
4747            );
4748        }
4749    }
4750
4751    pub fn handle_arthropod_melee_attack(
4752        &self,
4753        agent: &mut Agent,
4754        controller: &mut Controller,
4755        attack_data: &AttackData,
4756        tgt_data: &TargetData,
4757        read_data: &ReadData,
4758    ) {
4759        enum ActionStateTimers {
4760            TimersArthropodMelee = 0,
4761        }
4762        agent.combat_state.timers[ActionStateTimers::TimersArthropodMelee as usize] +=
4763            read_data.dt.0;
4764        if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4765        {
4766            // If already charging, keep charging if not in recover
4767            controller.push_basic_input(InputKind::Secondary);
4768        } else if attack_data.dist_sqrd > (2.5 * attack_data.min_attack_dist).powi(2) {
4769            // Charges at target if they are far enough away
4770            if attack_data.angle < 60.0 {
4771                controller.push_basic_input(InputKind::Secondary);
4772            }
4773        } else if attack_data.angle < 90.0
4774            && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2)
4775        {
4776            controller.inputs.move_dir = Vec2::zero();
4777            controller.push_basic_input(InputKind::Primary);
4778        } else {
4779            self.path_toward_target(
4780                agent,
4781                controller,
4782                tgt_data.pos.0,
4783                read_data,
4784                Path::AtTarget,
4785                None,
4786            );
4787        }
4788    }
4789
4790    pub fn handle_minotaur_attack(
4791        &self,
4792        agent: &mut Agent,
4793        controller: &mut Controller,
4794        attack_data: &AttackData,
4795        tgt_data: &TargetData,
4796        read_data: &ReadData,
4797    ) {
4798        const MINOTAUR_FRENZY_THRESHOLD: f32 = 0.5;
4799        const MINOTAUR_ATTACK_RANGE: f32 = 5.0;
4800        const MINOTAUR_CHARGE_DISTANCE: f32 = 15.0;
4801
4802        enum ActionStateFCounters {
4803            FCounterMinotaurAttack = 0,
4804        }
4805
4806        enum ActionStateConditions {
4807            ConditionJustCrippledOrCleaved = 0,
4808        }
4809
4810        enum Conditions {
4811            AttackToggle,
4812        }
4813
4814        enum Timers {
4815            CheeseTimer,
4816            CanSeeTarget,
4817            Reposition,
4818        }
4819
4820        let minotaur_attack_distance =
4821            self.body.map_or(0.0, |b| b.max_radius()) + MINOTAUR_ATTACK_RANGE;
4822        let health_fraction = self.health.map_or(1.0, |h| h.fraction());
4823        let home = agent.patrol_origin.unwrap_or(self.pos.0);
4824        let center = Vec2::new(home.x + 50.0, home.y + 75.0);
4825        let cheesed_from_above = tgt_data.pos.0.z > self.pos.0.z + 4.0;
4826        let center_cheesed = (center - self.pos.0.xy()).magnitude_squared() < 16.0_f32.powi(2);
4827        let pillar_cheesed = (center - tgt_data.pos.0.xy()).magnitude_squared() < 16.0_f32.powi(2);
4828        let cheesed = (pillar_cheesed || center_cheesed)
4829            && agent.combat_state.timers[Timers::CheeseTimer as usize] > 4.0;
4830        agent.combat_state.timers[Timers::CheeseTimer as usize] += read_data.dt.0;
4831        agent.combat_state.timers[Timers::CanSeeTarget as usize] += read_data.dt.0;
4832        agent.combat_state.timers[Timers::Reposition as usize] += read_data.dt.0;
4833        if agent.combat_state.timers[Timers::Reposition as usize] > 20.0 {
4834            agent.combat_state.timers[Timers::Reposition as usize] = 0.0;
4835        }
4836        let line_of_sight_with_target = || {
4837            entities_have_line_of_sight(
4838                self.pos,
4839                self.body,
4840                self.scale,
4841                tgt_data.pos,
4842                tgt_data.body,
4843                tgt_data.scale,
4844                read_data,
4845            )
4846        };
4847        if !line_of_sight_with_target() {
4848            agent.combat_state.timers[Timers::CanSeeTarget as usize] = 0.0;
4849        };
4850        let remote_spikes_action = || ControlAction::StartInput {
4851            input: InputKind::Ability(3),
4852            target_entity: None,
4853            select_pos: Some(tgt_data.pos.0),
4854        };
4855        // Sets action counter at start of combat
4856        if agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize]
4857            < MINOTAUR_FRENZY_THRESHOLD
4858            && health_fraction > MINOTAUR_FRENZY_THRESHOLD
4859        {
4860            agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize] =
4861                MINOTAUR_FRENZY_THRESHOLD;
4862        }
4863        if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
4864        {
4865            agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
4866        }
4867        if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
4868        {
4869            agent.combat_state.conditions[Conditions::AttackToggle as usize] = false;
4870            if agent.combat_state.timers[Timers::CheeseTimer as usize] > 10.0 {
4871                agent.combat_state.timers[Timers::CheeseTimer as usize] = 0.0;
4872            }
4873        }
4874        // when cheesed, throw axes and summon sprites.
4875        if cheesed_from_above || cheesed {
4876            if agent.combat_state.conditions[Conditions::AttackToggle as usize] {
4877                controller.push_basic_input(InputKind::Ability(2));
4878            } else {
4879                controller.push_action(remote_spikes_action());
4880            }
4881            //
4882            if center_cheesed {
4883                // when cheesed around the center pillar, try to reposition
4884                let dir_index = match agent.combat_state.timers[Timers::Reposition as usize] as i32
4885                {
4886                    0_i32..5_i32 => 0,
4887                    5_i32..10_i32 => 1,
4888                    10_i32..15_i32 => 2,
4889                    _ => 3,
4890                };
4891                let goto = Vec3::new(
4892                    center.x + (CARDINALS[dir_index].x * 25) as f32,
4893                    center.y + (CARDINALS[dir_index].y * 25) as f32,
4894                    tgt_data.pos.0.z,
4895                );
4896                self.path_toward_target(
4897                    agent,
4898                    controller,
4899                    goto,
4900                    read_data,
4901                    Path::AtTarget,
4902                    (attack_data.dist_sqrd
4903                        < (attack_data.min_attack_dist + MINOTAUR_ATTACK_RANGE / 3.0).powi(2))
4904                    .then_some(0.1),
4905                );
4906            }
4907        } else if health_fraction
4908            < agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize]
4909        {
4910            // Makes minotaur buff itself with frenzy
4911            controller.push_basic_input(InputKind::Ability(1));
4912            if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
4913            {
4914                agent.combat_state.counters
4915                    [ActionStateFCounters::FCounterMinotaurAttack as usize] = 0.0;
4916            }
4917        } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4918        {
4919            // If already charging, keep charging if not in recover
4920            controller.push_basic_input(InputKind::Ability(0));
4921        } else if matches!(self.char_state, CharacterState::ChargedMelee(c) if matches!(c.stage_section, StageSection::Charge) && c.timer < c.static_data.charge_duration)
4922        {
4923            // If already charging a melee attack, keep charging it if charging
4924            controller.push_basic_input(InputKind::Primary);
4925        } else if attack_data.dist_sqrd > MINOTAUR_CHARGE_DISTANCE.powi(2) {
4926            // Charges at target if they are far enough away
4927            if attack_data.angle < 60.0 {
4928                controller.push_basic_input(InputKind::Ability(0));
4929            }
4930        } else if attack_data.dist_sqrd < minotaur_attack_distance.powi(2) {
4931            if agent.combat_state.conditions
4932                [ActionStateConditions::ConditionJustCrippledOrCleaved as usize]
4933                && !self.char_state.is_attack()
4934            {
4935                // Cripple target if not just used cripple
4936                controller.push_basic_input(InputKind::Secondary);
4937                agent.combat_state.conditions
4938                    [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = false;
4939            } else if !self.char_state.is_attack() {
4940                // Cleave target if not just used cleave
4941                controller.push_basic_input(InputKind::Primary);
4942                agent.combat_state.conditions
4943                    [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = true;
4944            }
4945        }
4946        // Chase target, when target is above, retreat to chamber
4947        if cheesed_from_above {
4948            self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
4949        // delay chasing to counter wall cheese
4950        } else if agent.combat_state.timers[Timers::CanSeeTarget as usize] > 2.0
4951        // always chase when in hallway to boss chamber
4952        || (3.0..18.0).contains(&(self.pos.0.y - home.y))
4953        {
4954            self.path_toward_target(
4955                agent,
4956                controller,
4957                tgt_data.pos.0,
4958                read_data,
4959                Path::AtTarget,
4960                (attack_data.dist_sqrd
4961                    < (attack_data.min_attack_dist + MINOTAUR_ATTACK_RANGE / 3.0).powi(2))
4962                .then_some(0.1),
4963            );
4964        }
4965    }
4966
4967    pub fn handle_cyclops_attack(
4968        &self,
4969        agent: &mut Agent,
4970        controller: &mut Controller,
4971        attack_data: &AttackData,
4972        tgt_data: &TargetData,
4973        read_data: &ReadData,
4974    ) {
4975        // Primary
4976        const CYCLOPS_MELEE_RANGE: f32 = 9.0;
4977        // Secondary
4978        const CYCLOPS_FIRE_RANGE: f32 = 30.0;
4979        // Ability(1)
4980        const CYCLOPS_CHARGE_RANGE: f32 = 18.0;
4981        // Ability(0) - Ablity (2)
4982        const SHOCKWAVE_THRESHOLD: f32 = 0.6;
4983
4984        enum FCounters {
4985            ShockwaveThreshold = 0,
4986        }
4987        enum Timers {
4988            AttackChange = 0,
4989        }
4990
4991        if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 {
4992            agent.combat_state.timers[Timers::AttackChange as usize] = 0.0;
4993        }
4994
4995        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4996        // Sets counter at start of combat, using `condition` to keep track of whether
4997        // it was already initialized
4998        if !agent.combat_state.initialized {
4999            agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] =
5000                1.0 - SHOCKWAVE_THRESHOLD;
5001            agent.combat_state.initialized = true;
5002        } else if health_fraction
5003            < agent.combat_state.counters[FCounters::ShockwaveThreshold as usize]
5004        {
5005            // Scream when threshold is reached
5006            controller.push_basic_input(InputKind::Ability(2));
5007
5008            if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
5009            {
5010                agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] -=
5011                    SHOCKWAVE_THRESHOLD;
5012            }
5013        } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5014        {
5015            // If already AOEing, keep AOEing if not in recover
5016            controller.push_basic_input(InputKind::Ability(0));
5017        } else if attack_data.dist_sqrd > CYCLOPS_FIRE_RANGE.powi(2) {
5018            // Chase
5019            controller.push_basic_input(InputKind::Ability(1));
5020        } else if attack_data.dist_sqrd > CYCLOPS_CHARGE_RANGE.powi(2) {
5021            // Shoot after target if they attempt to "flee"
5022            controller.push_basic_input(InputKind::Secondary);
5023        } else if attack_data.dist_sqrd < CYCLOPS_MELEE_RANGE.powi(2) {
5024            if attack_data.angle < 60.0 {
5025                // Melee target if close enough and within angle
5026                controller.push_basic_input(InputKind::Primary);
5027            } else if attack_data.angle > 60.0 {
5028                // Scream if target exceeds angle but is close enough
5029                controller.push_basic_input(InputKind::Ability(0));
5030            }
5031        }
5032
5033        // Always path towards target
5034        self.path_toward_target(
5035            agent,
5036            controller,
5037            tgt_data.pos.0,
5038            read_data,
5039            Path::AtTarget,
5040            (attack_data.dist_sqrd
5041                < (attack_data.min_attack_dist + CYCLOPS_MELEE_RANGE / 2.0).powi(2))
5042            .then_some(0.1),
5043        );
5044    }
5045
5046    pub fn handle_dullahan_attack(
5047        &self,
5048        agent: &mut Agent,
5049        controller: &mut Controller,
5050        attack_data: &AttackData,
5051        tgt_data: &TargetData,
5052        read_data: &ReadData,
5053    ) {
5054        // Primary (12 Default / Melee)
5055        const MELEE_RANGE: f32 = 9.0;
5056        // Secondary (30 Default / Range)
5057        const LONG_RANGE: f32 = 30.0;
5058        // Ability(0) (0.1 aka 10% Default / AOE)
5059        const HP_THRESHOLD: f32 = 0.1;
5060        // Ability(1) (18 Default / Dash)
5061        const MID_RANGE: f32 = 18.0;
5062
5063        enum FCounters {
5064            HealthThreshold = 0,
5065        }
5066        enum Timers {
5067            AttackChange = 0,
5068        }
5069        if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 {
5070            agent.combat_state.timers[Timers::AttackChange as usize] = 0.0;
5071        }
5072
5073        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5074        // Sets counter at start of combat, using `condition` to keep track of whether
5075        // it was already initialized
5076        if !agent.combat_state.initialized {
5077            agent.combat_state.counters[FCounters::HealthThreshold as usize] = 1.0 - HP_THRESHOLD;
5078            agent.combat_state.initialized = true;
5079        } else if health_fraction < agent.combat_state.counters[FCounters::HealthThreshold as usize]
5080        {
5081            // InputKind when threshold is reached (Default is Ability(0))
5082            controller.push_basic_input(InputKind::Ability(0));
5083
5084            if matches!(
5085                self.char_state.ability_info().map(|ai| ai.input),
5086                Some(InputKind::Ability(0))
5087            ) && matches!(self.char_state.stage_section(), Some(StageSection::Recover))
5088            {
5089                agent.combat_state.counters[FCounters::HealthThreshold as usize] -= HP_THRESHOLD;
5090            }
5091        } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5092        {
5093            // If already InputKind, keep InputKind if not in recover (Default is Shockwave)
5094            controller.push_basic_input(InputKind::Ability(0));
5095        } else if attack_data.dist_sqrd > LONG_RANGE.powi(2) {
5096            // InputKind after target if they attempt to "flee" (>LONG)
5097            controller.push_basic_input(InputKind::Ability(1));
5098        } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
5099            // InputKind after target if they attempt to "flee" (MID-LONG)
5100            controller.push_basic_input(InputKind::Secondary);
5101        } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5102            if attack_data.angle < 60.0 {
5103                // InputKind target if close enough and within angle (<MELEE)
5104                controller.push_basic_input(InputKind::Primary);
5105            } else if attack_data.angle > 60.0 {
5106                // InputKind if target exceeds angle but is close enough (FLANK/STRAFE)
5107                controller.push_basic_input(InputKind::Ability(0));
5108            }
5109        }
5110
5111        // Path to target if too far away
5112        self.path_toward_target(
5113            agent,
5114            controller,
5115            tgt_data.pos.0,
5116            read_data,
5117            Path::AtTarget,
5118            (attack_data.dist_sqrd < (attack_data.min_attack_dist + MELEE_RANGE / 2.0).powi(2))
5119                .then_some(0.1),
5120        );
5121    }
5122
5123    pub fn handle_grave_warden_attack(
5124        &self,
5125        agent: &mut Agent,
5126        controller: &mut Controller,
5127        attack_data: &AttackData,
5128        tgt_data: &TargetData,
5129        read_data: &ReadData,
5130    ) {
5131        const GOLEM_MELEE_RANGE: f32 = 4.0;
5132        const GOLEM_LASER_RANGE: f32 = 30.0;
5133        const GOLEM_LONG_RANGE: f32 = 50.0;
5134        const GOLEM_TARGET_SPEED: f32 = 8.0;
5135
5136        enum ActionStateFCounters {
5137            FCounterGlayGolemAttack = 0,
5138        }
5139
5140        let golem_melee_range = self.body.map_or(0.0, |b| b.max_radius()) + GOLEM_MELEE_RANGE;
5141        // Fraction of health, used for activation of shockwave
5142        // If golem don't have health for some reason, assume it's full
5143        let health_fraction = self.health.map_or(1.0, |h| h.fraction());
5144        // Magnitude squared of cross product of target velocity with golem orientation
5145        let target_speed_cross_sqd = agent
5146            .target
5147            .as_ref()
5148            .map(|t| t.target)
5149            .and_then(|e| read_data.velocities.get(e))
5150            .map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared());
5151        let line_of_sight_with_target = || {
5152            entities_have_line_of_sight(
5153                self.pos,
5154                self.body,
5155                self.scale,
5156                tgt_data.pos,
5157                tgt_data.body,
5158                tgt_data.scale,
5159                read_data,
5160            )
5161        };
5162
5163        if attack_data.dist_sqrd < golem_melee_range.powi(2) {
5164            if agent.combat_state.counters[ActionStateFCounters::FCounterGlayGolemAttack as usize]
5165                < 7.5
5166            {
5167                // If target is close, whack them
5168                controller.push_basic_input(InputKind::Primary);
5169                agent.combat_state.counters
5170                    [ActionStateFCounters::FCounterGlayGolemAttack as usize] += read_data.dt.0;
5171            } else {
5172                // If whacked for too long, nuke them
5173                controller.push_basic_input(InputKind::Ability(1));
5174                if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
5175                {
5176                    agent.combat_state.counters
5177                        [ActionStateFCounters::FCounterGlayGolemAttack as usize] = 0.0;
5178                }
5179            }
5180        } else if attack_data.dist_sqrd < GOLEM_LASER_RANGE.powi(2) {
5181            if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5))
5182                || target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2)
5183                    && line_of_sight_with_target()
5184                    && attack_data.angle < 45.0
5185            {
5186                // If target in range threshold and haven't been lasering for more than 5
5187                // seconds already or if target is moving slow-ish, laser them
5188                controller.push_basic_input(InputKind::Secondary);
5189            } else if health_fraction < 0.7 {
5190                // Else target moving too fast for laser, shockwave time.
5191                // But only if damaged enough
5192                controller.push_basic_input(InputKind::Ability(0));
5193            }
5194        } else if attack_data.dist_sqrd < GOLEM_LONG_RANGE.powi(2) {
5195            if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) && line_of_sight_with_target() {
5196                // If target is far-ish and moving slow-ish, rocket them
5197                controller.push_basic_input(InputKind::Ability(1));
5198            } else if health_fraction < 0.7 {
5199                // Else target moving too fast for laser, shockwave time.
5200                // But only if damaged enough
5201                controller.push_basic_input(InputKind::Ability(0));
5202            }
5203        }
5204
5205        // Make grave warden move towards target
5206        self.path_toward_target(
5207            agent,
5208            controller,
5209            tgt_data.pos.0,
5210            read_data,
5211            Path::Separate,
5212            (attack_data.dist_sqrd
5213                < (attack_data.min_attack_dist + GOLEM_MELEE_RANGE / 1.5).powi(2))
5214            .then_some(0.1),
5215        );
5216    }
5217
5218    pub fn handle_tidal_warrior_attack(
5219        &self,
5220        agent: &mut Agent,
5221        controller: &mut Controller,
5222        attack_data: &AttackData,
5223        tgt_data: &TargetData,
5224        read_data: &ReadData,
5225    ) {
5226        const SCUTTLE_RANGE: f32 = 40.0;
5227        const BUBBLE_RANGE: f32 = 20.0;
5228        const MINION_SUMMON_THRESHOLD: f32 = 0.20;
5229
5230        enum ActionStateConditions {
5231            ConditionCounterInitialized = 0,
5232        }
5233
5234        enum ActionStateFCounters {
5235            FCounterMinionSummonThreshold = 0,
5236        }
5237
5238        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5239        let line_of_sight_with_target = || {
5240            entities_have_line_of_sight(
5241                self.pos,
5242                self.body,
5243                self.scale,
5244                tgt_data.pos,
5245                tgt_data.body,
5246                tgt_data.scale,
5247                read_data,
5248            )
5249        };
5250        let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
5251        // Sets counter at start of combat, using `condition` to keep track of whether
5252        // it was already initialized
5253        if !agent.combat_state.conditions
5254            [ActionStateConditions::ConditionCounterInitialized as usize]
5255        {
5256            agent.combat_state.counters
5257                [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5258                1.0 - MINION_SUMMON_THRESHOLD;
5259            agent.combat_state.conditions
5260                [ActionStateConditions::ConditionCounterInitialized as usize] = true;
5261        }
5262
5263        if agent.combat_state.counters[ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5264            > health_fraction
5265        {
5266            // Summon minions at particular thresholds of health
5267            controller.push_basic_input(InputKind::Ability(1));
5268
5269            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5270            {
5271                agent.combat_state.counters
5272                    [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5273                    MINION_SUMMON_THRESHOLD;
5274            }
5275        } else if attack_data.dist_sqrd < SCUTTLE_RANGE.powi(2) {
5276            if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5277            {
5278                // Keep scuttling if already in dash melee and not in recover
5279                controller.push_basic_input(InputKind::Secondary);
5280            } else if attack_data.dist_sqrd < BUBBLE_RANGE.powi(2) {
5281                if matches!(self.char_state, CharacterState::BasicBeam(c) if !matches!(c.stage_section, StageSection::Recover) && c.timer < Duration::from_secs(10))
5282                {
5283                    // Keep shooting bubbles at them if already in basic beam and not in recover and
5284                    // have not been bubbling too long
5285                    controller.push_basic_input(InputKind::Ability(0));
5286                } else if attack_data.in_min_range() && attack_data.angle < 60.0 {
5287                    // Pincer them if they're in range and angle
5288                    controller.push_basic_input(InputKind::Primary);
5289                } else if attack_data.angle < 30.0 && line_of_sight_with_target() {
5290                    // Start bubbling them if not close enough to do something else and in angle and
5291                    // can see target
5292                    controller.push_basic_input(InputKind::Ability(0));
5293                }
5294            } else if attack_data.angle < 90.0 && line_of_sight_with_target() {
5295                // Start scuttling if not close enough to do something else and in angle and can
5296                // see target
5297                controller.push_basic_input(InputKind::Secondary);
5298            }
5299        }
5300        let path = if tgt_data.pos.0.z < self.pos.0.z {
5301            home
5302        } else {
5303            tgt_data.pos.0
5304        };
5305        // attempt to path towards target, move away from exiit  if target is cheesing
5306        // from below
5307        self.path_toward_target(agent, controller, path, read_data, Path::AtTarget, None);
5308    }
5309
5310    pub fn handle_yeti_attack(
5311        &self,
5312        agent: &mut Agent,
5313        controller: &mut Controller,
5314        attack_data: &AttackData,
5315        tgt_data: &TargetData,
5316        read_data: &ReadData,
5317    ) {
5318        const ICE_SPIKES_RANGE: f32 = 15.0;
5319        const ICE_BREATH_RANGE: f32 = 10.0;
5320        const ICE_BREATH_TIMER: f32 = 10.0;
5321        const SNOWBALL_MAX_RANGE: f32 = 50.0;
5322
5323        enum ActionStateFCounters {
5324            FCounterYetiAttack = 0,
5325        }
5326
5327        agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize] +=
5328            read_data.dt.0;
5329
5330        if attack_data.dist_sqrd < ICE_BREATH_RANGE.powi(2) {
5331            if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(2))
5332            {
5333                // Keep using ice breath for 2 second
5334                controller.push_basic_input(InputKind::Ability(0));
5335            } else if agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize]
5336                > ICE_BREATH_TIMER
5337            {
5338                // Use ice breath if timer has gone for long enough
5339                controller.push_basic_input(InputKind::Ability(0));
5340
5341                if matches!(self.char_state, CharacterState::BasicBeam(_)) {
5342                    // Resets action counter when using beam
5343                    agent.combat_state.counters
5344                        [ActionStateFCounters::FCounterYetiAttack as usize] = 0.0;
5345                }
5346            } else if attack_data.in_min_range() {
5347                // Basic attack if on top of them
5348                controller.push_basic_input(InputKind::Primary);
5349            } else {
5350                // Use ice spikes if too far for other abilities
5351                controller.push_basic_input(InputKind::Secondary);
5352            }
5353        } else if attack_data.dist_sqrd < ICE_SPIKES_RANGE.powi(2) && attack_data.angle < 60.0 {
5354            // Use ice spikes if in range
5355            controller.push_basic_input(InputKind::Secondary);
5356        } else if attack_data.dist_sqrd < SNOWBALL_MAX_RANGE.powi(2) && attack_data.angle < 60.0 {
5357            // Otherwise, chuck all the snowballs
5358            controller.push_basic_input(InputKind::Ability(1));
5359        }
5360
5361        // Always attempt to path towards target
5362        self.path_toward_target(
5363            agent,
5364            controller,
5365            tgt_data.pos.0,
5366            read_data,
5367            Path::AtTarget,
5368            attack_data.in_min_range().then_some(0.1),
5369        );
5370    }
5371
5372    pub fn handle_elephant_attack(
5373        &self,
5374        agent: &mut Agent,
5375        controller: &mut Controller,
5376        attack_data: &AttackData,
5377        tgt_data: &TargetData,
5378        read_data: &ReadData,
5379        rng: &mut impl Rng,
5380    ) {
5381        const MELEE_RANGE: f32 = 10.0;
5382        const RANGED_RANGE: f32 = 20.0;
5383        const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
5384            desired_energy: 30.0,
5385            combo_scaling_buildup: 0,
5386        };
5387
5388        const GOUGE: InputKind = InputKind::Primary;
5389        const DASH: InputKind = InputKind::Secondary;
5390        const STOMP: InputKind = InputKind::Ability(0);
5391        const WATER: InputKind = InputKind::Ability(1);
5392        const VACUUM: InputKind = InputKind::Ability(2);
5393
5394        let could_use = |input| {
5395            Option::<AbilityInput>::from(input)
5396                .and_then(|ability_input| self.extract_ability(ability_input))
5397                .is_some_and(|ability_data| {
5398                    ability_data.could_use(
5399                        attack_data,
5400                        self,
5401                        tgt_data,
5402                        read_data,
5403                        ABILITY_PREFERENCES,
5404                    )
5405                })
5406        };
5407
5408        let dashing = matches!(self.char_state, CharacterState::DashMelee(_))
5409            && self.char_state.stage_section() != Some(StageSection::Recover);
5410
5411        if dashing {
5412            controller.push_basic_input(DASH);
5413        } else if rng.random_bool(0.05) {
5414            if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5415                if rng.random_bool(0.5) && could_use(STOMP) {
5416                    controller.push_basic_input(STOMP);
5417                } else {
5418                    controller.push_basic_input(GOUGE);
5419                }
5420            } else if attack_data.dist_sqrd < RANGED_RANGE.powi(2) {
5421                if rng.random_bool(0.5) {
5422                    controller.push_basic_input(WATER);
5423                } else if could_use(VACUUM) {
5424                    controller.push_basic_input(VACUUM);
5425                } else {
5426                    controller.push_basic_input(DASH);
5427                }
5428            } else {
5429                controller.push_basic_input(DASH);
5430            }
5431        }
5432
5433        self.path_toward_target(
5434            agent,
5435            controller,
5436            tgt_data.pos.0,
5437            read_data,
5438            Path::AtTarget,
5439            None,
5440        );
5441    }
5442
5443    pub fn handle_rocksnapper_attack(
5444        &self,
5445        agent: &mut Agent,
5446        controller: &mut Controller,
5447        attack_data: &AttackData,
5448        tgt_data: &TargetData,
5449        read_data: &ReadData,
5450    ) {
5451        const LEAP_TIMER: f32 = 3.0;
5452        const DASH_TIMER: f32 = 5.0;
5453        const LEAP_RANGE: f32 = 20.0;
5454        const MELEE_RANGE: f32 = 5.0;
5455
5456        enum ActionStateTimers {
5457            TimerRocksnapperDash = 0,
5458            TimerRocksnapperLeap = 1,
5459        }
5460        agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] +=
5461            read_data.dt.0;
5462        agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] +=
5463            read_data.dt.0;
5464
5465        if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5466        {
5467            // If already dashing, keep dashing if not in recover stage
5468            controller.push_basic_input(InputKind::Secondary);
5469        } else if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize]
5470            > DASH_TIMER
5471        {
5472            // Use dash if timer has gone for long enough
5473            controller.push_basic_input(InputKind::Secondary);
5474
5475            if matches!(self.char_state, CharacterState::DashMelee(_)) {
5476                // Resets action counter when using dash
5477                agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] = 0.0;
5478            }
5479        } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) && attack_data.angle < 90.0 {
5480            if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize]
5481                > LEAP_TIMER
5482            {
5483                // Use shockwave if timer has gone for long enough
5484                controller.push_basic_input(InputKind::Ability(0));
5485
5486                if matches!(self.char_state, CharacterState::LeapShockwave(_)) {
5487                    // Resets action timer when using leap shockwave
5488                    agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] =
5489                        0.0;
5490                }
5491            } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5492                // Basic attack if in melee range
5493                controller.push_basic_input(InputKind::Primary);
5494            }
5495        } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
5496            // Basic attack if in melee range
5497            controller.push_basic_input(InputKind::Primary);
5498        }
5499
5500        // Always attempt to path towards target
5501        self.path_toward_target(
5502            agent,
5503            controller,
5504            tgt_data.pos.0,
5505            read_data,
5506            Path::AtTarget,
5507            None,
5508        );
5509    }
5510
5511    pub fn handle_roshwalr_attack(
5512        &self,
5513        agent: &mut Agent,
5514        controller: &mut Controller,
5515        attack_data: &AttackData,
5516        tgt_data: &TargetData,
5517        read_data: &ReadData,
5518    ) {
5519        const SLOW_CHARGE_RANGE: f32 = 12.5;
5520        const SHOCKWAVE_RANGE: f32 = 12.5;
5521        const SHOCKWAVE_TIMER: f32 = 15.0;
5522        const MELEE_RANGE: f32 = 4.0;
5523
5524        enum ActionStateFCounters {
5525            FCounterRoshwalrAttack = 0,
5526        }
5527
5528        agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize] +=
5529            read_data.dt.0;
5530        if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5531        {
5532            // If already charging, keep charging if not in recover
5533            controller.push_basic_input(InputKind::Ability(0));
5534        } else if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 270.0 {
5535            if agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize]
5536                > SHOCKWAVE_TIMER
5537            {
5538                // Use shockwave if timer has gone for long enough
5539                controller.push_basic_input(InputKind::Ability(0));
5540
5541                if matches!(self.char_state, CharacterState::Shockwave(_)) {
5542                    // Resets action counter when using shockwave
5543                    agent.combat_state.counters
5544                        [ActionStateFCounters::FCounterRoshwalrAttack as usize] = 0.0;
5545                }
5546            } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
5547                // Basic attack if in melee range
5548                controller.push_basic_input(InputKind::Primary);
5549            }
5550        } else if attack_data.dist_sqrd > SLOW_CHARGE_RANGE.powi(2) {
5551            // Use slow charge if outside the range
5552            controller.push_basic_input(InputKind::Secondary);
5553        }
5554
5555        // Always attempt to path towards target
5556        self.path_toward_target(
5557            agent,
5558            controller,
5559            tgt_data.pos.0,
5560            read_data,
5561            Path::AtTarget,
5562            None,
5563        );
5564    }
5565
5566    pub fn handle_harvester_attack(
5567        &self,
5568        agent: &mut Agent,
5569        controller: &mut Controller,
5570        attack_data: &AttackData,
5571        tgt_data: &TargetData,
5572        read_data: &ReadData,
5573        rng: &mut impl Rng,
5574    ) {
5575        // === reference ===
5576        // Inputs:
5577        //   Primary: scythe
5578        //   Secondary: firebreath
5579        //   Auxiliary
5580        //     0: explosivepumpkin
5581        //     1: ensaringvines_sparse
5582        //     2: ensaringvines_dense
5583
5584        // === setup ===
5585
5586        // --- static ---
5587        // behaviour parameters
5588        const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60;
5589        const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30;
5590        const PATH_RANGE_FACTOR: f32 = 0.4; // get comfortably in range, but give player room to breathe
5591        const SCYTHE_RANGE_FACTOR: f32 = 0.75; // start attack while suitably in range
5592        const SCYTHE_AIM_FACTOR: f32 = 0.7;
5593        const FIREBREATH_RANGE_FACTOR: f32 = 0.7;
5594        const FIREBREATH_AIM_FACTOR: f32 = 0.8;
5595        const FIREBREATH_TIME_LIMIT: f32 = 4.0;
5596        const FIREBREATH_SHORT_TIME_LIMIT: f32 = 2.5; // cutoff sooner at close range
5597        const FIREBREATH_COOLDOWN: f32 = 3.5;
5598        const PUMPKIN_RANGE_FACTOR: f32 = 0.75;
5599        const CLOSE_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 7.0]; // variation in attacks at close range
5600        const MID_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 4.5]; //   ^                       mid
5601        const FAR_PUMPKIN_COOLDOWN_SPAN: [f32; 2] = [3.0, 5.0]; // allows for pathing to player between throws
5602
5603        // conditions
5604        const HAS_SUMMONED_FIRST_VINES: usize = 0;
5605        const HAS_SUMMONED_SECOND_VINES: usize = 1;
5606        // timers
5607        const FIREBREATH: usize = 0;
5608        const MIXUP: usize = 1;
5609        const FAR_PUMPKIN: usize = 2;
5610        //counters
5611        const CLOSE_MIXUP_COOLDOWN: usize = 0;
5612        const MID_MIXUP_COOLDOWN: usize = 1;
5613        const FAR_PUMPKIN_COOLDOWN: usize = 2;
5614
5615        // line of sight check
5616        let line_of_sight_with_target = || {
5617            entities_have_line_of_sight(
5618                self.pos,
5619                self.body,
5620                self.scale,
5621                tgt_data.pos,
5622                tgt_data.body,
5623                tgt_data.scale,
5624                read_data,
5625            )
5626        };
5627
5628        // --- dynamic ---
5629        // attack data
5630        let (scythe_range, scythe_angle) = {
5631            if let Some(AbilityData::BasicMelee { range, angle, .. }) =
5632                self.extract_ability(AbilityInput::Primary)
5633            {
5634                (range, angle)
5635            } else {
5636                (0.0, 0.0)
5637            }
5638        };
5639        let (firebreath_range, firebreath_angle) = {
5640            if let Some(AbilityData::BasicBeam { range, angle, .. }) =
5641                self.extract_ability(AbilityInput::Secondary)
5642            {
5643                (range, angle)
5644            } else {
5645                (0.0, 0.0)
5646            }
5647        };
5648        let pumpkin_speed = {
5649            if let Some(AbilityData::BasicRanged {
5650                projectile_speed, ..
5651            }) = self.extract_ability(AbilityInput::Auxiliary(0))
5652            {
5653                projectile_speed
5654            } else {
5655                0.0
5656            }
5657        };
5658        // calculated attack data
5659        let pumpkin_max_range =
5660            projectile_flat_range(pumpkin_speed, self.body.map_or(0.0, |b| b.height()));
5661
5662        // character state info
5663        let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
5664        let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
5665        let is_in_summon_recovery = matches!(self.char_state, CharacterState::SpriteSummon(data) if matches!(data.stage_section, StageSection::Recover));
5666        let firebreath_timer = if let CharacterState::BasicBeam(data) = self.char_state {
5667            data.timer
5668        } else {
5669            Default::default()
5670        };
5671        let is_using_mixup = is_using_firebreath || is_using_pumpkin;
5672
5673        // initialise randomised cooldowns
5674        if !agent.combat_state.initialized {
5675            agent.combat_state.initialized = true;
5676            agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5677                rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5678            agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5679                rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5680            agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5681                rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5682        }
5683
5684        // === main ===
5685
5686        // --- timers ---
5687        if is_in_summon_recovery {
5688            // reset all timers when done summoning
5689            agent.combat_state.timers[FIREBREATH] = 0.0;
5690            agent.combat_state.timers[MIXUP] = 0.0;
5691            agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5692        } else {
5693            // handle state timers
5694            if is_using_firebreath {
5695                agent.combat_state.timers[FIREBREATH] = 0.0;
5696            } else {
5697                agent.combat_state.timers[FIREBREATH] += read_data.dt.0;
5698            }
5699            if is_using_mixup {
5700                agent.combat_state.timers[MIXUP] = 0.0;
5701            } else {
5702                agent.combat_state.timers[MIXUP] += read_data.dt.0;
5703            }
5704            if is_using_pumpkin {
5705                agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5706            } else {
5707                agent.combat_state.timers[FAR_PUMPKIN] += read_data.dt.0;
5708            }
5709        }
5710
5711        // --- attacks ---
5712        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5713        // second vine summon
5714        if health_fraction < SECOND_VINE_CREATION_THRESHOLD
5715            && !agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES]
5716        {
5717            // use the dense vine summon
5718            controller.push_basic_input(InputKind::Ability(2));
5719            // wait till recovery before finishing
5720            if is_in_summon_recovery {
5721                agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES] = true;
5722            }
5723        }
5724        // first vine summon
5725        else if health_fraction < FIRST_VINE_CREATION_THRESHOLD
5726            && !agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES]
5727        {
5728            // use the sparse vine summon
5729            controller.push_basic_input(InputKind::Ability(1));
5730            // wait till recovery before finishing
5731            if is_in_summon_recovery {
5732                agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES] = true;
5733            }
5734        }
5735        // close range
5736        else if attack_data.dist_sqrd
5737            < (attack_data.body_dist + scythe_range * SCYTHE_RANGE_FACTOR).powi(2)
5738        {
5739            // if using firebreath, keep going under short time limit
5740            if is_using_firebreath
5741                && firebreath_timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME_LIMIT)
5742            {
5743                controller.push_basic_input(InputKind::Secondary);
5744            }
5745            // in scythe angle
5746            if attack_data.angle < scythe_angle * SCYTHE_AIM_FACTOR {
5747                // on timer, randomly mixup attacks
5748                if agent.combat_state.timers[MIXUP]
5749                    > agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN]
5750                // for now, no line of sight check for consitency in attacks
5751                {
5752                    // if on firebreath cooldown, throw pumpkin
5753                    if agent.combat_state.timers[FIREBREATH] < FIREBREATH_COOLDOWN {
5754                        controller.push_basic_input(InputKind::Ability(0));
5755                    }
5756                    // otherwise, randomise between firebreath and pumpkin
5757                    else if rng.random_bool(0.5) {
5758                        controller.push_basic_input(InputKind::Secondary);
5759                    } else {
5760                        controller.push_basic_input(InputKind::Ability(0));
5761                    }
5762                    // reset mixup cooldown if actually being used
5763                    if is_using_mixup {
5764                        agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5765                            rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5766                    }
5767                }
5768                // default to using scythe melee
5769                else {
5770                    controller.push_basic_input(InputKind::Primary);
5771                }
5772            }
5773        // mid range (line of sight not needed for these 'suppressing' attacks)
5774        } else if attack_data.dist_sqrd < firebreath_range.powi(2) {
5775            // if using firebreath, keep going under full time limit
5776            #[expect(clippy::if_same_then_else)]
5777            if is_using_firebreath
5778                && firebreath_timer < Duration::from_secs_f32(FIREBREATH_TIME_LIMIT)
5779            {
5780                controller.push_basic_input(InputKind::Secondary);
5781            }
5782            // start using firebreath if close enough, in angle, and off cooldown
5783            else if attack_data.dist_sqrd < (firebreath_range * FIREBREATH_RANGE_FACTOR).powi(2)
5784                && attack_data.angle < firebreath_angle * FIREBREATH_AIM_FACTOR
5785                && agent.combat_state.timers[FIREBREATH] > FIREBREATH_COOLDOWN
5786            {
5787                controller.push_basic_input(InputKind::Secondary);
5788            }
5789            // on mixup timer, throw a pumpkin
5790            else if agent.combat_state.timers[MIXUP]
5791                > agent.combat_state.counters[MID_MIXUP_COOLDOWN]
5792            {
5793                controller.push_basic_input(InputKind::Ability(0));
5794                // reset mixup cooldown if pumpkin is actually being used
5795                if is_using_pumpkin {
5796                    agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5797                        rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5798                }
5799            }
5800        }
5801        // long range (with line of sight)
5802        else if attack_data.dist_sqrd < (pumpkin_max_range * PUMPKIN_RANGE_FACTOR).powi(2)
5803            && agent.combat_state.timers[FAR_PUMPKIN]
5804                > agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN]
5805            && line_of_sight_with_target()
5806        {
5807            // throw pumpkin
5808            controller.push_basic_input(InputKind::Ability(0));
5809            // reset pumpkin cooldown if actually being used
5810            if is_using_pumpkin {
5811                agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5812                    rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5813            }
5814        }
5815
5816        // --- movement ---
5817        // closing gap
5818        if attack_data.dist_sqrd
5819            > (attack_data.body_dist + scythe_range * PATH_RANGE_FACTOR).powi(2)
5820        {
5821            self.path_toward_target(
5822                agent,
5823                controller,
5824                tgt_data.pos.0,
5825                read_data,
5826                Path::AtTarget,
5827                None,
5828            );
5829        }
5830        // closing angle
5831        else if attack_data.angle > 0.0 {
5832            // some movement is required to trigger re-orientation
5833            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
5834                .xy()
5835                .try_normalized()
5836                .unwrap_or_else(Vec2::zero)
5837                * 0.001; // scaled way down to minimise position change and keep close rotation consistent
5838        }
5839    }
5840
5841    pub fn handle_frostgigas_attack(
5842        &self,
5843        agent: &mut Agent,
5844        controller: &mut Controller,
5845        attack_data: &AttackData,
5846        tgt_data: &TargetData,
5847        read_data: &ReadData,
5848        rng: &mut impl Rng,
5849    ) {
5850        const GIGAS_MELEE_RANGE: f32 = 12.0;
5851        const GIGAS_SPIKE_RANGE: f32 = 16.0;
5852        const ICEBOMB_RANGE: f32 = 70.0;
5853        const GIGAS_LEAP_RANGE: f32 = 50.0;
5854        const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.;
5855        const FLASHFREEZE_RANGE: f32 = 30.;
5856
5857        enum ActionStateTimers {
5858            AttackChange,
5859            Bonk,
5860        }
5861
5862        enum ActionStateFCounters {
5863            FCounterMinionSummonThreshold = 0,
5864        }
5865
5866        enum ActionStateICounters {
5867            /// An ability that is forced to fully complete until moving on to
5868            /// other attacks.
5869            /// 1 = Leap shockwave, 2 = Flashfreeze, 3 = Spike summon,
5870            /// 4 = Whirlwind, 5 = Remote ice spikes, 6 = Ice bombs
5871            CurrentAbility = 0,
5872        }
5873
5874        let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0);
5875        let remote_spikes_action = || ControlAction::StartInput {
5876            input: InputKind::Ability(5),
5877            target_entity: None,
5878            select_pos: Some(tgt_data.pos.0),
5879        };
5880
5881        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5882        // Sets counter at start of combat, using `condition` to keep track of whether
5883        // it was already initialized
5884        if !agent.combat_state.initialized {
5885            agent.combat_state.counters
5886                [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5887                1.0 - MINION_SUMMON_THRESHOLD;
5888            agent.combat_state.initialized = true;
5889        }
5890
5891        // Update timers
5892        if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 {
5893            agent.combat_state.timers[ActionStateTimers::AttackChange as usize] = 0.0;
5894        } else {
5895            agent.combat_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0;
5896        }
5897        agent.combat_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0;
5898
5899        if health_fraction
5900            < agent.combat_state.counters
5901                [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5902        {
5903            // Summon minions at particular thresholds of health
5904            controller.push_basic_input(InputKind::Ability(3));
5905
5906            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5907            {
5908                agent.combat_state.counters
5909                    [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5910                    MINION_SUMMON_THRESHOLD;
5911            }
5912        // Continue casting any attacks that are forced to complete
5913        } else if let Some(ability) = Some(
5914            &mut agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize],
5915        )
5916        .filter(|i| **i != 0)
5917        {
5918            if *ability == 3 && should_use_targeted_spikes() {
5919                *ability = 5
5920            };
5921
5922            let reset = match ability {
5923                // Must be rolled
5924                1 => {
5925                    controller.push_basic_input(InputKind::Ability(1));
5926                    matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover))
5927                },
5928                // Attacker will have to run away here
5929                2 => {
5930                    controller.push_basic_input(InputKind::Ability(4));
5931                    matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover))
5932                },
5933                // Avoid the spikes!
5934                3 => {
5935                    controller.push_basic_input(InputKind::Ability(0));
5936                    matches!(self.char_state, CharacterState::SpriteSummon(c)
5937                        if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner)))
5938                },
5939                // Long whirlwind attack
5940                4 => {
5941                    controller.push_basic_input(InputKind::Ability(7));
5942                    matches!(self.char_state, CharacterState::RapidMelee(c) if matches!(c.stage_section, StageSection::Recover))
5943                },
5944                // Remote ice spikes
5945                5 => {
5946                    controller.push_action(remote_spikes_action());
5947                    matches!(self.char_state, CharacterState::SpriteSummon(c)
5948                        if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target)))
5949                },
5950                // Ice bombs
5951                6 => {
5952                    controller.push_basic_input(InputKind::Ability(2));
5953                    matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
5954                },
5955                // Should never happen
5956                _ => true,
5957            };
5958
5959            if reset {
5960                *ability = 0;
5961            }
5962        // If our target is nearby and above us, potentially cheesing, have a
5963        // chance of summoning remote ice spikes or throwing ice bombs.
5964        // Cheesing from less than 5 blocks away is usually not possible
5965        } else if attack_data.dist_sqrd > 5f32.powi(2)
5966            // Calculate the "cheesing factor" (height of the normalized position difference)
5967            && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6
5968            // Make it happen at about every 10 seconds!
5969            && rng.random_bool((0.2 * read_data.dt.0).min(1.0) as f64)
5970        {
5971            agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5972                rng.random_range(5..=6);
5973        } else if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
5974            // Bonk the target every 10-8 s
5975            if agent.combat_state.timers[ActionStateTimers::Bonk as usize] > 10. {
5976                controller.push_basic_input(InputKind::Ability(6));
5977
5978                if matches!(self.char_state, CharacterState::BasicMelee(c)
5979                    if matches!(c.stage_section, StageSection::Recover) &&
5980                    c.static_data.ability_info.ability.is_some_and(|meta| matches!(meta.ability, Ability::MainWeaponAux(6)))
5981                ) {
5982                    agent.combat_state.timers[ActionStateTimers::Bonk as usize] =
5983                        rng.random_range(0.0..3.0);
5984                }
5985            // Have a small chance at starting a mixup attack
5986            } else if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 4.0
5987                && rng.random_bool(0.1 * read_data.dt.0.min(1.0) as f64)
5988            {
5989                agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5990                    rng.random_range(1..=4);
5991            // Melee the target, do a whirlwind whenever he is trying to go
5992            // behind or after every 5s
5993            } else if attack_data.angle > 90.0
5994                || agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 5.0
5995            {
5996                // If our target is *very* behind, punish with a whirlwind
5997                if attack_data.angle > 120.0 {
5998                    agent.combat_state.int_counters
5999                        [ActionStateICounters::CurrentAbility as usize] = 4;
6000                } else {
6001                    controller.push_basic_input(InputKind::Secondary);
6002                }
6003            } else {
6004                controller.push_basic_input(InputKind::Primary);
6005            }
6006        } else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
6007            && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 2.0
6008        {
6009            if should_use_targeted_spikes() {
6010                controller.push_action(remote_spikes_action());
6011            } else {
6012                controller.push_basic_input(InputKind::Ability(0));
6013            }
6014        } else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2)
6015            && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 4.0
6016        {
6017            controller.push_basic_input(InputKind::Ability(4));
6018        // Start a leap after either every 3s or our target is not in LoS
6019        } else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2)
6020            && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 3.0
6021        {
6022            controller.push_basic_input(InputKind::Ability(1));
6023        } else if attack_data.dist_sqrd < ICEBOMB_RANGE.powi(2)
6024            && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 3.0
6025        {
6026            controller.push_basic_input(InputKind::Ability(2));
6027        // Spawn ice sprites under distant attackers
6028        } else {
6029            controller.push_action(remote_spikes_action());
6030        }
6031
6032        // Always attempt to path towards target
6033        self.path_toward_target(
6034            agent,
6035            controller,
6036            tgt_data.pos.0,
6037            read_data,
6038            Path::AtTarget,
6039            attack_data.in_min_range().then_some(0.1),
6040        );
6041    }
6042
6043    pub fn handle_boreal_hammer_attack(
6044        &self,
6045        agent: &mut Agent,
6046        controller: &mut Controller,
6047        attack_data: &AttackData,
6048        tgt_data: &TargetData,
6049        read_data: &ReadData,
6050        rng: &mut impl Rng,
6051    ) {
6052        enum ActionStateTimers {
6053            TimerHandleHammerAttack = 0,
6054        }
6055
6056        let has_energy = |need| self.energy.current() > need;
6057
6058        let use_leap = |controller: &mut Controller| {
6059            controller.push_basic_input(InputKind::Ability(0));
6060        };
6061
6062        agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] +=
6063            read_data.dt.0;
6064
6065        if attack_data.in_min_range() && attack_data.angle < 45.0 {
6066            controller.inputs.move_dir = Vec2::zero();
6067            if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 4.0
6068            {
6069                controller.push_cancel_input(InputKind::Secondary);
6070                agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] =
6071                    0.0;
6072            } else if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize]
6073                > 3.0
6074            {
6075                controller.push_basic_input(InputKind::Secondary);
6076            } else if has_energy(50.0) && rng.random_bool(0.9) {
6077                use_leap(controller);
6078            } else {
6079                controller.push_basic_input(InputKind::Primary);
6080            }
6081        } else {
6082            self.path_toward_target(
6083                agent,
6084                controller,
6085                tgt_data.pos.0,
6086                read_data,
6087                Path::Separate,
6088                None,
6089            );
6090
6091            if attack_data.dist_sqrd < 32.0f32.powi(2)
6092                && entities_have_line_of_sight(
6093                    self.pos,
6094                    self.body,
6095                    self.scale,
6096                    tgt_data.pos,
6097                    tgt_data.body,
6098                    tgt_data.scale,
6099                    read_data,
6100                )
6101            {
6102                if rng.random_bool(0.5) && has_energy(50.0) {
6103                    use_leap(controller);
6104                } else if agent.combat_state.timers
6105                    [ActionStateTimers::TimerHandleHammerAttack as usize]
6106                    > 2.0
6107                {
6108                    controller.push_basic_input(InputKind::Secondary);
6109                } else if agent.combat_state.timers
6110                    [ActionStateTimers::TimerHandleHammerAttack as usize]
6111                    > 4.0
6112                {
6113                    controller.push_cancel_input(InputKind::Secondary);
6114                    agent.combat_state.timers
6115                        [ActionStateTimers::TimerHandleHammerAttack as usize] = 0.0;
6116                }
6117            }
6118        }
6119    }
6120
6121    pub fn handle_boreal_bow_attack(
6122        &self,
6123        agent: &mut Agent,
6124        controller: &mut Controller,
6125        attack_data: &AttackData,
6126        tgt_data: &TargetData,
6127        read_data: &ReadData,
6128        rng: &mut impl Rng,
6129    ) {
6130        let line_of_sight_with_target = || {
6131            entities_have_line_of_sight(
6132                self.pos,
6133                self.body,
6134                self.scale,
6135                tgt_data.pos,
6136                tgt_data.body,
6137                tgt_data.scale,
6138                read_data,
6139            )
6140        };
6141
6142        let has_energy = |need| self.energy.current() > need;
6143
6144        let use_trap = |controller: &mut Controller| {
6145            controller.push_basic_input(InputKind::Ability(0));
6146        };
6147
6148        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6149            if rng.random_bool(0.5) && has_energy(15.0) {
6150                controller.push_basic_input(InputKind::Secondary);
6151            } else if attack_data.angle < 15.0 {
6152                controller.push_basic_input(InputKind::Primary);
6153            }
6154        } else if attack_data.dist_sqrd < (4.0 * attack_data.min_attack_dist).powi(2)
6155            && line_of_sight_with_target()
6156        {
6157            if rng.random_bool(0.5) && has_energy(15.0) {
6158                controller.push_basic_input(InputKind::Secondary);
6159            } else if has_energy(20.0) {
6160                use_trap(controller);
6161            }
6162        }
6163
6164        if has_energy(50.0) {
6165            if attack_data.dist_sqrd < (10.0 * attack_data.min_attack_dist).powi(2) {
6166                // Attempt to circle the target if neither too close nor too far
6167                if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6168                    &*read_data.terrain,
6169                    self.pos.0,
6170                    self.vel.0,
6171                    tgt_data.pos.0,
6172                    TraversalConfig {
6173                        min_tgt_dist: 1.25,
6174                        ..self.traversal_config
6175                    },
6176                    &read_data.time,
6177                ) {
6178                    self.unstuck_if(stuck, controller);
6179                    if line_of_sight_with_target() && attack_data.angle < 45.0 {
6180                        controller.inputs.move_dir = bearing
6181                            .xy()
6182                            .rotated_z(rng.random_range(0.5..1.57))
6183                            .try_normalized()
6184                            .unwrap_or_else(Vec2::zero)
6185                            * 2.0
6186                            * speed;
6187                    } else {
6188                        // Unless cannot see target, then move towards them
6189                        controller.inputs.move_dir =
6190                            bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6191                        self.jump_if(bearing.z > 1.5, controller);
6192                        controller.inputs.move_z = bearing.z;
6193                    }
6194                }
6195            } else {
6196                // Path to enemy if too far
6197                self.path_toward_target(
6198                    agent,
6199                    controller,
6200                    tgt_data.pos.0,
6201                    read_data,
6202                    Path::AtTarget,
6203                    None,
6204                );
6205            }
6206        } else {
6207            // Path to enemy for melee hits if need more energy
6208            self.path_toward_target(
6209                agent,
6210                controller,
6211                tgt_data.pos.0,
6212                read_data,
6213                Path::AtTarget,
6214                None,
6215            );
6216        }
6217    }
6218
6219    pub fn handle_firegigas_attack(
6220        &self,
6221        agent: &mut Agent,
6222        controller: &mut Controller,
6223        attack_data: &AttackData,
6224        tgt_data: &TargetData,
6225        read_data: &ReadData,
6226        rng: &mut impl Rng,
6227    ) {
6228        const MELEE_RANGE: f32 = 12.0;
6229        const RANGED_RANGE: f32 = 27.0;
6230        const LEAP_RANGE: f32 = 50.0;
6231        const MINION_SUMMON_THRESHOLD: f32 = 1.0 / 8.0;
6232        const OVERHEAT_DUR: f32 = 3.0;
6233        const FORCE_GAP_CLOSER_TIMEOUT: f32 = 10.0;
6234
6235        enum ActionStateTimers {
6236            Special,
6237            Overheat,
6238            OutOfMeleeRange,
6239        }
6240
6241        enum ActionStateFCounters {
6242            FCounterMinionSummonThreshold,
6243        }
6244
6245        enum ActionStateConditions {
6246            VerticalStrikeCombo,
6247            WhirlwindTwice,
6248        }
6249
6250        const FAST_SLASH: InputKind = InputKind::Primary;
6251        const FAST_THRUST: InputKind = InputKind::Secondary;
6252        const SLOW_SLASH: InputKind = InputKind::Ability(0);
6253        const SLOW_THRUST: InputKind = InputKind::Ability(1);
6254        const LAVA_LEAP: InputKind = InputKind::Ability(2);
6255        const VERTICAL_STRIKE: InputKind = InputKind::Ability(3);
6256        const OVERHEAT: InputKind = InputKind::Ability(4);
6257        const WHIRLWIND: InputKind = InputKind::Ability(5);
6258        const EXPLOSIVE_STRIKE: InputKind = InputKind::Ability(6);
6259        const FIRE_PILLARS: InputKind = InputKind::Ability(7);
6260        const TARGETED_FIRE_PILLAR: InputKind = InputKind::Ability(8);
6261        const ASHEN_SUMMONS: InputKind = InputKind::Ability(9);
6262        const PARRY_PUNISH: InputKind = InputKind::Ability(10);
6263
6264        fn choose_weighted<const N: usize>(
6265            rng: &mut impl Rng,
6266            choices: [(InputKind, f32); N],
6267        ) -> InputKind {
6268            choices
6269                .choose_weighted(rng, |(_, weight)| *weight)
6270                .expect("weights should be valid")
6271                .0
6272        }
6273
6274        // Basic melee strikes
6275        fn rand_basic(rng: &mut impl Rng, damage_fraction: f32) -> InputKind {
6276            choose_weighted(rng, [
6277                (FAST_SLASH, 2.0),
6278                (FAST_THRUST, 2.0),
6279                (SLOW_SLASH, 1.0 + damage_fraction),
6280                (SLOW_THRUST, 1.0 + damage_fraction),
6281            ])
6282        }
6283
6284        // Less frequent mixup attacks
6285        fn rand_special(rng: &mut impl Rng) -> InputKind {
6286            choose_weighted(rng, [
6287                (WHIRLWIND, 6.0),
6288                (VERTICAL_STRIKE, 6.0),
6289                (OVERHEAT, 6.0),
6290                (EXPLOSIVE_STRIKE, 1.0),
6291                (LAVA_LEAP, 1.0),
6292                (FIRE_PILLARS, 1.0),
6293            ])
6294        }
6295
6296        // Attacks capable of also hitting entities behind the gigas
6297        fn rand_aoe(rng: &mut impl Rng) -> InputKind {
6298            choose_weighted(rng, [
6299                (EXPLOSIVE_STRIKE, 1.0),
6300                (FIRE_PILLARS, 1.0),
6301                (WHIRLWIND, 2.0),
6302            ])
6303        }
6304
6305        // Attacks capable of also hitting entities further away
6306        fn rand_ranged(rng: &mut impl Rng) -> InputKind {
6307            choose_weighted(rng, [
6308                (EXPLOSIVE_STRIKE, 1.0),
6309                (FIRE_PILLARS, 1.0),
6310                (OVERHEAT, 1.0),
6311            ])
6312        }
6313
6314        let cast_targeted_fire_pillar = |c: &mut Controller| {
6315            c.push_action(ControlAction::StartInput {
6316                input: TARGETED_FIRE_PILLAR,
6317                target_entity: tgt_data.uid,
6318                select_pos: None,
6319            })
6320        };
6321
6322        fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6323            !matches!(
6324                char_state,
6325                CharacterState::LeapMelee(_)
6326                    | CharacterState::BasicMelee(_)
6327                    | CharacterState::BasicBeam(_)
6328                    | CharacterState::BasicSummon(_)
6329                    | CharacterState::SpriteSummon(_)
6330            )
6331        }
6332
6333        // Initializes counters at start of combat
6334        if !agent.combat_state.initialized {
6335            agent.combat_state.counters
6336                [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
6337                1.0 - MINION_SUMMON_THRESHOLD;
6338            agent.combat_state.initialized = true;
6339        }
6340
6341        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
6342        let damage_fraction = 1.0 - health_fraction;
6343        // Calculate the "cheesing factor" (height of the normalized position
6344        // difference), unless the target is airborne from our hit
6345        // Cheesing from close range is usually not possible
6346        let cheesed_from_above = !agent.combat_state.conditions
6347            [ActionStateConditions::VerticalStrikeCombo as usize]
6348            && attack_data.dist_sqrd > 5f32.powi(2)
6349            && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6;
6350        // Being in water also triggers this as there are a lot of exploits with water
6351        let cheesed_in_water = matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { kind: LiquidKind::Water, depth, .. }) if depth >= 2.0);
6352        let cheesed = cheesed_from_above || cheesed_in_water;
6353        let tgt_airborne = tgt_data
6354            .physics_state
6355            .is_some_and(|physics| physics.on_ground.is_none() && physics.in_liquid().is_none());
6356        let tgt_missed_parry = match tgt_data.char_state {
6357            Some(CharacterState::RiposteMelee(data)) => {
6358                matches!(data.stage_section, StageSection::Recover) && data.whiffed
6359            },
6360            Some(CharacterState::BasicBlock(data)) => {
6361                matches!(data.stage_section, StageSection::Recover)
6362                    && !data.static_data.parry_window.recover
6363                    && !data.is_parry
6364            },
6365            _ => false,
6366        };
6367        let casting_beam = matches!(self.char_state, CharacterState::BasicBeam(_))
6368            && self.char_state.stage_section() != Some(StageSection::Recover);
6369
6370        // Update timers
6371        agent.combat_state.timers[ActionStateTimers::Special as usize] += read_data.dt.0;
6372        if casting_beam {
6373            agent.combat_state.timers[ActionStateTimers::Overheat as usize] += read_data.dt.0;
6374        } else {
6375            agent.combat_state.timers[ActionStateTimers::Overheat as usize] = 0.0;
6376        }
6377        if attack_data.dist_sqrd > MELEE_RANGE.powi(2) {
6378            agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize] +=
6379                read_data.dt.0;
6380        } else {
6381            agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize] = 0.0;
6382        }
6383
6384        // Cast abilities
6385        if casting_beam
6386            && agent.combat_state.timers[ActionStateTimers::Overheat as usize] < OVERHEAT_DUR
6387        {
6388            controller.push_basic_input(OVERHEAT);
6389            controller.inputs.look_dir = self
6390                .ori
6391                .look_dir()
6392                .to_horizontal()
6393                .unwrap_or_else(|| self.ori.look_dir());
6394        } else if health_fraction
6395            < agent.combat_state.counters
6396                [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
6397        {
6398            // Summon minions at particular thresholds of health
6399            controller.push_basic_input(ASHEN_SUMMONS);
6400
6401            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
6402            {
6403                agent.combat_state.counters
6404                    [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
6405                    MINION_SUMMON_THRESHOLD;
6406            }
6407        } else if can_cast_new_ability(self.char_state) {
6408            if cheesed {
6409                cast_targeted_fire_pillar(controller);
6410            } else if agent.combat_state.conditions
6411                [ActionStateConditions::VerticalStrikeCombo as usize]
6412            {
6413                // If landed vertical strike combo target while they are airborne
6414                if tgt_airborne {
6415                    controller.push_basic_input(FAST_THRUST);
6416                }
6417
6418                agent.combat_state.conditions
6419                    [ActionStateConditions::VerticalStrikeCombo as usize] = false;
6420            } else if agent.combat_state.conditions[ActionStateConditions::WhirlwindTwice as usize]
6421            {
6422                controller.push_basic_input(WHIRLWIND);
6423                agent.combat_state.conditions[ActionStateConditions::WhirlwindTwice as usize] =
6424                    false;
6425            } else if agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize]
6426                > FORCE_GAP_CLOSER_TIMEOUT
6427            {
6428                // Use a gap closer if the target has been out of melee distance for a while
6429                controller.push_basic_input(LAVA_LEAP);
6430            } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
6431                if tgt_missed_parry {
6432                    controller.push_basic_input(PARRY_PUNISH);
6433                    agent.combat_state.conditions
6434                        [ActionStateConditions::VerticalStrikeCombo as usize] = true;
6435                } else if agent.combat_state.timers[ActionStateTimers::Special as usize] > 10.0 {
6436                    // Use a special ability periodically
6437                    let rand_special = rand_special(rng);
6438                    match rand_special {
6439                        VERTICAL_STRIKE => {
6440                            agent.combat_state.conditions
6441                                [ActionStateConditions::VerticalStrikeCombo as usize] = true
6442                        },
6443                        WHIRLWIND if rng.random_bool(0.2) => {
6444                            agent.combat_state.conditions
6445                                [ActionStateConditions::WhirlwindTwice as usize] = true
6446                        },
6447                        _ => {},
6448                    }
6449                    controller.push_basic_input(rand_special);
6450
6451                    agent.combat_state.timers[ActionStateTimers::Special as usize] =
6452                        rng.random_range(0.0..3.0 + 5.0 * damage_fraction);
6453                } else if attack_data.angle > 90.0 {
6454                    // Cast an aoe ability to hit the target if they are behind the entity
6455                    let rand_aoe = rand_aoe(rng);
6456                    match rand_aoe {
6457                        WHIRLWIND if rng.random_bool(0.2) => {
6458                            agent.combat_state.conditions
6459                                [ActionStateConditions::WhirlwindTwice as usize] = true
6460                        },
6461                        _ => {},
6462                    }
6463
6464                    controller.push_basic_input(rand_aoe);
6465                } else {
6466                    // Use a random basic melee hit
6467                    controller.push_basic_input(rand_basic(rng, damage_fraction));
6468                }
6469            } else if attack_data.dist_sqrd < RANGED_RANGE.powi(2) {
6470                // Use ranged ability if target is out of melee range
6471                if rng.random_bool(0.05) {
6472                    controller.push_basic_input(rand_ranged(rng));
6473                }
6474            } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) {
6475                // Use a gap closer if the target is even further away
6476                controller.push_basic_input(LAVA_LEAP);
6477            } else if rng.random_bool(0.1) {
6478                // Use a targeted fire pillar if the target is out of range of everything else
6479                cast_targeted_fire_pillar(controller);
6480            }
6481        }
6482
6483        self.path_toward_target(
6484            agent,
6485            controller,
6486            tgt_data.pos.0,
6487            read_data,
6488            Path::AtTarget,
6489            attack_data.in_min_range().then_some(0.1),
6490        );
6491
6492        // Get out of lava if submerged
6493        if self.physics_state.in_liquid().is_some() {
6494            controller.push_basic_input(InputKind::Jump);
6495        }
6496        if self.physics_state.in_liquid().is_some() {
6497            controller.inputs.move_z = 1.0;
6498        }
6499    }
6500
6501    pub fn handle_ashen_axe_attack(
6502        &self,
6503        agent: &mut Agent,
6504        controller: &mut Controller,
6505        attack_data: &AttackData,
6506        tgt_data: &TargetData,
6507        read_data: &ReadData,
6508        rng: &mut impl Rng,
6509    ) {
6510        const IMMOLATION_COOLDOWN: f32 = 50.0;
6511        const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
6512            desired_energy: 30.0,
6513            combo_scaling_buildup: 0,
6514        };
6515
6516        enum ActionStateTimers {
6517            SinceSelfImmolation,
6518        }
6519
6520        const DOUBLE_STRIKE: InputKind = InputKind::Primary;
6521        const FLAME_WAVE: InputKind = InputKind::Secondary;
6522        const KNOCKBACK_COMBO: InputKind = InputKind::Ability(0);
6523        const SELF_IMMOLATION: InputKind = InputKind::Ability(1);
6524
6525        fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6526            !matches!(
6527                char_state,
6528                CharacterState::ComboMelee2(_)
6529                    | CharacterState::Shockwave(_)
6530                    | CharacterState::SelfBuff(_)
6531            )
6532        }
6533
6534        let could_use = |input| {
6535            Option::<AbilityInput>::from(input)
6536                .and_then(|ability_input| self.extract_ability(ability_input))
6537                .is_some_and(|ability_data| {
6538                    ability_data.could_use(
6539                        attack_data,
6540                        self,
6541                        tgt_data,
6542                        read_data,
6543                        ABILITY_PREFERENCES,
6544                    )
6545                })
6546        };
6547
6548        // Initialize immolation cooldown to 0
6549        if !agent.combat_state.initialized {
6550            agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] =
6551                IMMOLATION_COOLDOWN;
6552            agent.combat_state.initialized = true;
6553        }
6554
6555        agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] +=
6556            read_data.dt.0;
6557
6558        if self
6559            .char_state
6560            .ability_info()
6561            .map(|ai| ai.input)
6562            .is_some_and(|input_kind| input_kind == KNOCKBACK_COMBO)
6563        {
6564            controller.push_basic_input(KNOCKBACK_COMBO);
6565        } else if can_cast_new_ability(self.char_state)
6566            && agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize]
6567                >= IMMOLATION_COOLDOWN
6568            && could_use(SELF_IMMOLATION)
6569        {
6570            agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] =
6571                rng.random_range(0.0..5.0);
6572
6573            controller.push_basic_input(SELF_IMMOLATION);
6574        } else if rng.random_bool(0.35) && could_use(KNOCKBACK_COMBO) {
6575            controller.push_basic_input(KNOCKBACK_COMBO);
6576        } else if could_use(DOUBLE_STRIKE) {
6577            controller.push_basic_input(DOUBLE_STRIKE);
6578        } else if rng.random_bool(0.2) && could_use(FLAME_WAVE) {
6579            controller.push_basic_input(FLAME_WAVE);
6580        }
6581
6582        self.path_toward_target(
6583            agent,
6584            controller,
6585            tgt_data.pos.0,
6586            read_data,
6587            Path::AtTarget,
6588            None,
6589        );
6590    }
6591
6592    pub fn handle_ashen_staff_attack(
6593        &self,
6594        agent: &mut Agent,
6595        controller: &mut Controller,
6596        attack_data: &AttackData,
6597        tgt_data: &TargetData,
6598        read_data: &ReadData,
6599        rng: &mut impl Rng,
6600    ) {
6601        const ABILITY_COOLDOWN: f32 = 50.0;
6602        const INITIAL_COOLDOWN: f32 = ABILITY_COOLDOWN - 10.0;
6603        const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
6604            desired_energy: 40.0,
6605            combo_scaling_buildup: 0,
6606        };
6607
6608        enum ActionStateTimers {
6609            SinceAbility,
6610        }
6611
6612        const FIREBALL: InputKind = InputKind::Primary;
6613        const FLAME_WALL: InputKind = InputKind::Ability(0);
6614        const SUMMON_CRUX: InputKind = InputKind::Ability(1);
6615
6616        fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6617            !matches!(
6618                char_state,
6619                CharacterState::BasicRanged(_)
6620                    | CharacterState::BasicBeam(_)
6621                    | CharacterState::RapidMelee(_)
6622                    | CharacterState::BasicAura(_)
6623            )
6624        }
6625
6626        let could_use = |input| {
6627            Option::<AbilityInput>::from(input)
6628                .and_then(|ability_input| self.extract_ability(ability_input))
6629                .is_some_and(|ability_data| {
6630                    ability_data.could_use(
6631                        attack_data,
6632                        self,
6633                        tgt_data,
6634                        read_data,
6635                        ABILITY_PREFERENCES,
6636                    )
6637                })
6638        };
6639
6640        // Initialize special ability cooldown
6641        if !agent.combat_state.initialized {
6642            agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] = INITIAL_COOLDOWN;
6643            agent.combat_state.initialized = true;
6644        }
6645
6646        agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] += read_data.dt.0;
6647
6648        if can_cast_new_ability(self.char_state)
6649            && agent.combat_state.timers[ActionStateTimers::SinceAbility as usize]
6650                >= ABILITY_COOLDOWN
6651            && (could_use(FLAME_WALL) || could_use(SUMMON_CRUX))
6652        {
6653            agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] =
6654                rng.random_range(0.0..5.0);
6655
6656            if could_use(FLAME_WALL) && (rng.random_bool(0.5) || !could_use(SUMMON_CRUX)) {
6657                controller.push_basic_input(FLAME_WALL);
6658            } else {
6659                controller.push_basic_input(SUMMON_CRUX);
6660            }
6661        } else if rng.random_bool(0.5) && could_use(FIREBALL) {
6662            controller.push_basic_input(FIREBALL);
6663        }
6664
6665        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6666            // Attempt to move away from target if too close
6667            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6668                &*read_data.terrain,
6669                self.pos.0,
6670                self.vel.0,
6671                tgt_data.pos.0,
6672                TraversalConfig {
6673                    min_tgt_dist: 1.25,
6674                    ..self.traversal_config
6675                },
6676                &read_data.time,
6677            ) {
6678                self.unstuck_if(stuck, controller);
6679                controller.inputs.move_dir =
6680                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6681            }
6682        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6683            // Else attempt to circle target if neither too close nor too far
6684            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6685                &*read_data.terrain,
6686                self.pos.0,
6687                self.vel.0,
6688                tgt_data.pos.0,
6689                TraversalConfig {
6690                    min_tgt_dist: 1.25,
6691                    ..self.traversal_config
6692                },
6693                &read_data.time,
6694            ) {
6695                self.unstuck_if(stuck, controller);
6696                if entities_have_line_of_sight(
6697                    self.pos,
6698                    self.body,
6699                    self.scale,
6700                    tgt_data.pos,
6701                    tgt_data.body,
6702                    tgt_data.scale,
6703                    read_data,
6704                ) && attack_data.angle < 45.0
6705                {
6706                    controller.inputs.move_dir = bearing
6707                        .xy()
6708                        .rotated_z(rng.random_range(-1.57..-0.5))
6709                        .try_normalized()
6710                        .unwrap_or_else(Vec2::zero)
6711                        * speed;
6712                } else {
6713                    // Unless cannot see target, then move towards them
6714                    controller.inputs.move_dir =
6715                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6716                    self.jump_if(bearing.z > 1.5, controller);
6717                    controller.inputs.move_z = bearing.z;
6718                }
6719            }
6720        } else {
6721            // If too far, move towards target
6722            self.path_toward_target(
6723                agent,
6724                controller,
6725                tgt_data.pos.0,
6726                read_data,
6727                Path::AtTarget,
6728                None,
6729            );
6730        }
6731    }
6732
6733    pub fn handle_cardinal_attack(
6734        &self,
6735        agent: &mut Agent,
6736        controller: &mut Controller,
6737        attack_data: &AttackData,
6738        tgt_data: &TargetData,
6739        read_data: &ReadData,
6740        rng: &mut impl Rng,
6741    ) {
6742        const DESIRED_ENERGY_LEVEL: f32 = 50.0;
6743        const DESIRED_COMBO_LEVEL: u32 = 8;
6744        const MINION_SUMMON_THRESHOLD: f32 = 0.10;
6745
6746        enum ActionStateConditions {
6747            ConditionCounterInitialized = 0,
6748        }
6749
6750        enum ActionStateFCounters {
6751            FCounterHealthThreshold = 0,
6752        }
6753
6754        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
6755        // Sets counter at start of combat, using `condition` to keep track of whether
6756        // it was already intitialized
6757        if !agent.combat_state.conditions
6758            [ActionStateConditions::ConditionCounterInitialized as usize]
6759        {
6760            agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
6761                1.0 - MINION_SUMMON_THRESHOLD;
6762            agent.combat_state.conditions
6763                [ActionStateConditions::ConditionCounterInitialized as usize] = true;
6764        }
6765
6766        if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
6767            > health_fraction
6768        {
6769            // Summon minions at particular thresholds of health
6770            controller.push_basic_input(InputKind::Ability(1));
6771
6772            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
6773            {
6774                agent.combat_state.counters
6775                    [ActionStateFCounters::FCounterHealthThreshold as usize] -=
6776                    MINION_SUMMON_THRESHOLD;
6777            }
6778        }
6779        // Logic to use abilities
6780        else if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
6781            && entities_have_line_of_sight(
6782                self.pos,
6783                self.body,
6784                self.scale,
6785                tgt_data.pos,
6786                tgt_data.body,
6787                tgt_data.scale,
6788                read_data,
6789            )
6790        {
6791            // If far enough away, and can see target, check which skill is appropriate to
6792            // use
6793            if self.energy.current() > DESIRED_ENERGY_LEVEL
6794                && read_data
6795                    .combos
6796                    .get(*self.entity)
6797                    .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
6798                && !read_data.buffs.get(*self.entity).iter().any(|buff| {
6799                    buff.iter_kind(BuffKind::Regeneration)
6800                        .peekable()
6801                        .peek()
6802                        .is_some()
6803                })
6804            {
6805                // If have enough energy and combo to use healing aura, do so
6806                controller.push_basic_input(InputKind::Secondary);
6807            } else if self
6808                .skill_set
6809                .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
6810                && self.energy.current() > DESIRED_ENERGY_LEVEL
6811                && !read_data.buffs.get(*self.entity).iter().any(|buff| {
6812                    buff.iter_kind(BuffKind::ProtectingWard)
6813                        .peekable()
6814                        .peek()
6815                        .is_some()
6816                })
6817            {
6818                // Use steam beam if target is far enough away, self is not buffed, and have
6819                // sufficient energy
6820                controller.push_basic_input(InputKind::Ability(0));
6821            } else {
6822                // If low on energy, use primary to attempt to regen energy
6823                // Or if at desired energy level but not able/willing to ward, just attack
6824                controller.push_basic_input(InputKind::Primary);
6825            }
6826        } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6827            if self.body.is_some_and(|b| b.is_humanoid())
6828                && self.energy.current()
6829                    > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
6830                && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
6831            {
6832                // Else use steam beam
6833                controller.push_basic_input(InputKind::Ability(0));
6834            } else if attack_data.angle < 15.0 {
6835                controller.push_basic_input(InputKind::Primary);
6836            }
6837        }
6838        // Logic to move. Intentionally kept separate from ability logic where possible
6839        // so duplicated work is less necessary.
6840        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6841            // Attempt to move away from target if too close
6842            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6843                &*read_data.terrain,
6844                self.pos.0,
6845                self.vel.0,
6846                tgt_data.pos.0,
6847                TraversalConfig {
6848                    min_tgt_dist: 1.25,
6849                    ..self.traversal_config
6850                },
6851                &read_data.time,
6852            ) {
6853                self.unstuck_if(stuck, controller);
6854                controller.inputs.move_dir =
6855                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6856            }
6857        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6858            // Else attempt to circle target if neither too close nor too far
6859            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6860                &*read_data.terrain,
6861                self.pos.0,
6862                self.vel.0,
6863                tgt_data.pos.0,
6864                TraversalConfig {
6865                    min_tgt_dist: 1.25,
6866                    ..self.traversal_config
6867                },
6868                &read_data.time,
6869            ) {
6870                self.unstuck_if(stuck, controller);
6871                if entities_have_line_of_sight(
6872                    self.pos,
6873                    self.body,
6874                    self.scale,
6875                    tgt_data.pos,
6876                    tgt_data.body,
6877                    tgt_data.scale,
6878                    read_data,
6879                ) && attack_data.angle < 45.0
6880                {
6881                    controller.inputs.move_dir = bearing
6882                        .xy()
6883                        .rotated_z(rng.random_range(0.5..1.57))
6884                        .try_normalized()
6885                        .unwrap_or_else(Vec2::zero)
6886                        * speed;
6887                } else {
6888                    // Unless cannot see target, then move towards them
6889                    controller.inputs.move_dir =
6890                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6891                    self.jump_if(bearing.z > 1.5, controller);
6892                    controller.inputs.move_z = bearing.z;
6893                }
6894            }
6895            // Sometimes try to roll
6896            if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
6897                && !matches!(self.char_state, CharacterState::BasicAura(_))
6898                && attack_data.dist_sqrd < 16.0f32.powi(2)
6899                && rng.random::<f32>() < 0.01
6900            {
6901                controller.push_basic_input(InputKind::Roll);
6902            }
6903        } else {
6904            // If too far, move towards target
6905            self.path_toward_target(
6906                agent,
6907                controller,
6908                tgt_data.pos.0,
6909                read_data,
6910                Path::AtTarget,
6911                None,
6912            );
6913        }
6914    }
6915
6916    pub fn handle_sea_bishop_attack(
6917        &self,
6918        agent: &mut Agent,
6919        controller: &mut Controller,
6920        attack_data: &AttackData,
6921        tgt_data: &TargetData,
6922        read_data: &ReadData,
6923        rng: &mut impl Rng,
6924    ) {
6925        let line_of_sight_with_target = || {
6926            entities_have_line_of_sight(
6927                self.pos,
6928                self.body,
6929                self.scale,
6930                tgt_data.pos,
6931                tgt_data.body,
6932                tgt_data.scale,
6933                read_data,
6934            )
6935        };
6936
6937        enum ActionStateTimers {
6938            TimerBeam = 0,
6939        }
6940        if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 6.0 {
6941            agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
6942        } else {
6943            agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
6944        }
6945
6946        // When enemy in sight beam for 3 seconds, every 6 seconds
6947        if line_of_sight_with_target()
6948            && agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 3.0
6949        {
6950            controller.push_basic_input(InputKind::Primary);
6951        }
6952        // Logic to move. Intentionally kept separate from ability logic where possible
6953        // so duplicated work is less necessary.
6954        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6955            // Attempt to move away from target if too close
6956            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6957                &*read_data.terrain,
6958                self.pos.0,
6959                self.vel.0,
6960                tgt_data.pos.0,
6961                TraversalConfig {
6962                    min_tgt_dist: 1.25,
6963                    ..self.traversal_config
6964                },
6965                &read_data.time,
6966            ) {
6967                self.unstuck_if(stuck, controller);
6968                controller.inputs.move_dir =
6969                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6970            }
6971        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6972            // Else attempt to circle target if neither too close nor too far
6973            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6974                &*read_data.terrain,
6975                self.pos.0,
6976                self.vel.0,
6977                tgt_data.pos.0,
6978                TraversalConfig {
6979                    min_tgt_dist: 1.25,
6980                    ..self.traversal_config
6981                },
6982                &read_data.time,
6983            ) {
6984                self.unstuck_if(stuck, controller);
6985                if line_of_sight_with_target() && attack_data.angle < 45.0 {
6986                    controller.inputs.move_dir = bearing
6987                        .xy()
6988                        .rotated_z(rng.random_range(0.5..1.57))
6989                        .try_normalized()
6990                        .unwrap_or_else(Vec2::zero)
6991                        * speed;
6992                } else {
6993                    // Unless cannot see target, then move towards them
6994                    controller.inputs.move_dir =
6995                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6996                    self.jump_if(bearing.z > 1.5, controller);
6997                    controller.inputs.move_z = bearing.z;
6998                }
6999            }
7000        } else {
7001            // If too far, move towards target
7002            self.path_toward_target(
7003                agent,
7004                controller,
7005                tgt_data.pos.0,
7006                read_data,
7007                Path::AtTarget,
7008                None,
7009            );
7010        }
7011    }
7012
7013    pub fn handle_cursekeeper_attack(
7014        &self,
7015        agent: &mut Agent,
7016        controller: &mut Controller,
7017        attack_data: &AttackData,
7018        tgt_data: &TargetData,
7019        read_data: &ReadData,
7020        rng: &mut impl Rng,
7021    ) {
7022        enum ActionStateTimers {
7023            TimerBeam,
7024            TimerSummon,
7025            SelectSummon,
7026        }
7027        if tgt_data.pos.0.z - self.pos.0.z > 3.5 {
7028            controller.push_action(ControlAction::StartInput {
7029                input: InputKind::Ability(4),
7030                target_entity: agent
7031                    .target
7032                    .as_ref()
7033                    .and_then(|t| read_data.uids.get(t.target))
7034                    .copied(),
7035                select_pos: None,
7036            });
7037        } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 12.0 {
7038            agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
7039        } else {
7040            agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
7041        }
7042
7043        if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
7044        {
7045            agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] = 0.0;
7046            agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] =
7047                rng.random_range(0..=3) as f32;
7048        } else {
7049            agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] += read_data.dt.0;
7050        }
7051
7052        if agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] > 32.0 {
7053            match agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] as i32 {
7054                0 => controller.push_basic_input(InputKind::Ability(0)),
7055                1 => controller.push_basic_input(InputKind::Ability(1)),
7056                2 => controller.push_basic_input(InputKind::Ability(2)),
7057                _ => controller.push_basic_input(InputKind::Ability(3)),
7058            }
7059        } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 6.0 {
7060            controller.push_basic_input(InputKind::Ability(5));
7061        } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 9.0 {
7062            controller.push_basic_input(InputKind::Primary);
7063        } else {
7064            controller.push_basic_input(InputKind::Secondary);
7065        }
7066
7067        if attack_data.dist_sqrd > 10_f32.powi(2)
7068            || agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 4.0
7069        {
7070            self.path_toward_target(
7071                agent,
7072                controller,
7073                tgt_data.pos.0,
7074                read_data,
7075                Path::AtTarget,
7076                None,
7077            );
7078        }
7079    }
7080
7081    pub fn handle_shamanic_spirit_attack(
7082        &self,
7083        agent: &mut Agent,
7084        controller: &mut Controller,
7085        attack_data: &AttackData,
7086        tgt_data: &TargetData,
7087        read_data: &ReadData,
7088    ) {
7089        if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
7090            controller.push_action(ControlAction::StartInput {
7091                input: InputKind::Secondary,
7092                target_entity: agent
7093                    .target
7094                    .as_ref()
7095                    .and_then(|t| read_data.uids.get(t.target))
7096                    .copied(),
7097                select_pos: None,
7098            });
7099        } else if attack_data.in_min_range() && attack_data.angle < 30.0 {
7100            controller.push_basic_input(InputKind::Primary);
7101            controller.inputs.move_dir = Vec2::zero();
7102        } else {
7103            self.path_toward_target(
7104                agent,
7105                controller,
7106                tgt_data.pos.0,
7107                read_data,
7108                Path::AtTarget,
7109                None,
7110            );
7111        }
7112    }
7113
7114    pub fn handle_cursekeeper_fake_attack(
7115        &self,
7116        controller: &mut Controller,
7117        attack_data: &AttackData,
7118    ) {
7119        if attack_data.dist_sqrd < 25_f32.powi(2) {
7120            controller.push_basic_input(InputKind::Primary);
7121        }
7122    }
7123
7124    pub fn handle_karkatha_attack(
7125        &self,
7126        agent: &mut Agent,
7127        controller: &mut Controller,
7128        attack_data: &AttackData,
7129        tgt_data: &TargetData,
7130        read_data: &ReadData,
7131        _rng: &mut impl Rng,
7132    ) {
7133        enum ActionStateTimers {
7134            RiposteTimer,
7135            SummonTimer,
7136        }
7137
7138        agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] += read_data.dt.0;
7139        agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] += read_data.dt.0;
7140        if matches!(self.char_state, CharacterState::RiposteMelee(c) if !matches!(c.stage_section, StageSection::Recover))
7141        {
7142            // Reset timer
7143            agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] = 0.0;
7144        }
7145        if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
7146        {
7147            // Reset timer
7148            agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] = 0.0;
7149        }
7150        // chase, move away from exiit if target is cheesing from below
7151        let home = agent.patrol_origin.unwrap_or(self.pos.0);
7152        let dest = if tgt_data.pos.0.z < self.pos.0.z {
7153            home
7154        } else {
7155            tgt_data.pos.0
7156        };
7157        if attack_data.in_min_range() {
7158            if agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] > 3.0 {
7159                controller.push_basic_input(InputKind::Ability(2));
7160            } else {
7161                controller.push_basic_input(InputKind::Primary);
7162            };
7163        } else if attack_data.dist_sqrd < 20.0_f32.powi(2) {
7164            if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] > 20.0 {
7165                controller.push_basic_input(InputKind::Ability(1));
7166            } else {
7167                controller.push_basic_input(InputKind::Secondary);
7168            }
7169        } else if attack_data.dist_sqrd < 30.0_f32.powi(2) {
7170            if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] < 10.0 {
7171                self.path_toward_target(
7172                    agent,
7173                    controller,
7174                    tgt_data.pos.0,
7175                    read_data,
7176                    Path::AtTarget,
7177                    None,
7178                );
7179            } else {
7180                controller.push_basic_input(InputKind::Ability(0));
7181            }
7182        } else {
7183            self.path_toward_target(agent, controller, dest, read_data, Path::AtTarget, None);
7184        }
7185    }
7186
7187    pub fn handle_dagon_attack(
7188        &self,
7189        agent: &mut Agent,
7190        controller: &mut Controller,
7191        attack_data: &AttackData,
7192        tgt_data: &TargetData,
7193        read_data: &ReadData,
7194    ) {
7195        enum ActionStateTimers {
7196            TimerDagon = 0,
7197        }
7198        let line_of_sight_with_target = || {
7199            entities_have_line_of_sight(
7200                self.pos,
7201                self.body,
7202                self.scale,
7203                tgt_data.pos,
7204                tgt_data.body,
7205                tgt_data.scale,
7206                read_data,
7207            )
7208        };
7209        // when cheesed from behind the entry, change position to retarget
7210        let home = agent.patrol_origin.unwrap_or(self.pos.0);
7211        let exit = Vec3::new(home.x - 6.0, home.y - 6.0, home.z);
7212        let (station_0, station_1) = (exit + 12.0, exit - 12.0);
7213        if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.5 {
7214            agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] = 0.0;
7215        }
7216        if !line_of_sight_with_target()
7217            && (tgt_data.pos.0 - exit).xy().magnitude_squared() < (10.0_f32).powi(2)
7218        {
7219            let station = if (tgt_data.pos.0 - station_0).xy().magnitude_squared()
7220                < (tgt_data.pos.0 - station_1).xy().magnitude_squared()
7221            {
7222                station_0
7223            } else {
7224                station_1
7225            };
7226            self.path_toward_target(agent, controller, station, read_data, Path::AtTarget, None);
7227        }
7228        // if target gets very close, shoot dagon bombs and lay out sea urchins
7229        else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
7230            if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
7231                controller.push_basic_input(InputKind::Primary);
7232                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7233            } else {
7234                controller.push_basic_input(InputKind::Secondary);
7235                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7236            }
7237            // if target in close range use steambeam and shoot dagon bombs
7238        } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
7239            controller.inputs.move_dir = Vec2::zero();
7240            if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
7241                controller.push_basic_input(InputKind::Primary);
7242                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7243            } else {
7244                controller.push_basic_input(InputKind::Ability(1));
7245            }
7246        } else if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) {
7247            // if enemy is far, heal and shoot bombs
7248            if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
7249                controller.push_basic_input(InputKind::Primary);
7250            } else {
7251                controller.push_basic_input(InputKind::Ability(2));
7252            }
7253            agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7254        } else if line_of_sight_with_target() {
7255            // if enemy in mid range shoot dagon bombs and steamwave
7256            if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
7257                controller.push_basic_input(InputKind::Primary);
7258                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7259            } else {
7260                controller.push_basic_input(InputKind::Ability(0));
7261                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7262            }
7263        }
7264        // chase
7265        let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
7266            Path::Separate
7267        } else {
7268            Path::AtTarget
7269        };
7270        self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
7271    }
7272
7273    pub fn handle_snaretongue_attack(
7274        &self,
7275        agent: &mut Agent,
7276        controller: &mut Controller,
7277        attack_data: &AttackData,
7278        read_data: &ReadData,
7279    ) {
7280        enum Timers {
7281            TimerAttack = 0,
7282        }
7283        let attack_timer = &mut agent.combat_state.timers[Timers::TimerAttack as usize];
7284        if *attack_timer > 2.5 {
7285            *attack_timer = 0.0;
7286        }
7287        // if target gets very close, use tongue attack and shockwave
7288        if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) {
7289            if *attack_timer > 0.5 {
7290                controller.push_basic_input(InputKind::Primary);
7291                *attack_timer += read_data.dt.0;
7292            } else {
7293                controller.push_basic_input(InputKind::Secondary);
7294                *attack_timer += read_data.dt.0;
7295            }
7296            // if target in close range use beam and shoot dagon bombs
7297        } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
7298            controller.inputs.move_dir = Vec2::zero();
7299            if *attack_timer > 2.0 {
7300                controller.push_basic_input(InputKind::Ability(0));
7301                *attack_timer += read_data.dt.0;
7302            } else {
7303                controller.push_basic_input(InputKind::Ability(1));
7304            }
7305        } else {
7306            // if target in midrange range shoot dagon bombs and heal
7307            if *attack_timer > 1.0 {
7308                controller.push_basic_input(InputKind::Ability(0));
7309                *attack_timer += read_data.dt.0;
7310            } else {
7311                controller.push_basic_input(InputKind::Ability(2));
7312                *attack_timer += read_data.dt.0;
7313            }
7314        }
7315    }
7316
7317    pub fn handle_deadwood(
7318        &self,
7319        agent: &mut Agent,
7320        controller: &mut Controller,
7321        attack_data: &AttackData,
7322        tgt_data: &TargetData,
7323        read_data: &ReadData,
7324    ) {
7325        const BEAM_RANGE: f32 = 20.0;
7326        const BEAM_TIME: Duration = Duration::from_secs(3);
7327        // combat_state.condition controls whether or not deadwood should beam or dash
7328        if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
7329        {
7330            // If already dashing, keep dashing and have move_dir set to forward
7331            controller.push_basic_input(InputKind::Secondary);
7332            controller.inputs.move_dir = self.ori.look_vec().xy();
7333        } else if attack_data.in_min_range() && attack_data.angle_xy < 10.0 {
7334            // If near target, dash at them and through them to get away
7335            controller.push_basic_input(InputKind::Secondary);
7336        } else if matches!(self.char_state, CharacterState::BasicBeam(s) if s.stage_section != StageSection::Recover && s.timer < BEAM_TIME)
7337        {
7338            // If already beaming, keep beaming if not beaming for over 5 seconds
7339            controller.push_basic_input(InputKind::Primary);
7340        } else if attack_data.dist_sqrd < BEAM_RANGE.powi(2) {
7341            // Else if in beam range, beam them
7342            if attack_data.angle_xy < 5.0 {
7343                controller.push_basic_input(InputKind::Primary);
7344            } else {
7345                // If not in angle, apply slight movement so deadwood orients itself correctly
7346                controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7347                    .xy()
7348                    .try_normalized()
7349                    .unwrap_or_else(Vec2::zero)
7350                    * 0.01;
7351            }
7352        } else {
7353            // Otherwise too far, move towards target
7354            self.path_toward_target(
7355                agent,
7356                controller,
7357                tgt_data.pos.0,
7358                read_data,
7359                Path::AtTarget,
7360                None,
7361            );
7362        }
7363    }
7364
7365    pub fn handle_mandragora(
7366        &self,
7367        agent: &mut Agent,
7368        controller: &mut Controller,
7369        attack_data: &AttackData,
7370        tgt_data: &TargetData,
7371        read_data: &ReadData,
7372    ) {
7373        const SCREAM_RANGE: f32 = 10.0; // hard-coded from scream.ron
7374
7375        enum ActionStateFCounters {
7376            FCounterHealthThreshold = 0,
7377        }
7378
7379        enum ActionStateConditions {
7380            ConditionHasScreamed = 0,
7381        }
7382
7383        if !agent.combat_state.initialized {
7384            agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
7385                self.health.map_or(0.0, |h| h.maximum());
7386            agent.combat_state.initialized = true;
7387        }
7388
7389        if !agent.combat_state.conditions[ActionStateConditions::ConditionHasScreamed as usize] {
7390            // If mandragora is still "sleeping" and hasn't screamed yet, do nothing until
7391            // target in range or until it's taken damage
7392            if self.health.is_some_and(|h| {
7393                h.current()
7394                    < agent.combat_state.counters
7395                        [ActionStateFCounters::FCounterHealthThreshold as usize]
7396            }) || attack_data.dist_sqrd < SCREAM_RANGE.powi(2)
7397            {
7398                agent.combat_state.conditions
7399                    [ActionStateConditions::ConditionHasScreamed as usize] = true;
7400                controller.push_basic_input(InputKind::Secondary);
7401            }
7402        } else {
7403            // Once mandragora has woken, move towards target and attack
7404            if attack_data.in_min_range() {
7405                controller.push_basic_input(InputKind::Primary);
7406            } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
7407                && entities_have_line_of_sight(
7408                    self.pos,
7409                    self.body,
7410                    self.scale,
7411                    tgt_data.pos,
7412                    tgt_data.body,
7413                    tgt_data.scale,
7414                    read_data,
7415                )
7416            {
7417                // If in pathing range and can see target, move towards them
7418                self.path_toward_target(
7419                    agent,
7420                    controller,
7421                    tgt_data.pos.0,
7422                    read_data,
7423                    Path::AtTarget,
7424                    None,
7425                );
7426            } else {
7427                // Otherwise, go back to sleep
7428                agent.combat_state.conditions
7429                    [ActionStateConditions::ConditionHasScreamed as usize] = false;
7430                agent.combat_state.counters
7431                    [ActionStateFCounters::FCounterHealthThreshold as usize] =
7432                    self.health.map_or(0.0, |h| h.maximum());
7433            }
7434        }
7435    }
7436
7437    pub fn handle_wood_golem(
7438        &self,
7439        agent: &mut Agent,
7440        controller: &mut Controller,
7441        attack_data: &AttackData,
7442        tgt_data: &TargetData,
7443        read_data: &ReadData,
7444        rng: &mut impl Rng,
7445    ) {
7446        // === reference ===
7447
7448        // Inputs:
7449        //   Primary: strike
7450        //   Secondary: spin
7451        //   Auxiliary
7452        //     0: shockwave
7453
7454        // === setup ===
7455
7456        // --- static ---
7457        // behaviour parameters
7458        const PATH_RANGE_FACTOR: f32 = 0.3; // get comfortably in range, but give player room to breathe
7459        const STRIKE_RANGE_FACTOR: f32 = 0.6; // start attack while suitably in range
7460        const STRIKE_AIM_FACTOR: f32 = 0.7;
7461        const SPIN_RANGE_FACTOR: f32 = 0.6;
7462        const SPIN_COOLDOWN: f32 = 1.5;
7463        const SPIN_RELAX_FACTOR: f32 = 0.2;
7464        const SHOCKWAVE_RANGE_FACTOR: f32 = 0.7;
7465        const SHOCKWAVE_AIM_FACTOR: f32 = 0.4;
7466        const SHOCKWAVE_COOLDOWN: f32 = 5.0;
7467        const MIXUP_COOLDOWN: f32 = 2.5;
7468        const MIXUP_RELAX_FACTOR: f32 = 0.3;
7469
7470        // timers
7471        const SPIN: usize = 0;
7472        const SHOCKWAVE: usize = 1;
7473        const MIXUP: usize = 2;
7474
7475        // --- dynamic ---
7476        // behaviour parameters
7477        let shockwave_min_range = self.body.map_or(0.0, |b| b.height() * 1.1);
7478
7479        // attack data
7480        let (strike_range, strike_angle) = {
7481            if let Some(AbilityData::BasicMelee { range, angle, .. }) =
7482                self.extract_ability(AbilityInput::Primary)
7483            {
7484                (range, angle)
7485            } else {
7486                (0.0, 0.0)
7487            }
7488        };
7489        let spin_range = {
7490            if let Some(AbilityData::BasicMelee { range, .. }) =
7491                self.extract_ability(AbilityInput::Secondary)
7492            {
7493                range
7494            } else {
7495                0.0
7496            }
7497        };
7498        let (shockwave_max_range, shockwave_angle) = {
7499            if let Some(AbilityData::Shockwave { range, angle, .. }) =
7500                self.extract_ability(AbilityInput::Auxiliary(0))
7501            {
7502                (range, angle)
7503            } else {
7504                (0.0, 0.0)
7505            }
7506        };
7507
7508        // re-used checks (makes separating timers and attacks easier)
7509        let is_in_spin_range = attack_data.dist_sqrd
7510            < (attack_data.body_dist + spin_range * SPIN_RANGE_FACTOR).powi(2);
7511        let is_in_strike_range = attack_data.dist_sqrd
7512            < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
7513        let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
7514
7515        // === main ===
7516
7517        // --- timers ---
7518        // spin
7519        let current_input = self.char_state.ability_info().map(|ai| ai.input);
7520        if matches!(current_input, Some(InputKind::Secondary)) {
7521            // reset when spinning
7522            agent.combat_state.timers[SPIN] = 0.0;
7523            agent.combat_state.timers[MIXUP] = 0.0;
7524        } else if is_in_spin_range && !(is_in_strike_range && is_in_strike_angle) {
7525            // increment within spin range and not in strike range + angle
7526            agent.combat_state.timers[SPIN] += read_data.dt.0;
7527        } else {
7528            // relax towards zero otherwise
7529            agent.combat_state.timers[SPIN] =
7530                (agent.combat_state.timers[SPIN] - read_data.dt.0 * SPIN_RELAX_FACTOR).max(0.0);
7531        }
7532        // shockwave
7533        if matches!(self.char_state, CharacterState::Shockwave(_)) {
7534            // reset when using shockwave
7535            agent.combat_state.timers[SHOCKWAVE] = 0.0;
7536            agent.combat_state.timers[MIXUP] = 0.0;
7537        } else {
7538            // increment otherwise
7539            agent.combat_state.timers[SHOCKWAVE] += read_data.dt.0;
7540        }
7541        // mixup
7542        if is_in_strike_range && is_in_strike_angle {
7543            // increment within strike range and angle
7544            agent.combat_state.timers[MIXUP] += read_data.dt.0;
7545        } else {
7546            // relax towards zero otherwise
7547            agent.combat_state.timers[MIXUP] =
7548                (agent.combat_state.timers[MIXUP] - read_data.dt.0 * MIXUP_RELAX_FACTOR).max(0.0);
7549        }
7550
7551        // --- attacks ---
7552        // strike range and angle
7553        if is_in_strike_range && is_in_strike_angle {
7554            // on timer, randomly mixup between all attacks
7555            if agent.combat_state.timers[MIXUP] > MIXUP_COOLDOWN {
7556                let randomise: u8 = rng.random_range(1..=3);
7557                match randomise {
7558                    1 => controller.push_basic_input(InputKind::Ability(0)), // shockwave
7559                    2 => controller.push_basic_input(InputKind::Primary),    // strike
7560                    _ => controller.push_basic_input(InputKind::Secondary),  // spin
7561                }
7562            }
7563            // default to strike
7564            else {
7565                controller.push_basic_input(InputKind::Primary);
7566            }
7567        }
7568        // spin range (or out of angle in strike range)
7569        else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) {
7570            // on timer, use spin attack to try and hit evasive target
7571            if agent.combat_state.timers[SPIN] > SPIN_COOLDOWN {
7572                controller.push_basic_input(InputKind::Secondary);
7573            }
7574            // otherwise, close angle (no action required)
7575        }
7576        // shockwave range and angle
7577        else if attack_data.dist_sqrd > shockwave_min_range.powi(2)
7578            && attack_data.dist_sqrd < (shockwave_max_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
7579            && attack_data.angle < shockwave_angle * SHOCKWAVE_AIM_FACTOR
7580        {
7581            // on timer, use shockwave
7582            if agent.combat_state.timers[SHOCKWAVE] > SHOCKWAVE_COOLDOWN {
7583                controller.push_basic_input(InputKind::Ability(0));
7584            }
7585            // otherwise, close gap and/or angle (no action required)
7586        }
7587
7588        // --- movement ---
7589        // closing gap
7590        if attack_data.dist_sqrd
7591            > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
7592        {
7593            self.path_toward_target(
7594                agent,
7595                controller,
7596                tgt_data.pos.0,
7597                read_data,
7598                Path::AtTarget,
7599                None,
7600            );
7601        }
7602        // closing angle
7603        else if attack_data.angle > 0.0 {
7604            // some movement is required to trigger re-orientation
7605            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7606                .xy()
7607                .try_normalized()
7608                .unwrap_or_else(Vec2::zero)
7609                * 0.001; // scaled way down to minimise position change and keep close rotation consistent
7610        }
7611    }
7612
7613    pub fn handle_gnarling_chieftain(
7614        &self,
7615        agent: &mut Agent,
7616        controller: &mut Controller,
7617        attack_data: &AttackData,
7618        tgt_data: &TargetData,
7619        read_data: &ReadData,
7620        rng: &mut impl Rng,
7621    ) {
7622        // === reference ===
7623        // Inputs
7624        //   Primary: flamestrike
7625        //   Secondary: firebarrage
7626        //   Auxiliary
7627        //     0: fireshockwave
7628        //     1: redtotem
7629        //     2: greentotem
7630        //     3: whitetotem
7631
7632        // === setup ===
7633
7634        // --- static ---
7635        // behaviour parameters
7636        const PATH_RANGE_FACTOR: f32 = 0.4;
7637        const STRIKE_RANGE_FACTOR: f32 = 0.7;
7638        const STRIKE_AIM_FACTOR: f32 = 0.8;
7639        const BARRAGE_RANGE_FACTOR: f32 = 0.8;
7640        const BARRAGE_AIM_FACTOR: f32 = 0.65;
7641        const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75;
7642        const TOTEM_COOLDOWN: f32 = 25.0;
7643        const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
7644        const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
7645        const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
7646
7647        // conditions
7648        const HAS_SUMMONED_FIRST_TOTEM: usize = 0;
7649        // timers
7650        const SUMMON_TOTEM: usize = 0;
7651        const HEAVY_ATTACK: usize = 1;
7652        // counters
7653        const HEAVY_ATTACK_COOLDOWN: usize = 0;
7654
7655        // line of sight check
7656        let line_of_sight_with_target = || {
7657            entities_have_line_of_sight(
7658                self.pos,
7659                self.body,
7660                self.scale,
7661                tgt_data.pos,
7662                tgt_data.body,
7663                tgt_data.scale,
7664                read_data,
7665            )
7666        };
7667
7668        // --- dynamic ---
7669        // attack data
7670        let (strike_range, strike_angle) = {
7671            if let Some(AbilityData::BasicMelee { range, angle, .. }) =
7672                self.extract_ability(AbilityInput::Primary)
7673            {
7674                (range, angle)
7675            } else {
7676                (0.0, 0.0)
7677            }
7678        };
7679        let (barrage_speed, barrage_spread, barrage_count) = {
7680            if let Some(AbilityData::BasicRanged {
7681                projectile_speed,
7682                projectile_spread,
7683                num_projectiles,
7684                ..
7685            }) = self.extract_ability(AbilityInput::Secondary)
7686            {
7687                (
7688                    projectile_speed,
7689                    projectile_spread,
7690                    num_projectiles.compute(self.heads.map_or(1, |heads| heads.amount() as u32)),
7691                )
7692            } else {
7693                (0.0, 0.0, 0)
7694            }
7695        };
7696        let shockwave_range = {
7697            if let Some(AbilityData::Shockwave { range, .. }) =
7698                self.extract_ability(AbilityInput::Auxiliary(0))
7699            {
7700                range
7701            } else {
7702                0.0
7703            }
7704        };
7705
7706        // calculated attack data
7707        let barrage_max_range =
7708            projectile_flat_range(barrage_speed, self.body.map_or(2.0, |b| b.height()));
7709        let barrange_angle = projectile_multi_angle(barrage_spread, barrage_count);
7710
7711        // re-used checks
7712        let is_in_strike_range = attack_data.dist_sqrd
7713            < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
7714        let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
7715
7716        // initialise randomised cooldowns
7717        if !agent.combat_state.initialized {
7718            agent.combat_state.initialized = true;
7719            agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
7720                rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
7721        }
7722
7723        // === main ===
7724
7725        // --- timers ---
7726        // resets
7727        match self.char_state {
7728            CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
7729                // reset when finished summoning
7730                agent.combat_state.timers[SUMMON_TOTEM] = 0.0;
7731                agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] = true;
7732            },
7733            CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
7734                // reset heavy attack on either ability
7735                agent.combat_state.counters[HEAVY_ATTACK] = 0.0;
7736                agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
7737                    rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
7738            },
7739            _ => {},
7740        }
7741        // totem (always increment)
7742        agent.combat_state.timers[SUMMON_TOTEM] += read_data.dt.0;
7743        // heavy attack (increment at different rates)
7744        if is_in_strike_range {
7745            // recharge at standard rate in strike range and angle
7746            if is_in_strike_angle {
7747                agent.combat_state.counters[HEAVY_ATTACK] += read_data.dt.0;
7748            } else {
7749                // If not in angle, charge heavy attack faster
7750                agent.combat_state.counters[HEAVY_ATTACK] +=
7751                    read_data.dt.0 * HEAVY_ATTACK_FAST_CHARGE_FACTOR;
7752            }
7753        } else {
7754            // If not in range, charge heavy attack faster
7755            agent.combat_state.counters[HEAVY_ATTACK] +=
7756                read_data.dt.0 * HEAVY_ATTACK_CHARGE_FACTOR;
7757        }
7758
7759        // --- attacks ---
7760        // start by summoning green totem
7761        if !agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] {
7762            controller.push_basic_input(InputKind::Ability(2));
7763        }
7764        // on timer, summon a new random totem
7765        else if agent.combat_state.timers[SUMMON_TOTEM] > TOTEM_COOLDOWN {
7766            controller.push_basic_input(InputKind::Ability(rng.random_range(1..=3)));
7767        }
7768        // on timer and in range, use a heavy attack
7769        // assumes: barrange_max_range * BARRAGE_RANGE_FACTOR > shockwave_range *
7770        // SHOCKWAVE_RANGE_FACTOR
7771        else if agent.combat_state.counters[HEAVY_ATTACK]
7772            > agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN]
7773            && attack_data.dist_sqrd < (barrage_max_range * BARRAGE_RANGE_FACTOR).powi(2)
7774        {
7775            // has line of sight
7776            if line_of_sight_with_target() {
7777                // out of barrage angle, use shockwave
7778                if attack_data.angle > barrange_angle * BARRAGE_AIM_FACTOR {
7779                    controller.push_basic_input(InputKind::Ability(0));
7780                }
7781                // in shockwave range, randomise between barrage and shockwave
7782                else if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
7783                {
7784                    if rng.random_bool(0.5) {
7785                        controller.push_basic_input(InputKind::Secondary);
7786                    } else {
7787                        controller.push_basic_input(InputKind::Ability(0));
7788                    }
7789                }
7790                // in range and angle, use barrage
7791                else {
7792                    controller.push_basic_input(InputKind::Secondary);
7793                }
7794                // otherwise, close gap and/or angle (no action required)
7795            }
7796            // no line of sight
7797            else {
7798                //  in range, use shockwave
7799                if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2) {
7800                    controller.push_basic_input(InputKind::Ability(0));
7801                }
7802                // otherwise, close gap (no action required)
7803            }
7804        }
7805        // if viable, default to flamestrike
7806        else if is_in_strike_range && is_in_strike_angle {
7807            controller.push_basic_input(InputKind::Primary);
7808        }
7809        // otherwise, close gap and/or angle (no action required)
7810
7811        // --- movement ---
7812        // closing gap
7813        if attack_data.dist_sqrd
7814            > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
7815        {
7816            self.path_toward_target(
7817                agent,
7818                controller,
7819                tgt_data.pos.0,
7820                read_data,
7821                Path::AtTarget,
7822                None,
7823            );
7824        }
7825        // closing angle
7826        else if attack_data.angle > 0.0 {
7827            // some movement is required to trigger re-orientation
7828            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7829                .xy()
7830                .try_normalized()
7831                .unwrap_or_else(Vec2::zero)
7832                * 0.001; // scaled way down to minimise position change and keep close rotation consistent
7833        }
7834    }
7835
7836    pub fn handle_sword_simple_attack(
7837        &self,
7838        agent: &mut Agent,
7839        controller: &mut Controller,
7840        attack_data: &AttackData,
7841        tgt_data: &TargetData,
7842        read_data: &ReadData,
7843    ) {
7844        const DASH_TIMER: usize = 0;
7845        agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
7846        if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
7847        {
7848            controller.push_basic_input(InputKind::Secondary);
7849        } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
7850            if agent.combat_state.timers[DASH_TIMER] > 2.0 {
7851                agent.combat_state.timers[DASH_TIMER] = 0.0;
7852            }
7853            controller.push_basic_input(InputKind::Primary);
7854        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
7855            && self
7856                .path_toward_target(
7857                    agent,
7858                    controller,
7859                    tgt_data.pos.0,
7860                    read_data,
7861                    Path::Separate,
7862                    None,
7863                )
7864                .is_some()
7865            && entities_have_line_of_sight(
7866                self.pos,
7867                self.body,
7868                self.scale,
7869                tgt_data.pos,
7870                tgt_data.body,
7871                tgt_data.scale,
7872                read_data,
7873            )
7874            && agent.combat_state.timers[DASH_TIMER] > 4.0
7875            && attack_data.angle < 45.0
7876        {
7877            controller.push_basic_input(InputKind::Secondary);
7878            agent.combat_state.timers[DASH_TIMER] = 0.0;
7879        } else {
7880            self.path_toward_target(
7881                agent,
7882                controller,
7883                tgt_data.pos.0,
7884                read_data,
7885                Path::AtTarget,
7886                None,
7887            );
7888        }
7889    }
7890
7891    pub fn handle_adlet_hunter(
7892        &self,
7893        agent: &mut Agent,
7894        controller: &mut Controller,
7895        attack_data: &AttackData,
7896        tgt_data: &TargetData,
7897        read_data: &ReadData,
7898        rng: &mut impl Rng,
7899    ) {
7900        const ROTATE_TIMER: usize = 0;
7901        const ROTATE_DIR_CONDITION: usize = 0;
7902        agent.combat_state.timers[ROTATE_TIMER] -= read_data.dt.0;
7903        if agent.combat_state.timers[ROTATE_TIMER] < 0.0 {
7904            agent.combat_state.conditions[ROTATE_DIR_CONDITION] = rng.random_bool(0.5);
7905            agent.combat_state.timers[ROTATE_TIMER] = rng.random::<f32>() * 5.0;
7906        }
7907        let primary = self.extract_ability(AbilityInput::Primary);
7908        let secondary = self.extract_ability(AbilityInput::Secondary);
7909        let could_use_input = |input| match input {
7910            InputKind::Primary => primary.as_ref().is_some_and(|p| {
7911                p.could_use(
7912                    attack_data,
7913                    self,
7914                    tgt_data,
7915                    read_data,
7916                    AbilityPreferences::default(),
7917                )
7918            }),
7919            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7920                s.could_use(
7921                    attack_data,
7922                    self,
7923                    tgt_data,
7924                    read_data,
7925                    AbilityPreferences::default(),
7926                )
7927            }),
7928            _ => false,
7929        };
7930        let move_forwards = if could_use_input(InputKind::Primary) {
7931            controller.push_basic_input(InputKind::Primary);
7932            false
7933        } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 8_f32.powi(2) {
7934            controller.push_basic_input(InputKind::Secondary);
7935            true
7936        } else {
7937            true
7938        };
7939
7940        if move_forwards && attack_data.dist_sqrd > 3_f32.powi(2) {
7941            self.path_toward_target(
7942                agent,
7943                controller,
7944                tgt_data.pos.0,
7945                read_data,
7946                Path::Separate,
7947                None,
7948            );
7949        } else {
7950            self.path_toward_target(
7951                agent,
7952                controller,
7953                tgt_data.pos.0,
7954                read_data,
7955                Path::Separate,
7956                None,
7957            );
7958            let dir = if agent.combat_state.conditions[ROTATE_DIR_CONDITION] {
7959                1.0
7960            } else {
7961                -1.0
7962            };
7963            controller.inputs.move_dir.rotate_z(PI / 2.0 * dir);
7964        }
7965    }
7966
7967    pub fn handle_adlet_icepicker(
7968        &self,
7969        agent: &mut Agent,
7970        controller: &mut Controller,
7971        attack_data: &AttackData,
7972        tgt_data: &TargetData,
7973        read_data: &ReadData,
7974    ) {
7975        let primary = self.extract_ability(AbilityInput::Primary);
7976        let secondary = self.extract_ability(AbilityInput::Secondary);
7977        let could_use_input = |input| match input {
7978            InputKind::Primary => primary.as_ref().is_some_and(|p| {
7979                p.could_use(
7980                    attack_data,
7981                    self,
7982                    tgt_data,
7983                    read_data,
7984                    AbilityPreferences::default(),
7985                )
7986            }),
7987            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7988                s.could_use(
7989                    attack_data,
7990                    self,
7991                    tgt_data,
7992                    read_data,
7993                    AbilityPreferences::default(),
7994                )
7995            }),
7996            _ => false,
7997        };
7998        let move_forwards = if could_use_input(InputKind::Primary) {
7999            controller.push_basic_input(InputKind::Primary);
8000            false
8001        } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 5_f32.powi(2) {
8002            controller.push_basic_input(InputKind::Secondary);
8003            false
8004        } else {
8005            true
8006        };
8007
8008        if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
8009            self.path_toward_target(
8010                agent,
8011                controller,
8012                tgt_data.pos.0,
8013                read_data,
8014                Path::Separate,
8015                None,
8016            );
8017        }
8018    }
8019
8020    pub fn handle_adlet_tracker(
8021        &self,
8022        agent: &mut Agent,
8023        controller: &mut Controller,
8024        attack_data: &AttackData,
8025        tgt_data: &TargetData,
8026        read_data: &ReadData,
8027    ) {
8028        const TRAP_TIMER: usize = 0;
8029        agent.combat_state.timers[TRAP_TIMER] += read_data.dt.0;
8030        if agent.combat_state.timers[TRAP_TIMER] > 20.0 {
8031            agent.combat_state.timers[TRAP_TIMER] = 0.0;
8032        }
8033        let primary = self.extract_ability(AbilityInput::Primary);
8034        let could_use_input = |input| match input {
8035            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8036                p.could_use(
8037                    attack_data,
8038                    self,
8039                    tgt_data,
8040                    read_data,
8041                    AbilityPreferences::default(),
8042                )
8043            }),
8044            _ => false,
8045        };
8046        let move_forwards = if agent.combat_state.timers[TRAP_TIMER] < 3.0 {
8047            controller.push_basic_input(InputKind::Secondary);
8048            false
8049        } else if could_use_input(InputKind::Primary) {
8050            controller.push_basic_input(InputKind::Primary);
8051            false
8052        } else {
8053            true
8054        };
8055
8056        if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
8057            self.path_toward_target(
8058                agent,
8059                controller,
8060                tgt_data.pos.0,
8061                read_data,
8062                Path::Separate,
8063                None,
8064            );
8065        }
8066    }
8067
8068    pub fn handle_adlet_elder(
8069        &self,
8070        agent: &mut Agent,
8071        controller: &mut Controller,
8072        attack_data: &AttackData,
8073        tgt_data: &TargetData,
8074        read_data: &ReadData,
8075        rng: &mut impl Rng,
8076    ) {
8077        const TRAP_TIMER: usize = 0;
8078        agent.combat_state.timers[TRAP_TIMER] -= read_data.dt.0;
8079        if matches!(self.char_state, CharacterState::BasicRanged(_)) {
8080            agent.combat_state.timers[TRAP_TIMER] = 15.0;
8081        }
8082        let primary = self.extract_ability(AbilityInput::Primary);
8083        let secondary = self.extract_ability(AbilityInput::Secondary);
8084        let abilities = [
8085            self.extract_ability(AbilityInput::Auxiliary(0)),
8086            self.extract_ability(AbilityInput::Auxiliary(1)),
8087        ];
8088        let could_use_input = |input| match input {
8089            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8090                p.could_use(
8091                    attack_data,
8092                    self,
8093                    tgt_data,
8094                    read_data,
8095                    AbilityPreferences::default(),
8096                )
8097            }),
8098            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8099                s.could_use(
8100                    attack_data,
8101                    self,
8102                    tgt_data,
8103                    read_data,
8104                    AbilityPreferences::default(),
8105                )
8106            }),
8107            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8108                a.could_use(
8109                    attack_data,
8110                    self,
8111                    tgt_data,
8112                    read_data,
8113                    AbilityPreferences::default(),
8114                )
8115            }),
8116            _ => false,
8117        };
8118        let move_forwards = if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
8119        {
8120            controller.push_basic_input(InputKind::Secondary);
8121            false
8122        } else if agent.combat_state.timers[TRAP_TIMER] < 0.0 && !tgt_data.considered_ranged() {
8123            controller.push_basic_input(InputKind::Ability(0));
8124            false
8125        } else if could_use_input(InputKind::Primary) {
8126            controller.push_basic_input(InputKind::Primary);
8127            false
8128        } else if could_use_input(InputKind::Secondary) && rng.random_bool(0.5) {
8129            controller.push_basic_input(InputKind::Secondary);
8130            false
8131        } else if could_use_input(InputKind::Ability(1)) {
8132            controller.push_basic_input(InputKind::Ability(1));
8133            false
8134        } else {
8135            true
8136        };
8137
8138        if matches!(self.char_state, CharacterState::LeapMelee(_)) {
8139            let tgt_vec = tgt_data.pos.0.xy() - self.pos.0.xy();
8140            if tgt_vec.magnitude_squared() > 2_f32.powi(2)
8141                && let Some(look_dir) = Dir::from_unnormalized(Vec3::from(tgt_vec))
8142            {
8143                controller.inputs.look_dir = look_dir;
8144            }
8145        }
8146
8147        if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
8148            self.path_toward_target(
8149                agent,
8150                controller,
8151                tgt_data.pos.0,
8152                read_data,
8153                Path::Separate,
8154                None,
8155            );
8156        }
8157    }
8158
8159    pub fn handle_icedrake(
8160        &self,
8161        agent: &mut Agent,
8162        controller: &mut Controller,
8163        attack_data: &AttackData,
8164        tgt_data: &TargetData,
8165        read_data: &ReadData,
8166        rng: &mut impl Rng,
8167    ) {
8168        let primary = self.extract_ability(AbilityInput::Primary);
8169        let secondary = self.extract_ability(AbilityInput::Secondary);
8170        let abilities = [
8171            self.extract_ability(AbilityInput::Auxiliary(0)),
8172            self.extract_ability(AbilityInput::Auxiliary(1)),
8173        ];
8174        let could_use_input = |input| match input {
8175            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8176                p.could_use(
8177                    attack_data,
8178                    self,
8179                    tgt_data,
8180                    read_data,
8181                    AbilityPreferences::default(),
8182                )
8183            }),
8184            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8185                s.could_use(
8186                    attack_data,
8187                    self,
8188                    tgt_data,
8189                    read_data,
8190                    AbilityPreferences::default(),
8191                )
8192            }),
8193            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8194                a.could_use(
8195                    attack_data,
8196                    self,
8197                    tgt_data,
8198                    read_data,
8199                    AbilityPreferences::default(),
8200                )
8201            }),
8202            _ => false,
8203        };
8204
8205        let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
8206            Some(input @ InputKind::Primary) => {
8207                if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
8208                    && could_use_input(input)
8209                {
8210                    controller.push_basic_input(input);
8211                    true
8212                } else {
8213                    false
8214                }
8215            },
8216            Some(input @ InputKind::Ability(1)) => {
8217                if self
8218                    .char_state
8219                    .timer()
8220                    .is_some_and(|t| t.as_secs_f32() < 3.0)
8221                    && could_use_input(input)
8222                {
8223                    controller.push_basic_input(input);
8224                    true
8225                } else {
8226                    false
8227                }
8228            },
8229            _ => false,
8230        };
8231
8232        let move_forwards = if !continued_attack {
8233            if could_use_input(InputKind::Primary) && rng.random_bool(0.4) {
8234                controller.push_basic_input(InputKind::Primary);
8235                false
8236            } else if could_use_input(InputKind::Secondary) && rng.random_bool(0.8) {
8237                controller.push_basic_input(InputKind::Secondary);
8238                false
8239            } else if could_use_input(InputKind::Ability(1)) && rng.random_bool(0.9) {
8240                controller.push_basic_input(InputKind::Ability(1));
8241                true
8242            } else if could_use_input(InputKind::Ability(0)) {
8243                controller.push_basic_input(InputKind::Ability(0));
8244                true
8245            } else {
8246                true
8247            }
8248        } else {
8249            false
8250        };
8251
8252        if move_forwards {
8253            self.path_toward_target(
8254                agent,
8255                controller,
8256                tgt_data.pos.0,
8257                read_data,
8258                Path::Separate,
8259                None,
8260            );
8261        }
8262    }
8263
8264    pub fn handle_hydra(
8265        &self,
8266        agent: &mut Agent,
8267        controller: &mut Controller,
8268        attack_data: &AttackData,
8269        tgt_data: &TargetData,
8270        read_data: &ReadData,
8271        rng: &mut impl Rng,
8272    ) {
8273        enum ActionStateTimers {
8274            RegrowHeadNoDamage,
8275            RegrowHeadNoAttack,
8276        }
8277
8278        let could_use_input = |input| {
8279            Option::from(input)
8280                .and_then(|ability| {
8281                    Some(self.extract_ability(ability)?.could_use(
8282                        attack_data,
8283                        self,
8284                        tgt_data,
8285                        read_data,
8286                        AbilityPreferences::default(),
8287                    ))
8288                })
8289                .unwrap_or(false)
8290        };
8291
8292        const FOCUS_ATTACK_RANGE: f32 = 5.0;
8293
8294        if attack_data.dist_sqrd < FOCUS_ATTACK_RANGE.powi(2) {
8295            agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] = 0.0;
8296        } else {
8297            agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] +=
8298                read_data.dt.0;
8299        }
8300
8301        if let Some(health) = self.health.filter(|health| health.last_change.amount < 0.0) {
8302            agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] =
8303                (read_data.time.0 - health.last_change.time.0) as f32;
8304        } else {
8305            agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] +=
8306                read_data.dt.0;
8307        }
8308
8309        if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
8310            match self.char_state {
8311                CharacterState::ChargedMelee(c) => {
8312                    if c.charge_frac() < 1.0 && could_use_input(input) {
8313                        controller.push_basic_input(input);
8314                    }
8315                },
8316                CharacterState::ChargedRanged(c) => {
8317                    if c.charge_frac() < 1.0 && could_use_input(input) {
8318                        controller.push_basic_input(input);
8319                    }
8320                },
8321                _ => {},
8322            }
8323        }
8324
8325        let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
8326            Some(input @ InputKind::Primary) => {
8327                if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
8328                    && could_use_input(input)
8329                {
8330                    controller.push_basic_input(input);
8331                    true
8332                } else {
8333                    false
8334                }
8335            },
8336            _ => false,
8337        };
8338
8339        let has_heads = self.heads.is_none_or(|heads| heads.amount() > 0);
8340
8341        let move_forwards = if !continued_attack {
8342            if could_use_input(InputKind::Ability(1))
8343                && rng.random_bool(0.9)
8344                && (agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] > 5.0
8345                    || agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize]
8346                        > 6.0)
8347                && self.heads.is_some_and(|heads| heads.amount_missing() > 0)
8348            {
8349                controller.push_basic_input(InputKind::Ability(2));
8350                false
8351            } else if has_heads && could_use_input(InputKind::Primary) && rng.random_bool(0.8) {
8352                controller.push_basic_input(InputKind::Primary);
8353                true
8354            } else if has_heads && could_use_input(InputKind::Secondary) && rng.random_bool(0.4) {
8355                controller.push_basic_input(InputKind::Secondary);
8356                false
8357            } else if has_heads && could_use_input(InputKind::Ability(1)) && rng.random_bool(0.6) {
8358                controller.push_basic_input(InputKind::Ability(1));
8359                true
8360            } else if !has_heads && could_use_input(InputKind::Ability(3)) && rng.random_bool(0.7) {
8361                controller.push_basic_input(InputKind::Ability(3));
8362                true
8363            } else if could_use_input(InputKind::Ability(0)) {
8364                controller.push_basic_input(InputKind::Ability(0));
8365                true
8366            } else {
8367                true
8368            }
8369        } else {
8370            true
8371        };
8372
8373        if move_forwards {
8374            if has_heads {
8375                self.path_toward_target(
8376                    agent,
8377                    controller,
8378                    tgt_data.pos.0,
8379                    read_data,
8380                    Path::Separate,
8381                    // Slow down if close to the target
8382                    (attack_data.dist_sqrd
8383                        < (2.5 + self.body.map_or(0.0, |b| b.front_radius())).powi(2))
8384                    .then_some(0.3),
8385                );
8386            } else {
8387                self.flee(agent, controller, read_data, tgt_data.pos);
8388            }
8389        }
8390    }
8391
8392    pub fn handle_random_abilities(
8393        &self,
8394        agent: &mut Agent,
8395        controller: &mut Controller,
8396        attack_data: &AttackData,
8397        tgt_data: &TargetData,
8398        read_data: &ReadData,
8399        rng: &mut impl Rng,
8400        primary_weight: u8,
8401        secondary_weight: u8,
8402        ability_weights: [u8; BASE_ABILITY_LIMIT],
8403    ) {
8404        let primary = self.extract_ability(AbilityInput::Primary);
8405        let secondary = self.extract_ability(AbilityInput::Secondary);
8406        let abilities = [
8407            self.extract_ability(AbilityInput::Auxiliary(0)),
8408            self.extract_ability(AbilityInput::Auxiliary(1)),
8409            self.extract_ability(AbilityInput::Auxiliary(2)),
8410            self.extract_ability(AbilityInput::Auxiliary(3)),
8411            self.extract_ability(AbilityInput::Auxiliary(4)),
8412        ];
8413        let could_use_input = |input| match input {
8414            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8415                p.could_use(
8416                    attack_data,
8417                    self,
8418                    tgt_data,
8419                    read_data,
8420                    AbilityPreferences::default(),
8421                )
8422            }),
8423            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8424                s.could_use(
8425                    attack_data,
8426                    self,
8427                    tgt_data,
8428                    read_data,
8429                    AbilityPreferences::default(),
8430                )
8431            }),
8432            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8433                a.could_use(
8434                    attack_data,
8435                    self,
8436                    tgt_data,
8437                    read_data,
8438                    AbilityPreferences::default(),
8439                )
8440            }),
8441            _ => false,
8442        };
8443
8444        let primary_chance = primary_weight as f64
8445            / ((primary_weight + secondary_weight + ability_weights.iter().sum::<u8>()) as f64)
8446                .max(0.01);
8447        let secondary_chance = secondary_weight as f64
8448            / ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
8449        let ability_chances = {
8450            let mut chances = [0.0; BASE_ABILITY_LIMIT];
8451            chances.iter_mut().enumerate().for_each(|(i, chance)| {
8452                *chance = ability_weights[i] as f64
8453                    / (ability_weights
8454                        .iter()
8455                        .enumerate()
8456                        .filter_map(|(j, weight)| if j >= i { Some(weight) } else { None })
8457                        .sum::<u8>() as f64)
8458                        .max(0.01)
8459            });
8460            chances
8461        };
8462
8463        if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
8464            match self.char_state {
8465                CharacterState::ChargedMelee(c) => {
8466                    if c.charge_frac() < 1.0 && could_use_input(input) {
8467                        controller.push_basic_input(input);
8468                    }
8469                },
8470                CharacterState::ChargedRanged(c) => {
8471                    if c.charge_frac() < 1.0 && could_use_input(input) {
8472                        controller.push_basic_input(input);
8473                    }
8474                },
8475                _ => {},
8476            }
8477        }
8478
8479        let move_forwards = if could_use_input(InputKind::Primary)
8480            && rng.random_bool(primary_chance)
8481        {
8482            controller.push_basic_input(InputKind::Primary);
8483            false
8484        } else if could_use_input(InputKind::Secondary) && rng.random_bool(secondary_chance) {
8485            controller.push_basic_input(InputKind::Secondary);
8486            false
8487        } else if could_use_input(InputKind::Ability(0)) && rng.random_bool(ability_chances[0]) {
8488            controller.push_basic_input(InputKind::Ability(0));
8489            false
8490        } else if could_use_input(InputKind::Ability(1)) && rng.random_bool(ability_chances[1]) {
8491            controller.push_basic_input(InputKind::Ability(1));
8492            false
8493        } else if could_use_input(InputKind::Ability(2)) && rng.random_bool(ability_chances[2]) {
8494            controller.push_basic_input(InputKind::Ability(2));
8495            false
8496        } else if could_use_input(InputKind::Ability(3)) && rng.random_bool(ability_chances[3]) {
8497            controller.push_basic_input(InputKind::Ability(3));
8498            false
8499        } else if could_use_input(InputKind::Ability(4)) && rng.random_bool(ability_chances[4]) {
8500            controller.push_basic_input(InputKind::Ability(4));
8501            false
8502        } else {
8503            true
8504        };
8505
8506        if move_forwards {
8507            self.path_toward_target(
8508                agent,
8509                controller,
8510                tgt_data.pos.0,
8511                read_data,
8512                Path::Separate,
8513                None,
8514            );
8515        }
8516    }
8517
8518    pub fn handle_simple_double_attack(
8519        &self,
8520        agent: &mut Agent,
8521        controller: &mut Controller,
8522        attack_data: &AttackData,
8523        tgt_data: &TargetData,
8524        read_data: &ReadData,
8525    ) {
8526        const MAX_ATTACK_RANGE: f32 = 20.0;
8527
8528        if attack_data.angle < 60.0 && attack_data.dist_sqrd < MAX_ATTACK_RANGE.powi(2) {
8529            controller.inputs.move_dir = Vec2::zero();
8530            if attack_data.in_min_range() {
8531                controller.push_basic_input(InputKind::Primary);
8532            } else {
8533                controller.push_basic_input(InputKind::Secondary);
8534            }
8535        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
8536            self.path_toward_target(
8537                agent,
8538                controller,
8539                tgt_data.pos.0,
8540                read_data,
8541                Path::Separate,
8542                None,
8543            );
8544        } else {
8545            self.path_toward_target(
8546                agent,
8547                controller,
8548                tgt_data.pos.0,
8549                read_data,
8550                Path::AtTarget,
8551                None,
8552            );
8553        }
8554    }
8555
8556    pub fn handle_clay_steed_attack(
8557        &self,
8558        agent: &mut Agent,
8559        controller: &mut Controller,
8560        attack_data: &AttackData,
8561        tgt_data: &TargetData,
8562        read_data: &ReadData,
8563    ) {
8564        enum ActionStateTimers {
8565            AttackTimer,
8566        }
8567        const HOOF_ATTACK_RANGE: f32 = 1.0;
8568        const HOOF_ATTACK_ANGLE: f32 = 50.0;
8569
8570        agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
8571        if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 10.0 {
8572            // Reset timer
8573            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
8574        }
8575
8576        if attack_data.angle < HOOF_ATTACK_ANGLE
8577            && attack_data.dist_sqrd
8578                < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.max_radius())).powi(2)
8579        {
8580            controller.inputs.move_dir = Vec2::zero();
8581            controller.push_basic_input(InputKind::Primary);
8582        } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
8583            controller.push_basic_input(InputKind::Secondary);
8584        } else {
8585            self.path_toward_target(
8586                agent,
8587                controller,
8588                tgt_data.pos.0,
8589                read_data,
8590                Path::AtTarget,
8591                None,
8592            );
8593        }
8594    }
8595
8596    pub fn handle_ancient_effigy_attack(
8597        &self,
8598        agent: &mut Agent,
8599        controller: &mut Controller,
8600        attack_data: &AttackData,
8601        tgt_data: &TargetData,
8602        read_data: &ReadData,
8603    ) {
8604        enum ActionStateTimers {
8605            BlastTimer,
8606        }
8607
8608        let home = agent.patrol_origin.unwrap_or(self.pos.0);
8609        let line_of_sight_with_target = || {
8610            entities_have_line_of_sight(
8611                self.pos,
8612                self.body,
8613                self.scale,
8614                tgt_data.pos,
8615                tgt_data.body,
8616                tgt_data.scale,
8617                read_data,
8618            )
8619        };
8620        agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] += read_data.dt.0;
8621
8622        if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] > 6.0 {
8623            agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] = 0.0;
8624        }
8625        if line_of_sight_with_target() {
8626            if attack_data.in_min_range() {
8627                controller.push_basic_input(InputKind::Secondary);
8628            } else if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] < 2.0 {
8629                controller.push_basic_input(InputKind::Primary);
8630            } else {
8631                self.path_toward_target(
8632                    agent,
8633                    controller,
8634                    tgt_data.pos.0,
8635                    read_data,
8636                    Path::Separate,
8637                    None,
8638                );
8639            }
8640        } else {
8641            // if target is hiding, don't follow, guard the room
8642            if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
8643                self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
8644            }
8645        }
8646    }
8647
8648    pub fn handle_clay_golem_attack(
8649        &self,
8650        agent: &mut Agent,
8651        controller: &mut Controller,
8652        attack_data: &AttackData,
8653        tgt_data: &TargetData,
8654        read_data: &ReadData,
8655    ) {
8656        const MIN_DASH_RANGE: f32 = 15.0;
8657
8658        enum ActionStateTimers {
8659            AttackTimer,
8660        }
8661
8662        let line_of_sight_with_target = || {
8663            entities_have_line_of_sight(
8664                self.pos,
8665                self.body,
8666                self.scale,
8667                tgt_data.pos,
8668                tgt_data.body,
8669                tgt_data.scale,
8670                read_data,
8671            )
8672        };
8673        let spawn = agent.patrol_origin.unwrap_or(self.pos.0);
8674        let home = Vec3::new(spawn.x - 32.0, spawn.y - 12.0, spawn.z);
8675        let is_home = (home - self.pos.0).xy().magnitude_squared() < (3.0_f32).powi(2);
8676        agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
8677        if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
8678            // Reset timer
8679            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
8680        }
8681        if line_of_sight_with_target() {
8682            controller.inputs.move_dir = Vec2::zero();
8683            if attack_data.in_min_range() {
8684                controller.push_basic_input(InputKind::Primary);
8685            } else if attack_data.dist_sqrd > MIN_DASH_RANGE.powi(2) {
8686                controller.push_basic_input(InputKind::Secondary);
8687            } else {
8688                self.path_toward_target(
8689                    agent,
8690                    controller,
8691                    tgt_data.pos.0,
8692                    read_data,
8693                    Path::AtTarget,
8694                    None,
8695                );
8696            }
8697        } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.0 {
8698            if !is_home {
8699                // if target is wall cheesing, reposition
8700                self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
8701            } else {
8702                self.path_toward_target(agent, controller, spawn, read_data, Path::Separate, None);
8703            }
8704        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
8705            self.path_toward_target(
8706                agent,
8707                controller,
8708                tgt_data.pos.0,
8709                read_data,
8710                Path::Separate,
8711                None,
8712            );
8713        }
8714    }
8715
8716    pub fn handle_haniwa_soldier(
8717        &self,
8718        agent: &mut Agent,
8719        controller: &mut Controller,
8720        attack_data: &AttackData,
8721        tgt_data: &TargetData,
8722        read_data: &ReadData,
8723    ) {
8724        const DEFENSIVE_CONDITION: usize = 0;
8725        const RIPOSTE_TIMER: usize = 0;
8726        const MODE_CYCLE_TIMER: usize = 1;
8727
8728        let primary = self.extract_ability(AbilityInput::Primary);
8729        let secondary = self.extract_ability(AbilityInput::Secondary);
8730        let could_use_input = |input| match input {
8731            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8732                p.could_use(
8733                    attack_data,
8734                    self,
8735                    tgt_data,
8736                    read_data,
8737                    AbilityPreferences::default(),
8738                )
8739            }),
8740            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8741                s.could_use(
8742                    attack_data,
8743                    self,
8744                    tgt_data,
8745                    read_data,
8746                    AbilityPreferences::default(),
8747                )
8748            }),
8749            _ => false,
8750        };
8751
8752        agent.combat_state.timers[RIPOSTE_TIMER] += read_data.dt.0;
8753        agent.combat_state.timers[MODE_CYCLE_TIMER] += read_data.dt.0;
8754
8755        if agent.combat_state.timers[MODE_CYCLE_TIMER] > 7.0 {
8756            agent.combat_state.conditions[DEFENSIVE_CONDITION] =
8757                !agent.combat_state.conditions[DEFENSIVE_CONDITION];
8758            agent.combat_state.timers[MODE_CYCLE_TIMER] = 0.0;
8759        }
8760
8761        if matches!(self.char_state, CharacterState::RiposteMelee(_)) {
8762            agent.combat_state.timers[RIPOSTE_TIMER] = 0.0;
8763        }
8764
8765        let try_move = if agent.combat_state.conditions[DEFENSIVE_CONDITION] {
8766            controller.push_basic_input(InputKind::Block);
8767            true
8768        } else if agent.combat_state.timers[RIPOSTE_TIMER] > 10.0
8769            && could_use_input(InputKind::Secondary)
8770        {
8771            controller.push_basic_input(InputKind::Secondary);
8772            false
8773        } else if could_use_input(InputKind::Primary) {
8774            controller.push_basic_input(InputKind::Primary);
8775            false
8776        } else {
8777            true
8778        };
8779
8780        if try_move && attack_data.dist_sqrd > 2_f32.powi(2) {
8781            self.path_toward_target(
8782                agent,
8783                controller,
8784                tgt_data.pos.0,
8785                read_data,
8786                Path::Separate,
8787                None,
8788            );
8789        }
8790    }
8791
8792    pub fn handle_haniwa_guard(
8793        &self,
8794        agent: &mut Agent,
8795        controller: &mut Controller,
8796        attack_data: &AttackData,
8797        tgt_data: &TargetData,
8798        read_data: &ReadData,
8799        rng: &mut impl Rng,
8800    ) {
8801        const BACKPEDAL_DIST: f32 = 5.0;
8802        const ROTATE_CCW_CONDITION: usize = 0;
8803        const FLURRY_TIMER: usize = 0;
8804        const BACKPEDAL_TIMER: usize = 1;
8805        const SWITCH_ROTATE_TIMER: usize = 2;
8806        const SWITCH_ROTATE_COUNTER: usize = 0;
8807
8808        let primary = self.extract_ability(AbilityInput::Primary);
8809        let secondary = self.extract_ability(AbilityInput::Secondary);
8810        let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
8811        let could_use_input = |input| match input {
8812            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8813                p.could_use(
8814                    attack_data,
8815                    self,
8816                    tgt_data,
8817                    read_data,
8818                    AbilityPreferences::default(),
8819                )
8820            }),
8821            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8822                s.could_use(
8823                    attack_data,
8824                    self,
8825                    tgt_data,
8826                    read_data,
8827                    AbilityPreferences::default(),
8828                )
8829            }),
8830            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8831                a.could_use(
8832                    attack_data,
8833                    self,
8834                    tgt_data,
8835                    read_data,
8836                    AbilityPreferences::default(),
8837                )
8838            }),
8839            _ => false,
8840        };
8841
8842        if !agent.combat_state.initialized {
8843            agent.combat_state.conditions[ROTATE_CCW_CONDITION] = rng.random_bool(0.5);
8844            agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.random_range(5.0..20.0);
8845            agent.combat_state.initialized = true;
8846        }
8847
8848        let continue_flurry = match self.char_state {
8849            CharacterState::BasicMelee(_) => {
8850                agent.combat_state.timers[FLURRY_TIMER] += read_data.dt.0;
8851                false
8852            },
8853            CharacterState::RapidMelee(c) => {
8854                agent.combat_state.timers[FLURRY_TIMER] = 0.0;
8855                !matches!(c.stage_section, StageSection::Recover)
8856            },
8857            CharacterState::ComboMelee2(_) => {
8858                agent.combat_state.timers[BACKPEDAL_TIMER] = 0.0;
8859                false
8860            },
8861            _ => false,
8862        };
8863        agent.combat_state.timers[SWITCH_ROTATE_TIMER] += read_data.dt.0;
8864        agent.combat_state.timers[BACKPEDAL_TIMER] += read_data.dt.0;
8865
8866        if agent.combat_state.timers[SWITCH_ROTATE_TIMER]
8867            > agent.combat_state.counters[SWITCH_ROTATE_COUNTER]
8868        {
8869            agent.combat_state.conditions[ROTATE_CCW_CONDITION] =
8870                !agent.combat_state.conditions[ROTATE_CCW_CONDITION];
8871            agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.random_range(5.0..20.0);
8872        }
8873
8874        let move_farther = attack_data.dist_sqrd < BACKPEDAL_DIST.powi(2);
8875        let move_closer = if continue_flurry && could_use_input(InputKind::Secondary) {
8876            controller.push_basic_input(InputKind::Secondary);
8877            false
8878        } else if agent.combat_state.timers[BACKPEDAL_TIMER] > 10.0
8879            && move_farther
8880            && could_use_input(InputKind::Ability(0))
8881        {
8882            controller.push_basic_input(InputKind::Ability(0));
8883            false
8884        } else if agent.combat_state.timers[FLURRY_TIMER] > 6.0
8885            && could_use_input(InputKind::Secondary)
8886        {
8887            controller.push_basic_input(InputKind::Secondary);
8888            false
8889        } else if could_use_input(InputKind::Primary) {
8890            controller.push_basic_input(InputKind::Primary);
8891            false
8892        } else {
8893            true
8894        };
8895
8896        if let Some((bearing, speed, stuck)) = agent.chaser.chase(
8897            &*read_data.terrain,
8898            self.pos.0,
8899            self.vel.0,
8900            tgt_data.pos.0,
8901            TraversalConfig {
8902                min_tgt_dist: 1.25,
8903                ..self.traversal_config
8904            },
8905            &read_data.time,
8906        ) {
8907            self.unstuck_if(stuck, controller);
8908            if entities_have_line_of_sight(
8909                self.pos,
8910                self.body,
8911                self.scale,
8912                tgt_data.pos,
8913                tgt_data.body,
8914                tgt_data.scale,
8915                read_data,
8916            ) && attack_data.angle < 45.0
8917            {
8918                let angle = match (
8919                    agent.combat_state.conditions[ROTATE_CCW_CONDITION],
8920                    move_closer,
8921                    move_farther,
8922                ) {
8923                    (true, true, false) => rng.random_range(-1.5..-0.5),
8924                    (true, false, true) => rng.random_range(-2.2..-1.7),
8925                    (true, _, _) => rng.random_range(-1.7..-1.5),
8926                    (false, true, false) => rng.random_range(0.5..1.5),
8927                    (false, false, true) => rng.random_range(1.7..2.2),
8928                    (false, _, _) => rng.random_range(1.5..1.7),
8929                };
8930                controller.inputs.move_dir = bearing
8931                    .xy()
8932                    .rotated_z(angle)
8933                    .try_normalized()
8934                    .unwrap_or_else(Vec2::zero)
8935                    * speed;
8936            } else {
8937                controller.inputs.move_dir =
8938                    bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
8939                self.jump_if(bearing.z > 1.5, controller);
8940            }
8941        }
8942    }
8943
8944    pub fn handle_haniwa_archer(
8945        &self,
8946        agent: &mut Agent,
8947        controller: &mut Controller,
8948        attack_data: &AttackData,
8949        tgt_data: &TargetData,
8950        read_data: &ReadData,
8951    ) {
8952        const KICK_TIMER: usize = 0;
8953        const EXPLOSIVE_TIMER: usize = 1;
8954
8955        let primary = self.extract_ability(AbilityInput::Primary);
8956        let secondary = self.extract_ability(AbilityInput::Secondary);
8957        let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
8958        let could_use_input = |input| match input {
8959            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8960                p.could_use(
8961                    attack_data,
8962                    self,
8963                    tgt_data,
8964                    read_data,
8965                    AbilityPreferences::default(),
8966                )
8967            }),
8968            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8969                s.could_use(
8970                    attack_data,
8971                    self,
8972                    tgt_data,
8973                    read_data,
8974                    AbilityPreferences::default(),
8975                )
8976            }),
8977            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8978                a.could_use(
8979                    attack_data,
8980                    self,
8981                    tgt_data,
8982                    read_data,
8983                    AbilityPreferences::default(),
8984                )
8985            }),
8986            _ => false,
8987        };
8988
8989        agent.combat_state.timers[KICK_TIMER] += read_data.dt.0;
8990        agent.combat_state.timers[EXPLOSIVE_TIMER] += read_data.dt.0;
8991
8992        match self.char_state.ability_info().map(|ai| ai.input) {
8993            Some(InputKind::Secondary) => {
8994                agent.combat_state.timers[KICK_TIMER] = 0.0;
8995            },
8996            Some(InputKind::Ability(0)) => {
8997                agent.combat_state.timers[EXPLOSIVE_TIMER] = 0.0;
8998            },
8999            _ => {},
9000        }
9001
9002        if agent.combat_state.timers[KICK_TIMER] > 4.0 && could_use_input(InputKind::Secondary) {
9003            controller.push_basic_input(InputKind::Secondary);
9004        } else if agent.combat_state.timers[EXPLOSIVE_TIMER] > 15.0
9005            && could_use_input(InputKind::Ability(0))
9006        {
9007            controller.push_basic_input(InputKind::Ability(0));
9008        } else if could_use_input(InputKind::Primary) {
9009            controller.push_basic_input(InputKind::Primary);
9010        } else {
9011            self.path_toward_target(
9012                agent,
9013                controller,
9014                tgt_data.pos.0,
9015                read_data,
9016                Path::Separate,
9017                None,
9018            );
9019        }
9020    }
9021
9022    pub fn handle_terracotta_statue_attack(
9023        &self,
9024        agent: &mut Agent,
9025        controller: &mut Controller,
9026        attack_data: &AttackData,
9027        read_data: &ReadData,
9028    ) {
9029        enum Conditions {
9030            AttackToggle,
9031        }
9032        let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
9033        // stay centered
9034        if (home - self.pos.0).xy().magnitude_squared() > (2.0_f32).powi(2) {
9035            self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
9036        } else if !agent.combat_state.conditions[Conditions::AttackToggle as usize] {
9037            // always begin with sprite summon
9038            controller.push_basic_input(InputKind::Primary);
9039        } else {
9040            controller.inputs.move_dir = Vec2::zero();
9041            if attack_data.dist_sqrd < 8.5f32.powi(2) {
9042                // sprite summon
9043                controller.push_basic_input(InputKind::Primary);
9044            } else {
9045                // projectile
9046                controller.push_basic_input(InputKind::Secondary);
9047            }
9048        }
9049        if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
9050        {
9051            agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
9052        }
9053    }
9054
9055    pub fn handle_jiangshi_attack(
9056        &self,
9057        agent: &mut Agent,
9058        controller: &mut Controller,
9059        attack_data: &AttackData,
9060        tgt_data: &TargetData,
9061        read_data: &ReadData,
9062    ) {
9063        if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
9064            controller.push_action(ControlAction::StartInput {
9065                input: InputKind::Secondary,
9066                target_entity: agent
9067                    .target
9068                    .as_ref()
9069                    .and_then(|t| read_data.uids.get(t.target))
9070                    .copied(),
9071                select_pos: None,
9072            });
9073        } else if attack_data.dist_sqrd < 12.0f32.powi(2) {
9074            controller.push_basic_input(InputKind::Primary);
9075        }
9076
9077        self.path_toward_target(
9078            agent,
9079            controller,
9080            tgt_data.pos.0,
9081            read_data,
9082            Path::AtTarget,
9083            None,
9084        );
9085    }
9086}