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_elephant_attack(
4938        &self,
4939        agent: &mut Agent,
4940        controller: &mut Controller,
4941        attack_data: &AttackData,
4942        tgt_data: &TargetData,
4943        read_data: &ReadData,
4944        rng: &mut impl Rng,
4945    ) {
4946        const MELEE_RANGE: f32 = 10.0;
4947        const RANGED_RANGE: f32 = 20.0;
4948        const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
4949            desired_energy: 30.0,
4950            combo_scaling_buildup: 0,
4951        };
4952
4953        const GOUGE: InputKind = InputKind::Primary;
4954        const DASH: InputKind = InputKind::Secondary;
4955        const STOMP: InputKind = InputKind::Ability(0);
4956        const WATER: InputKind = InputKind::Ability(1);
4957        const VACUUM: InputKind = InputKind::Ability(2);
4958
4959        let could_use = |input| {
4960            Option::<AbilityInput>::from(input)
4961                .and_then(|ability_input| self.extract_ability(ability_input))
4962                .is_some_and(|ability_data| {
4963                    ability_data.could_use(
4964                        attack_data,
4965                        self,
4966                        tgt_data,
4967                        read_data,
4968                        ABILITY_PREFERENCES,
4969                    )
4970                })
4971        };
4972
4973        let dashing = matches!(self.char_state, CharacterState::DashMelee(_))
4974            && self.char_state.stage_section() != Some(StageSection::Recover);
4975
4976        if dashing {
4977            controller.push_basic_input(DASH);
4978        } else if rng.random_bool(0.05) {
4979            if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
4980                if rng.random_bool(0.5) && could_use(STOMP) {
4981                    controller.push_basic_input(STOMP);
4982                } else {
4983                    controller.push_basic_input(GOUGE);
4984                }
4985            } else if attack_data.dist_sqrd < RANGED_RANGE.powi(2) {
4986                if rng.random_bool(0.5) {
4987                    controller.push_basic_input(WATER);
4988                } else if could_use(VACUUM) {
4989                    controller.push_basic_input(VACUUM);
4990                } else {
4991                    controller.push_basic_input(DASH);
4992                }
4993            } else {
4994                controller.push_basic_input(DASH);
4995            }
4996        }
4997
4998        self.path_toward_target(
4999            agent,
5000            controller,
5001            tgt_data.pos.0,
5002            read_data,
5003            Path::AtTarget,
5004            None,
5005        );
5006    }
5007
5008    pub fn handle_rocksnapper_attack(
5009        &self,
5010        agent: &mut Agent,
5011        controller: &mut Controller,
5012        attack_data: &AttackData,
5013        tgt_data: &TargetData,
5014        read_data: &ReadData,
5015    ) {
5016        const LEAP_TIMER: f32 = 3.0;
5017        const DASH_TIMER: f32 = 5.0;
5018        const LEAP_RANGE: f32 = 20.0;
5019        const MELEE_RANGE: f32 = 5.0;
5020
5021        enum ActionStateTimers {
5022            TimerRocksnapperDash = 0,
5023            TimerRocksnapperLeap = 1,
5024        }
5025        agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] +=
5026            read_data.dt.0;
5027        agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] +=
5028            read_data.dt.0;
5029
5030        if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5031        {
5032            // If already dashing, keep dashing if not in recover stage
5033            controller.push_basic_input(InputKind::Secondary);
5034        } else if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize]
5035            > DASH_TIMER
5036        {
5037            // Use dash if timer has gone for long enough
5038            controller.push_basic_input(InputKind::Secondary);
5039
5040            if matches!(self.char_state, CharacterState::DashMelee(_)) {
5041                // Resets action counter when using dash
5042                agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] = 0.0;
5043            }
5044        } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) && attack_data.angle < 90.0 {
5045            if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize]
5046                > LEAP_TIMER
5047            {
5048                // Use shockwave if timer has gone for long enough
5049                controller.push_basic_input(InputKind::Ability(0));
5050
5051                if matches!(self.char_state, CharacterState::LeapShockwave(_)) {
5052                    // Resets action timer when using leap shockwave
5053                    agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] =
5054                        0.0;
5055                }
5056            } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5057                // Basic attack if in melee range
5058                controller.push_basic_input(InputKind::Primary);
5059            }
5060        } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
5061            // Basic attack if in melee range
5062            controller.push_basic_input(InputKind::Primary);
5063        }
5064
5065        // Always attempt to path towards target
5066        self.path_toward_target(
5067            agent,
5068            controller,
5069            tgt_data.pos.0,
5070            read_data,
5071            Path::AtTarget,
5072            None,
5073        );
5074    }
5075
5076    pub fn handle_roshwalr_attack(
5077        &self,
5078        agent: &mut Agent,
5079        controller: &mut Controller,
5080        attack_data: &AttackData,
5081        tgt_data: &TargetData,
5082        read_data: &ReadData,
5083    ) {
5084        const SLOW_CHARGE_RANGE: f32 = 12.5;
5085        const SHOCKWAVE_RANGE: f32 = 12.5;
5086        const SHOCKWAVE_TIMER: f32 = 15.0;
5087        const MELEE_RANGE: f32 = 4.0;
5088
5089        enum ActionStateFCounters {
5090            FCounterRoshwalrAttack = 0,
5091        }
5092
5093        agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize] +=
5094            read_data.dt.0;
5095        if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5096        {
5097            // If already charging, keep charging if not in recover
5098            controller.push_basic_input(InputKind::Ability(0));
5099        } else if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 270.0 {
5100            if agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize]
5101                > SHOCKWAVE_TIMER
5102            {
5103                // Use shockwave if timer has gone for long enough
5104                controller.push_basic_input(InputKind::Ability(0));
5105
5106                if matches!(self.char_state, CharacterState::Shockwave(_)) {
5107                    // Resets action counter when using shockwave
5108                    agent.combat_state.counters
5109                        [ActionStateFCounters::FCounterRoshwalrAttack as usize] = 0.0;
5110                }
5111            } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
5112                // Basic attack if in melee range
5113                controller.push_basic_input(InputKind::Primary);
5114            }
5115        } else if attack_data.dist_sqrd > SLOW_CHARGE_RANGE.powi(2) {
5116            // Use slow charge if outside the range
5117            controller.push_basic_input(InputKind::Secondary);
5118        }
5119
5120        // Always attempt to path towards target
5121        self.path_toward_target(
5122            agent,
5123            controller,
5124            tgt_data.pos.0,
5125            read_data,
5126            Path::AtTarget,
5127            None,
5128        );
5129    }
5130
5131    pub fn handle_harvester_attack(
5132        &self,
5133        agent: &mut Agent,
5134        controller: &mut Controller,
5135        attack_data: &AttackData,
5136        tgt_data: &TargetData,
5137        read_data: &ReadData,
5138        rng: &mut impl Rng,
5139    ) {
5140        // === reference ===
5141        // Inputs:
5142        //   Primary: scythe
5143        //   Secondary: firebreath
5144        //   Auxiliary
5145        //     0: explosivepumpkin
5146        //     1: ensaringvines_sparse
5147        //     2: ensaringvines_dense
5148
5149        // === setup ===
5150
5151        // --- static ---
5152        // behaviour parameters
5153        const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60;
5154        const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30;
5155        const PATH_RANGE_FACTOR: f32 = 0.4; // get comfortably in range, but give player room to breathe
5156        const SCYTHE_RANGE_FACTOR: f32 = 0.75; // start attack while suitably in range
5157        const SCYTHE_AIM_FACTOR: f32 = 0.7;
5158        const FIREBREATH_RANGE_FACTOR: f32 = 0.7;
5159        const FIREBREATH_AIM_FACTOR: f32 = 0.8;
5160        const FIREBREATH_TIME_LIMIT: f32 = 4.0;
5161        const FIREBREATH_SHORT_TIME_LIMIT: f32 = 2.5; // cutoff sooner at close range
5162        const FIREBREATH_COOLDOWN: f32 = 3.5;
5163        const PUMPKIN_RANGE_FACTOR: f32 = 0.75;
5164        const CLOSE_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 7.0]; // variation in attacks at close range
5165        const MID_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 4.5]; //   ^                       mid
5166        const FAR_PUMPKIN_COOLDOWN_SPAN: [f32; 2] = [3.0, 5.0]; // allows for pathing to player between throws
5167
5168        // conditions
5169        const HAS_SUMMONED_FIRST_VINES: usize = 0;
5170        const HAS_SUMMONED_SECOND_VINES: usize = 1;
5171        // timers
5172        const FIREBREATH: usize = 0;
5173        const MIXUP: usize = 1;
5174        const FAR_PUMPKIN: usize = 2;
5175        //counters
5176        const CLOSE_MIXUP_COOLDOWN: usize = 0;
5177        const MID_MIXUP_COOLDOWN: usize = 1;
5178        const FAR_PUMPKIN_COOLDOWN: usize = 2;
5179
5180        // line of sight check
5181        let line_of_sight_with_target = || {
5182            entities_have_line_of_sight(
5183                self.pos,
5184                self.body,
5185                self.scale,
5186                tgt_data.pos,
5187                tgt_data.body,
5188                tgt_data.scale,
5189                read_data,
5190            )
5191        };
5192
5193        // --- dynamic ---
5194        // attack data
5195        let (scythe_range, scythe_angle) = {
5196            if let Some(AbilityData::BasicMelee { range, angle, .. }) =
5197                self.extract_ability(AbilityInput::Primary)
5198            {
5199                (range, angle)
5200            } else {
5201                (0.0, 0.0)
5202            }
5203        };
5204        let (firebreath_range, firebreath_angle) = {
5205            if let Some(AbilityData::BasicBeam { range, angle, .. }) =
5206                self.extract_ability(AbilityInput::Secondary)
5207            {
5208                (range, angle)
5209            } else {
5210                (0.0, 0.0)
5211            }
5212        };
5213        let pumpkin_speed = {
5214            if let Some(AbilityData::BasicRanged {
5215                projectile_speed, ..
5216            }) = self.extract_ability(AbilityInput::Auxiliary(0))
5217            {
5218                projectile_speed
5219            } else {
5220                0.0
5221            }
5222        };
5223        // calculated attack data
5224        let pumpkin_max_range =
5225            projectile_flat_range(pumpkin_speed, self.body.map_or(0.0, |b| b.height()));
5226
5227        // character state info
5228        let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
5229        let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
5230        let is_in_summon_recovery = matches!(self.char_state, CharacterState::SpriteSummon(data) if matches!(data.stage_section, StageSection::Recover));
5231        let firebreath_timer = if let CharacterState::BasicBeam(data) = self.char_state {
5232            data.timer
5233        } else {
5234            Default::default()
5235        };
5236        let is_using_mixup = is_using_firebreath || is_using_pumpkin;
5237
5238        // initialise randomised cooldowns
5239        if !agent.combat_state.initialized {
5240            agent.combat_state.initialized = true;
5241            agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5242                rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5243            agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5244                rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5245            agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5246                rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5247        }
5248
5249        // === main ===
5250
5251        // --- timers ---
5252        if is_in_summon_recovery {
5253            // reset all timers when done summoning
5254            agent.combat_state.timers[FIREBREATH] = 0.0;
5255            agent.combat_state.timers[MIXUP] = 0.0;
5256            agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5257        } else {
5258            // handle state timers
5259            if is_using_firebreath {
5260                agent.combat_state.timers[FIREBREATH] = 0.0;
5261            } else {
5262                agent.combat_state.timers[FIREBREATH] += read_data.dt.0;
5263            }
5264            if is_using_mixup {
5265                agent.combat_state.timers[MIXUP] = 0.0;
5266            } else {
5267                agent.combat_state.timers[MIXUP] += read_data.dt.0;
5268            }
5269            if is_using_pumpkin {
5270                agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5271            } else {
5272                agent.combat_state.timers[FAR_PUMPKIN] += read_data.dt.0;
5273            }
5274        }
5275
5276        // --- attacks ---
5277        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5278        // second vine summon
5279        if health_fraction < SECOND_VINE_CREATION_THRESHOLD
5280            && !agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES]
5281        {
5282            // use the dense vine summon
5283            controller.push_basic_input(InputKind::Ability(2));
5284            // wait till recovery before finishing
5285            if is_in_summon_recovery {
5286                agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES] = true;
5287            }
5288        }
5289        // first vine summon
5290        else if health_fraction < FIRST_VINE_CREATION_THRESHOLD
5291            && !agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES]
5292        {
5293            // use the sparse vine summon
5294            controller.push_basic_input(InputKind::Ability(1));
5295            // wait till recovery before finishing
5296            if is_in_summon_recovery {
5297                agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES] = true;
5298            }
5299        }
5300        // close range
5301        else if attack_data.dist_sqrd
5302            < (attack_data.body_dist + scythe_range * SCYTHE_RANGE_FACTOR).powi(2)
5303        {
5304            // if using firebreath, keep going under short time limit
5305            if is_using_firebreath
5306                && firebreath_timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME_LIMIT)
5307            {
5308                controller.push_basic_input(InputKind::Secondary);
5309            }
5310            // in scythe angle
5311            if attack_data.angle < scythe_angle * SCYTHE_AIM_FACTOR {
5312                // on timer, randomly mixup attacks
5313                if agent.combat_state.timers[MIXUP]
5314                    > agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN]
5315                // for now, no line of sight check for consitency in attacks
5316                {
5317                    // if on firebreath cooldown, throw pumpkin
5318                    if agent.combat_state.timers[FIREBREATH] < FIREBREATH_COOLDOWN {
5319                        controller.push_basic_input(InputKind::Ability(0));
5320                    }
5321                    // otherwise, randomise between firebreath and pumpkin
5322                    else if rng.random_bool(0.5) {
5323                        controller.push_basic_input(InputKind::Secondary);
5324                    } else {
5325                        controller.push_basic_input(InputKind::Ability(0));
5326                    }
5327                    // reset mixup cooldown if actually being used
5328                    if is_using_mixup {
5329                        agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5330                            rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5331                    }
5332                }
5333                // default to using scythe melee
5334                else {
5335                    controller.push_basic_input(InputKind::Primary);
5336                }
5337            }
5338        // mid range (line of sight not needed for these 'suppressing' attacks)
5339        } else if attack_data.dist_sqrd < firebreath_range.powi(2) {
5340            // if using firebreath, keep going under full time limit
5341            #[expect(clippy::if_same_then_else)]
5342            if is_using_firebreath
5343                && firebreath_timer < Duration::from_secs_f32(FIREBREATH_TIME_LIMIT)
5344            {
5345                controller.push_basic_input(InputKind::Secondary);
5346            }
5347            // start using firebreath if close enough, in angle, and off cooldown
5348            else if attack_data.dist_sqrd < (firebreath_range * FIREBREATH_RANGE_FACTOR).powi(2)
5349                && attack_data.angle < firebreath_angle * FIREBREATH_AIM_FACTOR
5350                && agent.combat_state.timers[FIREBREATH] > FIREBREATH_COOLDOWN
5351            {
5352                controller.push_basic_input(InputKind::Secondary);
5353            }
5354            // on mixup timer, throw a pumpkin
5355            else if agent.combat_state.timers[MIXUP]
5356                > agent.combat_state.counters[MID_MIXUP_COOLDOWN]
5357            {
5358                controller.push_basic_input(InputKind::Ability(0));
5359                // reset mixup cooldown if pumpkin is actually being used
5360                if is_using_pumpkin {
5361                    agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5362                        rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5363                }
5364            }
5365        }
5366        // long range (with line of sight)
5367        else if attack_data.dist_sqrd < (pumpkin_max_range * PUMPKIN_RANGE_FACTOR).powi(2)
5368            && agent.combat_state.timers[FAR_PUMPKIN]
5369                > agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN]
5370            && line_of_sight_with_target()
5371        {
5372            // throw pumpkin
5373            controller.push_basic_input(InputKind::Ability(0));
5374            // reset pumpkin cooldown if actually being used
5375            if is_using_pumpkin {
5376                agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5377                    rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5378            }
5379        }
5380
5381        // --- movement ---
5382        // closing gap
5383        if attack_data.dist_sqrd
5384            > (attack_data.body_dist + scythe_range * PATH_RANGE_FACTOR).powi(2)
5385        {
5386            self.path_toward_target(
5387                agent,
5388                controller,
5389                tgt_data.pos.0,
5390                read_data,
5391                Path::AtTarget,
5392                None,
5393            );
5394        }
5395        // closing angle
5396        else if attack_data.angle > 0.0 {
5397            // some movement is required to trigger re-orientation
5398            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
5399                .xy()
5400                .try_normalized()
5401                .unwrap_or_else(Vec2::zero)
5402                * 0.001; // scaled way down to minimise position change and keep close rotation consistent
5403        }
5404    }
5405
5406    pub fn handle_frostgigas_attack(
5407        &self,
5408        agent: &mut Agent,
5409        controller: &mut Controller,
5410        attack_data: &AttackData,
5411        tgt_data: &TargetData,
5412        read_data: &ReadData,
5413        rng: &mut impl Rng,
5414    ) {
5415        const GIGAS_MELEE_RANGE: f32 = 12.0;
5416        const GIGAS_SPIKE_RANGE: f32 = 16.0;
5417        const ICEBOMB_RANGE: f32 = 70.0;
5418        const GIGAS_LEAP_RANGE: f32 = 50.0;
5419        const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.;
5420        const FLASHFREEZE_RANGE: f32 = 30.;
5421
5422        enum ActionStateTimers {
5423            AttackChange,
5424            Bonk,
5425        }
5426
5427        enum ActionStateFCounters {
5428            FCounterMinionSummonThreshold = 0,
5429        }
5430
5431        enum ActionStateICounters {
5432            /// An ability that is forced to fully complete until moving on to
5433            /// other attacks.
5434            /// 1 = Leap shockwave, 2 = Flashfreeze, 3 = Spike summon,
5435            /// 4 = Whirlwind, 5 = Remote ice spikes, 6 = Ice bombs
5436            CurrentAbility = 0,
5437        }
5438
5439        let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0);
5440        let remote_spikes_action = || ControlAction::StartInput {
5441            input: InputKind::Ability(5),
5442            target_entity: None,
5443            select_pos: Some(tgt_data.pos.0),
5444        };
5445
5446        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5447        // Sets counter at start of combat, using `condition` to keep track of whether
5448        // it was already initialized
5449        if !agent.combat_state.initialized {
5450            agent.combat_state.counters
5451                [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5452                1.0 - MINION_SUMMON_THRESHOLD;
5453            agent.combat_state.initialized = true;
5454        }
5455
5456        // Update timers
5457        if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 {
5458            agent.combat_state.timers[ActionStateTimers::AttackChange as usize] = 0.0;
5459        } else {
5460            agent.combat_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0;
5461        }
5462        agent.combat_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0;
5463
5464        if health_fraction
5465            < agent.combat_state.counters
5466                [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5467        {
5468            // Summon minions at particular thresholds of health
5469            controller.push_basic_input(InputKind::Ability(3));
5470
5471            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5472            {
5473                agent.combat_state.counters
5474                    [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5475                    MINION_SUMMON_THRESHOLD;
5476            }
5477        // Continue casting any attacks that are forced to complete
5478        } else if let Some(ability) = Some(
5479            &mut agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize],
5480        )
5481        .filter(|i| **i != 0)
5482        {
5483            if *ability == 3 && should_use_targeted_spikes() {
5484                *ability = 5
5485            };
5486
5487            let reset = match ability {
5488                // Must be rolled
5489                1 => {
5490                    controller.push_basic_input(InputKind::Ability(1));
5491                    matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover))
5492                },
5493                // Attacker will have to run away here
5494                2 => {
5495                    controller.push_basic_input(InputKind::Ability(4));
5496                    matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover))
5497                },
5498                // Avoid the spikes!
5499                3 => {
5500                    controller.push_basic_input(InputKind::Ability(0));
5501                    matches!(self.char_state, CharacterState::SpriteSummon(c)
5502                        if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner)))
5503                },
5504                // Long whirlwind attack
5505                4 => {
5506                    controller.push_basic_input(InputKind::Ability(7));
5507                    matches!(self.char_state, CharacterState::RapidMelee(c) if matches!(c.stage_section, StageSection::Recover))
5508                },
5509                // Remote ice spikes
5510                5 => {
5511                    controller.push_action(remote_spikes_action());
5512                    matches!(self.char_state, CharacterState::SpriteSummon(c)
5513                        if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target)))
5514                },
5515                // Ice bombs
5516                6 => {
5517                    controller.push_basic_input(InputKind::Ability(2));
5518                    matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
5519                },
5520                // Should never happen
5521                _ => true,
5522            };
5523
5524            if reset {
5525                *ability = 0;
5526            }
5527        // If our target is nearby and above us, potentially cheesing, have a
5528        // chance of summoning remote ice spikes or throwing ice bombs.
5529        // Cheesing from less than 5 blocks away is usually not possible
5530        } else if attack_data.dist_sqrd > 5f32.powi(2)
5531            // Calculate the "cheesing factor" (height of the normalized position difference)
5532            && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6
5533            // Make it happen at about every 10 seconds!
5534            && rng.random_bool((0.2 * read_data.dt.0).min(1.0) as f64)
5535        {
5536            agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5537                rng.random_range(5..=6);
5538        } else if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
5539            // Bonk the target every 10-8 s
5540            if agent.combat_state.timers[ActionStateTimers::Bonk as usize] > 10. {
5541                controller.push_basic_input(InputKind::Ability(6));
5542
5543                if matches!(self.char_state, CharacterState::BasicMelee(c)
5544                    if matches!(c.stage_section, StageSection::Recover) &&
5545                    c.static_data.ability_info.ability.is_some_and(|meta| matches!(meta.ability, Ability::MainWeaponAux(6)))
5546                ) {
5547                    agent.combat_state.timers[ActionStateTimers::Bonk as usize] =
5548                        rng.random_range(0.0..3.0);
5549                }
5550            // Have a small chance at starting a mixup attack
5551            } else if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 4.0
5552                && rng.random_bool(0.1 * read_data.dt.0.min(1.0) as f64)
5553            {
5554                agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5555                    rng.random_range(1..=4);
5556            // Melee the target, do a whirlwind whenever he is trying to go
5557            // behind or after every 5s
5558            } else if attack_data.angle > 90.0
5559                || agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 5.0
5560            {
5561                // If our target is *very* behind, punish with a whirlwind
5562                if attack_data.angle > 120.0 {
5563                    agent.combat_state.int_counters
5564                        [ActionStateICounters::CurrentAbility as usize] = 4;
5565                } else {
5566                    controller.push_basic_input(InputKind::Secondary);
5567                }
5568            } else {
5569                controller.push_basic_input(InputKind::Primary);
5570            }
5571        } else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
5572            && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 2.0
5573        {
5574            if should_use_targeted_spikes() {
5575                controller.push_action(remote_spikes_action());
5576            } else {
5577                controller.push_basic_input(InputKind::Ability(0));
5578            }
5579        } else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2)
5580            && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 4.0
5581        {
5582            controller.push_basic_input(InputKind::Ability(4));
5583        // Start a leap after either every 3s or our target is not in LoS
5584        } else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2)
5585            && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 3.0
5586        {
5587            controller.push_basic_input(InputKind::Ability(1));
5588        } else if attack_data.dist_sqrd < ICEBOMB_RANGE.powi(2)
5589            && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 3.0
5590        {
5591            controller.push_basic_input(InputKind::Ability(2));
5592        // Spawn ice sprites under distant attackers
5593        } else {
5594            controller.push_action(remote_spikes_action());
5595        }
5596
5597        // Always attempt to path towards target
5598        self.path_toward_target(
5599            agent,
5600            controller,
5601            tgt_data.pos.0,
5602            read_data,
5603            Path::AtTarget,
5604            attack_data.in_min_range().then_some(0.1),
5605        );
5606    }
5607
5608    pub fn handle_boreal_hammer_attack(
5609        &self,
5610        agent: &mut Agent,
5611        controller: &mut Controller,
5612        attack_data: &AttackData,
5613        tgt_data: &TargetData,
5614        read_data: &ReadData,
5615        rng: &mut impl Rng,
5616    ) {
5617        enum ActionStateTimers {
5618            TimerHandleHammerAttack = 0,
5619        }
5620
5621        let has_energy = |need| self.energy.current() > need;
5622
5623        let use_leap = |controller: &mut Controller| {
5624            controller.push_basic_input(InputKind::Ability(0));
5625        };
5626
5627        agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] +=
5628            read_data.dt.0;
5629
5630        if attack_data.in_min_range() && attack_data.angle < 45.0 {
5631            controller.inputs.move_dir = Vec2::zero();
5632            if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 4.0
5633            {
5634                controller.push_cancel_input(InputKind::Secondary);
5635                agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] =
5636                    0.0;
5637            } else if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize]
5638                > 3.0
5639            {
5640                controller.push_basic_input(InputKind::Secondary);
5641            } else if has_energy(50.0) && rng.random_bool(0.9) {
5642                use_leap(controller);
5643            } else {
5644                controller.push_basic_input(InputKind::Primary);
5645            }
5646        } else {
5647            self.path_toward_target(
5648                agent,
5649                controller,
5650                tgt_data.pos.0,
5651                read_data,
5652                Path::Separate,
5653                None,
5654            );
5655
5656            if attack_data.dist_sqrd < 32.0f32.powi(2)
5657                && entities_have_line_of_sight(
5658                    self.pos,
5659                    self.body,
5660                    self.scale,
5661                    tgt_data.pos,
5662                    tgt_data.body,
5663                    tgt_data.scale,
5664                    read_data,
5665                )
5666            {
5667                if rng.random_bool(0.5) && has_energy(50.0) {
5668                    use_leap(controller);
5669                } else if agent.combat_state.timers
5670                    [ActionStateTimers::TimerHandleHammerAttack as usize]
5671                    > 2.0
5672                {
5673                    controller.push_basic_input(InputKind::Secondary);
5674                } else if agent.combat_state.timers
5675                    [ActionStateTimers::TimerHandleHammerAttack as usize]
5676                    > 4.0
5677                {
5678                    controller.push_cancel_input(InputKind::Secondary);
5679                    agent.combat_state.timers
5680                        [ActionStateTimers::TimerHandleHammerAttack as usize] = 0.0;
5681                }
5682            }
5683        }
5684    }
5685
5686    pub fn handle_boreal_bow_attack(
5687        &self,
5688        agent: &mut Agent,
5689        controller: &mut Controller,
5690        attack_data: &AttackData,
5691        tgt_data: &TargetData,
5692        read_data: &ReadData,
5693        rng: &mut impl Rng,
5694    ) {
5695        let line_of_sight_with_target = || {
5696            entities_have_line_of_sight(
5697                self.pos,
5698                self.body,
5699                self.scale,
5700                tgt_data.pos,
5701                tgt_data.body,
5702                tgt_data.scale,
5703                read_data,
5704            )
5705        };
5706
5707        let has_energy = |need| self.energy.current() > need;
5708
5709        let use_trap = |controller: &mut Controller| {
5710            controller.push_basic_input(InputKind::Ability(0));
5711        };
5712
5713        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5714            if rng.random_bool(0.5) && has_energy(15.0) {
5715                controller.push_basic_input(InputKind::Secondary);
5716            } else if attack_data.angle < 15.0 {
5717                controller.push_basic_input(InputKind::Primary);
5718            }
5719        } else if attack_data.dist_sqrd < (4.0 * attack_data.min_attack_dist).powi(2)
5720            && line_of_sight_with_target()
5721        {
5722            if rng.random_bool(0.5) && has_energy(15.0) {
5723                controller.push_basic_input(InputKind::Secondary);
5724            } else if has_energy(20.0) {
5725                use_trap(controller);
5726            }
5727        }
5728
5729        if has_energy(50.0) {
5730            if attack_data.dist_sqrd < (10.0 * attack_data.min_attack_dist).powi(2) {
5731                // Attempt to circle the target if neither too close nor too far
5732                if let Some((bearing, speed, stuck)) = agent.chaser.chase(
5733                    &*read_data.terrain,
5734                    self.pos.0,
5735                    self.vel.0,
5736                    tgt_data.pos.0,
5737                    TraversalConfig {
5738                        min_tgt_dist: 1.25,
5739                        ..self.traversal_config
5740                    },
5741                    &read_data.time,
5742                ) {
5743                    self.unstuck_if(stuck, controller);
5744                    if line_of_sight_with_target() && attack_data.angle < 45.0 {
5745                        controller.inputs.move_dir = bearing
5746                            .xy()
5747                            .rotated_z(rng.random_range(0.5..1.57))
5748                            .try_normalized()
5749                            .unwrap_or_else(Vec2::zero)
5750                            * 2.0
5751                            * speed;
5752                    } else {
5753                        // Unless cannot see target, then move towards them
5754                        controller.inputs.move_dir =
5755                            bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5756                        self.jump_if(bearing.z > 1.5, controller);
5757                        controller.inputs.move_z = bearing.z;
5758                    }
5759                }
5760            } else {
5761                // Path to enemy if too far
5762                self.path_toward_target(
5763                    agent,
5764                    controller,
5765                    tgt_data.pos.0,
5766                    read_data,
5767                    Path::AtTarget,
5768                    None,
5769                );
5770            }
5771        } else {
5772            // Path to enemy for melee hits if need more energy
5773            self.path_toward_target(
5774                agent,
5775                controller,
5776                tgt_data.pos.0,
5777                read_data,
5778                Path::AtTarget,
5779                None,
5780            );
5781        }
5782    }
5783
5784    pub fn handle_firegigas_attack(
5785        &self,
5786        agent: &mut Agent,
5787        controller: &mut Controller,
5788        attack_data: &AttackData,
5789        tgt_data: &TargetData,
5790        read_data: &ReadData,
5791        rng: &mut impl Rng,
5792    ) {
5793        const MELEE_RANGE: f32 = 12.0;
5794        const RANGED_RANGE: f32 = 27.0;
5795        const LEAP_RANGE: f32 = 50.0;
5796        const MINION_SUMMON_THRESHOLD: f32 = 1.0 / 8.0;
5797        const OVERHEAT_DUR: f32 = 3.0;
5798        const FORCE_GAP_CLOSER_TIMEOUT: f32 = 10.0;
5799
5800        enum ActionStateTimers {
5801            Special,
5802            Overheat,
5803            OutOfMeleeRange,
5804        }
5805
5806        enum ActionStateFCounters {
5807            FCounterMinionSummonThreshold,
5808        }
5809
5810        enum ActionStateConditions {
5811            VerticalStrikeCombo,
5812            WhirlwindTwice,
5813        }
5814
5815        const FAST_SLASH: InputKind = InputKind::Primary;
5816        const FAST_THRUST: InputKind = InputKind::Secondary;
5817        const SLOW_SLASH: InputKind = InputKind::Ability(0);
5818        const SLOW_THRUST: InputKind = InputKind::Ability(1);
5819        const LAVA_LEAP: InputKind = InputKind::Ability(2);
5820        const VERTICAL_STRIKE: InputKind = InputKind::Ability(3);
5821        const OVERHEAT: InputKind = InputKind::Ability(4);
5822        const WHIRLWIND: InputKind = InputKind::Ability(5);
5823        const EXPLOSIVE_STRIKE: InputKind = InputKind::Ability(6);
5824        const FIRE_PILLARS: InputKind = InputKind::Ability(7);
5825        const TARGETED_FIRE_PILLAR: InputKind = InputKind::Ability(8);
5826        const ASHEN_SUMMONS: InputKind = InputKind::Ability(9);
5827        const PARRY_PUNISH: InputKind = InputKind::Ability(10);
5828
5829        fn choose_weighted<const N: usize>(
5830            rng: &mut impl Rng,
5831            choices: [(InputKind, f32); N],
5832        ) -> InputKind {
5833            choices
5834                .choose_weighted(rng, |(_, weight)| *weight)
5835                .expect("weights should be valid")
5836                .0
5837        }
5838
5839        // Basic melee strikes
5840        fn rand_basic(rng: &mut impl Rng, damage_fraction: f32) -> InputKind {
5841            choose_weighted(rng, [
5842                (FAST_SLASH, 2.0),
5843                (FAST_THRUST, 2.0),
5844                (SLOW_SLASH, 1.0 + damage_fraction),
5845                (SLOW_THRUST, 1.0 + damage_fraction),
5846            ])
5847        }
5848
5849        // Less frequent mixup attacks
5850        fn rand_special(rng: &mut impl Rng) -> InputKind {
5851            choose_weighted(rng, [
5852                (WHIRLWIND, 6.0),
5853                (VERTICAL_STRIKE, 6.0),
5854                (OVERHEAT, 6.0),
5855                (EXPLOSIVE_STRIKE, 1.0),
5856                (LAVA_LEAP, 1.0),
5857                (FIRE_PILLARS, 1.0),
5858            ])
5859        }
5860
5861        // Attacks capable of also hitting entities behind the gigas
5862        fn rand_aoe(rng: &mut impl Rng) -> InputKind {
5863            choose_weighted(rng, [
5864                (EXPLOSIVE_STRIKE, 1.0),
5865                (FIRE_PILLARS, 1.0),
5866                (WHIRLWIND, 2.0),
5867            ])
5868        }
5869
5870        // Attacks capable of also hitting entities further away
5871        fn rand_ranged(rng: &mut impl Rng) -> InputKind {
5872            choose_weighted(rng, [
5873                (EXPLOSIVE_STRIKE, 1.0),
5874                (FIRE_PILLARS, 1.0),
5875                (OVERHEAT, 1.0),
5876            ])
5877        }
5878
5879        let cast_targeted_fire_pillar = |c: &mut Controller| {
5880            c.push_action(ControlAction::StartInput {
5881                input: TARGETED_FIRE_PILLAR,
5882                target_entity: tgt_data.uid,
5883                select_pos: None,
5884            })
5885        };
5886
5887        fn can_cast_new_ability(char_state: &CharacterState) -> bool {
5888            !matches!(
5889                char_state,
5890                CharacterState::LeapMelee(_)
5891                    | CharacterState::BasicMelee(_)
5892                    | CharacterState::BasicBeam(_)
5893                    | CharacterState::BasicSummon(_)
5894                    | CharacterState::SpriteSummon(_)
5895            )
5896        }
5897
5898        // Initializes counters at start of combat
5899        if !agent.combat_state.initialized {
5900            agent.combat_state.counters
5901                [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5902                1.0 - MINION_SUMMON_THRESHOLD;
5903            agent.combat_state.initialized = true;
5904        }
5905
5906        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5907        let damage_fraction = 1.0 - health_fraction;
5908        // Calculate the "cheesing factor" (height of the normalized position
5909        // difference), unless the target is airborne from our hit
5910        // Cheesing from close range is usually not possible
5911        let cheesed_from_above = !agent.combat_state.conditions
5912            [ActionStateConditions::VerticalStrikeCombo as usize]
5913            && attack_data.dist_sqrd > 5f32.powi(2)
5914            && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6;
5915        // Being in water also triggers this as there are a lot of exploits with water
5916        let cheesed_in_water = matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { kind: LiquidKind::Water, depth, .. }) if depth >= 2.0);
5917        let cheesed = cheesed_from_above || cheesed_in_water;
5918        let tgt_airborne = tgt_data
5919            .physics_state
5920            .is_some_and(|physics| physics.on_ground.is_none() && physics.in_liquid().is_none());
5921        let tgt_missed_parry = match tgt_data.char_state {
5922            Some(CharacterState::RiposteMelee(data)) => {
5923                matches!(data.stage_section, StageSection::Recover) && data.whiffed
5924            },
5925            Some(CharacterState::BasicBlock(data)) => {
5926                matches!(data.stage_section, StageSection::Recover)
5927                    && !data.static_data.parry_window.recover
5928                    && !data.is_parry
5929            },
5930            _ => false,
5931        };
5932        let casting_beam = matches!(self.char_state, CharacterState::BasicBeam(_))
5933            && self.char_state.stage_section() != Some(StageSection::Recover);
5934
5935        // Update timers
5936        agent.combat_state.timers[ActionStateTimers::Special as usize] += read_data.dt.0;
5937        if casting_beam {
5938            agent.combat_state.timers[ActionStateTimers::Overheat as usize] += read_data.dt.0;
5939        } else {
5940            agent.combat_state.timers[ActionStateTimers::Overheat as usize] = 0.0;
5941        }
5942        if attack_data.dist_sqrd > MELEE_RANGE.powi(2) {
5943            agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize] +=
5944                read_data.dt.0;
5945        } else {
5946            agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize] = 0.0;
5947        }
5948
5949        // Cast abilities
5950        if casting_beam
5951            && agent.combat_state.timers[ActionStateTimers::Overheat as usize] < OVERHEAT_DUR
5952        {
5953            controller.push_basic_input(OVERHEAT);
5954            controller.inputs.look_dir = self
5955                .ori
5956                .look_dir()
5957                .to_horizontal()
5958                .unwrap_or_else(|| self.ori.look_dir());
5959        } else if health_fraction
5960            < agent.combat_state.counters
5961                [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5962        {
5963            // Summon minions at particular thresholds of health
5964            controller.push_basic_input(ASHEN_SUMMONS);
5965
5966            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5967            {
5968                agent.combat_state.counters
5969                    [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5970                    MINION_SUMMON_THRESHOLD;
5971            }
5972        } else if can_cast_new_ability(self.char_state) {
5973            if cheesed {
5974                cast_targeted_fire_pillar(controller);
5975            } else if agent.combat_state.conditions
5976                [ActionStateConditions::VerticalStrikeCombo as usize]
5977            {
5978                // If landed vertical strike combo target while they are airborne
5979                if tgt_airborne {
5980                    controller.push_basic_input(FAST_THRUST);
5981                }
5982
5983                agent.combat_state.conditions
5984                    [ActionStateConditions::VerticalStrikeCombo as usize] = false;
5985            } else if agent.combat_state.conditions[ActionStateConditions::WhirlwindTwice as usize]
5986            {
5987                controller.push_basic_input(WHIRLWIND);
5988                agent.combat_state.conditions[ActionStateConditions::WhirlwindTwice as usize] =
5989                    false;
5990            } else if agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize]
5991                > FORCE_GAP_CLOSER_TIMEOUT
5992            {
5993                // Use a gap closer if the target has been out of melee distance for a while
5994                controller.push_basic_input(LAVA_LEAP);
5995            } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5996                if tgt_missed_parry {
5997                    controller.push_basic_input(PARRY_PUNISH);
5998                    agent.combat_state.conditions
5999                        [ActionStateConditions::VerticalStrikeCombo as usize] = true;
6000                } else if agent.combat_state.timers[ActionStateTimers::Special as usize] > 10.0 {
6001                    // Use a special ability periodically
6002                    let rand_special = rand_special(rng);
6003                    match rand_special {
6004                        VERTICAL_STRIKE => {
6005                            agent.combat_state.conditions
6006                                [ActionStateConditions::VerticalStrikeCombo as usize] = true
6007                        },
6008                        WHIRLWIND if rng.random_bool(0.2) => {
6009                            agent.combat_state.conditions
6010                                [ActionStateConditions::WhirlwindTwice as usize] = true
6011                        },
6012                        _ => {},
6013                    }
6014                    controller.push_basic_input(rand_special);
6015
6016                    agent.combat_state.timers[ActionStateTimers::Special as usize] =
6017                        rng.random_range(0.0..3.0 + 5.0 * damage_fraction);
6018                } else if attack_data.angle > 90.0 {
6019                    // Cast an aoe ability to hit the target if they are behind the entity
6020                    let rand_aoe = rand_aoe(rng);
6021                    match rand_aoe {
6022                        WHIRLWIND if rng.random_bool(0.2) => {
6023                            agent.combat_state.conditions
6024                                [ActionStateConditions::WhirlwindTwice as usize] = true
6025                        },
6026                        _ => {},
6027                    }
6028
6029                    controller.push_basic_input(rand_aoe);
6030                } else {
6031                    // Use a random basic melee hit
6032                    controller.push_basic_input(rand_basic(rng, damage_fraction));
6033                }
6034            } else if attack_data.dist_sqrd < RANGED_RANGE.powi(2) {
6035                // Use ranged ability if target is out of melee range
6036                if rng.random_bool(0.05) {
6037                    controller.push_basic_input(rand_ranged(rng));
6038                }
6039            } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) {
6040                // Use a gap closer if the target is even further away
6041                controller.push_basic_input(LAVA_LEAP);
6042            } else if rng.random_bool(0.1) {
6043                // Use a targeted fire pillar if the target is out of range of everything else
6044                cast_targeted_fire_pillar(controller);
6045            }
6046        }
6047
6048        self.path_toward_target(
6049            agent,
6050            controller,
6051            tgt_data.pos.0,
6052            read_data,
6053            Path::AtTarget,
6054            attack_data.in_min_range().then_some(0.1),
6055        );
6056
6057        // Get out of lava if submerged
6058        if self.physics_state.in_liquid().is_some() {
6059            controller.push_basic_input(InputKind::Jump);
6060        }
6061        if self.physics_state.in_liquid().is_some() {
6062            controller.inputs.move_z = 1.0;
6063        }
6064    }
6065
6066    pub fn handle_ashen_axe_attack(
6067        &self,
6068        agent: &mut Agent,
6069        controller: &mut Controller,
6070        attack_data: &AttackData,
6071        tgt_data: &TargetData,
6072        read_data: &ReadData,
6073        rng: &mut impl Rng,
6074    ) {
6075        const IMMOLATION_COOLDOWN: f32 = 50.0;
6076        const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
6077            desired_energy: 30.0,
6078            combo_scaling_buildup: 0,
6079        };
6080
6081        enum ActionStateTimers {
6082            SinceSelfImmolation,
6083        }
6084
6085        const DOUBLE_STRIKE: InputKind = InputKind::Primary;
6086        const FLAME_WAVE: InputKind = InputKind::Secondary;
6087        const KNOCKBACK_COMBO: InputKind = InputKind::Ability(0);
6088        const SELF_IMMOLATION: InputKind = InputKind::Ability(1);
6089
6090        fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6091            !matches!(
6092                char_state,
6093                CharacterState::ComboMelee2(_)
6094                    | CharacterState::Shockwave(_)
6095                    | CharacterState::SelfBuff(_)
6096            )
6097        }
6098
6099        let could_use = |input| {
6100            Option::<AbilityInput>::from(input)
6101                .and_then(|ability_input| self.extract_ability(ability_input))
6102                .is_some_and(|ability_data| {
6103                    ability_data.could_use(
6104                        attack_data,
6105                        self,
6106                        tgt_data,
6107                        read_data,
6108                        ABILITY_PREFERENCES,
6109                    )
6110                })
6111        };
6112
6113        // Initialize immolation cooldown to 0
6114        if !agent.combat_state.initialized {
6115            agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] =
6116                IMMOLATION_COOLDOWN;
6117            agent.combat_state.initialized = true;
6118        }
6119
6120        agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] +=
6121            read_data.dt.0;
6122
6123        if self
6124            .char_state
6125            .ability_info()
6126            .map(|ai| ai.input)
6127            .is_some_and(|input_kind| input_kind == KNOCKBACK_COMBO)
6128        {
6129            controller.push_basic_input(KNOCKBACK_COMBO);
6130        } else if can_cast_new_ability(self.char_state)
6131            && agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize]
6132                >= IMMOLATION_COOLDOWN
6133            && could_use(SELF_IMMOLATION)
6134        {
6135            agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] =
6136                rng.random_range(0.0..5.0);
6137
6138            controller.push_basic_input(SELF_IMMOLATION);
6139        } else if rng.random_bool(0.35) && could_use(KNOCKBACK_COMBO) {
6140            controller.push_basic_input(KNOCKBACK_COMBO);
6141        } else if could_use(DOUBLE_STRIKE) {
6142            controller.push_basic_input(DOUBLE_STRIKE);
6143        } else if rng.random_bool(0.2) && could_use(FLAME_WAVE) {
6144            controller.push_basic_input(FLAME_WAVE);
6145        }
6146
6147        self.path_toward_target(
6148            agent,
6149            controller,
6150            tgt_data.pos.0,
6151            read_data,
6152            Path::AtTarget,
6153            None,
6154        );
6155    }
6156
6157    pub fn handle_ashen_staff_attack(
6158        &self,
6159        agent: &mut Agent,
6160        controller: &mut Controller,
6161        attack_data: &AttackData,
6162        tgt_data: &TargetData,
6163        read_data: &ReadData,
6164        rng: &mut impl Rng,
6165    ) {
6166        const ABILITY_COOLDOWN: f32 = 50.0;
6167        const INITIAL_COOLDOWN: f32 = ABILITY_COOLDOWN - 10.0;
6168        const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
6169            desired_energy: 40.0,
6170            combo_scaling_buildup: 0,
6171        };
6172
6173        enum ActionStateTimers {
6174            SinceAbility,
6175        }
6176
6177        const FIREBALL: InputKind = InputKind::Primary;
6178        const FLAME_WALL: InputKind = InputKind::Ability(0);
6179        const SUMMON_CRUX: InputKind = InputKind::Ability(1);
6180
6181        fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6182            !matches!(
6183                char_state,
6184                CharacterState::BasicRanged(_)
6185                    | CharacterState::BasicBeam(_)
6186                    | CharacterState::RapidMelee(_)
6187                    | CharacterState::BasicAura(_)
6188            )
6189        }
6190
6191        let could_use = |input| {
6192            Option::<AbilityInput>::from(input)
6193                .and_then(|ability_input| self.extract_ability(ability_input))
6194                .is_some_and(|ability_data| {
6195                    ability_data.could_use(
6196                        attack_data,
6197                        self,
6198                        tgt_data,
6199                        read_data,
6200                        ABILITY_PREFERENCES,
6201                    )
6202                })
6203        };
6204
6205        // Initialize special ability cooldown
6206        if !agent.combat_state.initialized {
6207            agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] = INITIAL_COOLDOWN;
6208            agent.combat_state.initialized = true;
6209        }
6210
6211        agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] += read_data.dt.0;
6212
6213        if can_cast_new_ability(self.char_state)
6214            && agent.combat_state.timers[ActionStateTimers::SinceAbility as usize]
6215                >= ABILITY_COOLDOWN
6216            && (could_use(FLAME_WALL) || could_use(SUMMON_CRUX))
6217        {
6218            agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] =
6219                rng.random_range(0.0..5.0);
6220
6221            if could_use(FLAME_WALL) && (rng.random_bool(0.5) || !could_use(SUMMON_CRUX)) {
6222                controller.push_basic_input(FLAME_WALL);
6223            } else {
6224                controller.push_basic_input(SUMMON_CRUX);
6225            }
6226        } else if rng.random_bool(0.5) && could_use(FIREBALL) {
6227            controller.push_basic_input(FIREBALL);
6228        }
6229
6230        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6231            // Attempt to move away from target if too close
6232            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6233                &*read_data.terrain,
6234                self.pos.0,
6235                self.vel.0,
6236                tgt_data.pos.0,
6237                TraversalConfig {
6238                    min_tgt_dist: 1.25,
6239                    ..self.traversal_config
6240                },
6241                &read_data.time,
6242            ) {
6243                self.unstuck_if(stuck, controller);
6244                controller.inputs.move_dir =
6245                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6246            }
6247        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6248            // Else attempt to circle target if neither too close nor too far
6249            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6250                &*read_data.terrain,
6251                self.pos.0,
6252                self.vel.0,
6253                tgt_data.pos.0,
6254                TraversalConfig {
6255                    min_tgt_dist: 1.25,
6256                    ..self.traversal_config
6257                },
6258                &read_data.time,
6259            ) {
6260                self.unstuck_if(stuck, controller);
6261                if entities_have_line_of_sight(
6262                    self.pos,
6263                    self.body,
6264                    self.scale,
6265                    tgt_data.pos,
6266                    tgt_data.body,
6267                    tgt_data.scale,
6268                    read_data,
6269                ) && attack_data.angle < 45.0
6270                {
6271                    controller.inputs.move_dir = bearing
6272                        .xy()
6273                        .rotated_z(rng.random_range(-1.57..-0.5))
6274                        .try_normalized()
6275                        .unwrap_or_else(Vec2::zero)
6276                        * speed;
6277                } else {
6278                    // Unless cannot see target, then move towards them
6279                    controller.inputs.move_dir =
6280                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6281                    self.jump_if(bearing.z > 1.5, controller);
6282                    controller.inputs.move_z = bearing.z;
6283                }
6284            }
6285        } else {
6286            // If too far, move towards target
6287            self.path_toward_target(
6288                agent,
6289                controller,
6290                tgt_data.pos.0,
6291                read_data,
6292                Path::AtTarget,
6293                None,
6294            );
6295        }
6296    }
6297
6298    pub fn handle_cardinal_attack(
6299        &self,
6300        agent: &mut Agent,
6301        controller: &mut Controller,
6302        attack_data: &AttackData,
6303        tgt_data: &TargetData,
6304        read_data: &ReadData,
6305        rng: &mut impl Rng,
6306    ) {
6307        const DESIRED_ENERGY_LEVEL: f32 = 50.0;
6308        const DESIRED_COMBO_LEVEL: u32 = 8;
6309        const MINION_SUMMON_THRESHOLD: f32 = 0.10;
6310
6311        enum ActionStateConditions {
6312            ConditionCounterInitialized = 0,
6313        }
6314
6315        enum ActionStateFCounters {
6316            FCounterHealthThreshold = 0,
6317        }
6318
6319        let health_fraction = self.health.map_or(0.5, |h| h.fraction());
6320        // Sets counter at start of combat, using `condition` to keep track of whether
6321        // it was already intitialized
6322        if !agent.combat_state.conditions
6323            [ActionStateConditions::ConditionCounterInitialized as usize]
6324        {
6325            agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
6326                1.0 - MINION_SUMMON_THRESHOLD;
6327            agent.combat_state.conditions
6328                [ActionStateConditions::ConditionCounterInitialized as usize] = true;
6329        }
6330
6331        if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
6332            > health_fraction
6333        {
6334            // Summon minions at particular thresholds of health
6335            controller.push_basic_input(InputKind::Ability(1));
6336
6337            if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
6338            {
6339                agent.combat_state.counters
6340                    [ActionStateFCounters::FCounterHealthThreshold as usize] -=
6341                    MINION_SUMMON_THRESHOLD;
6342            }
6343        }
6344        // Logic to use abilities
6345        else if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
6346            && entities_have_line_of_sight(
6347                self.pos,
6348                self.body,
6349                self.scale,
6350                tgt_data.pos,
6351                tgt_data.body,
6352                tgt_data.scale,
6353                read_data,
6354            )
6355        {
6356            // If far enough away, and can see target, check which skill is appropriate to
6357            // use
6358            if self.energy.current() > DESIRED_ENERGY_LEVEL
6359                && read_data
6360                    .combos
6361                    .get(*self.entity)
6362                    .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
6363                && !read_data.buffs.get(*self.entity).iter().any(|buff| {
6364                    buff.iter_kind(BuffKind::Regeneration)
6365                        .peekable()
6366                        .peek()
6367                        .is_some()
6368                })
6369            {
6370                // If have enough energy and combo to use healing aura, do so
6371                controller.push_basic_input(InputKind::Secondary);
6372            } else if self
6373                .skill_set
6374                .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
6375                && self.energy.current() > DESIRED_ENERGY_LEVEL
6376                && !read_data.buffs.get(*self.entity).iter().any(|buff| {
6377                    buff.iter_kind(BuffKind::ProtectingWard)
6378                        .peekable()
6379                        .peek()
6380                        .is_some()
6381                })
6382            {
6383                // Use steam beam if target is far enough away, self is not buffed, and have
6384                // sufficient energy
6385                controller.push_basic_input(InputKind::Ability(0));
6386            } else {
6387                // If low on energy, use primary to attempt to regen energy
6388                // Or if at desired energy level but not able/willing to ward, just attack
6389                controller.push_basic_input(InputKind::Primary);
6390            }
6391        } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6392            if self.body.is_some_and(|b| b.is_humanoid())
6393                && self.energy.current()
6394                    > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
6395                && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
6396            {
6397                // Else use steam beam
6398                controller.push_basic_input(InputKind::Ability(0));
6399            } else if attack_data.angle < 15.0 {
6400                controller.push_basic_input(InputKind::Primary);
6401            }
6402        }
6403        // Logic to move. Intentionally kept separate from ability logic where possible
6404        // so duplicated work is less necessary.
6405        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6406            // Attempt to move away from target if too close
6407            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6408                &*read_data.terrain,
6409                self.pos.0,
6410                self.vel.0,
6411                tgt_data.pos.0,
6412                TraversalConfig {
6413                    min_tgt_dist: 1.25,
6414                    ..self.traversal_config
6415                },
6416                &read_data.time,
6417            ) {
6418                self.unstuck_if(stuck, controller);
6419                controller.inputs.move_dir =
6420                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6421            }
6422        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6423            // Else attempt to circle target if neither too close nor too far
6424            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6425                &*read_data.terrain,
6426                self.pos.0,
6427                self.vel.0,
6428                tgt_data.pos.0,
6429                TraversalConfig {
6430                    min_tgt_dist: 1.25,
6431                    ..self.traversal_config
6432                },
6433                &read_data.time,
6434            ) {
6435                self.unstuck_if(stuck, controller);
6436                if entities_have_line_of_sight(
6437                    self.pos,
6438                    self.body,
6439                    self.scale,
6440                    tgt_data.pos,
6441                    tgt_data.body,
6442                    tgt_data.scale,
6443                    read_data,
6444                ) && attack_data.angle < 45.0
6445                {
6446                    controller.inputs.move_dir = bearing
6447                        .xy()
6448                        .rotated_z(rng.random_range(0.5..1.57))
6449                        .try_normalized()
6450                        .unwrap_or_else(Vec2::zero)
6451                        * speed;
6452                } else {
6453                    // Unless cannot see target, then move towards them
6454                    controller.inputs.move_dir =
6455                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6456                    self.jump_if(bearing.z > 1.5, controller);
6457                    controller.inputs.move_z = bearing.z;
6458                }
6459            }
6460            // Sometimes try to roll
6461            if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
6462                && !matches!(self.char_state, CharacterState::BasicAura(_))
6463                && attack_data.dist_sqrd < 16.0f32.powi(2)
6464                && rng.random::<f32>() < 0.01
6465            {
6466                controller.push_basic_input(InputKind::Roll);
6467            }
6468        } else {
6469            // If too far, move towards target
6470            self.path_toward_target(
6471                agent,
6472                controller,
6473                tgt_data.pos.0,
6474                read_data,
6475                Path::AtTarget,
6476                None,
6477            );
6478        }
6479    }
6480
6481    pub fn handle_sea_bishop_attack(
6482        &self,
6483        agent: &mut Agent,
6484        controller: &mut Controller,
6485        attack_data: &AttackData,
6486        tgt_data: &TargetData,
6487        read_data: &ReadData,
6488        rng: &mut impl Rng,
6489    ) {
6490        let line_of_sight_with_target = || {
6491            entities_have_line_of_sight(
6492                self.pos,
6493                self.body,
6494                self.scale,
6495                tgt_data.pos,
6496                tgt_data.body,
6497                tgt_data.scale,
6498                read_data,
6499            )
6500        };
6501
6502        enum ActionStateTimers {
6503            TimerBeam = 0,
6504        }
6505        if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 6.0 {
6506            agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
6507        } else {
6508            agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
6509        }
6510
6511        // When enemy in sight beam for 3 seconds, every 6 seconds
6512        if line_of_sight_with_target()
6513            && agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 3.0
6514        {
6515            controller.push_basic_input(InputKind::Primary);
6516        }
6517        // Logic to move. Intentionally kept separate from ability logic where possible
6518        // so duplicated work is less necessary.
6519        if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6520            // Attempt to move away from target if too close
6521            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6522                &*read_data.terrain,
6523                self.pos.0,
6524                self.vel.0,
6525                tgt_data.pos.0,
6526                TraversalConfig {
6527                    min_tgt_dist: 1.25,
6528                    ..self.traversal_config
6529                },
6530                &read_data.time,
6531            ) {
6532                self.unstuck_if(stuck, controller);
6533                controller.inputs.move_dir =
6534                    -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6535            }
6536        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6537            // Else attempt to circle target if neither too close nor too far
6538            if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6539                &*read_data.terrain,
6540                self.pos.0,
6541                self.vel.0,
6542                tgt_data.pos.0,
6543                TraversalConfig {
6544                    min_tgt_dist: 1.25,
6545                    ..self.traversal_config
6546                },
6547                &read_data.time,
6548            ) {
6549                self.unstuck_if(stuck, controller);
6550                if line_of_sight_with_target() && attack_data.angle < 45.0 {
6551                    controller.inputs.move_dir = bearing
6552                        .xy()
6553                        .rotated_z(rng.random_range(0.5..1.57))
6554                        .try_normalized()
6555                        .unwrap_or_else(Vec2::zero)
6556                        * speed;
6557                } else {
6558                    // Unless cannot see target, then move towards them
6559                    controller.inputs.move_dir =
6560                        bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6561                    self.jump_if(bearing.z > 1.5, controller);
6562                    controller.inputs.move_z = bearing.z;
6563                }
6564            }
6565        } else {
6566            // If too far, move towards target
6567            self.path_toward_target(
6568                agent,
6569                controller,
6570                tgt_data.pos.0,
6571                read_data,
6572                Path::AtTarget,
6573                None,
6574            );
6575        }
6576    }
6577
6578    pub fn handle_cursekeeper_attack(
6579        &self,
6580        agent: &mut Agent,
6581        controller: &mut Controller,
6582        attack_data: &AttackData,
6583        tgt_data: &TargetData,
6584        read_data: &ReadData,
6585        rng: &mut impl Rng,
6586    ) {
6587        enum ActionStateTimers {
6588            TimerBeam,
6589            TimerSummon,
6590            SelectSummon,
6591        }
6592        if tgt_data.pos.0.z - self.pos.0.z > 3.5 {
6593            controller.push_action(ControlAction::StartInput {
6594                input: InputKind::Ability(4),
6595                target_entity: agent
6596                    .target
6597                    .as_ref()
6598                    .and_then(|t| read_data.uids.get(t.target))
6599                    .copied(),
6600                select_pos: None,
6601            });
6602        } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 12.0 {
6603            agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
6604        } else {
6605            agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
6606        }
6607
6608        if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
6609        {
6610            agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] = 0.0;
6611            agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] =
6612                rng.random_range(0..=3) as f32;
6613        } else {
6614            agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] += read_data.dt.0;
6615        }
6616
6617        if agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] > 32.0 {
6618            match agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] as i32 {
6619                0 => controller.push_basic_input(InputKind::Ability(0)),
6620                1 => controller.push_basic_input(InputKind::Ability(1)),
6621                2 => controller.push_basic_input(InputKind::Ability(2)),
6622                _ => controller.push_basic_input(InputKind::Ability(3)),
6623            }
6624        } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 6.0 {
6625            controller.push_basic_input(InputKind::Ability(5));
6626        } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 9.0 {
6627            controller.push_basic_input(InputKind::Primary);
6628        } else {
6629            controller.push_basic_input(InputKind::Secondary);
6630        }
6631
6632        if attack_data.dist_sqrd > 10_f32.powi(2)
6633            || agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 4.0
6634        {
6635            self.path_toward_target(
6636                agent,
6637                controller,
6638                tgt_data.pos.0,
6639                read_data,
6640                Path::AtTarget,
6641                None,
6642            );
6643        }
6644    }
6645
6646    pub fn handle_shamanic_spirit_attack(
6647        &self,
6648        agent: &mut Agent,
6649        controller: &mut Controller,
6650        attack_data: &AttackData,
6651        tgt_data: &TargetData,
6652        read_data: &ReadData,
6653    ) {
6654        if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
6655            controller.push_action(ControlAction::StartInput {
6656                input: InputKind::Secondary,
6657                target_entity: agent
6658                    .target
6659                    .as_ref()
6660                    .and_then(|t| read_data.uids.get(t.target))
6661                    .copied(),
6662                select_pos: None,
6663            });
6664        } else if attack_data.in_min_range() && attack_data.angle < 30.0 {
6665            controller.push_basic_input(InputKind::Primary);
6666            controller.inputs.move_dir = Vec2::zero();
6667        } else {
6668            self.path_toward_target(
6669                agent,
6670                controller,
6671                tgt_data.pos.0,
6672                read_data,
6673                Path::AtTarget,
6674                None,
6675            );
6676        }
6677    }
6678
6679    pub fn handle_cursekeeper_fake_attack(
6680        &self,
6681        controller: &mut Controller,
6682        attack_data: &AttackData,
6683    ) {
6684        if attack_data.dist_sqrd < 25_f32.powi(2) {
6685            controller.push_basic_input(InputKind::Primary);
6686        }
6687    }
6688
6689    pub fn handle_karkatha_attack(
6690        &self,
6691        agent: &mut Agent,
6692        controller: &mut Controller,
6693        attack_data: &AttackData,
6694        tgt_data: &TargetData,
6695        read_data: &ReadData,
6696        _rng: &mut impl Rng,
6697    ) {
6698        enum ActionStateTimers {
6699            RiposteTimer,
6700            SummonTimer,
6701        }
6702
6703        agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] += read_data.dt.0;
6704        agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] += read_data.dt.0;
6705        if matches!(self.char_state, CharacterState::RiposteMelee(c) if !matches!(c.stage_section, StageSection::Recover))
6706        {
6707            // Reset timer
6708            agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] = 0.0;
6709        }
6710        if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
6711        {
6712            // Reset timer
6713            agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] = 0.0;
6714        }
6715        // chase, move away from exiit if target is cheesing from below
6716        let home = agent.patrol_origin.unwrap_or(self.pos.0);
6717        let dest = if tgt_data.pos.0.z < self.pos.0.z {
6718            home
6719        } else {
6720            tgt_data.pos.0
6721        };
6722        if attack_data.in_min_range() {
6723            if agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] > 3.0 {
6724                controller.push_basic_input(InputKind::Ability(2));
6725            } else {
6726                controller.push_basic_input(InputKind::Primary);
6727            };
6728        } else if attack_data.dist_sqrd < 20.0_f32.powi(2) {
6729            if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] > 20.0 {
6730                controller.push_basic_input(InputKind::Ability(1));
6731            } else {
6732                controller.push_basic_input(InputKind::Secondary);
6733            }
6734        } else if attack_data.dist_sqrd < 30.0_f32.powi(2) {
6735            if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] < 10.0 {
6736                self.path_toward_target(
6737                    agent,
6738                    controller,
6739                    tgt_data.pos.0,
6740                    read_data,
6741                    Path::AtTarget,
6742                    None,
6743                );
6744            } else {
6745                controller.push_basic_input(InputKind::Ability(0));
6746            }
6747        } else {
6748            self.path_toward_target(agent, controller, dest, read_data, Path::AtTarget, None);
6749        }
6750    }
6751
6752    pub fn handle_dagon_attack(
6753        &self,
6754        agent: &mut Agent,
6755        controller: &mut Controller,
6756        attack_data: &AttackData,
6757        tgt_data: &TargetData,
6758        read_data: &ReadData,
6759    ) {
6760        enum ActionStateTimers {
6761            TimerDagon = 0,
6762        }
6763        let line_of_sight_with_target = || {
6764            entities_have_line_of_sight(
6765                self.pos,
6766                self.body,
6767                self.scale,
6768                tgt_data.pos,
6769                tgt_data.body,
6770                tgt_data.scale,
6771                read_data,
6772            )
6773        };
6774        // when cheesed from behind the entry, change position to retarget
6775        let home = agent.patrol_origin.unwrap_or(self.pos.0);
6776        let exit = Vec3::new(home.x - 6.0, home.y - 6.0, home.z);
6777        let (station_0, station_1) = (exit + 12.0, exit - 12.0);
6778        if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.5 {
6779            agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] = 0.0;
6780        }
6781        if !line_of_sight_with_target()
6782            && (tgt_data.pos.0 - exit).xy().magnitude_squared() < (10.0_f32).powi(2)
6783        {
6784            let station = if (tgt_data.pos.0 - station_0).xy().magnitude_squared()
6785                < (tgt_data.pos.0 - station_1).xy().magnitude_squared()
6786            {
6787                station_0
6788            } else {
6789                station_1
6790            };
6791            self.path_toward_target(agent, controller, station, read_data, Path::AtTarget, None);
6792        }
6793        // if target gets very close, shoot dagon bombs and lay out sea urchins
6794        else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6795            if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
6796                controller.push_basic_input(InputKind::Primary);
6797                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6798            } else {
6799                controller.push_basic_input(InputKind::Secondary);
6800                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6801            }
6802            // if target in close range use steambeam and shoot dagon bombs
6803        } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
6804            controller.inputs.move_dir = Vec2::zero();
6805            if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
6806                controller.push_basic_input(InputKind::Primary);
6807                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6808            } else {
6809                controller.push_basic_input(InputKind::Ability(1));
6810            }
6811        } else if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) {
6812            // if enemy is far, heal and shoot bombs
6813            if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
6814                controller.push_basic_input(InputKind::Primary);
6815            } else {
6816                controller.push_basic_input(InputKind::Ability(2));
6817            }
6818            agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6819        } else if line_of_sight_with_target() {
6820            // if enemy in mid range shoot dagon bombs and steamwave
6821            if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
6822                controller.push_basic_input(InputKind::Primary);
6823                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6824            } else {
6825                controller.push_basic_input(InputKind::Ability(0));
6826                agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6827            }
6828        }
6829        // chase
6830        let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6831            Path::Separate
6832        } else {
6833            Path::AtTarget
6834        };
6835        self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
6836    }
6837
6838    pub fn handle_snaretongue_attack(
6839        &self,
6840        agent: &mut Agent,
6841        controller: &mut Controller,
6842        attack_data: &AttackData,
6843        read_data: &ReadData,
6844    ) {
6845        enum Timers {
6846            TimerAttack = 0,
6847        }
6848        let attack_timer = &mut agent.combat_state.timers[Timers::TimerAttack as usize];
6849        if *attack_timer > 2.5 {
6850            *attack_timer = 0.0;
6851        }
6852        // if target gets very close, use tongue attack and shockwave
6853        if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) {
6854            if *attack_timer > 0.5 {
6855                controller.push_basic_input(InputKind::Primary);
6856                *attack_timer += read_data.dt.0;
6857            } else {
6858                controller.push_basic_input(InputKind::Secondary);
6859                *attack_timer += read_data.dt.0;
6860            }
6861            // if target in close range use beam and shoot dagon bombs
6862        } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
6863            controller.inputs.move_dir = Vec2::zero();
6864            if *attack_timer > 2.0 {
6865                controller.push_basic_input(InputKind::Ability(0));
6866                *attack_timer += read_data.dt.0;
6867            } else {
6868                controller.push_basic_input(InputKind::Ability(1));
6869            }
6870        } else {
6871            // if target in midrange range shoot dagon bombs and heal
6872            if *attack_timer > 1.0 {
6873                controller.push_basic_input(InputKind::Ability(0));
6874                *attack_timer += read_data.dt.0;
6875            } else {
6876                controller.push_basic_input(InputKind::Ability(2));
6877                *attack_timer += read_data.dt.0;
6878            }
6879        }
6880    }
6881
6882    pub fn handle_deadwood(
6883        &self,
6884        agent: &mut Agent,
6885        controller: &mut Controller,
6886        attack_data: &AttackData,
6887        tgt_data: &TargetData,
6888        read_data: &ReadData,
6889    ) {
6890        const BEAM_RANGE: f32 = 20.0;
6891        const BEAM_TIME: Duration = Duration::from_secs(3);
6892        // combat_state.condition controls whether or not deadwood should beam or dash
6893        if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
6894        {
6895            // If already dashing, keep dashing and have move_dir set to forward
6896            controller.push_basic_input(InputKind::Secondary);
6897            controller.inputs.move_dir = self.ori.look_vec().xy();
6898        } else if attack_data.in_min_range() && attack_data.angle_xy < 10.0 {
6899            // If near target, dash at them and through them to get away
6900            controller.push_basic_input(InputKind::Secondary);
6901        } else if matches!(self.char_state, CharacterState::BasicBeam(s) if s.stage_section != StageSection::Recover && s.timer < BEAM_TIME)
6902        {
6903            // If already beaming, keep beaming if not beaming for over 5 seconds
6904            controller.push_basic_input(InputKind::Primary);
6905        } else if attack_data.dist_sqrd < BEAM_RANGE.powi(2) {
6906            // Else if in beam range, beam them
6907            if attack_data.angle_xy < 5.0 {
6908                controller.push_basic_input(InputKind::Primary);
6909            } else {
6910                // If not in angle, apply slight movement so deadwood orients itself correctly
6911                controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
6912                    .xy()
6913                    .try_normalized()
6914                    .unwrap_or_else(Vec2::zero)
6915                    * 0.01;
6916            }
6917        } else {
6918            // Otherwise too far, move towards target
6919            self.path_toward_target(
6920                agent,
6921                controller,
6922                tgt_data.pos.0,
6923                read_data,
6924                Path::AtTarget,
6925                None,
6926            );
6927        }
6928    }
6929
6930    pub fn handle_mandragora(
6931        &self,
6932        agent: &mut Agent,
6933        controller: &mut Controller,
6934        attack_data: &AttackData,
6935        tgt_data: &TargetData,
6936        read_data: &ReadData,
6937    ) {
6938        const SCREAM_RANGE: f32 = 10.0; // hard-coded from scream.ron
6939
6940        enum ActionStateFCounters {
6941            FCounterHealthThreshold = 0,
6942        }
6943
6944        enum ActionStateConditions {
6945            ConditionHasScreamed = 0,
6946        }
6947
6948        if !agent.combat_state.initialized {
6949            agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
6950                self.health.map_or(0.0, |h| h.maximum());
6951            agent.combat_state.initialized = true;
6952        }
6953
6954        if !agent.combat_state.conditions[ActionStateConditions::ConditionHasScreamed as usize] {
6955            // If mandragora is still "sleeping" and hasn't screamed yet, do nothing until
6956            // target in range or until it's taken damage
6957            if self.health.is_some_and(|h| {
6958                h.current()
6959                    < agent.combat_state.counters
6960                        [ActionStateFCounters::FCounterHealthThreshold as usize]
6961            }) || attack_data.dist_sqrd < SCREAM_RANGE.powi(2)
6962            {
6963                agent.combat_state.conditions
6964                    [ActionStateConditions::ConditionHasScreamed as usize] = true;
6965                controller.push_basic_input(InputKind::Secondary);
6966            }
6967        } else {
6968            // Once mandragora has woken, move towards target and attack
6969            if attack_data.in_min_range() {
6970                controller.push_basic_input(InputKind::Primary);
6971            } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
6972                && entities_have_line_of_sight(
6973                    self.pos,
6974                    self.body,
6975                    self.scale,
6976                    tgt_data.pos,
6977                    tgt_data.body,
6978                    tgt_data.scale,
6979                    read_data,
6980                )
6981            {
6982                // If in pathing range and can see target, move towards them
6983                self.path_toward_target(
6984                    agent,
6985                    controller,
6986                    tgt_data.pos.0,
6987                    read_data,
6988                    Path::AtTarget,
6989                    None,
6990                );
6991            } else {
6992                // Otherwise, go back to sleep
6993                agent.combat_state.conditions
6994                    [ActionStateConditions::ConditionHasScreamed as usize] = false;
6995                agent.combat_state.counters
6996                    [ActionStateFCounters::FCounterHealthThreshold as usize] =
6997                    self.health.map_or(0.0, |h| h.maximum());
6998            }
6999        }
7000    }
7001
7002    pub fn handle_wood_golem(
7003        &self,
7004        agent: &mut Agent,
7005        controller: &mut Controller,
7006        attack_data: &AttackData,
7007        tgt_data: &TargetData,
7008        read_data: &ReadData,
7009        rng: &mut impl Rng,
7010    ) {
7011        // === reference ===
7012
7013        // Inputs:
7014        //   Primary: strike
7015        //   Secondary: spin
7016        //   Auxiliary
7017        //     0: shockwave
7018
7019        // === setup ===
7020
7021        // --- static ---
7022        // behaviour parameters
7023        const PATH_RANGE_FACTOR: f32 = 0.3; // get comfortably in range, but give player room to breathe
7024        const STRIKE_RANGE_FACTOR: f32 = 0.6; // start attack while suitably in range
7025        const STRIKE_AIM_FACTOR: f32 = 0.7;
7026        const SPIN_RANGE_FACTOR: f32 = 0.6;
7027        const SPIN_COOLDOWN: f32 = 1.5;
7028        const SPIN_RELAX_FACTOR: f32 = 0.2;
7029        const SHOCKWAVE_RANGE_FACTOR: f32 = 0.7;
7030        const SHOCKWAVE_AIM_FACTOR: f32 = 0.4;
7031        const SHOCKWAVE_COOLDOWN: f32 = 5.0;
7032        const MIXUP_COOLDOWN: f32 = 2.5;
7033        const MIXUP_RELAX_FACTOR: f32 = 0.3;
7034
7035        // timers
7036        const SPIN: usize = 0;
7037        const SHOCKWAVE: usize = 1;
7038        const MIXUP: usize = 2;
7039
7040        // --- dynamic ---
7041        // behaviour parameters
7042        let shockwave_min_range = self.body.map_or(0.0, |b| b.height() * 1.1);
7043
7044        // attack data
7045        let (strike_range, strike_angle) = {
7046            if let Some(AbilityData::BasicMelee { range, angle, .. }) =
7047                self.extract_ability(AbilityInput::Primary)
7048            {
7049                (range, angle)
7050            } else {
7051                (0.0, 0.0)
7052            }
7053        };
7054        let spin_range = {
7055            if let Some(AbilityData::BasicMelee { range, .. }) =
7056                self.extract_ability(AbilityInput::Secondary)
7057            {
7058                range
7059            } else {
7060                0.0
7061            }
7062        };
7063        let (shockwave_max_range, shockwave_angle) = {
7064            if let Some(AbilityData::Shockwave { range, angle, .. }) =
7065                self.extract_ability(AbilityInput::Auxiliary(0))
7066            {
7067                (range, angle)
7068            } else {
7069                (0.0, 0.0)
7070            }
7071        };
7072
7073        // re-used checks (makes separating timers and attacks easier)
7074        let is_in_spin_range = attack_data.dist_sqrd
7075            < (attack_data.body_dist + spin_range * SPIN_RANGE_FACTOR).powi(2);
7076        let is_in_strike_range = attack_data.dist_sqrd
7077            < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
7078        let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
7079
7080        // === main ===
7081
7082        // --- timers ---
7083        // spin
7084        let current_input = self.char_state.ability_info().map(|ai| ai.input);
7085        if matches!(current_input, Some(InputKind::Secondary)) {
7086            // reset when spinning
7087            agent.combat_state.timers[SPIN] = 0.0;
7088            agent.combat_state.timers[MIXUP] = 0.0;
7089        } else if is_in_spin_range && !(is_in_strike_range && is_in_strike_angle) {
7090            // increment within spin range and not in strike range + angle
7091            agent.combat_state.timers[SPIN] += read_data.dt.0;
7092        } else {
7093            // relax towards zero otherwise
7094            agent.combat_state.timers[SPIN] =
7095                (agent.combat_state.timers[SPIN] - read_data.dt.0 * SPIN_RELAX_FACTOR).max(0.0);
7096        }
7097        // shockwave
7098        if matches!(self.char_state, CharacterState::Shockwave(_)) {
7099            // reset when using shockwave
7100            agent.combat_state.timers[SHOCKWAVE] = 0.0;
7101            agent.combat_state.timers[MIXUP] = 0.0;
7102        } else {
7103            // increment otherwise
7104            agent.combat_state.timers[SHOCKWAVE] += read_data.dt.0;
7105        }
7106        // mixup
7107        if is_in_strike_range && is_in_strike_angle {
7108            // increment within strike range and angle
7109            agent.combat_state.timers[MIXUP] += read_data.dt.0;
7110        } else {
7111            // relax towards zero otherwise
7112            agent.combat_state.timers[MIXUP] =
7113                (agent.combat_state.timers[MIXUP] - read_data.dt.0 * MIXUP_RELAX_FACTOR).max(0.0);
7114        }
7115
7116        // --- attacks ---
7117        // strike range and angle
7118        if is_in_strike_range && is_in_strike_angle {
7119            // on timer, randomly mixup between all attacks
7120            if agent.combat_state.timers[MIXUP] > MIXUP_COOLDOWN {
7121                let randomise: u8 = rng.random_range(1..=3);
7122                match randomise {
7123                    1 => controller.push_basic_input(InputKind::Ability(0)), // shockwave
7124                    2 => controller.push_basic_input(InputKind::Primary),    // strike
7125                    _ => controller.push_basic_input(InputKind::Secondary),  // spin
7126                }
7127            }
7128            // default to strike
7129            else {
7130                controller.push_basic_input(InputKind::Primary);
7131            }
7132        }
7133        // spin range (or out of angle in strike range)
7134        else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) {
7135            // on timer, use spin attack to try and hit evasive target
7136            if agent.combat_state.timers[SPIN] > SPIN_COOLDOWN {
7137                controller.push_basic_input(InputKind::Secondary);
7138            }
7139            // otherwise, close angle (no action required)
7140        }
7141        // shockwave range and angle
7142        else if attack_data.dist_sqrd > shockwave_min_range.powi(2)
7143            && attack_data.dist_sqrd < (shockwave_max_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
7144            && attack_data.angle < shockwave_angle * SHOCKWAVE_AIM_FACTOR
7145        {
7146            // on timer, use shockwave
7147            if agent.combat_state.timers[SHOCKWAVE] > SHOCKWAVE_COOLDOWN {
7148                controller.push_basic_input(InputKind::Ability(0));
7149            }
7150            // otherwise, close gap and/or angle (no action required)
7151        }
7152
7153        // --- movement ---
7154        // closing gap
7155        if attack_data.dist_sqrd
7156            > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
7157        {
7158            self.path_toward_target(
7159                agent,
7160                controller,
7161                tgt_data.pos.0,
7162                read_data,
7163                Path::AtTarget,
7164                None,
7165            );
7166        }
7167        // closing angle
7168        else if attack_data.angle > 0.0 {
7169            // some movement is required to trigger re-orientation
7170            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7171                .xy()
7172                .try_normalized()
7173                .unwrap_or_else(Vec2::zero)
7174                * 0.001; // scaled way down to minimise position change and keep close rotation consistent
7175        }
7176    }
7177
7178    pub fn handle_gnarling_chieftain(
7179        &self,
7180        agent: &mut Agent,
7181        controller: &mut Controller,
7182        attack_data: &AttackData,
7183        tgt_data: &TargetData,
7184        read_data: &ReadData,
7185        rng: &mut impl Rng,
7186    ) {
7187        // === reference ===
7188        // Inputs
7189        //   Primary: flamestrike
7190        //   Secondary: firebarrage
7191        //   Auxiliary
7192        //     0: fireshockwave
7193        //     1: redtotem
7194        //     2: greentotem
7195        //     3: whitetotem
7196
7197        // === setup ===
7198
7199        // --- static ---
7200        // behaviour parameters
7201        const PATH_RANGE_FACTOR: f32 = 0.4;
7202        const STRIKE_RANGE_FACTOR: f32 = 0.7;
7203        const STRIKE_AIM_FACTOR: f32 = 0.8;
7204        const BARRAGE_RANGE_FACTOR: f32 = 0.8;
7205        const BARRAGE_AIM_FACTOR: f32 = 0.65;
7206        const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75;
7207        const TOTEM_COOLDOWN: f32 = 25.0;
7208        const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
7209        const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
7210        const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
7211
7212        // conditions
7213        const HAS_SUMMONED_FIRST_TOTEM: usize = 0;
7214        // timers
7215        const SUMMON_TOTEM: usize = 0;
7216        const HEAVY_ATTACK: usize = 1;
7217        // counters
7218        const HEAVY_ATTACK_COOLDOWN: usize = 0;
7219
7220        // line of sight check
7221        let line_of_sight_with_target = || {
7222            entities_have_line_of_sight(
7223                self.pos,
7224                self.body,
7225                self.scale,
7226                tgt_data.pos,
7227                tgt_data.body,
7228                tgt_data.scale,
7229                read_data,
7230            )
7231        };
7232
7233        // --- dynamic ---
7234        // attack data
7235        let (strike_range, strike_angle) = {
7236            if let Some(AbilityData::BasicMelee { range, angle, .. }) =
7237                self.extract_ability(AbilityInput::Primary)
7238            {
7239                (range, angle)
7240            } else {
7241                (0.0, 0.0)
7242            }
7243        };
7244        let (barrage_speed, barrage_spread, barrage_count) = {
7245            if let Some(AbilityData::BasicRanged {
7246                projectile_speed,
7247                projectile_spread,
7248                num_projectiles,
7249                ..
7250            }) = self.extract_ability(AbilityInput::Secondary)
7251            {
7252                (
7253                    projectile_speed,
7254                    projectile_spread,
7255                    num_projectiles.compute(self.heads.map_or(1, |heads| heads.amount() as u32)),
7256                )
7257            } else {
7258                (0.0, 0.0, 0)
7259            }
7260        };
7261        let shockwave_range = {
7262            if let Some(AbilityData::Shockwave { range, .. }) =
7263                self.extract_ability(AbilityInput::Auxiliary(0))
7264            {
7265                range
7266            } else {
7267                0.0
7268            }
7269        };
7270
7271        // calculated attack data
7272        let barrage_max_range =
7273            projectile_flat_range(barrage_speed, self.body.map_or(2.0, |b| b.height()));
7274        let barrange_angle = projectile_multi_angle(barrage_spread, barrage_count);
7275
7276        // re-used checks
7277        let is_in_strike_range = attack_data.dist_sqrd
7278            < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
7279        let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
7280
7281        // initialise randomised cooldowns
7282        if !agent.combat_state.initialized {
7283            agent.combat_state.initialized = true;
7284            agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
7285                rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
7286        }
7287
7288        // === main ===
7289
7290        // --- timers ---
7291        // resets
7292        match self.char_state {
7293            CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
7294                // reset when finished summoning
7295                agent.combat_state.timers[SUMMON_TOTEM] = 0.0;
7296                agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] = true;
7297            },
7298            CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
7299                // reset heavy attack on either ability
7300                agent.combat_state.counters[HEAVY_ATTACK] = 0.0;
7301                agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
7302                    rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
7303            },
7304            _ => {},
7305        }
7306        // totem (always increment)
7307        agent.combat_state.timers[SUMMON_TOTEM] += read_data.dt.0;
7308        // heavy attack (increment at different rates)
7309        if is_in_strike_range {
7310            // recharge at standard rate in strike range and angle
7311            if is_in_strike_angle {
7312                agent.combat_state.counters[HEAVY_ATTACK] += read_data.dt.0;
7313            } else {
7314                // If not in angle, charge heavy attack faster
7315                agent.combat_state.counters[HEAVY_ATTACK] +=
7316                    read_data.dt.0 * HEAVY_ATTACK_FAST_CHARGE_FACTOR;
7317            }
7318        } else {
7319            // If not in range, charge heavy attack faster
7320            agent.combat_state.counters[HEAVY_ATTACK] +=
7321                read_data.dt.0 * HEAVY_ATTACK_CHARGE_FACTOR;
7322        }
7323
7324        // --- attacks ---
7325        // start by summoning green totem
7326        if !agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] {
7327            controller.push_basic_input(InputKind::Ability(2));
7328        }
7329        // on timer, summon a new random totem
7330        else if agent.combat_state.timers[SUMMON_TOTEM] > TOTEM_COOLDOWN {
7331            controller.push_basic_input(InputKind::Ability(rng.random_range(1..=3)));
7332        }
7333        // on timer and in range, use a heavy attack
7334        // assumes: barrange_max_range * BARRAGE_RANGE_FACTOR > shockwave_range *
7335        // SHOCKWAVE_RANGE_FACTOR
7336        else if agent.combat_state.counters[HEAVY_ATTACK]
7337            > agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN]
7338            && attack_data.dist_sqrd < (barrage_max_range * BARRAGE_RANGE_FACTOR).powi(2)
7339        {
7340            // has line of sight
7341            if line_of_sight_with_target() {
7342                // out of barrage angle, use shockwave
7343                if attack_data.angle > barrange_angle * BARRAGE_AIM_FACTOR {
7344                    controller.push_basic_input(InputKind::Ability(0));
7345                }
7346                // in shockwave range, randomise between barrage and shockwave
7347                else if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
7348                {
7349                    if rng.random_bool(0.5) {
7350                        controller.push_basic_input(InputKind::Secondary);
7351                    } else {
7352                        controller.push_basic_input(InputKind::Ability(0));
7353                    }
7354                }
7355                // in range and angle, use barrage
7356                else {
7357                    controller.push_basic_input(InputKind::Secondary);
7358                }
7359                // otherwise, close gap and/or angle (no action required)
7360            }
7361            // no line of sight
7362            else {
7363                //  in range, use shockwave
7364                if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2) {
7365                    controller.push_basic_input(InputKind::Ability(0));
7366                }
7367                // otherwise, close gap (no action required)
7368            }
7369        }
7370        // if viable, default to flamestrike
7371        else if is_in_strike_range && is_in_strike_angle {
7372            controller.push_basic_input(InputKind::Primary);
7373        }
7374        // otherwise, close gap and/or angle (no action required)
7375
7376        // --- movement ---
7377        // closing gap
7378        if attack_data.dist_sqrd
7379            > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
7380        {
7381            self.path_toward_target(
7382                agent,
7383                controller,
7384                tgt_data.pos.0,
7385                read_data,
7386                Path::AtTarget,
7387                None,
7388            );
7389        }
7390        // closing angle
7391        else if attack_data.angle > 0.0 {
7392            // some movement is required to trigger re-orientation
7393            controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7394                .xy()
7395                .try_normalized()
7396                .unwrap_or_else(Vec2::zero)
7397                * 0.001; // scaled way down to minimise position change and keep close rotation consistent
7398        }
7399    }
7400
7401    pub fn handle_sword_simple_attack(
7402        &self,
7403        agent: &mut Agent,
7404        controller: &mut Controller,
7405        attack_data: &AttackData,
7406        tgt_data: &TargetData,
7407        read_data: &ReadData,
7408    ) {
7409        const DASH_TIMER: usize = 0;
7410        agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
7411        if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
7412        {
7413            controller.push_basic_input(InputKind::Secondary);
7414        } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
7415            if agent.combat_state.timers[DASH_TIMER] > 2.0 {
7416                agent.combat_state.timers[DASH_TIMER] = 0.0;
7417            }
7418            controller.push_basic_input(InputKind::Primary);
7419        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
7420            && self
7421                .path_toward_target(
7422                    agent,
7423                    controller,
7424                    tgt_data.pos.0,
7425                    read_data,
7426                    Path::Separate,
7427                    None,
7428                )
7429                .is_some()
7430            && entities_have_line_of_sight(
7431                self.pos,
7432                self.body,
7433                self.scale,
7434                tgt_data.pos,
7435                tgt_data.body,
7436                tgt_data.scale,
7437                read_data,
7438            )
7439            && agent.combat_state.timers[DASH_TIMER] > 4.0
7440            && attack_data.angle < 45.0
7441        {
7442            controller.push_basic_input(InputKind::Secondary);
7443            agent.combat_state.timers[DASH_TIMER] = 0.0;
7444        } else {
7445            self.path_toward_target(
7446                agent,
7447                controller,
7448                tgt_data.pos.0,
7449                read_data,
7450                Path::AtTarget,
7451                None,
7452            );
7453        }
7454    }
7455
7456    pub fn handle_adlet_hunter(
7457        &self,
7458        agent: &mut Agent,
7459        controller: &mut Controller,
7460        attack_data: &AttackData,
7461        tgt_data: &TargetData,
7462        read_data: &ReadData,
7463        rng: &mut impl Rng,
7464    ) {
7465        const ROTATE_TIMER: usize = 0;
7466        const ROTATE_DIR_CONDITION: usize = 0;
7467        agent.combat_state.timers[ROTATE_TIMER] -= read_data.dt.0;
7468        if agent.combat_state.timers[ROTATE_TIMER] < 0.0 {
7469            agent.combat_state.conditions[ROTATE_DIR_CONDITION] = rng.random_bool(0.5);
7470            agent.combat_state.timers[ROTATE_TIMER] = rng.random::<f32>() * 5.0;
7471        }
7472        let primary = self.extract_ability(AbilityInput::Primary);
7473        let secondary = self.extract_ability(AbilityInput::Secondary);
7474        let could_use_input = |input| match input {
7475            InputKind::Primary => primary.as_ref().is_some_and(|p| {
7476                p.could_use(
7477                    attack_data,
7478                    self,
7479                    tgt_data,
7480                    read_data,
7481                    AbilityPreferences::default(),
7482                )
7483            }),
7484            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7485                s.could_use(
7486                    attack_data,
7487                    self,
7488                    tgt_data,
7489                    read_data,
7490                    AbilityPreferences::default(),
7491                )
7492            }),
7493            _ => false,
7494        };
7495        let move_forwards = if could_use_input(InputKind::Primary) {
7496            controller.push_basic_input(InputKind::Primary);
7497            false
7498        } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 8_f32.powi(2) {
7499            controller.push_basic_input(InputKind::Secondary);
7500            true
7501        } else {
7502            true
7503        };
7504
7505        if move_forwards && attack_data.dist_sqrd > 3_f32.powi(2) {
7506            self.path_toward_target(
7507                agent,
7508                controller,
7509                tgt_data.pos.0,
7510                read_data,
7511                Path::Separate,
7512                None,
7513            );
7514        } else {
7515            self.path_toward_target(
7516                agent,
7517                controller,
7518                tgt_data.pos.0,
7519                read_data,
7520                Path::Separate,
7521                None,
7522            );
7523            let dir = if agent.combat_state.conditions[ROTATE_DIR_CONDITION] {
7524                1.0
7525            } else {
7526                -1.0
7527            };
7528            controller.inputs.move_dir.rotate_z(PI / 2.0 * dir);
7529        }
7530    }
7531
7532    pub fn handle_adlet_icepicker(
7533        &self,
7534        agent: &mut Agent,
7535        controller: &mut Controller,
7536        attack_data: &AttackData,
7537        tgt_data: &TargetData,
7538        read_data: &ReadData,
7539    ) {
7540        let primary = self.extract_ability(AbilityInput::Primary);
7541        let secondary = self.extract_ability(AbilityInput::Secondary);
7542        let could_use_input = |input| match input {
7543            InputKind::Primary => primary.as_ref().is_some_and(|p| {
7544                p.could_use(
7545                    attack_data,
7546                    self,
7547                    tgt_data,
7548                    read_data,
7549                    AbilityPreferences::default(),
7550                )
7551            }),
7552            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7553                s.could_use(
7554                    attack_data,
7555                    self,
7556                    tgt_data,
7557                    read_data,
7558                    AbilityPreferences::default(),
7559                )
7560            }),
7561            _ => false,
7562        };
7563        let move_forwards = if could_use_input(InputKind::Primary) {
7564            controller.push_basic_input(InputKind::Primary);
7565            false
7566        } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 5_f32.powi(2) {
7567            controller.push_basic_input(InputKind::Secondary);
7568            false
7569        } else {
7570            true
7571        };
7572
7573        if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7574            self.path_toward_target(
7575                agent,
7576                controller,
7577                tgt_data.pos.0,
7578                read_data,
7579                Path::Separate,
7580                None,
7581            );
7582        }
7583    }
7584
7585    pub fn handle_adlet_tracker(
7586        &self,
7587        agent: &mut Agent,
7588        controller: &mut Controller,
7589        attack_data: &AttackData,
7590        tgt_data: &TargetData,
7591        read_data: &ReadData,
7592    ) {
7593        const TRAP_TIMER: usize = 0;
7594        agent.combat_state.timers[TRAP_TIMER] += read_data.dt.0;
7595        if agent.combat_state.timers[TRAP_TIMER] > 20.0 {
7596            agent.combat_state.timers[TRAP_TIMER] = 0.0;
7597        }
7598        let primary = self.extract_ability(AbilityInput::Primary);
7599        let could_use_input = |input| match input {
7600            InputKind::Primary => primary.as_ref().is_some_and(|p| {
7601                p.could_use(
7602                    attack_data,
7603                    self,
7604                    tgt_data,
7605                    read_data,
7606                    AbilityPreferences::default(),
7607                )
7608            }),
7609            _ => false,
7610        };
7611        let move_forwards = if agent.combat_state.timers[TRAP_TIMER] < 3.0 {
7612            controller.push_basic_input(InputKind::Secondary);
7613            false
7614        } else if could_use_input(InputKind::Primary) {
7615            controller.push_basic_input(InputKind::Primary);
7616            false
7617        } else {
7618            true
7619        };
7620
7621        if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7622            self.path_toward_target(
7623                agent,
7624                controller,
7625                tgt_data.pos.0,
7626                read_data,
7627                Path::Separate,
7628                None,
7629            );
7630        }
7631    }
7632
7633    pub fn handle_adlet_elder(
7634        &self,
7635        agent: &mut Agent,
7636        controller: &mut Controller,
7637        attack_data: &AttackData,
7638        tgt_data: &TargetData,
7639        read_data: &ReadData,
7640        rng: &mut impl Rng,
7641    ) {
7642        const TRAP_TIMER: usize = 0;
7643        agent.combat_state.timers[TRAP_TIMER] -= read_data.dt.0;
7644        if matches!(self.char_state, CharacterState::BasicRanged(_)) {
7645            agent.combat_state.timers[TRAP_TIMER] = 15.0;
7646        }
7647        let primary = self.extract_ability(AbilityInput::Primary);
7648        let secondary = self.extract_ability(AbilityInput::Secondary);
7649        let abilities = [
7650            self.extract_ability(AbilityInput::Auxiliary(0)),
7651            self.extract_ability(AbilityInput::Auxiliary(1)),
7652        ];
7653        let could_use_input = |input| match input {
7654            InputKind::Primary => primary.as_ref().is_some_and(|p| {
7655                p.could_use(
7656                    attack_data,
7657                    self,
7658                    tgt_data,
7659                    read_data,
7660                    AbilityPreferences::default(),
7661                )
7662            }),
7663            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7664                s.could_use(
7665                    attack_data,
7666                    self,
7667                    tgt_data,
7668                    read_data,
7669                    AbilityPreferences::default(),
7670                )
7671            }),
7672            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7673                a.could_use(
7674                    attack_data,
7675                    self,
7676                    tgt_data,
7677                    read_data,
7678                    AbilityPreferences::default(),
7679                )
7680            }),
7681            _ => false,
7682        };
7683        let move_forwards = if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
7684        {
7685            controller.push_basic_input(InputKind::Secondary);
7686            false
7687        } else if agent.combat_state.timers[TRAP_TIMER] < 0.0 && !tgt_data.considered_ranged() {
7688            controller.push_basic_input(InputKind::Ability(0));
7689            false
7690        } else if could_use_input(InputKind::Primary) {
7691            controller.push_basic_input(InputKind::Primary);
7692            false
7693        } else if could_use_input(InputKind::Secondary) && rng.random_bool(0.5) {
7694            controller.push_basic_input(InputKind::Secondary);
7695            false
7696        } else if could_use_input(InputKind::Ability(1)) {
7697            controller.push_basic_input(InputKind::Ability(1));
7698            false
7699        } else {
7700            true
7701        };
7702
7703        if matches!(self.char_state, CharacterState::LeapMelee(_)) {
7704            let tgt_vec = tgt_data.pos.0.xy() - self.pos.0.xy();
7705            if tgt_vec.magnitude_squared() > 2_f32.powi(2)
7706                && let Some(look_dir) = Dir::from_unnormalized(Vec3::from(tgt_vec))
7707            {
7708                controller.inputs.look_dir = look_dir;
7709            }
7710        }
7711
7712        if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7713            self.path_toward_target(
7714                agent,
7715                controller,
7716                tgt_data.pos.0,
7717                read_data,
7718                Path::Separate,
7719                None,
7720            );
7721        }
7722    }
7723
7724    pub fn handle_icedrake(
7725        &self,
7726        agent: &mut Agent,
7727        controller: &mut Controller,
7728        attack_data: &AttackData,
7729        tgt_data: &TargetData,
7730        read_data: &ReadData,
7731        rng: &mut impl Rng,
7732    ) {
7733        let primary = self.extract_ability(AbilityInput::Primary);
7734        let secondary = self.extract_ability(AbilityInput::Secondary);
7735        let abilities = [
7736            self.extract_ability(AbilityInput::Auxiliary(0)),
7737            self.extract_ability(AbilityInput::Auxiliary(1)),
7738        ];
7739        let could_use_input = |input| match input {
7740            InputKind::Primary => primary.as_ref().is_some_and(|p| {
7741                p.could_use(
7742                    attack_data,
7743                    self,
7744                    tgt_data,
7745                    read_data,
7746                    AbilityPreferences::default(),
7747                )
7748            }),
7749            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7750                s.could_use(
7751                    attack_data,
7752                    self,
7753                    tgt_data,
7754                    read_data,
7755                    AbilityPreferences::default(),
7756                )
7757            }),
7758            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7759                a.could_use(
7760                    attack_data,
7761                    self,
7762                    tgt_data,
7763                    read_data,
7764                    AbilityPreferences::default(),
7765                )
7766            }),
7767            _ => false,
7768        };
7769
7770        let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
7771            Some(input @ InputKind::Primary) => {
7772                if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
7773                    && could_use_input(input)
7774                {
7775                    controller.push_basic_input(input);
7776                    true
7777                } else {
7778                    false
7779                }
7780            },
7781            Some(input @ InputKind::Ability(1)) => {
7782                if self
7783                    .char_state
7784                    .timer()
7785                    .is_some_and(|t| t.as_secs_f32() < 3.0)
7786                    && could_use_input(input)
7787                {
7788                    controller.push_basic_input(input);
7789                    true
7790                } else {
7791                    false
7792                }
7793            },
7794            _ => false,
7795        };
7796
7797        let move_forwards = if !continued_attack {
7798            if could_use_input(InputKind::Primary) && rng.random_bool(0.4) {
7799                controller.push_basic_input(InputKind::Primary);
7800                false
7801            } else if could_use_input(InputKind::Secondary) && rng.random_bool(0.8) {
7802                controller.push_basic_input(InputKind::Secondary);
7803                false
7804            } else if could_use_input(InputKind::Ability(1)) && rng.random_bool(0.9) {
7805                controller.push_basic_input(InputKind::Ability(1));
7806                true
7807            } else if could_use_input(InputKind::Ability(0)) {
7808                controller.push_basic_input(InputKind::Ability(0));
7809                true
7810            } else {
7811                true
7812            }
7813        } else {
7814            false
7815        };
7816
7817        if move_forwards {
7818            self.path_toward_target(
7819                agent,
7820                controller,
7821                tgt_data.pos.0,
7822                read_data,
7823                Path::Separate,
7824                None,
7825            );
7826        }
7827    }
7828
7829    pub fn handle_hydra(
7830        &self,
7831        agent: &mut Agent,
7832        controller: &mut Controller,
7833        attack_data: &AttackData,
7834        tgt_data: &TargetData,
7835        read_data: &ReadData,
7836        rng: &mut impl Rng,
7837    ) {
7838        enum ActionStateTimers {
7839            RegrowHeadNoDamage,
7840            RegrowHeadNoAttack,
7841        }
7842
7843        let could_use_input = |input| {
7844            Option::from(input)
7845                .and_then(|ability| {
7846                    Some(self.extract_ability(ability)?.could_use(
7847                        attack_data,
7848                        self,
7849                        tgt_data,
7850                        read_data,
7851                        AbilityPreferences::default(),
7852                    ))
7853                })
7854                .unwrap_or(false)
7855        };
7856
7857        const FOCUS_ATTACK_RANGE: f32 = 5.0;
7858
7859        if attack_data.dist_sqrd < FOCUS_ATTACK_RANGE.powi(2) {
7860            agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] = 0.0;
7861        } else {
7862            agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] +=
7863                read_data.dt.0;
7864        }
7865
7866        if let Some(health) = self.health.filter(|health| health.last_change.amount < 0.0) {
7867            agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] =
7868                (read_data.time.0 - health.last_change.time.0) as f32;
7869        } else {
7870            agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] +=
7871                read_data.dt.0;
7872        }
7873
7874        if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
7875            match self.char_state {
7876                CharacterState::ChargedMelee(c) => {
7877                    if c.charge_frac() < 1.0 && could_use_input(input) {
7878                        controller.push_basic_input(input);
7879                    }
7880                },
7881                CharacterState::ChargedRanged(c) => {
7882                    if c.charge_frac() < 1.0 && could_use_input(input) {
7883                        controller.push_basic_input(input);
7884                    }
7885                },
7886                _ => {},
7887            }
7888        }
7889
7890        let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
7891            Some(input @ InputKind::Primary) => {
7892                if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
7893                    && could_use_input(input)
7894                {
7895                    controller.push_basic_input(input);
7896                    true
7897                } else {
7898                    false
7899                }
7900            },
7901            _ => false,
7902        };
7903
7904        let has_heads = self.heads.is_none_or(|heads| heads.amount() > 0);
7905
7906        let move_forwards = if !continued_attack {
7907            if could_use_input(InputKind::Ability(1))
7908                && rng.random_bool(0.9)
7909                && (agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] > 5.0
7910                    || agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize]
7911                        > 6.0)
7912                && self.heads.is_some_and(|heads| heads.amount_missing() > 0)
7913            {
7914                controller.push_basic_input(InputKind::Ability(2));
7915                false
7916            } else if has_heads && could_use_input(InputKind::Primary) && rng.random_bool(0.8) {
7917                controller.push_basic_input(InputKind::Primary);
7918                true
7919            } else if has_heads && could_use_input(InputKind::Secondary) && rng.random_bool(0.4) {
7920                controller.push_basic_input(InputKind::Secondary);
7921                false
7922            } else if has_heads && could_use_input(InputKind::Ability(1)) && rng.random_bool(0.6) {
7923                controller.push_basic_input(InputKind::Ability(1));
7924                true
7925            } else if !has_heads && could_use_input(InputKind::Ability(3)) && rng.random_bool(0.7) {
7926                controller.push_basic_input(InputKind::Ability(3));
7927                true
7928            } else if could_use_input(InputKind::Ability(0)) {
7929                controller.push_basic_input(InputKind::Ability(0));
7930                true
7931            } else {
7932                true
7933            }
7934        } else {
7935            true
7936        };
7937
7938        if move_forwards {
7939            if has_heads {
7940                self.path_toward_target(
7941                    agent,
7942                    controller,
7943                    tgt_data.pos.0,
7944                    read_data,
7945                    Path::Separate,
7946                    // Slow down if close to the target
7947                    (attack_data.dist_sqrd
7948                        < (2.5 + self.body.map_or(0.0, |b| b.front_radius())).powi(2))
7949                    .then_some(0.3),
7950                );
7951            } else {
7952                self.flee(agent, controller, read_data, tgt_data.pos);
7953            }
7954        }
7955    }
7956
7957    pub fn handle_random_abilities(
7958        &self,
7959        agent: &mut Agent,
7960        controller: &mut Controller,
7961        attack_data: &AttackData,
7962        tgt_data: &TargetData,
7963        read_data: &ReadData,
7964        rng: &mut impl Rng,
7965        primary_weight: u8,
7966        secondary_weight: u8,
7967        ability_weights: [u8; BASE_ABILITY_LIMIT],
7968    ) {
7969        let primary = self.extract_ability(AbilityInput::Primary);
7970        let secondary = self.extract_ability(AbilityInput::Secondary);
7971        let abilities = [
7972            self.extract_ability(AbilityInput::Auxiliary(0)),
7973            self.extract_ability(AbilityInput::Auxiliary(1)),
7974            self.extract_ability(AbilityInput::Auxiliary(2)),
7975            self.extract_ability(AbilityInput::Auxiliary(3)),
7976            self.extract_ability(AbilityInput::Auxiliary(4)),
7977        ];
7978        let could_use_input = |input| match input {
7979            InputKind::Primary => primary.as_ref().is_some_and(|p| {
7980                p.could_use(
7981                    attack_data,
7982                    self,
7983                    tgt_data,
7984                    read_data,
7985                    AbilityPreferences::default(),
7986                )
7987            }),
7988            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7989                s.could_use(
7990                    attack_data,
7991                    self,
7992                    tgt_data,
7993                    read_data,
7994                    AbilityPreferences::default(),
7995                )
7996            }),
7997            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7998                a.could_use(
7999                    attack_data,
8000                    self,
8001                    tgt_data,
8002                    read_data,
8003                    AbilityPreferences::default(),
8004                )
8005            }),
8006            _ => false,
8007        };
8008
8009        let primary_chance = primary_weight as f64
8010            / ((primary_weight + secondary_weight + ability_weights.iter().sum::<u8>()) as f64)
8011                .max(0.01);
8012        let secondary_chance = secondary_weight as f64
8013            / ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
8014        let ability_chances = {
8015            let mut chances = [0.0; BASE_ABILITY_LIMIT];
8016            chances.iter_mut().enumerate().for_each(|(i, chance)| {
8017                *chance = ability_weights[i] as f64
8018                    / (ability_weights
8019                        .iter()
8020                        .enumerate()
8021                        .filter_map(|(j, weight)| if j >= i { Some(weight) } else { None })
8022                        .sum::<u8>() as f64)
8023                        .max(0.01)
8024            });
8025            chances
8026        };
8027
8028        if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
8029            match self.char_state {
8030                CharacterState::ChargedMelee(c) => {
8031                    if c.charge_frac() < 1.0 && could_use_input(input) {
8032                        controller.push_basic_input(input);
8033                    }
8034                },
8035                CharacterState::ChargedRanged(c) => {
8036                    if c.charge_frac() < 1.0 && could_use_input(input) {
8037                        controller.push_basic_input(input);
8038                    }
8039                },
8040                _ => {},
8041            }
8042        }
8043
8044        let move_forwards = if could_use_input(InputKind::Primary)
8045            && rng.random_bool(primary_chance)
8046        {
8047            controller.push_basic_input(InputKind::Primary);
8048            false
8049        } else if could_use_input(InputKind::Secondary) && rng.random_bool(secondary_chance) {
8050            controller.push_basic_input(InputKind::Secondary);
8051            false
8052        } else if could_use_input(InputKind::Ability(0)) && rng.random_bool(ability_chances[0]) {
8053            controller.push_basic_input(InputKind::Ability(0));
8054            false
8055        } else if could_use_input(InputKind::Ability(1)) && rng.random_bool(ability_chances[1]) {
8056            controller.push_basic_input(InputKind::Ability(1));
8057            false
8058        } else if could_use_input(InputKind::Ability(2)) && rng.random_bool(ability_chances[2]) {
8059            controller.push_basic_input(InputKind::Ability(2));
8060            false
8061        } else if could_use_input(InputKind::Ability(3)) && rng.random_bool(ability_chances[3]) {
8062            controller.push_basic_input(InputKind::Ability(3));
8063            false
8064        } else if could_use_input(InputKind::Ability(4)) && rng.random_bool(ability_chances[4]) {
8065            controller.push_basic_input(InputKind::Ability(4));
8066            false
8067        } else {
8068            true
8069        };
8070
8071        if move_forwards {
8072            self.path_toward_target(
8073                agent,
8074                controller,
8075                tgt_data.pos.0,
8076                read_data,
8077                Path::Separate,
8078                None,
8079            );
8080        }
8081    }
8082
8083    pub fn handle_simple_double_attack(
8084        &self,
8085        agent: &mut Agent,
8086        controller: &mut Controller,
8087        attack_data: &AttackData,
8088        tgt_data: &TargetData,
8089        read_data: &ReadData,
8090    ) {
8091        const MAX_ATTACK_RANGE: f32 = 20.0;
8092
8093        if attack_data.angle < 60.0 && attack_data.dist_sqrd < MAX_ATTACK_RANGE.powi(2) {
8094            controller.inputs.move_dir = Vec2::zero();
8095            if attack_data.in_min_range() {
8096                controller.push_basic_input(InputKind::Primary);
8097            } else {
8098                controller.push_basic_input(InputKind::Secondary);
8099            }
8100        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
8101            self.path_toward_target(
8102                agent,
8103                controller,
8104                tgt_data.pos.0,
8105                read_data,
8106                Path::Separate,
8107                None,
8108            );
8109        } else {
8110            self.path_toward_target(
8111                agent,
8112                controller,
8113                tgt_data.pos.0,
8114                read_data,
8115                Path::AtTarget,
8116                None,
8117            );
8118        }
8119    }
8120
8121    pub fn handle_clay_steed_attack(
8122        &self,
8123        agent: &mut Agent,
8124        controller: &mut Controller,
8125        attack_data: &AttackData,
8126        tgt_data: &TargetData,
8127        read_data: &ReadData,
8128    ) {
8129        enum ActionStateTimers {
8130            AttackTimer,
8131        }
8132        const HOOF_ATTACK_RANGE: f32 = 1.0;
8133        const HOOF_ATTACK_ANGLE: f32 = 50.0;
8134
8135        agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
8136        if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 10.0 {
8137            // Reset timer
8138            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
8139        }
8140
8141        if attack_data.angle < HOOF_ATTACK_ANGLE
8142            && attack_data.dist_sqrd
8143                < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.max_radius())).powi(2)
8144        {
8145            controller.inputs.move_dir = Vec2::zero();
8146            controller.push_basic_input(InputKind::Primary);
8147        } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
8148            controller.push_basic_input(InputKind::Secondary);
8149        } else {
8150            self.path_toward_target(
8151                agent,
8152                controller,
8153                tgt_data.pos.0,
8154                read_data,
8155                Path::AtTarget,
8156                None,
8157            );
8158        }
8159    }
8160
8161    pub fn handle_ancient_effigy_attack(
8162        &self,
8163        agent: &mut Agent,
8164        controller: &mut Controller,
8165        attack_data: &AttackData,
8166        tgt_data: &TargetData,
8167        read_data: &ReadData,
8168    ) {
8169        enum ActionStateTimers {
8170            BlastTimer,
8171        }
8172
8173        let home = agent.patrol_origin.unwrap_or(self.pos.0);
8174        let line_of_sight_with_target = || {
8175            entities_have_line_of_sight(
8176                self.pos,
8177                self.body,
8178                self.scale,
8179                tgt_data.pos,
8180                tgt_data.body,
8181                tgt_data.scale,
8182                read_data,
8183            )
8184        };
8185        agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] += read_data.dt.0;
8186
8187        if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] > 6.0 {
8188            agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] = 0.0;
8189        }
8190        if line_of_sight_with_target() {
8191            if attack_data.in_min_range() {
8192                controller.push_basic_input(InputKind::Secondary);
8193            } else if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] < 2.0 {
8194                controller.push_basic_input(InputKind::Primary);
8195            } else {
8196                self.path_toward_target(
8197                    agent,
8198                    controller,
8199                    tgt_data.pos.0,
8200                    read_data,
8201                    Path::Separate,
8202                    None,
8203                );
8204            }
8205        } else {
8206            // if target is hiding, don't follow, guard the room
8207            if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
8208                self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
8209            }
8210        }
8211    }
8212
8213    pub fn handle_clay_golem_attack(
8214        &self,
8215        agent: &mut Agent,
8216        controller: &mut Controller,
8217        attack_data: &AttackData,
8218        tgt_data: &TargetData,
8219        read_data: &ReadData,
8220    ) {
8221        const MIN_DASH_RANGE: f32 = 15.0;
8222
8223        enum ActionStateTimers {
8224            AttackTimer,
8225        }
8226
8227        let line_of_sight_with_target = || {
8228            entities_have_line_of_sight(
8229                self.pos,
8230                self.body,
8231                self.scale,
8232                tgt_data.pos,
8233                tgt_data.body,
8234                tgt_data.scale,
8235                read_data,
8236            )
8237        };
8238        let spawn = agent.patrol_origin.unwrap_or(self.pos.0);
8239        let home = Vec3::new(spawn.x - 32.0, spawn.y - 12.0, spawn.z);
8240        let is_home = (home - self.pos.0).xy().magnitude_squared() < (3.0_f32).powi(2);
8241        agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
8242        if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
8243            // Reset timer
8244            agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
8245        }
8246        if line_of_sight_with_target() {
8247            controller.inputs.move_dir = Vec2::zero();
8248            if attack_data.in_min_range() {
8249                controller.push_basic_input(InputKind::Primary);
8250            } else if attack_data.dist_sqrd > MIN_DASH_RANGE.powi(2) {
8251                controller.push_basic_input(InputKind::Secondary);
8252            } else {
8253                self.path_toward_target(
8254                    agent,
8255                    controller,
8256                    tgt_data.pos.0,
8257                    read_data,
8258                    Path::AtTarget,
8259                    None,
8260                );
8261            }
8262        } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.0 {
8263            if !is_home {
8264                // if target is wall cheesing, reposition
8265                self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
8266            } else {
8267                self.path_toward_target(agent, controller, spawn, read_data, Path::Separate, None);
8268            }
8269        } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
8270            self.path_toward_target(
8271                agent,
8272                controller,
8273                tgt_data.pos.0,
8274                read_data,
8275                Path::Separate,
8276                None,
8277            );
8278        }
8279    }
8280
8281    pub fn handle_haniwa_soldier(
8282        &self,
8283        agent: &mut Agent,
8284        controller: &mut Controller,
8285        attack_data: &AttackData,
8286        tgt_data: &TargetData,
8287        read_data: &ReadData,
8288    ) {
8289        const DEFENSIVE_CONDITION: usize = 0;
8290        const RIPOSTE_TIMER: usize = 0;
8291        const MODE_CYCLE_TIMER: usize = 1;
8292
8293        let primary = self.extract_ability(AbilityInput::Primary);
8294        let secondary = self.extract_ability(AbilityInput::Secondary);
8295        let could_use_input = |input| match input {
8296            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8297                p.could_use(
8298                    attack_data,
8299                    self,
8300                    tgt_data,
8301                    read_data,
8302                    AbilityPreferences::default(),
8303                )
8304            }),
8305            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8306                s.could_use(
8307                    attack_data,
8308                    self,
8309                    tgt_data,
8310                    read_data,
8311                    AbilityPreferences::default(),
8312                )
8313            }),
8314            _ => false,
8315        };
8316
8317        agent.combat_state.timers[RIPOSTE_TIMER] += read_data.dt.0;
8318        agent.combat_state.timers[MODE_CYCLE_TIMER] += read_data.dt.0;
8319
8320        if agent.combat_state.timers[MODE_CYCLE_TIMER] > 7.0 {
8321            agent.combat_state.conditions[DEFENSIVE_CONDITION] =
8322                !agent.combat_state.conditions[DEFENSIVE_CONDITION];
8323            agent.combat_state.timers[MODE_CYCLE_TIMER] = 0.0;
8324        }
8325
8326        if matches!(self.char_state, CharacterState::RiposteMelee(_)) {
8327            agent.combat_state.timers[RIPOSTE_TIMER] = 0.0;
8328        }
8329
8330        let try_move = if agent.combat_state.conditions[DEFENSIVE_CONDITION] {
8331            controller.push_basic_input(InputKind::Block);
8332            true
8333        } else if agent.combat_state.timers[RIPOSTE_TIMER] > 10.0
8334            && could_use_input(InputKind::Secondary)
8335        {
8336            controller.push_basic_input(InputKind::Secondary);
8337            false
8338        } else if could_use_input(InputKind::Primary) {
8339            controller.push_basic_input(InputKind::Primary);
8340            false
8341        } else {
8342            true
8343        };
8344
8345        if try_move && attack_data.dist_sqrd > 2_f32.powi(2) {
8346            self.path_toward_target(
8347                agent,
8348                controller,
8349                tgt_data.pos.0,
8350                read_data,
8351                Path::Separate,
8352                None,
8353            );
8354        }
8355    }
8356
8357    pub fn handle_haniwa_guard(
8358        &self,
8359        agent: &mut Agent,
8360        controller: &mut Controller,
8361        attack_data: &AttackData,
8362        tgt_data: &TargetData,
8363        read_data: &ReadData,
8364        rng: &mut impl Rng,
8365    ) {
8366        const BACKPEDAL_DIST: f32 = 5.0;
8367        const ROTATE_CCW_CONDITION: usize = 0;
8368        const FLURRY_TIMER: usize = 0;
8369        const BACKPEDAL_TIMER: usize = 1;
8370        const SWITCH_ROTATE_TIMER: usize = 2;
8371        const SWITCH_ROTATE_COUNTER: usize = 0;
8372
8373        let primary = self.extract_ability(AbilityInput::Primary);
8374        let secondary = self.extract_ability(AbilityInput::Secondary);
8375        let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
8376        let could_use_input = |input| match input {
8377            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8378                p.could_use(
8379                    attack_data,
8380                    self,
8381                    tgt_data,
8382                    read_data,
8383                    AbilityPreferences::default(),
8384                )
8385            }),
8386            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8387                s.could_use(
8388                    attack_data,
8389                    self,
8390                    tgt_data,
8391                    read_data,
8392                    AbilityPreferences::default(),
8393                )
8394            }),
8395            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8396                a.could_use(
8397                    attack_data,
8398                    self,
8399                    tgt_data,
8400                    read_data,
8401                    AbilityPreferences::default(),
8402                )
8403            }),
8404            _ => false,
8405        };
8406
8407        if !agent.combat_state.initialized {
8408            agent.combat_state.conditions[ROTATE_CCW_CONDITION] = rng.random_bool(0.5);
8409            agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.random_range(5.0..20.0);
8410            agent.combat_state.initialized = true;
8411        }
8412
8413        let continue_flurry = match self.char_state {
8414            CharacterState::BasicMelee(_) => {
8415                agent.combat_state.timers[FLURRY_TIMER] += read_data.dt.0;
8416                false
8417            },
8418            CharacterState::RapidMelee(c) => {
8419                agent.combat_state.timers[FLURRY_TIMER] = 0.0;
8420                !matches!(c.stage_section, StageSection::Recover)
8421            },
8422            CharacterState::ComboMelee2(_) => {
8423                agent.combat_state.timers[BACKPEDAL_TIMER] = 0.0;
8424                false
8425            },
8426            _ => false,
8427        };
8428        agent.combat_state.timers[SWITCH_ROTATE_TIMER] += read_data.dt.0;
8429        agent.combat_state.timers[BACKPEDAL_TIMER] += read_data.dt.0;
8430
8431        if agent.combat_state.timers[SWITCH_ROTATE_TIMER]
8432            > agent.combat_state.counters[SWITCH_ROTATE_COUNTER]
8433        {
8434            agent.combat_state.conditions[ROTATE_CCW_CONDITION] =
8435                !agent.combat_state.conditions[ROTATE_CCW_CONDITION];
8436            agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.random_range(5.0..20.0);
8437        }
8438
8439        let move_farther = attack_data.dist_sqrd < BACKPEDAL_DIST.powi(2);
8440        let move_closer = if continue_flurry && could_use_input(InputKind::Secondary) {
8441            controller.push_basic_input(InputKind::Secondary);
8442            false
8443        } else if agent.combat_state.timers[BACKPEDAL_TIMER] > 10.0
8444            && move_farther
8445            && could_use_input(InputKind::Ability(0))
8446        {
8447            controller.push_basic_input(InputKind::Ability(0));
8448            false
8449        } else if agent.combat_state.timers[FLURRY_TIMER] > 6.0
8450            && could_use_input(InputKind::Secondary)
8451        {
8452            controller.push_basic_input(InputKind::Secondary);
8453            false
8454        } else if could_use_input(InputKind::Primary) {
8455            controller.push_basic_input(InputKind::Primary);
8456            false
8457        } else {
8458            true
8459        };
8460
8461        if let Some((bearing, speed, stuck)) = agent.chaser.chase(
8462            &*read_data.terrain,
8463            self.pos.0,
8464            self.vel.0,
8465            tgt_data.pos.0,
8466            TraversalConfig {
8467                min_tgt_dist: 1.25,
8468                ..self.traversal_config
8469            },
8470            &read_data.time,
8471        ) {
8472            self.unstuck_if(stuck, controller);
8473            if entities_have_line_of_sight(
8474                self.pos,
8475                self.body,
8476                self.scale,
8477                tgt_data.pos,
8478                tgt_data.body,
8479                tgt_data.scale,
8480                read_data,
8481            ) && attack_data.angle < 45.0
8482            {
8483                let angle = match (
8484                    agent.combat_state.conditions[ROTATE_CCW_CONDITION],
8485                    move_closer,
8486                    move_farther,
8487                ) {
8488                    (true, true, false) => rng.random_range(-1.5..-0.5),
8489                    (true, false, true) => rng.random_range(-2.2..-1.7),
8490                    (true, _, _) => rng.random_range(-1.7..-1.5),
8491                    (false, true, false) => rng.random_range(0.5..1.5),
8492                    (false, false, true) => rng.random_range(1.7..2.2),
8493                    (false, _, _) => rng.random_range(1.5..1.7),
8494                };
8495                controller.inputs.move_dir = bearing
8496                    .xy()
8497                    .rotated_z(angle)
8498                    .try_normalized()
8499                    .unwrap_or_else(Vec2::zero)
8500                    * speed;
8501            } else {
8502                controller.inputs.move_dir =
8503                    bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
8504                self.jump_if(bearing.z > 1.5, controller);
8505            }
8506        }
8507    }
8508
8509    pub fn handle_haniwa_archer(
8510        &self,
8511        agent: &mut Agent,
8512        controller: &mut Controller,
8513        attack_data: &AttackData,
8514        tgt_data: &TargetData,
8515        read_data: &ReadData,
8516    ) {
8517        const KICK_TIMER: usize = 0;
8518        const EXPLOSIVE_TIMER: usize = 1;
8519
8520        let primary = self.extract_ability(AbilityInput::Primary);
8521        let secondary = self.extract_ability(AbilityInput::Secondary);
8522        let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
8523        let could_use_input = |input| match input {
8524            InputKind::Primary => primary.as_ref().is_some_and(|p| {
8525                p.could_use(
8526                    attack_data,
8527                    self,
8528                    tgt_data,
8529                    read_data,
8530                    AbilityPreferences::default(),
8531                )
8532            }),
8533            InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8534                s.could_use(
8535                    attack_data,
8536                    self,
8537                    tgt_data,
8538                    read_data,
8539                    AbilityPreferences::default(),
8540                )
8541            }),
8542            InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8543                a.could_use(
8544                    attack_data,
8545                    self,
8546                    tgt_data,
8547                    read_data,
8548                    AbilityPreferences::default(),
8549                )
8550            }),
8551            _ => false,
8552        };
8553
8554        agent.combat_state.timers[KICK_TIMER] += read_data.dt.0;
8555        agent.combat_state.timers[EXPLOSIVE_TIMER] += read_data.dt.0;
8556
8557        match self.char_state.ability_info().map(|ai| ai.input) {
8558            Some(InputKind::Secondary) => {
8559                agent.combat_state.timers[KICK_TIMER] = 0.0;
8560            },
8561            Some(InputKind::Ability(0)) => {
8562                agent.combat_state.timers[EXPLOSIVE_TIMER] = 0.0;
8563            },
8564            _ => {},
8565        }
8566
8567        if agent.combat_state.timers[KICK_TIMER] > 4.0 && could_use_input(InputKind::Secondary) {
8568            controller.push_basic_input(InputKind::Secondary);
8569        } else if agent.combat_state.timers[EXPLOSIVE_TIMER] > 15.0
8570            && could_use_input(InputKind::Ability(0))
8571        {
8572            controller.push_basic_input(InputKind::Ability(0));
8573        } else if could_use_input(InputKind::Primary) {
8574            controller.push_basic_input(InputKind::Primary);
8575        } else {
8576            self.path_toward_target(
8577                agent,
8578                controller,
8579                tgt_data.pos.0,
8580                read_data,
8581                Path::Separate,
8582                None,
8583            );
8584        }
8585    }
8586
8587    pub fn handle_terracotta_statue_attack(
8588        &self,
8589        agent: &mut Agent,
8590        controller: &mut Controller,
8591        attack_data: &AttackData,
8592        read_data: &ReadData,
8593    ) {
8594        enum Conditions {
8595            AttackToggle,
8596        }
8597        let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
8598        // stay centered
8599        if (home - self.pos.0).xy().magnitude_squared() > (2.0_f32).powi(2) {
8600            self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
8601        } else if !agent.combat_state.conditions[Conditions::AttackToggle as usize] {
8602            // always begin with sprite summon
8603            controller.push_basic_input(InputKind::Primary);
8604        } else {
8605            controller.inputs.move_dir = Vec2::zero();
8606            if attack_data.dist_sqrd < 8.5f32.powi(2) {
8607                // sprite summon
8608                controller.push_basic_input(InputKind::Primary);
8609            } else {
8610                // projectile
8611                controller.push_basic_input(InputKind::Secondary);
8612            }
8613        }
8614        if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
8615        {
8616            agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
8617        }
8618    }
8619
8620    pub fn handle_jiangshi_attack(
8621        &self,
8622        agent: &mut Agent,
8623        controller: &mut Controller,
8624        attack_data: &AttackData,
8625        tgt_data: &TargetData,
8626        read_data: &ReadData,
8627    ) {
8628        if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
8629            controller.push_action(ControlAction::StartInput {
8630                input: InputKind::Secondary,
8631                target_entity: agent
8632                    .target
8633                    .as_ref()
8634                    .and_then(|t| read_data.uids.get(t.target))
8635                    .copied(),
8636                select_pos: None,
8637            });
8638        } else if attack_data.dist_sqrd < 12.0f32.powi(2) {
8639            controller.push_basic_input(InputKind::Primary);
8640        }
8641
8642        self.path_toward_target(
8643            agent,
8644            controller,
8645            tgt_data.pos.0,
8646            read_data,
8647            Path::AtTarget,
8648            None,
8649        );
8650    }
8651}