From 387ea165988de8a72d0371b6f066ec4dfca532db Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 21 Jan 2023 14:02:27 -0500 Subject: [PATCH] Non-attack potion of sword AI --- common/src/comp/agent.rs | 3 + server/agent/src/attack.rs | 283 ++++++++++++++++++++++++++++++-- server/agent/src/data.rs | 325 +++++++++++++++++++++---------------- 3 files changed, 457 insertions(+), 154 deletions(-) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 491b273f5a..ab02458c9c 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -40,6 +40,8 @@ const ACTIONSTATE_NUMBER_OF_CONCURRENT_INT_COUNTERS: usize = 5; /// The number of booleans that a single Action node can track concurrently /// Define constants within a given action node to index between them. const ACTIONSTATE_NUMBER_OF_CONCURRENT_CONDITIONS: usize = 5; +/// The number of positions that can be remembered by an agent +const ACTIONSTATE_NUMBER_OF_CONCURRENT_POSITIONS: usize = 5; #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum Alignment { @@ -630,6 +632,7 @@ pub struct ActionState { pub counters: [f32; ACTIONSTATE_NUMBER_OF_CONCURRENT_COUNTERS], pub conditions: [bool; ACTIONSTATE_NUMBER_OF_CONCURRENT_CONDITIONS], pub int_counters: [u8; ACTIONSTATE_NUMBER_OF_CONCURRENT_INT_COUNTERS], + pub positions: [Option>; ACTIONSTATE_NUMBER_OF_CONCURRENT_POSITIONS], pub initialized: bool, } diff --git a/server/agent/src/attack.rs b/server/agent/src/attack.rs index c3aa4cc151..e8e4d01d06 100644 --- a/server/agent/src/attack.rs +++ b/server/agent/src/attack.rs @@ -1,12 +1,12 @@ use crate::{consts::MAX_PATH_DIST, data::*, util::entities_have_line_of_sight}; use common::{ comp::{ - ability::{self, Ability, ActiveAbilities, AuxiliaryAbility, Capability}, + ability::{ActiveAbilities, AuxiliaryAbility}, buff::BuffKind, item::tool::AbilityContext, skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent, - Controller, InputKind, Stance, + Controller, InputKind, }, path::TraversalConfig, states::{self_buff, sprite_summon, utils::StageSection}, @@ -15,7 +15,6 @@ use common::{ vol::ReadVol, }; use rand::{prelude::SliceRandom, Rng}; -use specs::saveload::MarkerAllocator; use std::{f32::consts::PI, time::Duration}; use vek::*; @@ -510,11 +509,6 @@ impl<'a> AgentData<'a> { } } - enum IntCounters { - Tactics = 0, - ActionMode = 1, - } - if !agent.action_state.initialized { let available_tactics = { let mut tactics = Vec::new(); @@ -737,7 +731,7 @@ impl<'a> AgentData<'a> { } agent.action_state.int_counters[IntCounters::ActionMode as usize] = - ActionMode::Guarded as u8; + ActionMode::Reckless as u8; } enum ActionMode { @@ -757,15 +751,282 @@ impl<'a> AgentData<'a> { } } + if let Some(health) = self.health { + agent.action_state.int_counters[IntCounters::ActionMode as usize] = + if health.fraction() < 0.25 { + agent.action_state.positions[Positions::GuardedCover as usize] = None; + ActionMode::Fleeing as u8 + } else if health.fraction() < 0.75 { + agent.action_state.positions[Positions::Flee as usize] = None; + ActionMode::Guarded as u8 + } else { + agent.action_state.positions[Positions::GuardedCover as usize] = None; + agent.action_state.positions[Positions::Flee as usize] = None; + ActionMode::Reckless as u8 + }; + } + + enum IntCounters { + Tactics = 0, + ActionMode = 1, + } + + enum Timers { + GuardedCycle = 0, + PosTimeOut = 1, + } + + enum Conditions { + GuardedAttack = 0, + RollingBreakThrough = 1, + } + + enum FloatCounters { + GuardedTimer = 0, + } + + enum Positions { + GuardedCover = 0, + Flee = 1, + } + + if self.vel.0.magnitude_squared() < 1_f32.powi(2) { + agent.action_state.timers[Timers::PosTimeOut as usize] += read_data.dt.0; + } else { + agent.action_state.timers[Timers::PosTimeOut as usize] = 0.0; + } + + if agent.action_state.timers[Timers::PosTimeOut as usize] > 2.0 { + agent.action_state.positions.iter_mut().for_each(|pos| { + *pos = None; + }); + agent.action_state.timers[Timers::PosTimeOut as usize] = 0.0; + } + let attempt_attack = match ActionMode::from_u8( agent.action_state.int_counters[IntCounters::ActionMode as usize], ) { ActionMode::Reckless => true, - ActionMode::Guarded => true, - ActionMode::Fleeing => false, + ActionMode::Guarded => { + agent.action_state.timers[Timers::GuardedCycle as usize] += read_data.dt.0; + if agent.action_state.timers[Timers::GuardedCycle as usize] + > agent.action_state.counters[FloatCounters::GuardedTimer as usize] + { + agent.action_state.timers[Timers::GuardedCycle as usize] = 0.0; + agent.action_state.counters[FloatCounters::GuardedTimer as usize] = + rng.gen_range(3.0..8.0); + agent.action_state.conditions[Conditions::GuardedAttack as usize] ^= true; + } + if let Some(pos) = agent.action_state.positions[Positions::GuardedCover as usize] { + if pos.distance_squared(self.pos.0) < 3_f32.powi(2) { + agent.action_state.positions[Positions::GuardedCover as usize] = None; + } + } + if agent.action_state.conditions[Conditions::GuardedAttack as usize] { + agent.action_state.positions[Positions::GuardedCover as usize] = None; + true + } else { + if attack_data.dist_sqrd > 10_f32.powi(2) { + // Choose random point to either side when looking at target and move + // towards it + if let Some(pos) = + agent.action_state.positions[Positions::GuardedCover as usize] + { + if pos.distance_squared(self.pos.0) < 5_f32.powi(2) { + agent.action_state.positions[Positions::GuardedCover as usize] = + None; + } + self.path_toward_target( + agent, + controller, + pos, + read_data, + Path::Separate, + None, + ); + } else { + agent.action_state.positions[Positions::GuardedCover as usize] = { + let rand_dir = { + let dir = (tgt_data.pos.0 - self.pos.0) + .try_normalized() + .unwrap_or(Vec3::unit_x()) + .xy(); + if rng.gen_bool(0.5) { + dir.rotated_z(PI / 2.0 + rng.gen_range(-0.75..0.0)) + } else { + dir.rotated_z(-PI / 2.0 + rng.gen_range(-0.0..0.75)) + } + }; + let attempted_dist = rng.gen_range(6.0..16.0); + let actual_dist = read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z() * 0.5, + self.pos.0 + + Vec3::unit_z() * 0.5 + + rand_dir * attempted_dist, + ) + .until(Block::is_solid) + .cast() + .0 + - 1.0; + Some(self.pos.0 + rand_dir * actual_dist) + }; + } + } else { + if let Some(pos) = + agent.action_state.positions[Positions::GuardedCover as usize] + { + self.path_toward_target( + agent, + controller, + pos, + read_data, + Path::Separate, + None, + ); + if agent.action_state.conditions + [Conditions::RollingBreakThrough as usize] + { + controller.push_basic_input(InputKind::Roll); + agent.action_state.conditions + [Conditions::RollingBreakThrough as usize] = false; + } + if tgt_data.char_state.map_or(false, |cs| cs.is_melee_attack()) { + controller.push_basic_input(InputKind::Block); + } + } else { + agent.action_state.positions[Positions::GuardedCover as usize] = { + let backwards = (self.pos.0 - tgt_data.pos.0) + .try_normalized() + .unwrap_or(Vec3::unit_x()) + .xy(); + let pos = if read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z() * 0.5, + self.pos.0 + Vec3::unit_z() * 0.5 + backwards * 6.0, + ) + .until(Block::is_solid) + .cast() + .0 + > 5.0 + { + self.pos.0 + backwards * 5.0 + } else { + agent.action_state.conditions + [Conditions::RollingBreakThrough as usize] = true; + self.pos.0 + - backwards + * read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z() * 0.5, + self.pos.0 + Vec3::unit_z() * 0.5 + - backwards * 10.0, + ) + .until(Block::is_solid) + .cast() + .0 + - 1.0 + }; + Some(pos) + } + } + } + false + } + }, + ActionMode::Fleeing => { + if agent.action_state.conditions[Conditions::RollingBreakThrough as usize] { + controller.push_basic_input(InputKind::Roll); + agent.action_state.conditions[Conditions::RollingBreakThrough as usize] = false; + } + if let Some(pos) = agent.action_state.positions[Positions::Flee as usize] { + if let Some(dir) = Dir::from_unnormalized(pos - self.pos.0) { + controller.inputs.look_dir = dir; + } + if pos.distance_squared(self.pos.0) < 5_f32.powi(2) { + agent.action_state.positions[Positions::Flee as usize] = None; + } + self.path_toward_target( + agent, + controller, + pos, + read_data, + Path::Separate, + None, + ); + } else { + agent.action_state.positions[Positions::Flee as usize] = { + let rand_dir = { + let dir = (self.pos.0 - tgt_data.pos.0) + .try_normalized() + .unwrap_or(Vec3::unit_x()) + .xy(); + dir.rotated_z(rng.gen_range(-0.75..0.75)) + }; + let attempted_dist = rng.gen_range(16.0..26.0); + let actual_dist = read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z() * 0.5, + self.pos.0 + Vec3::unit_z() * 0.5 + rand_dir * attempted_dist, + ) + .until(Block::is_solid) + .cast() + .0 + - 1.0; + if actual_dist < 10.0 { + let dist = read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z() * 0.5, + self.pos.0 + Vec3::unit_z() * 0.5 - rand_dir * attempted_dist, + ) + .until(Block::is_solid) + .cast() + .0 + - 1.0; + agent.action_state.conditions + [Conditions::RollingBreakThrough as usize] = true; + Some(self.pos.0 - rand_dir * actual_dist) + } else { + Some(self.pos.0 + rand_dir * actual_dist) + } + }; + } + false + }, }; let attack_failed = if attempt_attack { + let context = AbilityContext::from(self.stance); + let extract_ability = |input: AbilityInput| { + AbilityData::from_ability( + &self + .active_abilities + .activate_ability( + input, + Some(self.inventory), + self.skill_set, + self.body, + Some(self.char_state), + context, + ) + .unwrap_or_default() + .0, + ) + }; + let primary = extract_ability(AbilityInput::Primary); + let secondary = extract_ability(AbilityInput::Secondary); + let abilities = [ + extract_ability(AbilityInput::Auxiliary(0)), + extract_ability(AbilityInput::Auxiliary(1)), + extract_ability(AbilityInput::Auxiliary(2)), + extract_ability(AbilityInput::Auxiliary(3)), + extract_ability(AbilityInput::Auxiliary(4)), + ]; match SwordTactics::from_u8( agent.action_state.int_counters[IntCounters::Tactics as usize], ) { diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 7cd74e9a0c..e684d49556 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -1,5 +1,6 @@ use common::{ comp::{ + ability::CharacterAbility, buff::{BuffKind, Buffs}, group, item::MaterialStatManifest, @@ -193,153 +194,191 @@ pub enum Path { Partial, } -pub struct ComboMeleeData { - pub range: f32, - pub angle: f32, - pub energy: f32, +pub enum AbilityData { + ComboMelee { + range: f32, + angle: f32, + energy_per_strike: f32, + }, + 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, + }, } -impl ComboMeleeData { - pub fn could_use(&self, attack_data: &AttackData, agent_data: &AgentData) -> bool { - attack_data.dist_sqrd - < (self.range + agent_data.body.map_or(0.0, |b| b.max_radius())).powi(2) - && attack_data.angle < self.angle - && agent_data.energy.current() >= self.energy +impl AbilityData { + pub fn from_ability(ability: &CharacterAbility) -> Option { + use CharacterAbility::*; + let inner = match ability { + ComboMelee2 { + strikes, + energy_cost_per_strike, + .. + } => { + let (range, angle) = strikes + .iter() + .map(|s| (s.melee_constructor.range, s.melee_constructor.angle)) + .fold( + (100.0, 360.0), + |(r1, a1): (f32, f32), (r2, a2): (f32, f32)| (r1.min(r2), a1.min(a2)), + ); + Self::ComboMelee { + range, + angle, + energy_per_strike: *energy_cost_per_strike, + } + }, + 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, + }, + _ => return None, + }; + Some(inner) } -} - -pub struct FinisherMeleeData { - pub range: f32, - pub angle: f32, - pub energy: f32, - pub combo: u32, -} - -impl FinisherMeleeData { - pub fn could_use(&self, attack_data: &AttackData, agent_data: &AgentData) -> bool { - attack_data.dist_sqrd - < (self.range + agent_data.body.map_or(0.0, |b| b.max_radius())).powi(2) - && attack_data.angle < self.angle - && agent_data.energy.current() >= self.energy - && agent_data - .combo - .map_or(false, |c| c.counter() >= self.combo) - } - - pub fn use_desirable(&self, tgt_data: &TargetData, agent_data: &AgentData) -> bool { - let combo_factor = - agent_data.combo.map_or(0, |c| c.counter()) as f32 / self.combo as f32 * 2.0; - let tgt_health_factor = tgt_data.health.map_or(0.0, |h| h.current()) / 50.0; - let self_health_factor = agent_data.health.map_or(0.0, |h| h.current()) / 50.0; - // Use becomes more desirable if either self or target is close to death - combo_factor > tgt_health_factor.min(self_health_factor) - } -} - -pub struct SelfBuffData { - pub buff: BuffKind, - pub energy: f32, -} - -impl SelfBuffData { - pub fn could_use(&self, agent_data: &AgentData) -> bool { - agent_data.energy.current() >= self.energy - } - - pub fn use_desirable(&self, agent_data: &AgentData) -> bool { - agent_data - .buffs - .map_or(false, |buffs| !buffs.contains(self.buff)) - } -} - -pub struct DiveMeleeData { - pub range: f32, - pub angle: f32, - pub energy: f32, -} - -impl DiveMeleeData { - // Hack here refers to agents using the mildly unintended method of roll jumping - // to achieve the required downwards vertical speed to enter dive melee when on - // flat ground. - pub fn npc_should_use_hack(&self, agent_data: &AgentData, tgt_data: &TargetData) -> bool { - let dist_sqrd_2d = agent_data.pos.0.xy().distance_squared(tgt_data.pos.0.xy()); - agent_data.energy.current() > self.energy - && agent_data.physics_state.on_ground.is_some() - && agent_data.pos.0.z >= tgt_data.pos.0.z - && dist_sqrd_2d - > ((self.range + agent_data.body.map_or(0.0, |b| b.max_radius())) / 2.0).powi(2) - && dist_sqrd_2d - < (self.range + agent_data.body.map_or(0.0, |b| b.max_radius()) + 5.0).powi(2) - } -} - -pub struct BlockData { - pub angle: f32, - // Should probably just always use 5 or so unless riposte melee - pub range: f32, - pub energy: f32, -} - -impl BlockData { - pub fn could_use(&self, attack_data: &AttackData, agent_data: &AgentData) -> bool { - attack_data.dist_sqrd - < (self.range + agent_data.body.map_or(0.0, |b| b.max_radius())).powi(2) - && attack_data.angle < self.angle - && agent_data.energy.current() >= self.energy - } -} - -pub struct DashMeleeData { - pub range: f32, - pub angle: f32, - pub initial_energy: f32, - pub energy_drain: f32, - pub speed: f32, - pub charge_dur: f32, -} - -impl DashMeleeData { - // 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; pub fn could_use(&self, attack_data: &AttackData, agent_data: &AgentData) -> bool { - let charge_dur = self.charge_dur(agent_data); - let charge_dist = charge_dur * self.speed * Self::BASE_SPEED; - let attack_dist = - charge_dist + self.range + agent_data.body.map_or(0.0, |b| b.max_radius()); - let ori_gap = Self::ORI_RATE * charge_dur; - attack_data.dist_sqrd < attack_dist.powi(2) - && attack_data.angle < self.angle + ori_gap - && agent_data.energy.current() > self.initial_energy - } - - pub fn use_desirable(&self, attack_data: &AttackData, agent_data: &AgentData) -> bool { - let charge_dist = self.charge_dur(agent_data) * self.speed * Self::BASE_SPEED; - attack_data.dist_sqrd / charge_dist.powi(2) > 0.75_f32.powi(2) - } - - fn charge_dur(&self, agent_data: &AgentData) -> f32 { - ((agent_data.energy.current() - self.initial_energy) / self.energy_drain) - .clamp(0.0, self.charge_dur) - } -} - -pub struct RapidMeleeData { - pub range: f32, - pub angle: f32, - pub energy: f32, - pub strikes: u32, -} - -impl RapidMeleeData { - pub fn could_use(&self, attack_data: &AttackData, agent_data: &AgentData) -> bool { - attack_data.dist_sqrd - < (self.range + agent_data.body.map_or(0.0, |b| b.max_radius())).powi(2) - && attack_data.angle < self.angle - && agent_data.energy.current() > self.energy * self.strikes as f32 + let melee_check = |range: f32, angle| { + attack_data.dist_sqrd + < (range + agent_data.body.map_or(0.0, |b| b.max_radius())).powi(2) + && attack_data.angle < angle + }; + let energy_check = |energy| agent_data.energy.current() >= energy; + let combo_check = |combo| agent_data.combo.map_or(false, |c| c.counter() >= combo); + use AbilityData::*; + match self { + ComboMelee { + range, + angle, + energy_per_strike, + } => melee_check(*range, *angle) && energy_check(*energy_per_strike), + FinisherMelee { + range, + angle, + energy, + combo, + } => melee_check(*range, *angle) && 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) && 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; + melee_check(attack_dist, angle + ori_gap) + && 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) + && energy_check(*energy_per_strike * *strikes as f32) + && combo_check(*combo) + }, + } } }