use crate::util::*;
use common::{
comp::{
ability::{Amount, CharacterAbility, BASE_ABILITY_LIMIT},
body::parts::Heads,
buff::{BuffKind, Buffs},
character_state::AttackFilters,
group,
inventory::{
item::{
tool::{AbilityMap, ToolKind},
ItemKind, MaterialStatManifest,
},
slot::EquipSlot,
},
ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy, Health, Inventory,
LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Presence, Scale, SkillSet, Stance,
Stats, Vel,
},
consts::GRAVITY,
event, event_emitters,
link::Is,
mounting::{Mount, Rider, VolumeRider},
path::TraversalConfig,
resources::{DeltaTime, Time, TimeOfDay},
rtsim::{Actor, RtSimEntity},
states::utils::{ForcedMovement, StageSection},
terrain::TerrainGrid,
uid::{IdMaps, Uid},
};
use common_base::dev_panic;
use specs::{shred, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData};
event_emitters! {
pub struct AgentEvents[AgentEmitters] {
chat: event::ChatEvent,
sound: event::SoundEvent,
process_trade_action: event::ProcessTradeActionEvent,
}
}
pub struct AgentData<'a> {
pub entity: &'a EcsEntity,
pub uid: &'a Uid,
pub pos: &'a Pos,
pub vel: &'a Vel,
pub ori: &'a Ori,
pub energy: &'a Energy,
pub body: Option<&'a Body>,
pub inventory: &'a Inventory,
pub skill_set: &'a SkillSet,
pub physics_state: &'a PhysicsState,
pub alignment: Option<&'a Alignment>,
pub traversal_config: TraversalConfig,
pub scale: f32,
pub damage: f32,
pub light_emitter: Option<&'a LightEmitter>,
pub glider_equipped: bool,
pub is_gliding: bool,
pub health: Option<&'a Health>,
pub heads: Option<&'a Heads>,
pub char_state: &'a CharacterState,
pub active_abilities: &'a ActiveAbilities,
pub combo: Option<&'a Combo>,
pub buffs: Option<&'a Buffs>,
pub stats: Option<&'a Stats>,
pub poise: Option<&'a Poise>,
pub stance: Option<&'a Stance>,
pub cached_spatial_grid: &'a common::CachedSpatialGrid,
pub msm: &'a MaterialStatManifest,
pub rtsim_entity: Option<&'a RtSimEntity>,
}
pub struct TargetData<'a> {
pub pos: &'a Pos,
pub ori: Option<&'a Ori>,
pub body: Option<&'a Body>,
pub scale: Option<&'a Scale>,
pub char_state: Option<&'a CharacterState>,
pub health: Option<&'a Health>,
pub buffs: Option<&'a Buffs>,
pub drawn_weapons: (Option<ToolKind>, Option<ToolKind>),
}
impl<'a> TargetData<'a> {
pub fn new(pos: &'a Pos, target: EcsEntity, read_data: &'a ReadData) -> Self {
Self {
pos,
ori: read_data.orientations.get(target),
body: read_data.bodies.get(target),
scale: read_data.scales.get(target),
char_state: read_data.char_states.get(target),
health: read_data.healths.get(target),
buffs: read_data.buffs.get(target),
drawn_weapons: {
let slotted_tool = |inv: &Inventory, slot| {
if let Some(ItemKind::Tool(tool)) =
inv.equipped(slot).map(|i| i.kind()).as_deref()
{
Some(tool.kind)
} else {
None
}
};
read_data
.inventories
.get(target)
.map_or((None, None), |inv| {
(
slotted_tool(inv, EquipSlot::ActiveMainhand),
slotted_tool(inv, EquipSlot::ActiveOffhand),
)
})
},
}
}
pub fn considered_ranged(&self) -> bool {
let is_ranged_tool = |tool| match tool {
Some(
ToolKind::Sword
| ToolKind::Axe
| ToolKind::Hammer
| ToolKind::Dagger
| ToolKind::Shield
| ToolKind::Spear
| ToolKind::Farming
| ToolKind::Pick
| ToolKind::Shovel
| ToolKind::Natural
| ToolKind::Empty,
)
| None => false,
Some(
ToolKind::Bow
| ToolKind::Staff
| ToolKind::Sceptre
| ToolKind::Blowgun
| ToolKind::Debug
| ToolKind::Instrument,
) => true,
};
is_ranged_tool(self.drawn_weapons.0) || is_ranged_tool(self.drawn_weapons.1)
}
}
pub struct AttackData {
pub body_dist: f32,
pub min_attack_dist: f32,
pub dist_sqrd: f32,
pub angle: f32,
pub angle_xy: f32,
}
impl AttackData {
pub fn in_min_range(&self) -> bool { self.dist_sqrd < self.min_attack_dist.powi(2) }
}
pub enum ActionMode {
Reckless = 0,
Guarded = 1,
Fleeing = 2,
}
impl ActionMode {
pub fn from_u8(x: u8) -> Self {
match x {
0 => ActionMode::Reckless,
1 => ActionMode::Guarded,
2 => ActionMode::Fleeing,
_ => ActionMode::Guarded,
}
}
}
#[derive(Eq, PartialEq)]
pub enum Tactic {
SimpleMelee,
SimpleFlyingMelee,
SimpleBackstab,
ElevatedRanged,
Turret,
FixedTurret,
RotatingTurret,
RadialTurret,
FieryTornado,
SimpleDouble,
ClayGolem,
ClaySteed,
AncientEffigy,
RandomAbilities {
primary: u8,
secondary: u8,
abilities: [u8; BASE_ABILITY_LIMIT],
},
Axe,
Hammer,
Sword,
Bow,
Staff,
Sceptre,
SwordSimple,
CircleCharge {
radius: u32,
circle_time: u32,
},
QuadLowRanged,
TailSlap,
QuadLowQuick,
QuadLowBasic,
QuadLowBeam,
QuadMedJump,
QuadMedBasic,
QuadMedHoof,
Theropod,
BirdLargeBreathe,
BirdLargeFire,
BirdLargeBasic,
Wyvern,
BirdMediumBasic,
ArthropodMelee,
ArthropodRanged,
ArthropodAmbush,
Mindflayer,
Minotaur,
GraveWarden,
TidalWarrior,
Karkatha,
Yeti,
Harvester,
StoneGolem,
Deadwood,
Mandragora,
WoodGolem,
IronGolem,
GnarlingChieftain,
OrganAura,
Dagon,
Snaretongue,
Cardinal,
SeaBishop,
Rocksnapper,
Roshwalr,
FrostGigas,
BorealHammer,
BorealBow,
Dullahan,
Cyclops,
IceDrake,
Hydra,
Flamekeeper,
Forgemaster,
AdletHunter,
AdletIcepicker,
AdletTracker,
AdletElder,
HaniwaSoldier,
HaniwaGuard,
HaniwaArcher,
TerracottaStatue,
Cursekeeper,
CursekeeperFake,
ShamanicSpirit,
Jiangshi,
VampireBat,
BloodmoonBat,
BloodmoonHeiress,
}
#[derive(Copy, Clone, Debug)]
pub enum SwordTactics {
Unskilled = 0,
Basic = 1,
HeavySimple = 2,
AgileSimple = 3,
DefensiveSimple = 4,
CripplingSimple = 5,
CleavingSimple = 6,
HeavyAdvanced = 7,
AgileAdvanced = 8,
DefensiveAdvanced = 9,
CripplingAdvanced = 10,
CleavingAdvanced = 11,
}
impl SwordTactics {
pub fn from_u8(x: u8) -> Self {
use SwordTactics::*;
match x {
0 => Unskilled,
1 => Basic,
2 => HeavySimple,
3 => AgileSimple,
4 => DefensiveSimple,
5 => CripplingSimple,
6 => CleavingSimple,
7 => HeavyAdvanced,
8 => AgileAdvanced,
9 => DefensiveAdvanced,
10 => CripplingAdvanced,
11 => CleavingAdvanced,
_ => Unskilled,
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum AxeTactics {
Unskilled = 0,
SavageSimple = 1,
MercilessSimple = 2,
RivingSimple = 3,
SavageIntermediate = 4,
MercilessIntermediate = 5,
RivingIntermediate = 6,
SavageAdvanced = 7,
MercilessAdvanced = 8,
RivingAdvanced = 9,
}
impl AxeTactics {
pub fn from_u8(x: u8) -> Self {
use AxeTactics::*;
match x {
0 => Unskilled,
1 => SavageSimple,
2 => MercilessSimple,
3 => RivingSimple,
4 => SavageIntermediate,
5 => MercilessIntermediate,
6 => RivingIntermediate,
7 => SavageAdvanced,
8 => MercilessAdvanced,
9 => RivingAdvanced,
_ => Unskilled,
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum HammerTactics {
Unskilled = 0,
Simple = 1,
AttackSimple = 2,
SupportSimple = 3,
AttackIntermediate = 4,
SupportIntermediate = 5,
AttackAdvanced = 6,
SupportAdvanced = 7,
AttackExpert = 8,
SupportExpert = 9,
}
impl HammerTactics {
pub fn from_u8(x: u8) -> Self {
use HammerTactics::*;
match x {
0 => Unskilled,
1 => Simple,
2 => AttackSimple,
3 => SupportSimple,
4 => AttackIntermediate,
5 => SupportIntermediate,
6 => AttackAdvanced,
7 => SupportAdvanced,
8 => AttackExpert,
9 => SupportExpert,
_ => Unskilled,
}
}
}
#[derive(SystemData)]
pub struct ReadData<'a> {
pub entities: Entities<'a>,
pub id_maps: Read<'a, IdMaps>,
pub dt: Read<'a, DeltaTime>,
pub time: Read<'a, Time>,
pub cached_spatial_grid: Read<'a, common::CachedSpatialGrid>,
pub group_manager: Read<'a, group::GroupManager>,
pub energies: ReadStorage<'a, Energy>,
pub positions: ReadStorage<'a, Pos>,
pub velocities: ReadStorage<'a, Vel>,
pub orientations: ReadStorage<'a, Ori>,
pub scales: ReadStorage<'a, Scale>,
pub healths: ReadStorage<'a, Health>,
pub heads: ReadStorage<'a, Heads>,
pub inventories: ReadStorage<'a, Inventory>,
pub stats: ReadStorage<'a, Stats>,
pub skill_set: ReadStorage<'a, SkillSet>,
pub physics_states: ReadStorage<'a, PhysicsState>,
pub char_states: ReadStorage<'a, CharacterState>,
pub uids: ReadStorage<'a, Uid>,
pub groups: ReadStorage<'a, group::Group>,
pub terrain: ReadExpect<'a, TerrainGrid>,
pub alignments: ReadStorage<'a, Alignment>,
pub bodies: ReadStorage<'a, Body>,
pub is_mounts: ReadStorage<'a, Is<Mount>>,
pub is_riders: ReadStorage<'a, Is<Rider>>,
pub is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
pub time_of_day: Read<'a, TimeOfDay>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
#[cfg(feature = "worldgen")]
pub world: ReadExpect<'a, std::sync::Arc<world::World>>,
pub rtsim_entities: ReadStorage<'a, RtSimEntity>,
pub buffs: ReadStorage<'a, Buffs>,
pub combos: ReadStorage<'a, Combo>,
pub active_abilities: ReadStorage<'a, ActiveAbilities>,
pub loot_owners: ReadStorage<'a, LootOwner>,
pub msm: ReadExpect<'a, MaterialStatManifest>,
pub poises: ReadStorage<'a, Poise>,
pub stances: ReadStorage<'a, Stance>,
pub presences: ReadStorage<'a, Presence>,
pub ability_map: ReadExpect<'a, AbilityMap>,
}
impl<'a> ReadData<'a> {
pub fn lookup_actor(&self, actor: Actor) -> Option<EcsEntity> {
match actor {
Actor::Character(character_id) => self.id_maps.character_entity(character_id),
Actor::Npc(npc_id) => self.id_maps.rtsim_entity(RtSimEntity(npc_id)),
}
}
}
pub enum Path {
Full,
Separate,
Partial,
}
#[derive(Copy, Clone, Debug)]
pub enum AbilityData {
ComboMelee {
range: f32,
angle: f32,
energy_per_strike: f32,
forced_movement: Option<ForcedMovement>,
},
FinisherMelee {
range: f32,
angle: f32,
energy: f32,
combo: u32,
combo_scales: bool,
},
SelfBuff {
buff: BuffKind,
energy: f32,
combo: u32,
combo_scales: bool,
},
DiveMelee {
range: f32,
angle: f32,
energy: f32,
},
DashMelee {
range: f32,
angle: f32,
initial_energy: f32,
energy_drain: f32,
speed: f32,
charge_dur: f32,
},
RapidMelee {
range: f32,
angle: f32,
energy_per_strike: f32,
strikes: u32,
combo: u32,
},
ChargedMelee {
range: f32,
angle: f32,
initial_energy: f32,
energy_drain: f32,
charge_dur: f32,
},
RiposteMelee {
range: f32,
angle: f32,
energy: f32,
},
BasicBlock {
energy: f32,
blocked_attacks: AttackFilters,
angle: f32,
},
BasicRanged {
energy: f32,
projectile_speed: f32,
projectile_spread: f32,
num_projectiles: Amount,
},
BasicMelee {
energy: f32,
range: f32,
angle: f32,
},
LeapMelee {
energy: f32,
range: f32,
angle: f32,
forward_leap: f32,
vertical_leap: f32,
leap_dur: f32,
},
BasicBeam {
energy_drain: f32,
range: f32,
angle: f32,
ori_rate: f32,
},
Shockwave {
energy: f32,
angle: f32,
range: f32,
combo: u32,
},
StaticAura {
energy: f32,
},
RegrowHead {
energy: f32,
},
}
#[derive(Copy, Clone, Debug, Default)]
pub struct AbilityPreferences {
pub desired_energy: f32,
pub combo_scaling_buildup: u32,
}
impl AbilityData {
pub fn from_ability(ability: &CharacterAbility) -> Option<Self> {
use CharacterAbility::*;
let inner = match ability {
ComboMelee2 {
strikes,
energy_cost_per_strike,
..
} => {
let (range, angle, forced_movement) = strikes
.iter()
.map(|s| {
(
s.melee_constructor.range,
s.melee_constructor.angle,
s.movement.buildup.map(|m| m * s.buildup_duration),
)
})
.fold(
(100.0, 360.0, None),
|(r1, a1, m1): (f32, f32, Option<ForcedMovement>),
(r2, a2, m2): (f32, f32, Option<ForcedMovement>)| {
(r1.min(r2), a1.min(a2), m1.or(m2))
},
);
Self::ComboMelee {
range,
angle,
energy_per_strike: *energy_cost_per_strike,
forced_movement,
}
},
FinisherMelee {
energy_cost,
melee_constructor,
minimum_combo,
scaling,
..
} => Self::FinisherMelee {
energy: *energy_cost,
range: melee_constructor.range,
angle: melee_constructor.angle,
combo: *minimum_combo,
combo_scales: scaling.is_some(),
},
SelfBuff {
buff_kind,
energy_cost,
combo_cost,
combo_scaling,
..
} => Self::SelfBuff {
buff: *buff_kind,
energy: *energy_cost,
combo: *combo_cost,
combo_scales: combo_scaling.is_some(),
},
DiveMelee {
energy_cost,
melee_constructor,
..
} => Self::DiveMelee {
energy: *energy_cost,
range: melee_constructor.range,
angle: melee_constructor.angle,
},
DashMelee {
energy_cost,
energy_drain,
forward_speed,
melee_constructor,
charge_duration,
..
} => Self::DashMelee {
initial_energy: *energy_cost,
energy_drain: *energy_drain,
range: melee_constructor.range,
angle: melee_constructor.angle,
charge_dur: *charge_duration,
speed: *forward_speed,
},
RapidMelee {
energy_cost,
max_strikes,
minimum_combo,
melee_constructor,
..
} => Self::RapidMelee {
energy_per_strike: *energy_cost,
range: melee_constructor.range,
angle: melee_constructor.angle,
strikes: max_strikes.unwrap_or(100),
combo: *minimum_combo,
},
ChargedMelee {
energy_cost,
energy_drain,
charge_duration,
melee_constructor,
..
} => Self::ChargedMelee {
initial_energy: *energy_cost,
energy_drain: *energy_drain,
charge_dur: *charge_duration,
range: melee_constructor.range,
angle: melee_constructor.angle,
},
RiposteMelee {
energy_cost,
melee_constructor,
..
} => Self::RiposteMelee {
energy: *energy_cost,
range: melee_constructor.range,
angle: melee_constructor.angle,
},
BasicBlock {
max_angle,
energy_cost,
blocked_attacks,
..
} => Self::BasicBlock {
energy: *energy_cost,
angle: *max_angle,
blocked_attacks: *blocked_attacks,
},
BasicRanged {
energy_cost,
projectile_speed,
projectile_spread,
num_projectiles,
..
} => Self::BasicRanged {
energy: *energy_cost,
projectile_speed: *projectile_speed,
projectile_spread: *projectile_spread,
num_projectiles: *num_projectiles,
},
BasicMelee {
energy_cost,
melee_constructor,
..
} => Self::BasicMelee {
energy: *energy_cost,
range: melee_constructor.range,
angle: melee_constructor.angle,
},
LeapMelee {
energy_cost,
movement_duration,
melee_constructor,
forward_leap_strength,
vertical_leap_strength,
..
} => Self::LeapMelee {
energy: *energy_cost,
leap_dur: *movement_duration,
range: melee_constructor.range,
angle: melee_constructor.angle,
forward_leap: *forward_leap_strength,
vertical_leap: *vertical_leap_strength,
},
BasicBeam {
range,
max_angle,
ori_rate,
energy_drain,
..
} => Self::BasicBeam {
range: *range,
angle: *max_angle,
ori_rate: *ori_rate,
energy_drain: *energy_drain,
},
Shockwave {
energy_cost,
shockwave_angle,
shockwave_speed,
shockwave_duration,
minimum_combo,
..
} => Self::Shockwave {
energy: *energy_cost,
angle: *shockwave_angle,
range: *shockwave_speed * *shockwave_duration,
combo: minimum_combo.unwrap_or(0),
},
StaticAura { energy_cost, .. } => Self::StaticAura {
energy: *energy_cost,
},
RegrowHead { energy_cost, .. } => Self::RegrowHead {
energy: *energy_cost,
},
_ => {
dev_panic!(
"Agent tried to use ability with a character state they haven't learned to \
understand"
);
return None;
},
};
Some(inner)
}
pub fn could_use(
&self,
attack_data: &AttackData,
agent_data: &AgentData,
tgt_data: &TargetData,
read_data: &ReadData,
ability_preferences: AbilityPreferences,
) -> bool {
let melee_check = |range: f32, angle, forced_movement: Option<ForcedMovement>| {
let (range_inc, min_mult) = forced_movement.map_or((0.0, 0.0), |fm| match fm {
ForcedMovement::Forward(speed) => (speed * 15.0, 1.0),
ForcedMovement::Reverse(speed) => (-speed, 1.0),
ForcedMovement::Leap {
vertical, forward, ..
} => (
{
let dur = vertical * 2.0 / GRAVITY;
forward * dur * 0.75
},
0.0,
),
_ => (0.0, 0.0),
});
let body_rad = agent_data.body.map_or(0.0, |b| b.max_radius());
attack_data.dist_sqrd < (range + range_inc + body_rad).powi(2)
&& attack_data.angle < angle
&& attack_data.dist_sqrd > (range_inc * min_mult).powi(2)
};
let energy_check = |energy: f32| {
agent_data.energy.current() >= energy
&& (energy < f32::EPSILON
|| agent_data.energy.current() >= ability_preferences.desired_energy)
};
let combo_check = |combo, scales| {
let additional_combo = if scales {
ability_preferences.combo_scaling_buildup
} else {
0
};
agent_data
.combo
.map_or(false, |c| c.counter() >= combo + additional_combo)
};
let attack_kind_check = |attacks: AttackFilters| {
tgt_data
.char_state
.and_then(|cs| cs.attack_kind())
.map_or(false, |ak| attacks.applies(ak))
};
let ranged_check = |proj_speed| {
let max_horiz_dist: f32 = {
let flight_time = proj_speed * 2_f32.sqrt() / GRAVITY;
proj_speed * 2_f32.sqrt() / 2.0 * flight_time
};
attack_data.dist_sqrd < max_horiz_dist.powi(2)
&& entities_have_line_of_sight(
agent_data.pos,
agent_data.body,
agent_data.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
let beam_check = |range: f32, angle, ori_rate: f32| {
let angle_inc = ori_rate.to_degrees();
attack_data.dist_sqrd < range.powi(2)
&& attack_data.angle < angle + angle_inc
&& entities_have_line_of_sight(
agent_data.pos,
agent_data.body,
agent_data.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
)
};
use AbilityData::*;
match self {
ComboMelee {
range,
angle,
energy_per_strike,
forced_movement,
} => melee_check(*range, *angle, *forced_movement) && energy_check(*energy_per_strike),
FinisherMelee {
range,
angle,
energy,
combo,
combo_scales,
} => {
melee_check(*range, *angle, None)
&& energy_check(*energy)
&& combo_check(*combo, *combo_scales)
},
SelfBuff {
buff,
energy,
combo,
combo_scales,
} => {
energy_check(*energy)
&& combo_check(*combo, *combo_scales)
&& agent_data
.buffs
.map_or(false, |buffs| !buffs.contains(*buff))
},
DiveMelee {
range,
angle,
energy,
} => melee_check(*range, *angle, None) && energy_check(*energy),
DashMelee {
range,
angle,
initial_energy,
energy_drain,
speed,
charge_dur,
} => {
const BASE_SPEED: f32 = 3.0;
const ORI_RATE: f32 = 30.0;
let charge_dur = ((agent_data.energy.current() - initial_energy) / energy_drain)
.clamp(0.0, *charge_dur);
let charge_dist = charge_dur * speed * BASE_SPEED;
let attack_dist = charge_dist + range;
let ori_gap = ORI_RATE * charge_dur;
melee_check(attack_dist, angle + ori_gap, None)
&& energy_check(*initial_energy)
&& attack_data.dist_sqrd / charge_dist.powi(2) > 0.75_f32.powi(2)
},
RapidMelee {
range,
angle,
energy_per_strike,
strikes,
combo,
} => {
melee_check(*range, *angle, None)
&& energy_check(*energy_per_strike * *strikes as f32)
&& combo_check(*combo, false)
},
ChargedMelee {
range,
angle,
initial_energy,
energy_drain,
charge_dur,
} => {
melee_check(*range, *angle, None)
&& energy_check(*initial_energy + *energy_drain * *charge_dur)
},
RiposteMelee {
energy,
range,
angle,
} => {
melee_check(*range, *angle, None)
&& energy_check(*energy)
&& tgt_data.char_state.map_or(false, |cs| {
cs.is_melee_attack()
&& matches!(
cs.stage_section(),
Some(
StageSection::Buildup
| StageSection::Charge
| StageSection::Movement
)
)
})
},
BasicBlock {
energy,
angle,
blocked_attacks,
} => {
melee_check(25.0, *angle, None)
&& energy_check(*energy)
&& attack_kind_check(*blocked_attacks)
&& tgt_data
.char_state
.and_then(|cs| cs.stage_section())
.map_or(false, |ss| !matches!(ss, StageSection::Recover))
},
BasicRanged {
energy,
projectile_speed,
projectile_spread: _,
num_projectiles: _,
} => ranged_check(*projectile_speed) && energy_check(*energy),
BasicMelee {
energy,
range,
angle,
} => melee_check(*range, *angle, None) && energy_check(*energy),
LeapMelee {
energy,
range,
angle,
leap_dur,
forward_leap,
vertical_leap,
} => {
use common::states::utils::MovementDirection;
let forced_move = Some(ForcedMovement::Leap {
vertical: *vertical_leap * *leap_dur * 2.0,
forward: *forward_leap,
progress: 0.0,
direction: MovementDirection::Look,
});
melee_check(*range, *angle, forced_move) && energy_check(*energy)
},
BasicBeam {
energy_drain,
range,
angle,
ori_rate,
} => beam_check(*range, *angle, *ori_rate) && energy_check(*energy_drain * 3.0),
Shockwave {
energy,
range,
angle,
combo,
} => {
melee_check(*range, *angle, None)
&& energy_check(*energy)
&& combo_check(*combo, false)
},
StaticAura { energy } => energy_check(*energy),
RegrowHead { energy } => energy_check(*energy),
}
}
}