veloren_server_agent/
data.rs

1use crate::util::*;
2use common::{
3    comp::{
4        ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy, Health, Inventory,
5        LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Presence, Scale, SkillSet, Stance,
6        Stats, Vel,
7        ability::{Amount, BASE_ABILITY_LIMIT, CharacterAbility},
8        body::parts::Heads,
9        buff::{BuffKind, Buffs},
10        character_state::AttackFilters,
11        group,
12        inventory::{
13            item::{
14                ItemKind, MaterialStatManifest,
15                tool::{AbilityMap, ToolKind},
16            },
17            slot::EquipSlot,
18        },
19    },
20    consts::GRAVITY,
21    event, event_emitters,
22    interaction::Interactors,
23    link::Is,
24    mounting::{Mount, Rider, VolumeRider},
25    path::TraversalConfig,
26    resources::{DeltaTime, Time, TimeOfDay},
27    rtsim::RtSimEntity,
28    states::utils::{ForcedMovement, StageSection},
29    terrain::TerrainGrid,
30    uid::{IdMaps, Uid},
31};
32use common_base::dev_panic;
33use specs::{Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData, shred};
34
35event_emitters! {
36    pub struct AgentEvents[AgentEmitters] {
37        chat: event::ChatEvent,
38        sound: event::SoundEvent,
39        process_trade_action: event::ProcessTradeActionEvent,
40    }
41}
42
43// TODO: Move rtsim back into AgentData after rtsim2 when it has a separate
44// crate
45pub struct AgentData<'a> {
46    pub entity: &'a EcsEntity,
47    pub uid: &'a Uid,
48    /// If the agent is riding something this is the root mounts position.
49    pub pos: &'a Pos,
50    /// If the agent is riding something this is the root mounts velocity.
51    pub vel: &'a Vel,
52    pub ori: &'a Ori,
53    pub energy: &'a Energy,
54    pub body: Option<&'a Body>,
55    pub inventory: &'a Inventory,
56    pub skill_set: &'a SkillSet,
57    pub physics_state: &'a PhysicsState,
58    pub alignment: Option<&'a Alignment>,
59    pub traversal_config: TraversalConfig,
60    pub scale: f32,
61    pub damage: f32,
62    pub light_emitter: Option<&'a LightEmitter>,
63    pub glider_equipped: bool,
64    pub is_gliding: bool,
65    pub health: Option<&'a Health>,
66    pub heads: Option<&'a Heads>,
67    pub char_state: &'a CharacterState,
68    pub active_abilities: &'a ActiveAbilities,
69    pub combo: Option<&'a Combo>,
70    pub buffs: Option<&'a Buffs>,
71    pub stats: Option<&'a Stats>,
72    pub poise: Option<&'a Poise>,
73    pub stance: Option<&'a Stance>,
74    pub cached_spatial_grid: &'a common::CachedSpatialGrid,
75    pub msm: &'a MaterialStatManifest,
76    pub rtsim_entity: Option<&'a RtSimEntity>,
77}
78
79pub struct TargetData<'a> {
80    pub pos: &'a Pos,
81    pub ori: Option<&'a Ori>,
82    pub body: Option<&'a Body>,
83    pub scale: Option<&'a Scale>,
84    pub char_state: Option<&'a CharacterState>,
85    pub health: Option<&'a Health>,
86    pub buffs: Option<&'a Buffs>,
87    pub drawn_weapons: (Option<ToolKind>, Option<ToolKind>),
88}
89
90impl<'a> TargetData<'a> {
91    pub fn new(pos: &'a Pos, target: EcsEntity, read_data: &'a ReadData) -> Self {
92        Self {
93            pos,
94            ori: read_data.orientations.get(target),
95            body: read_data.bodies.get(target),
96            scale: read_data.scales.get(target),
97            char_state: read_data.char_states.get(target),
98            health: read_data.healths.get(target),
99            buffs: read_data.buffs.get(target),
100            drawn_weapons: {
101                let slotted_tool = |inv: &Inventory, slot| {
102                    if let Some(ItemKind::Tool(tool)) =
103                        inv.equipped(slot).map(|i| i.kind()).as_deref()
104                    {
105                        Some(tool.kind)
106                    } else {
107                        None
108                    }
109                };
110                read_data
111                    .inventories
112                    .get(target)
113                    .map_or((None, None), |inv| {
114                        (
115                            slotted_tool(inv, EquipSlot::ActiveMainhand),
116                            slotted_tool(inv, EquipSlot::ActiveOffhand),
117                        )
118                    })
119            },
120        }
121    }
122
123    pub fn considered_ranged(&self) -> bool {
124        let is_ranged_tool = |tool| match tool {
125            Some(
126                ToolKind::Sword
127                | ToolKind::Axe
128                | ToolKind::Hammer
129                | ToolKind::Dagger
130                | ToolKind::Shield
131                | ToolKind::Spear
132                | ToolKind::Farming
133                | ToolKind::Pick
134                | ToolKind::Shovel
135                | ToolKind::Natural
136                | ToolKind::Throwable
137                | ToolKind::Empty,
138            )
139            | None => false,
140            Some(
141                ToolKind::Bow
142                | ToolKind::Staff
143                | ToolKind::Sceptre
144                | ToolKind::Blowgun
145                | ToolKind::Debug
146                | ToolKind::Instrument,
147            ) => true,
148        };
149        is_ranged_tool(self.drawn_weapons.0) || is_ranged_tool(self.drawn_weapons.1)
150    }
151}
152
153pub struct AttackData {
154    pub body_dist: f32,
155    /// Assumes attacker is facing the enemy
156    pub min_attack_dist: f32,
157    pub dist_sqrd: f32,
158    pub angle: f32,
159    pub angle_xy: f32,
160}
161
162impl AttackData {
163    pub fn in_min_range(&self) -> bool { self.dist_sqrd < self.min_attack_dist.powi(2) }
164}
165
166pub enum ActionMode {
167    Reckless = 0,
168    Guarded = 1,
169    Fleeing = 2,
170}
171
172impl ActionMode {
173    pub fn from_u8(x: u8) -> Self {
174        match x {
175            0 => ActionMode::Reckless,
176            1 => ActionMode::Guarded,
177            2 => ActionMode::Fleeing,
178            _ => ActionMode::Guarded,
179        }
180    }
181}
182
183#[derive(Eq, PartialEq)]
184// When adding a new variant, first decide if it should instead fall under one
185// of the pre-existing tactics
186pub enum Tactic {
187    // General tactics
188    SimpleMelee,
189    SimpleFlyingMelee,
190    SimpleBackstab,
191    ElevatedRanged,
192    Turret,
193    FixedTurret,
194    RotatingTurret,
195    RadialTurret,
196    FieryTornado,
197    SimpleDouble,
198    ClayGolem,
199    ClaySteed,
200    AncientEffigy,
201    // u8s are weights that each ability gets used, if it can be used
202    RandomAbilities {
203        primary: u8,
204        secondary: u8,
205        abilities: [u8; BASE_ABILITY_LIMIT],
206    },
207
208    // Tool specific tactics
209    Axe,
210    Hammer,
211    Sword,
212    Bow,
213    Staff,
214    Sceptre,
215    // TODO: Remove tactic and ability spec
216    SwordSimple,
217
218    // Broad creature tactics
219    CircleCharge {
220        radius: u32,
221        circle_time: u32,
222    },
223    QuadLowRanged,
224    TailSlap,
225    QuadLowQuick,
226    QuadLowBasic,
227    QuadLowBeam,
228    QuadMedJump,
229    QuadMedBasic,
230    QuadMedHoof,
231    Theropod,
232    BirdLargeBreathe,
233    BirdLargeFire,
234    BirdLargeBasic,
235    Wyvern,
236    BirdMediumBasic,
237    ArthropodMelee,
238    ArthropodRanged,
239    ArthropodAmbush,
240
241    // Specific species tactics
242    Mindflayer,
243    Minotaur,
244    GraveWarden,
245    TidalWarrior,
246    Karkatha,
247    Yeti,
248    Harvester,
249    StoneGolem,
250    Deadwood,
251    Mandragora,
252    WoodGolem,
253    IronGolem,
254    GnarlingChieftain,
255    OrganAura,
256    Dagon,
257    Snaretongue,
258    Cardinal,
259    SeaBishop,
260    Rocksnapper,
261    Roshwalr,
262    FrostGigas,
263    BorealHammer,
264    BorealBow,
265    Dullahan,
266    Cyclops,
267    IceDrake,
268    Hydra,
269    Flamekeeper,
270    Forgemaster,
271
272    // Adlets
273    AdletHunter,
274    AdletIcepicker,
275    AdletTracker,
276    AdletElder,
277
278    // Haniwa
279    HaniwaSoldier,
280    HaniwaGuard,
281    HaniwaArcher,
282
283    // Terracotta
284    TerracottaStatue,
285    Cursekeeper,
286    CursekeeperFake,
287    ShamanicSpirit,
288    Jiangshi,
289
290    // VampireCastle
291    VampireBat,
292    BloodmoonBat,
293    BloodmoonHeiress,
294}
295
296#[derive(Copy, Clone, Debug)]
297pub enum SwordTactics {
298    Unskilled = 0,
299    Basic = 1,
300    HeavySimple = 2,
301    AgileSimple = 3,
302    DefensiveSimple = 4,
303    CripplingSimple = 5,
304    CleavingSimple = 6,
305    HeavyAdvanced = 7,
306    AgileAdvanced = 8,
307    DefensiveAdvanced = 9,
308    CripplingAdvanced = 10,
309    CleavingAdvanced = 11,
310}
311
312impl SwordTactics {
313    pub fn from_u8(x: u8) -> Self {
314        use SwordTactics::*;
315        match x {
316            0 => Unskilled,
317            1 => Basic,
318            2 => HeavySimple,
319            3 => AgileSimple,
320            4 => DefensiveSimple,
321            5 => CripplingSimple,
322            6 => CleavingSimple,
323            7 => HeavyAdvanced,
324            8 => AgileAdvanced,
325            9 => DefensiveAdvanced,
326            10 => CripplingAdvanced,
327            11 => CleavingAdvanced,
328            _ => Unskilled,
329        }
330    }
331}
332
333#[derive(Copy, Clone, Debug)]
334pub enum AxeTactics {
335    Unskilled = 0,
336    SavageSimple = 1,
337    MercilessSimple = 2,
338    RivingSimple = 3,
339    SavageIntermediate = 4,
340    MercilessIntermediate = 5,
341    RivingIntermediate = 6,
342    SavageAdvanced = 7,
343    MercilessAdvanced = 8,
344    RivingAdvanced = 9,
345}
346
347impl AxeTactics {
348    pub fn from_u8(x: u8) -> Self {
349        use AxeTactics::*;
350        match x {
351            0 => Unskilled,
352            1 => SavageSimple,
353            2 => MercilessSimple,
354            3 => RivingSimple,
355            4 => SavageIntermediate,
356            5 => MercilessIntermediate,
357            6 => RivingIntermediate,
358            7 => SavageAdvanced,
359            8 => MercilessAdvanced,
360            9 => RivingAdvanced,
361            _ => Unskilled,
362        }
363    }
364}
365
366#[derive(Copy, Clone, Debug)]
367pub enum HammerTactics {
368    Unskilled = 0,
369    Simple = 1,
370    AttackSimple = 2,
371    SupportSimple = 3,
372    AttackIntermediate = 4,
373    SupportIntermediate = 5,
374    AttackAdvanced = 6,
375    SupportAdvanced = 7,
376    AttackExpert = 8,
377    SupportExpert = 9,
378}
379
380impl HammerTactics {
381    pub fn from_u8(x: u8) -> Self {
382        use HammerTactics::*;
383        match x {
384            0 => Unskilled,
385            1 => Simple,
386            2 => AttackSimple,
387            3 => SupportSimple,
388            4 => AttackIntermediate,
389            5 => SupportIntermediate,
390            6 => AttackAdvanced,
391            7 => SupportAdvanced,
392            8 => AttackExpert,
393            9 => SupportExpert,
394            _ => Unskilled,
395        }
396    }
397}
398
399#[derive(SystemData)]
400pub struct ReadData<'a> {
401    pub entities: Entities<'a>,
402    pub id_maps: Read<'a, IdMaps>,
403    pub dt: Read<'a, DeltaTime>,
404    pub time: Read<'a, Time>,
405    pub cached_spatial_grid: Read<'a, common::CachedSpatialGrid>,
406    pub group_manager: Read<'a, group::GroupManager>,
407    pub energies: ReadStorage<'a, Energy>,
408    pub positions: ReadStorage<'a, Pos>,
409    pub velocities: ReadStorage<'a, Vel>,
410    pub orientations: ReadStorage<'a, Ori>,
411    pub scales: ReadStorage<'a, Scale>,
412    pub healths: ReadStorage<'a, Health>,
413    pub heads: ReadStorage<'a, Heads>,
414    pub inventories: ReadStorage<'a, Inventory>,
415    pub stats: ReadStorage<'a, Stats>,
416    pub skill_set: ReadStorage<'a, SkillSet>,
417    pub physics_states: ReadStorage<'a, PhysicsState>,
418    pub char_states: ReadStorage<'a, CharacterState>,
419    pub uids: ReadStorage<'a, Uid>,
420    pub groups: ReadStorage<'a, group::Group>,
421    pub terrain: ReadExpect<'a, TerrainGrid>,
422    pub alignments: ReadStorage<'a, Alignment>,
423    pub bodies: ReadStorage<'a, Body>,
424    pub is_mounts: ReadStorage<'a, Is<Mount>>,
425    pub is_riders: ReadStorage<'a, Is<Rider>>,
426    pub is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
427    pub interactors: ReadStorage<'a, Interactors>,
428    pub time_of_day: Read<'a, TimeOfDay>,
429    pub light_emitter: ReadStorage<'a, LightEmitter>,
430    #[cfg(feature = "worldgen")]
431    pub world: ReadExpect<'a, std::sync::Arc<world::World>>,
432    pub rtsim_entities: ReadStorage<'a, RtSimEntity>,
433    pub buffs: ReadStorage<'a, Buffs>,
434    pub combos: ReadStorage<'a, Combo>,
435    pub active_abilities: ReadStorage<'a, ActiveAbilities>,
436    pub loot_owners: ReadStorage<'a, LootOwner>,
437    pub msm: ReadExpect<'a, MaterialStatManifest>,
438    pub poises: ReadStorage<'a, Poise>,
439    pub stances: ReadStorage<'a, Stance>,
440    pub presences: ReadStorage<'a, Presence>,
441    pub ability_map: ReadExpect<'a, AbilityMap>,
442}
443
444pub enum Path {
445    Full,
446    Separate,
447    Partial,
448}
449
450#[derive(Copy, Clone, Debug)]
451pub enum AbilityData {
452    ComboMelee {
453        range: f32,
454        angle: f32,
455        energy_per_strike: f32,
456        forced_movement: Option<ForcedMovement>,
457    },
458    FinisherMelee {
459        range: f32,
460        angle: f32,
461        energy: f32,
462        combo: u32,
463        combo_scales: bool,
464    },
465    SelfBuff {
466        buff: BuffKind,
467        energy: f32,
468        combo: u32,
469        combo_scales: bool,
470    },
471    DiveMelee {
472        range: f32,
473        angle: f32,
474        energy: f32,
475    },
476    DashMelee {
477        range: f32,
478        angle: f32,
479        initial_energy: f32,
480        energy_drain: f32,
481        speed: f32,
482        charge_dur: f32,
483    },
484    RapidMelee {
485        range: f32,
486        angle: f32,
487        energy_per_strike: f32,
488        strikes: u32,
489        combo: u32,
490    },
491    ChargedMelee {
492        range: f32,
493        angle: f32,
494        initial_energy: f32,
495        energy_drain: f32,
496        charge_dur: f32,
497    },
498    RiposteMelee {
499        range: f32,
500        angle: f32,
501        energy: f32,
502    },
503    BasicBlock {
504        energy: f32,
505        blocked_attacks: AttackFilters,
506        angle: f32,
507    },
508    BasicRanged {
509        energy: f32,
510        projectile_speed: f32,
511        projectile_spread: f32,
512        num_projectiles: Amount,
513    },
514    BasicMelee {
515        energy: f32,
516        range: f32,
517        angle: f32,
518    },
519    LeapMelee {
520        energy: f32,
521        range: f32,
522        angle: f32,
523        forward_leap: f32,
524        vertical_leap: f32,
525        leap_dur: f32,
526    },
527    BasicBeam {
528        energy_drain: f32,
529        range: f32,
530        angle: f32,
531        ori_rate: f32,
532    },
533    Shockwave {
534        energy: f32,
535        angle: f32,
536        range: f32,
537        combo: u32,
538    },
539    // Note, buff check not done as auras could be non-buff and auras could target either in or
540    // out of group
541    StaticAura {
542        energy: f32,
543    },
544    RegrowHead {
545        energy: f32,
546    },
547}
548
549#[derive(Copy, Clone, Debug, Default)]
550pub struct AbilityPreferences {
551    pub desired_energy: f32,
552    pub combo_scaling_buildup: u32,
553}
554
555impl AbilityData {
556    pub fn from_ability(ability: &CharacterAbility) -> Option<Self> {
557        use CharacterAbility::*;
558        let inner = match ability {
559            ComboMelee2 {
560                strikes,
561                energy_cost_per_strike,
562                ..
563            } => {
564                let (range, angle, forced_movement) = strikes
565                    .iter()
566                    .map(|s| {
567                        (
568                            s.melee_constructor.range,
569                            s.melee_constructor.angle,
570                            s.movement.buildup.map(|m| m * s.buildup_duration),
571                        )
572                    })
573                    .fold(
574                        (100.0, 360.0, None),
575                        |(r1, a1, m1): (f32, f32, Option<ForcedMovement>),
576                         (r2, a2, m2): (f32, f32, Option<ForcedMovement>)| {
577                            (r1.min(r2), a1.min(a2), m1.or(m2))
578                        },
579                    );
580                Self::ComboMelee {
581                    range,
582                    angle,
583                    energy_per_strike: *energy_cost_per_strike,
584                    forced_movement,
585                }
586            },
587            FinisherMelee {
588                energy_cost,
589                melee_constructor,
590                minimum_combo,
591                scaling,
592                ..
593            } => Self::FinisherMelee {
594                energy: *energy_cost,
595                range: melee_constructor.range,
596                angle: melee_constructor.angle,
597                combo: *minimum_combo,
598                combo_scales: scaling.is_some(),
599            },
600            SelfBuff {
601                buff_kind,
602                energy_cost,
603                combo_cost,
604                combo_scaling,
605                ..
606            } => Self::SelfBuff {
607                buff: *buff_kind,
608                energy: *energy_cost,
609                combo: *combo_cost,
610                combo_scales: combo_scaling.is_some(),
611            },
612            DiveMelee {
613                energy_cost,
614                melee_constructor,
615                ..
616            } => Self::DiveMelee {
617                energy: *energy_cost,
618                range: melee_constructor.range,
619                angle: melee_constructor.angle,
620            },
621            DashMelee {
622                energy_cost,
623                energy_drain,
624                forward_speed,
625                melee_constructor,
626                charge_duration,
627                ..
628            } => Self::DashMelee {
629                initial_energy: *energy_cost,
630                energy_drain: *energy_drain,
631                range: melee_constructor.range,
632                angle: melee_constructor.angle,
633                charge_dur: *charge_duration,
634                speed: *forward_speed,
635            },
636            RapidMelee {
637                energy_cost,
638                max_strikes,
639                minimum_combo,
640                melee_constructor,
641                ..
642            } => Self::RapidMelee {
643                energy_per_strike: *energy_cost,
644                range: melee_constructor.range,
645                angle: melee_constructor.angle,
646                strikes: max_strikes.unwrap_or(100),
647                combo: *minimum_combo,
648            },
649            ChargedMelee {
650                energy_cost,
651                energy_drain,
652                charge_duration,
653                melee_constructor,
654                ..
655            } => Self::ChargedMelee {
656                initial_energy: *energy_cost,
657                energy_drain: *energy_drain,
658                charge_dur: *charge_duration,
659                range: melee_constructor.range,
660                angle: melee_constructor.angle,
661            },
662            RiposteMelee {
663                energy_cost,
664                melee_constructor,
665                ..
666            } => Self::RiposteMelee {
667                energy: *energy_cost,
668                range: melee_constructor.range,
669                angle: melee_constructor.angle,
670            },
671            BasicBlock {
672                max_angle,
673                energy_cost,
674                blocked_attacks,
675                ..
676            } => Self::BasicBlock {
677                energy: *energy_cost,
678                angle: *max_angle,
679                blocked_attacks: *blocked_attacks,
680            },
681            BasicRanged {
682                energy_cost,
683                projectile_speed,
684                projectile_spread,
685                num_projectiles,
686                ..
687            } => Self::BasicRanged {
688                energy: *energy_cost,
689                projectile_speed: *projectile_speed,
690                projectile_spread: *projectile_spread,
691                num_projectiles: *num_projectiles,
692            },
693            BasicMelee {
694                energy_cost,
695                melee_constructor,
696                ..
697            } => Self::BasicMelee {
698                energy: *energy_cost,
699                range: melee_constructor.range,
700                angle: melee_constructor.angle,
701            },
702            LeapMelee {
703                energy_cost,
704                movement_duration,
705                melee_constructor,
706                forward_leap_strength,
707                vertical_leap_strength,
708                ..
709            } => Self::LeapMelee {
710                energy: *energy_cost,
711                leap_dur: *movement_duration,
712                range: melee_constructor.range,
713                angle: melee_constructor.angle,
714                forward_leap: *forward_leap_strength,
715                vertical_leap: *vertical_leap_strength,
716            },
717            BasicBeam {
718                range,
719                max_angle,
720                ori_rate,
721                energy_drain,
722                ..
723            } => Self::BasicBeam {
724                range: *range,
725                angle: *max_angle,
726                ori_rate: *ori_rate,
727                energy_drain: *energy_drain,
728            },
729            Shockwave {
730                energy_cost,
731                shockwave_angle,
732                shockwave_speed,
733                shockwave_duration,
734                minimum_combo,
735                ..
736            } => Self::Shockwave {
737                energy: *energy_cost,
738                angle: *shockwave_angle,
739                range: *shockwave_speed * *shockwave_duration,
740                combo: minimum_combo.unwrap_or(0),
741            },
742            StaticAura { energy_cost, .. } => Self::StaticAura {
743                energy: *energy_cost,
744            },
745            RegrowHead { energy_cost, .. } => Self::RegrowHead {
746                energy: *energy_cost,
747            },
748            _ => {
749                dev_panic!(
750                    "Agent tried to use ability with a character state they haven't learned to \
751                     understand"
752                );
753                return None;
754            },
755        };
756        Some(inner)
757    }
758
759    pub fn could_use(
760        &self,
761        attack_data: &AttackData,
762        agent_data: &AgentData,
763        tgt_data: &TargetData,
764        read_data: &ReadData,
765        ability_preferences: AbilityPreferences,
766    ) -> bool {
767        let melee_check = |range: f32, angle, forced_movement: Option<ForcedMovement>| {
768            let (range_inc, min_mult) = forced_movement.map_or((0.0, 0.0), |fm| match fm {
769                ForcedMovement::Forward(speed) => (speed * 15.0, 1.0),
770                ForcedMovement::Reverse(speed) => (-speed, 1.0),
771                ForcedMovement::Leap {
772                    vertical, forward, ..
773                } => (
774                    {
775                        let dur = vertical * 2.0 / GRAVITY;
776                        // 0.75 factor to allow for fact that agent looks down as they approach, so
777                        // won't go as far
778                        forward * dur * 0.75
779                    },
780                    0.0,
781                ),
782                _ => (0.0, 0.0),
783            });
784            let body_rad = agent_data.body.map_or(0.0, |b| b.max_radius());
785            attack_data.dist_sqrd < (range + range_inc + body_rad).powi(2)
786                && attack_data.angle < angle
787                && attack_data.dist_sqrd > (range_inc * min_mult).powi(2)
788        };
789        let energy_check = |energy: f32| {
790            agent_data.energy.current() >= energy
791                && (energy < f32::EPSILON
792                    || agent_data.energy.current() >= ability_preferences.desired_energy)
793        };
794        let combo_check = |combo, scales| {
795            let additional_combo = if scales {
796                ability_preferences.combo_scaling_buildup
797            } else {
798                0
799            };
800            agent_data
801                .combo
802                .is_some_and(|c| c.counter() >= combo + additional_combo)
803        };
804        let attack_kind_check = |attacks: AttackFilters| {
805            tgt_data
806                .char_state
807                .and_then(|cs| cs.attack_kind())
808                .is_some_and(|ak| attacks.applies(ak))
809        };
810        let ranged_check = |proj_speed| {
811            let max_horiz_dist: f32 = {
812                let flight_time = proj_speed * 2_f32.sqrt() / GRAVITY;
813                proj_speed * 2_f32.sqrt() / 2.0 * flight_time
814            };
815            attack_data.dist_sqrd < max_horiz_dist.powi(2)
816                && entities_have_line_of_sight(
817                    agent_data.pos,
818                    agent_data.body,
819                    agent_data.scale,
820                    tgt_data.pos,
821                    tgt_data.body,
822                    tgt_data.scale,
823                    read_data,
824                )
825        };
826        let beam_check = |range: f32, angle, ori_rate: f32| {
827            let angle_inc = ori_rate.to_degrees();
828            attack_data.dist_sqrd < range.powi(2)
829                && attack_data.angle < angle + angle_inc
830                && entities_have_line_of_sight(
831                    agent_data.pos,
832                    agent_data.body,
833                    agent_data.scale,
834                    tgt_data.pos,
835                    tgt_data.body,
836                    tgt_data.scale,
837                    read_data,
838                )
839        };
840        use AbilityData::*;
841        match self {
842            ComboMelee {
843                range,
844                angle,
845                energy_per_strike,
846                forced_movement,
847            } => melee_check(*range, *angle, *forced_movement) && energy_check(*energy_per_strike),
848            FinisherMelee {
849                range,
850                angle,
851                energy,
852                combo,
853                combo_scales,
854            } => {
855                melee_check(*range, *angle, None)
856                    && energy_check(*energy)
857                    && combo_check(*combo, *combo_scales)
858            },
859            SelfBuff {
860                buff,
861                energy,
862                combo,
863                combo_scales,
864            } => {
865                energy_check(*energy)
866                    && combo_check(*combo, *combo_scales)
867                    && agent_data.buffs.is_some_and(|buffs| !buffs.contains(*buff))
868            },
869            DiveMelee {
870                range,
871                angle,
872                energy,
873            } => melee_check(*range, *angle, None) && energy_check(*energy),
874            DashMelee {
875                range,
876                angle,
877                initial_energy,
878                energy_drain,
879                speed,
880                charge_dur,
881            } => {
882                // TODO: Maybe figure out better way of pulling in base accel from body and
883                // accounting for friction?
884                const BASE_SPEED: f32 = 3.0;
885                const ORI_RATE: f32 = 30.0;
886                let charge_dur = ((agent_data.energy.current() - initial_energy) / energy_drain)
887                    .clamp(0.0, *charge_dur);
888                let charge_dist = charge_dur * speed * BASE_SPEED;
889                let attack_dist = charge_dist + range;
890                let ori_gap = ORI_RATE * charge_dur;
891                // TODO: Replace None with actual forced movement later
892                melee_check(attack_dist, angle + ori_gap, None)
893                    && energy_check(*initial_energy)
894                    && attack_data.dist_sqrd / charge_dist.powi(2) > 0.75_f32.powi(2)
895            },
896            RapidMelee {
897                range,
898                angle,
899                energy_per_strike,
900                strikes,
901                combo,
902            } => {
903                melee_check(*range, *angle, None)
904                    && energy_check(*energy_per_strike * *strikes as f32)
905                    && combo_check(*combo, false)
906            },
907            ChargedMelee {
908                range,
909                angle,
910                initial_energy,
911                energy_drain,
912                charge_dur,
913            } => {
914                melee_check(*range, *angle, None)
915                    && energy_check(*initial_energy + *energy_drain * *charge_dur)
916            },
917            RiposteMelee {
918                energy,
919                range,
920                angle,
921            } => {
922                melee_check(*range, *angle, None)
923                    && energy_check(*energy)
924                    && tgt_data.char_state.is_some_and(|cs| {
925                        cs.is_melee_attack()
926                            && matches!(
927                                cs.stage_section(),
928                                Some(
929                                    StageSection::Buildup
930                                        | StageSection::Charge
931                                        | StageSection::Movement
932                                )
933                            )
934                    })
935            },
936            BasicBlock {
937                energy,
938                angle,
939                blocked_attacks,
940            } => {
941                melee_check(25.0, *angle, None)
942                    && energy_check(*energy)
943                    && attack_kind_check(*blocked_attacks)
944                    && tgt_data
945                        .char_state
946                        .and_then(|cs| cs.stage_section())
947                        .is_some_and(|ss| !matches!(ss, StageSection::Recover))
948            },
949            BasicRanged {
950                energy,
951                projectile_speed,
952                projectile_spread: _,
953                num_projectiles: _,
954            } => ranged_check(*projectile_speed) && energy_check(*energy),
955            BasicMelee {
956                energy,
957                range,
958                angle,
959            } => melee_check(*range, *angle, None) && energy_check(*energy),
960            LeapMelee {
961                energy,
962                range,
963                angle,
964                leap_dur,
965                forward_leap,
966                vertical_leap,
967            } => {
968                use common::states::utils::MovementDirection;
969                let forced_move = Some(ForcedMovement::Leap {
970                    vertical: *vertical_leap * *leap_dur * 2.0,
971                    forward: *forward_leap,
972                    progress: 0.0,
973                    direction: MovementDirection::Look,
974                });
975                melee_check(*range, *angle, forced_move) && energy_check(*energy)
976            },
977            BasicBeam {
978                energy_drain,
979                range,
980                angle,
981                ori_rate,
982            } => beam_check(*range, *angle, *ori_rate) && energy_check(*energy_drain * 3.0),
983            Shockwave {
984                energy,
985                range,
986                angle,
987                combo,
988            } => {
989                melee_check(*range, *angle, None)
990                    && energy_check(*energy)
991                    && combo_check(*combo, false)
992            },
993            StaticAura { energy } => energy_check(*energy),
994            RegrowHead { energy } => energy_check(*energy),
995        }
996    }
997}