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