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, energy_regen: 10,
), ),
range: 6.0, range: 6.0,
angle: 5.0, angle: 45.0,
), ),
buildup_duration: 0.2, buildup_duration: 0.2,
swing_duration: 0.1, swing_duration: 0.1,

View File

@ -339,6 +339,23 @@ impl CharacterState {
|| matches!(self, CharacterState::Roll(s) if s.stage_section == StageSection::Movement) || 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 { pub fn can_perform_mounted(&self) -> bool {
matches!( matches!(
self, 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 // Determines if a character state should be returned to when using another
// ability from that character state // ability from that character state
pub fn should_be_returned_to(&self) -> bool { pub fn should_be_returned_to(&self) -> bool {

View File

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

View File

@ -5,7 +5,7 @@ use crate::{
}; };
use common::{ use common::{
comp::{ comp::{
ability::{self, AbilityKind, ActiveAbilities, AuxiliaryAbility}, ability::{self, AbilityKind, ActiveAbilities, AuxiliaryAbility, Capability},
buff::BuffKind, buff::BuffKind,
skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent,
@ -459,6 +459,21 @@ impl<'a> AgentData<'a> {
combo_factor > tgt_health_factor.min(self_health_factor) 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; use ability::SwordStance;
let stance = |stance| match stance { let stance = |stance| match stance {
1 => SwordStance::Offensive, 1 => SwordStance::Offensive,
@ -515,10 +530,10 @@ impl<'a> AgentData<'a> {
SwordStance::Defensive => { SwordStance::Defensive => {
// Defensive combo // Defensive combo
set_sword_ability(0, 12); set_sword_ability(0, 12);
// Defensive bulwark
set_sword_ability(1, 13);
// Defensive retreat // Defensive retreat
set_sword_ability(2, 14); set_sword_ability(1, 14);
// Defensive bulwark
set_sword_ability(2, 13);
// Balanced finisher // Balanced finisher
set_sword_ability(3, 0); set_sword_ability(3, 0);
}, },
@ -772,7 +787,106 @@ impl<'a> AgentData<'a> {
} }
}, },
SwordStance::Defensive => { 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 => { SwordStance::Mobility => {
fallback_tactics(agent, controller); fallback_tactics(agent, controller);

View File

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

View File

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

View File

@ -9,7 +9,7 @@ use crate::{
}; };
use i18n::Localization; use i18n::Localization;
use common::comp::{BuffKind, Buffs, CharacterState, Energy, Health, Inventory}; use common::comp::{BuffKind, Buffs, CharacterState, Energy, Health};
use conrod_core::{ use conrod_core::{
color, color,
image::Id, image::Id,
@ -46,7 +46,6 @@ pub struct BuffsBar<'a> {
global_state: &'a GlobalState, global_state: &'a GlobalState,
health: &'a Health, health: &'a Health,
energy: &'a Energy, energy: &'a Energy,
inventory: &'a Inventory,
} }
impl<'a> BuffsBar<'a> { impl<'a> BuffsBar<'a> {
@ -62,7 +61,6 @@ impl<'a> BuffsBar<'a> {
global_state: &'a GlobalState, global_state: &'a GlobalState,
health: &'a Health, health: &'a Health,
energy: &'a Energy, energy: &'a Energy,
inventory: &'a Inventory,
) -> Self { ) -> Self {
Self { Self {
imgs, imgs,
@ -77,7 +75,6 @@ impl<'a> BuffsBar<'a> {
global_state, global_state,
health, health,
energy, energy,
inventory,
} }
} }
} }
@ -129,7 +126,7 @@ impl<'a> Widget for BuffsBar<'a> {
.desc_font_size(self.fonts.cyri.scale(12)) .desc_font_size(self.fonts.cyri.scale(12))
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.desc_text_color(TEXT_COLOR); .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 { if let BuffPosition::Bar = buff_position {
let decayed_health = 1.0 - self.health.maximum() / self.health.base_max(); let decayed_health = 1.0 - self.health.maximum() / self.health.base_max();
let show_health = self.global_state.settings.interface.always_show_bars 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) .top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0)
.set(state.ids.member_energy[i], ui); .set(state.ids.member_energy[i], ui);
if let Some(buffs) = buffs { 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 // Limit displayed buffs to 11
let buff_count = buff_icons.len().min(11); let buff_count = buff_icons.len().min(11);
total_buff_count += buff_count; total_buff_count += buff_count;

View File

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

View File

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