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