Non-attack potion of sword AI

This commit is contained in:
Sam 2023-01-21 14:02:27 -05:00
parent ca9fdfa291
commit 387ea16598
3 changed files with 457 additions and 154 deletions

View File

@ -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<Vec3<f32>>; ACTIONSTATE_NUMBER_OF_CONCURRENT_POSITIONS],
pub initialized: bool,
}

View File

@ -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],
) {

View File

@ -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<Self> {
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)
},
}
}
}