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