Defensive stance AI

This commit is contained in:
Sam 2022-10-08 14:46:32 -04:00
parent 7dcb3582e6
commit c97bfdfb94
10 changed files with 237 additions and 51 deletions

View File

@ -9,7 +9,7 @@ ComboMelee2(
energy_regen: 10,
),
range: 6.0,
angle: 5.0,
angle: 45.0,
),
buildup_duration: 0.2,
swing_duration: 0.1,

View File

@ -339,6 +339,23 @@ impl CharacterState {
|| matches!(self, CharacterState::Roll(s) if s.stage_section == StageSection::Movement)
}
pub fn is_melee_attack(&self) -> bool {
matches!(
self,
CharacterState::BasicMelee(_)
| CharacterState::DashMelee(_)
| CharacterState::ComboMelee(_)
| CharacterState::ComboMelee2(_)
| CharacterState::LeapMelee(_)
| CharacterState::SpinMelee(_)
| CharacterState::ChargedMelee(_)
| CharacterState::FinisherMelee(_)
| CharacterState::DiveMelee(_)
| CharacterState::RiposteMelee(_)
| CharacterState::RapidMelee(_)
)
}
pub fn can_perform_mounted(&self) -> bool {
matches!(
self,
@ -769,6 +786,50 @@ impl CharacterState {
}
}
pub fn timer(&self) -> Option<Duration> {
match &self {
CharacterState::Idle(_) => None,
CharacterState::Talk => None,
CharacterState::Climb(_) => None,
CharacterState::Wallrun(_) => None,
CharacterState::Skate(_) => None,
CharacterState::Glide(data) => Some(data.timer),
CharacterState::GlideWield(_) => None,
CharacterState::Stunned(data) => Some(data.timer),
CharacterState::Sit => None,
CharacterState::Dance => None,
CharacterState::BasicBlock(data) => Some(data.timer),
CharacterState::Roll(data) => Some(data.timer),
CharacterState::Wielding(_) => None,
CharacterState::Equipping(data) => Some(data.timer),
CharacterState::ComboMelee(data) => Some(data.timer),
CharacterState::ComboMelee2(data) => Some(data.timer),
CharacterState::BasicMelee(data) => Some(data.timer),
CharacterState::BasicRanged(data) => Some(data.timer),
CharacterState::Boost(data) => Some(data.timer),
CharacterState::DashMelee(data) => Some(data.timer),
CharacterState::LeapMelee(data) => Some(data.timer),
CharacterState::SpinMelee(data) => Some(data.timer),
CharacterState::ChargedMelee(data) => Some(data.timer),
CharacterState::ChargedRanged(data) => Some(data.timer),
CharacterState::RepeaterRanged(data) => Some(data.timer),
CharacterState::Shockwave(data) => Some(data.timer),
CharacterState::BasicBeam(data) => Some(data.timer),
CharacterState::BasicAura(data) => Some(data.timer),
CharacterState::Blink(data) => Some(data.timer),
CharacterState::BasicSummon(data) => Some(data.timer),
CharacterState::SelfBuff(data) => Some(data.timer),
CharacterState::SpriteSummon(data) => Some(data.timer),
CharacterState::UseItem(data) => Some(data.timer),
CharacterState::SpriteInteract(data) => Some(data.timer),
CharacterState::FinisherMelee(data) => Some(data.timer),
CharacterState::Music(data) => Some(data.timer),
CharacterState::DiveMelee(data) => Some(data.timer),
CharacterState::RiposteMelee(data) => Some(data.timer),
CharacterState::RapidMelee(data) => Some(data.timer),
}
}
// Determines if a character state should be returned to when using another
// ability from that character state
pub fn should_be_returned_to(&self) -> bool {

View File

@ -1,4 +1,4 @@
use super::utils::handle_climb;
use super::utils::*;
use crate::{
comp::{
character_state::OutputEvents, fluid_dynamics::angle_of_attack, inventory::slot::EquipSlot,
@ -13,7 +13,7 @@ use crate::{
util::{Dir, Plane, Projection},
};
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
use std::{f32::consts::PI, time::Duration};
use vek::*;
const PITCH_SLOW_TIME: f32 = 0.5;
@ -26,7 +26,7 @@ pub struct Data {
pub planform_area: f32,
pub ori: Ori,
last_vel: Vel,
timer: f32,
pub timer: Duration,
inputs_disabled: bool,
}
@ -45,7 +45,7 @@ impl Data {
planform_area,
ori,
last_vel: Vel::zero(),
timer: 0.0,
timer: Duration::default(),
inputs_disabled: true,
}
}
@ -66,7 +66,7 @@ impl Data {
.xy()
.try_normalized()
.map_or(0.0, |ld| {
PI * 0.1 * ld.dot(move_dir) * self.timer.min(PITCH_SLOW_TIME)
PI * 0.1 * ld.dot(move_dir) * self.timer.as_secs_f32().min(PITCH_SLOW_TIME)
/ PITCH_SLOW_TIME
}),
)
@ -133,7 +133,7 @@ impl CharacterBehavior for Data {
.unwrap_or_else(Dir::up);
let global_roll = tgt_dir_up.rotation_between(tgt_up);
let global_pitch = angle_of_attack(&tgt_dir_ori, &flow_dir)
* self.timer.min(PITCH_SLOW_TIME)
* self.timer.as_secs_f32().min(PITCH_SLOW_TIME)
/ PITCH_SLOW_TIME;
self.ori.slerped_towards(
@ -192,7 +192,7 @@ impl CharacterBehavior for Data {
update.character = CharacterState::Glide(Self {
ori,
last_vel: *data.vel,
timer: self.timer + data.dt.0,
timer: tick_attack_or_default(data, self.timer, None),
inputs_disabled,
..*self
});

View File

@ -5,7 +5,7 @@ use crate::{
};
use common::{
comp::{
ability::{self, AbilityKind, ActiveAbilities, AuxiliaryAbility},
ability::{self, AbilityKind, ActiveAbilities, AuxiliaryAbility, Capability},
buff::BuffKind,
skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent,
@ -459,6 +459,21 @@ impl<'a> AgentData<'a> {
combo_factor > tgt_health_factor.min(self_health_factor)
}
}
struct SelfBuffData {
buff: BuffKind,
energy: f32,
}
impl SelfBuffData {
fn could_use(&self, agent_data: &AgentData) -> bool {
agent_data.energy.current() >= self.energy
}
fn use_desirable(&self, agent_data: &AgentData) -> bool {
agent_data
.buffs
.map_or(false, |buffs| !buffs.contains(self.buff))
}
}
use ability::SwordStance;
let stance = |stance| match stance {
1 => SwordStance::Offensive,
@ -515,10 +530,10 @@ impl<'a> AgentData<'a> {
SwordStance::Defensive => {
// Defensive combo
set_sword_ability(0, 12);
// Defensive bulwark
set_sword_ability(1, 13);
// Defensive retreat
set_sword_ability(2, 14);
set_sword_ability(1, 14);
// Defensive bulwark
set_sword_ability(2, 13);
// Balanced finisher
set_sword_ability(3, 0);
},
@ -772,7 +787,106 @@ impl<'a> AgentData<'a> {
}
},
SwordStance::Defensive => {
fallback_tactics(agent, controller);
const DEFENSIVE_COMBO: ComboMeleeData = ComboMeleeData {
min_range: 0.0,
max_range: 2.5,
angle: 35.0,
energy: 5.0,
};
const DEFENSIVE_RETREAT: ComboMeleeData = ComboMeleeData {
min_range: 0.0,
max_range: 3.0,
angle: 35.0,
energy: 10.0,
};
const DEFENSIVE_BULWARK: SelfBuffData = SelfBuffData {
buff: BuffKind::ProtectingWard,
energy: 40.0,
};
const DESIRED_ENERGY: f32 = 50.0;
let mut try_block = || {
if let Some(char_state) = tgt_data.char_state {
matches!(char_state.stage_section(), Some(StageSection::Buildup))
&& char_state.is_melee_attack()
&& char_state
.timer()
.map_or(false, |timer| rng.gen::<f32>() < timer.as_secs_f32() * 4.0)
&& self
.char_state
.ability_info()
.and_then(|a| a.ability_meta)
.map(|m| m.capabilities)
.map_or(false, |c| c.contains(Capability::BLOCK_INTERRUPT))
} else {
false
}
};
if read_data.time.0
- self
.health
.map_or(read_data.time.0, |h| h.last_change.time.0)
< read_data.dt.0 as f64 * 2.0
{
// If attacked in last couple ticks, stop standing still
agent.action_state.condition = false;
} else if matches!(
self.char_state.ability_info().and_then(|info| info.input),
Some(InputKind::Ability(1))
) {
// If used defensive retreat, stand still for a little bit to bait people
// forward
agent.action_state.condition = true;
};
if self.energy.current() < DESIRED_ENERGY {
fallback_tactics(agent, controller);
} else if !in_stance(SwordStance::Defensive) {
controller.push_basic_input(InputKind::Ability(0));
} else if DEFENSIVE_BULWARK.could_use(self) && DEFENSIVE_BULWARK.use_desirable(self)
{
controller.push_basic_input(InputKind::Ability(2));
} else if BALANCED_FINISHER.could_use(attack_data, self)
&& BALANCED_FINISHER.use_desirable(tgt_data, self)
{
controller.push_basic_input(InputKind::Ability(3));
advance(
agent,
controller,
BALANCED_FINISHER.range,
BALANCED_FINISHER.angle,
);
} else if try_block() {
controller.push_basic_input(InputKind::Block);
} else if DEFENSIVE_RETREAT.could_use(attack_data, self) && rng.gen::<f32>() < 0.2 {
controller.push_basic_input(InputKind::Ability(1));
if !agent.action_state.condition {
advance(
agent,
controller,
DEFENSIVE_RETREAT.max_range,
DEFENSIVE_RETREAT.angle,
);
}
} else if DEFENSIVE_COMBO.could_use(attack_data, self) {
controller.push_basic_input(InputKind::Primary);
if !agent.action_state.condition {
advance(
agent,
controller,
DEFENSIVE_COMBO.max_range,
DEFENSIVE_COMBO.angle,
);
}
} else if !agent.action_state.condition {
advance(
agent,
controller,
DEFENSIVE_COMBO.max_range,
DEFENSIVE_COMBO.angle,
);
}
},
SwordStance::Mobility => {
fallback_tactics(agent, controller);

View File

@ -42,6 +42,7 @@ pub struct AgentData<'a> {
pub char_state: &'a CharacterState,
pub active_abilities: &'a ActiveAbilities,
pub combo: Option<&'a Combo>,
pub buffs: Option<&'a Buffs>,
pub cached_spatial_grid: &'a common::CachedSpatialGrid,
pub msm: &'a MaterialStatManifest,
}

View File

@ -198,6 +198,7 @@ impl<'a> System<'a> for Sys {
char_state,
active_abilities,
combo,
buffs: read_data.buffs.get(entity),
cached_spatial_grid: &read_data.cached_spatial_grid,
msm: &read_data.msm,
};

View File

@ -9,7 +9,7 @@ use crate::{
};
use i18n::Localization;
use common::comp::{BuffKind, Buffs, CharacterState, Energy, Health, Inventory};
use common::comp::{BuffKind, Buffs, CharacterState, Energy, Health};
use conrod_core::{
color,
image::Id,
@ -46,7 +46,6 @@ pub struct BuffsBar<'a> {
global_state: &'a GlobalState,
health: &'a Health,
energy: &'a Energy,
inventory: &'a Inventory,
}
impl<'a> BuffsBar<'a> {
@ -62,7 +61,6 @@ impl<'a> BuffsBar<'a> {
global_state: &'a GlobalState,
health: &'a Health,
energy: &'a Energy,
inventory: &'a Inventory,
) -> Self {
Self {
imgs,
@ -77,7 +75,6 @@ impl<'a> BuffsBar<'a> {
global_state,
health,
energy,
inventory,
}
}
}
@ -129,7 +126,7 @@ impl<'a> Widget for BuffsBar<'a> {
.desc_font_size(self.fonts.cyri.scale(12))
.font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR);
let buff_icons = BuffIcon::icons_vec(self.buffs, self.char_state, Some(self.inventory));
let buff_icons = BuffIcon::icons_vec(self.buffs, self.char_state);
if let BuffPosition::Bar = buff_position {
let decayed_health = 1.0 - self.health.maximum() / self.health.base_max();
let show_health = self.global_state.settings.interface.always_show_bars

View File

@ -505,7 +505,7 @@ impl<'a> Widget for Group<'a> {
.top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0)
.set(state.ids.member_energy[i], ui);
if let Some(buffs) = buffs {
let buff_icons = BuffIcon::icons_vec(buffs, char_state, Some(inventory));
let buff_icons = BuffIcon::icons_vec(buffs, char_state);
// Limit displayed buffs to 11
let buff_count = buff_icons.len().min(11);
total_buff_count += buff_count;

View File

@ -84,7 +84,7 @@ use common::{
combat,
comp::{
self,
ability::AuxiliaryAbility,
ability::{self, AuxiliaryAbility},
fluid_dynamics,
inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason},
item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality},
@ -553,36 +553,55 @@ impl<'a> BuffIcon<'a> {
}
}
pub fn icons_vec(
buffs: &comp::Buffs,
char_state: &comp::CharacterState,
inv: Option<&'a comp::Inventory>,
) -> Vec<Self> {
pub fn icons_vec(buffs: &comp::Buffs, char_state: &comp::CharacterState) -> Vec<Self> {
buffs
.iter_active()
.map(BuffIcon::from_buff)
.chain(BuffIcon::from_char_state(char_state, inv).into_iter())
.chain(BuffIcon::from_char_state(char_state).into_iter())
.collect::<Vec<_>>()
}
fn from_char_state(
char_state: &comp::CharacterState,
inv: Option<&'a comp::Inventory>,
) -> Option<Self> {
let ability_id = || {
char_state
.ability_info()
.and_then(|info| info.ability)
.and_then(|ability| ability.ability_id(inv))
};
use comp::CharacterState::*;
match char_state {
ComboMelee2(data) if data.static_data.is_stance => ability_id().map(|id| BuffIcon {
fn from_char_state(char_state: &comp::CharacterState) -> Option<Self> {
use ability::{AbilityKind, SwordStance};
if let Some(ability_kind) = char_state
.ability_info()
.and_then(|info| info.ability_meta)
.and_then(|meta| meta.kind)
{
let id = match ability_kind {
AbilityKind::Sword(SwordStance::Balanced) => {
"common.abilities.sword.balanced_combo"
},
AbilityKind::Sword(SwordStance::Offensive) => {
"common.abilities.sword.offensive_combo"
},
AbilityKind::Sword(SwordStance::Crippling) => {
"common.abilities.sword.crippling_combo"
},
AbilityKind::Sword(SwordStance::Cleaving) => {
"common.abilities.sword.cleaving_combo"
},
AbilityKind::Sword(SwordStance::Defensive) => {
"common.abilities.sword.defensive_combo"
},
AbilityKind::Sword(SwordStance::Parrying) => {
"common.abilities.sword.parrying_combo"
},
AbilityKind::Sword(SwordStance::Heavy) => "common.abilities.sword.heavy_combo",
AbilityKind::Sword(SwordStance::Mobility) => {
"common.abilities.sword.mobility_combo"
},
AbilityKind::Sword(SwordStance::Reaching) => {
"common.abilities.sword.reaching_combo"
},
};
Some(BuffIcon {
kind: BuffIconKind::Ability { ability_id: id },
is_buff: true,
dur: None,
}),
_ => None,
})
} else {
None
}
}
@ -2197,7 +2216,6 @@ impl Hud {
} else {
0.0
},
inventory,
char_state,
});
// Only render bubble if nearby or if its me and setting is on
@ -3027,12 +3045,11 @@ impl Hud {
}
// Buffs
if let (Some(player_buffs), Some(health), Some(energy), Some(char_state), Some(inventory)) = (
if let (Some(player_buffs), Some(health), Some(energy), Some(char_state)) = (
buffs.get(info.viewpoint_entity),
healths.get(entity),
energies.get(entity),
char_states.get(entity),
inventories.get(entity),
) {
for event in BuffsBar::new(
&self.imgs,
@ -3046,7 +3063,6 @@ impl Hud {
global_state,
health,
energy,
inventory,
)
.set(self.ids.buffs, ui_widgets)
{

View File

@ -9,9 +9,7 @@ use crate::{
settings::{ControlSettings, InterfaceSettings},
ui::{fonts::Fonts, Ingameable},
};
use common::comp::{
Buffs, CharacterState, Energy, Health, Inventory, SpeechBubble, SpeechBubbleType,
};
use common::comp::{Buffs, CharacterState, Energy, Health, SpeechBubble, SpeechBubbleType};
use conrod_core::{
color,
position::Align,
@ -71,7 +69,6 @@ pub struct Info<'a> {
pub buffs: &'a Buffs,
pub energy: Option<&'a Energy>,
pub combat_rating: f32,
pub inventory: &'a Inventory,
pub char_state: &'a CharacterState,
}
@ -202,7 +199,6 @@ impl<'a> Widget for Overhead<'a> {
buffs,
energy,
combat_rating,
inventory,
char_state,
}) = self.info
{
@ -233,7 +229,7 @@ impl<'a> Widget for Overhead<'a> {
};
// Buffs
// Alignment
let buff_icons = BuffIcon::icons_vec(buffs, char_state, Some(inventory));
let buff_icons = BuffIcon::icons_vec(buffs, char_state);
let buff_count = buff_icons.len().min(11);
Rectangle::fill_with([168.0, 100.0], color::TRANSPARENT)
.x_y(-1.0, name_y + 60.0)