mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Defensive stance AI
This commit is contained in:
parent
7dcb3582e6
commit
c97bfdfb94
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user