mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
804 lines
25 KiB
Rust
804 lines
25 KiB
Rust
use crate::util::*;
|
|
use common::{
|
|
comp::{
|
|
ability::{CharacterAbility, MAX_ABILITIES},
|
|
buff::{BuffKind, Buffs},
|
|
character_state::AttackFilters,
|
|
group,
|
|
inventory::{
|
|
item::{tool::ToolKind, ItemKind, MaterialStatManifest},
|
|
slot::EquipSlot,
|
|
},
|
|
ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy, Health, Inventory,
|
|
LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Presence, PresenceKind, Scale,
|
|
SkillSet, Stance, Stats, Vel,
|
|
},
|
|
consts::GRAVITY,
|
|
link::Is,
|
|
mounting::{Mount, Rider, VolumeRider},
|
|
path::TraversalConfig,
|
|
resources::{DeltaTime, Time, TimeOfDay},
|
|
rtsim::{Actor, RtSimEntity},
|
|
states::utils::{ForcedMovement, StageSection},
|
|
terrain::TerrainGrid,
|
|
uid::{Uid, UidAllocator},
|
|
};
|
|
use specs::{
|
|
shred::ResourceId, Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage,
|
|
SystemData, World,
|
|
};
|
|
|
|
// TODO: Move rtsim back into AgentData after rtsim2 when it has a separate
|
|
// crate
|
|
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,
|
|
#[allow(dead_code)] // may be useful for pathing
|
|
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 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 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,
|
|
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::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 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)]
|
|
// When adding a new variant, first decide if it should instead fall under one
|
|
// of the pre-existing tactics
|
|
pub enum Tactic {
|
|
// General tactics
|
|
SimpleMelee,
|
|
SimpleFlyingMelee,
|
|
SimpleBackstab,
|
|
ElevatedRanged,
|
|
Turret,
|
|
FixedTurret,
|
|
RotatingTurret,
|
|
RadialTurret,
|
|
// u8s are weights that each ability gets used, if it can be used
|
|
RandomAbilities {
|
|
primary: u8,
|
|
secondary: u8,
|
|
abilities: [u8; MAX_ABILITIES],
|
|
},
|
|
|
|
// Tool specific tactics
|
|
Axe,
|
|
Hammer,
|
|
Sword,
|
|
Bow,
|
|
Staff,
|
|
Sceptre,
|
|
// TODO: Remove tactic and ability spec
|
|
SwordSimple,
|
|
|
|
// Broad creature tactics
|
|
CircleCharge {
|
|
radius: u32,
|
|
circle_time: u32,
|
|
},
|
|
QuadLowRanged,
|
|
TailSlap,
|
|
QuadLowQuick,
|
|
QuadLowBasic,
|
|
QuadLowBeam,
|
|
QuadMedJump,
|
|
QuadMedBasic,
|
|
QuadMedHoof,
|
|
Theropod,
|
|
BirdLargeBreathe,
|
|
BirdLargeFire,
|
|
BirdLargeBasic,
|
|
Wyvern,
|
|
BirdMediumBasic,
|
|
ArthropodMelee,
|
|
ArthropodRanged,
|
|
ArthropodAmbush,
|
|
|
|
// Specific species tactics
|
|
Mindflayer,
|
|
Minotaur,
|
|
ClayGolem,
|
|
TidalWarrior,
|
|
Yeti,
|
|
Harvester,
|
|
StoneGolem,
|
|
Deadwood,
|
|
Mandragora,
|
|
WoodGolem,
|
|
GnarlingChieftain,
|
|
OrganAura,
|
|
Dagon,
|
|
Cardinal,
|
|
SeaBishop,
|
|
Roshwalr,
|
|
FrostGigas,
|
|
BorealHammer,
|
|
Dullahan,
|
|
Cyclops,
|
|
IceDrake,
|
|
|
|
// Adlets
|
|
AdletHunter,
|
|
AdletIcepicker,
|
|
AdletTracker,
|
|
AdletElder,
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
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(SystemData)]
|
|
pub struct ReadData<'a> {
|
|
pub entities: Entities<'a>,
|
|
pub uid_allocator: Read<'a, UidAllocator>,
|
|
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 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, 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>,
|
|
}
|
|
|
|
impl<'a> ReadData<'a> {
|
|
pub fn lookup_actor(&self, actor: Actor) -> Option<EcsEntity> {
|
|
// TODO: We really shouldn't be doing a linear search here. The only saving
|
|
// grace is that the set of entities that fit each case should be
|
|
// *relatively* small.
|
|
match actor {
|
|
Actor::Character(character_id) => (&self.entities, &self.presences)
|
|
.join()
|
|
.find(|(_, p)| p.kind == PresenceKind::Character(character_id))
|
|
.map(|(entity, _)| entity),
|
|
Actor::Npc(npc_id) => (&self.entities, &self.rtsim_entities)
|
|
.join()
|
|
.find(|(_, e)| e.0 == npc_id)
|
|
.map(|(entity, _)| entity),
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
},
|
|
SelfBuff {
|
|
buff: BuffKind,
|
|
energy: f32,
|
|
},
|
|
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,
|
|
},
|
|
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,
|
|
},
|
|
}
|
|
|
|
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,
|
|
..
|
|
} => Self::FinisherMelee {
|
|
energy: *energy_cost,
|
|
range: melee_constructor.range,
|
|
angle: melee_constructor.angle,
|
|
combo: *minimum_combo,
|
|
},
|
|
SelfBuff {
|
|
buff_kind,
|
|
energy_cost,
|
|
..
|
|
} => Self::SelfBuff {
|
|
buff: *buff_kind,
|
|
energy: *energy_cost,
|
|
},
|
|
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,
|
|
..
|
|
} => Self::BasicRanged {
|
|
energy: *energy_cost,
|
|
projectile_speed: *projectile_speed,
|
|
},
|
|
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,
|
|
},
|
|
_ => return None,
|
|
};
|
|
Some(inner)
|
|
}
|
|
|
|
pub fn could_use(
|
|
&self,
|
|
attack_data: &AttackData,
|
|
agent_data: &AgentData,
|
|
tgt_data: &TargetData,
|
|
read_data: &ReadData,
|
|
desired_energy: f32,
|
|
) -> 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;
|
|
// 0.75 factor to allow for fact that agent looks down as they approach, so
|
|
// won't go as far
|
|
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() >= desired_energy)
|
|
};
|
|
let combo_check = |combo| agent_data.combo.map_or(false, |c| c.counter() >= 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,
|
|
} => melee_check(*range, *angle, None) && energy_check(*energy) && combo_check(*combo),
|
|
SelfBuff { buff, energy } => {
|
|
energy_check(*energy)
|
|
&& 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,
|
|
} => {
|
|
// TODO: Maybe figure out better way of pulling in base accel from body and
|
|
// accounting for friction?
|
|
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;
|
|
// TODO: Replace None with actual forced movement later
|
|
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)
|
|
},
|
|
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,
|
|
} => 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),
|
|
}
|
|
}
|
|
}
|