Skip to main content

veloren_server_agent/
attack.rs

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