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