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