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, Option), } 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>, pub is_riders: ReadStorage<'a, Is>, pub is_volume_riders: ReadStorage<'a, Is>, pub time_of_day: Read<'a, TimeOfDay>, pub light_emitter: ReadStorage<'a, LightEmitter>, #[cfg(feature = "worldgen")] pub world: ReadExpect<'a, Arc>, 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 { // 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, }, 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 { 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), (r2, a2, m2): (f32, f32, Option)| { (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| { 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), } } }