veloren_server_agent/
attack.rs

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