Hammer AI

This commit is contained in:
Sam 2024-04-13 17:18:10 -04:00
parent 0dc261c70a
commit aeb887963e
6 changed files with 479 additions and 17 deletions

2
Cargo.lock generated
View File

@ -7151,8 +7151,10 @@ dependencies = [
"lazy_static",
"rand 0.8.5",
"specs",
"tracing",
"vek 0.16.1",
"veloren-common",
"veloren-common-base",
"veloren-common-dynlib",
"veloren-rtsim",
]

View File

@ -8,13 +8,13 @@
loadout: Inline((
inherit: Asset("common.loadout.dungeon.cultist.cultist"),
active_hands: InHands((Choice([
(2, ModularWeapon(tool: Axe, material: Orichalcum, hands: One)),
(4, Item("common.items.weapons.sword.cultist")),
(2, Item("common.items.weapons.staff.cultist_staff")),
// (2, ModularWeapon(tool: Axe, material: Orichalcum, hands: One)),
// (4, Item("common.items.weapons.sword.cultist")),
// (2, Item("common.items.weapons.staff.cultist_staff")),
(2, Item("common.items.weapons.hammer.cultist_purp_2h-0")),
(2, ModularWeapon(tool: Hammer, material: Orichalcum, hands: One)),
(2, Item("common.items.weapons.bow.velorite")),
(1, Item("common.items.weapons.sceptre.sceptre_velorite_0")),
// (2, ModularWeapon(tool: Hammer, material: Orichalcum, hands: One)),
// (2, Item("common.items.weapons.bow.velorite")),
// (1, Item("common.items.weapons.sceptre.sceptre_velorite_0")),
]), None)),
)),
items: [

View File

@ -62,6 +62,7 @@ pub const MAX_MELEE_POISE_PRECISION: f32 = 0.5;
pub const MAX_BLOCK_POISE_COST: f32 = 25.0;
pub const PARRY_BONUS_MULTIPLIER: f32 = 2.0;
pub const FALLBACK_BLOCK_STRENGTH: f32 = 5.0;
pub const BEHIND_TARGET_ANGLE: f32 = 45.0;
#[derive(Copy, Clone)]
pub struct AttackerInfo<'a> {
@ -658,9 +659,8 @@ impl Attack {
target.char_state.map_or(false, |cs| cs.is_stunned())
},
CombatRequirement::BehindTarget => {
const REQUIRED_ANGLE: f32 = 45.0;
if let Some(ori) = target.ori {
ori.look_vec().angle_between(dir.with_z(0.0)) < REQUIRED_ANGLE
ori.look_vec().angle_between(dir.with_z(0.0)) < BEHIND_TARGET_ANGLE
} else {
false
}

View File

@ -10,6 +10,7 @@ be-dyn-lib = []
[dependencies]
common = { package = "veloren-common", path = "../../common"}
common-base = { package = "veloren-common-base", path = "../../common/base" }
common-dynlib = { package = "veloren-common-dynlib", path = "../../common/dynlib", optional = true}
rtsim = { package = "veloren-rtsim", path = "../../rtsim" }
@ -18,3 +19,4 @@ vek = { workspace = true }
rand = { workspace = true, features = ["small_rng"] }
itertools = { workspace = true }
lazy_static = { workspace = true }
tracing = { workspace = true }

View File

@ -4,11 +4,12 @@ use crate::{
util::{entities_have_line_of_sight, handle_attack_aggression},
};
use common::{
combat::{self, AttackSource},
comp::{
ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, BASE_ABILITY_LIMIT},
buff::BuffKind,
item::tool::AbilityContext,
skills::{AxeSkill, BowSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
Ability, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction,
ControlEvent, Controller, Fluid, InputKind,
},
@ -271,14 +272,391 @@ impl<'a> AgentData<'a> {
pub fn handle_hammer_attack(
&self,
_agent: &mut Agent,
_controller: &mut Controller,
_attack_data: &AttackData,
_tgt_data: &TargetData,
_read_data: &ReadData,
_rng: &mut impl Rng,
agent: &mut Agent,
controller: &mut Controller,
attack_data: &AttackData,
tgt_data: &TargetData,
read_data: &ReadData,
rng: &mut impl Rng,
) {
// TODO
if !agent.combat_state.initialized {
agent.combat_state.initialized = true;
let available_tactics = {
let mut tactics = Vec::new();
let try_tactic = |skill, tactic, tactics: &mut Vec<HammerTactics>| {
if self.skill_set.has_skill(Skill::Hammer(skill)) {
tactics.push(tactic);
}
};
try_tactic(
HammerSkill::Thunderclap,
HammerTactics::AttackExpert,
&mut tactics,
);
try_tactic(
HammerSkill::Judgement,
HammerTactics::SupportExpert,
&mut tactics,
);
if tactics.is_empty() {
try_tactic(
HammerSkill::IronTempest,
HammerTactics::AttackAdvanced,
&mut tactics,
);
try_tactic(
HammerSkill::Rampart,
HammerTactics::SupportAdvanced,
&mut tactics,
);
}
if tactics.is_empty() {
try_tactic(
HammerSkill::Retaliate,
HammerTactics::AttackIntermediate,
&mut tactics,
);
try_tactic(
HammerSkill::PileDriver,
HammerTactics::SupportIntermediate,
&mut tactics,
);
}
if tactics.is_empty() {
try_tactic(
HammerSkill::Tremor,
HammerTactics::AttackSimple,
&mut tactics,
);
try_tactic(
HammerSkill::HeavyWhorl,
HammerTactics::SupportSimple,
&mut tactics,
);
}
if tactics.is_empty() {
try_tactic(
HammerSkill::ScornfulSwipe,
HammerTactics::Simple,
&mut tactics,
);
}
if tactics.is_empty() {
tactics.push(HammerTactics::Unskilled);
}
tactics
};
let tactic = available_tactics
.choose(rng)
.copied()
.unwrap_or(HammerTactics::Unskilled);
agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
let set_ability = |controller: &mut Controller, slot, skill| {
controller.push_event(ControlEvent::ChangeAbility {
slot,
auxiliary_key,
new_ability: AuxiliaryAbility::MainWeapon(skill),
});
};
let mut set_random = |controller: &mut Controller, slot, options: &mut Vec<usize>| {
if options.is_empty() {
return;
}
let i = rng.gen_range(0..options.len());
set_ability(controller, slot, options.swap_remove(i));
};
match tactic {
HammerTactics::Unskilled => {},
HammerTactics::Simple => {
// Scornful swipe
set_ability(controller, 0, 0);
},
HammerTactics::AttackSimple => {
// Scornful swipe
set_ability(controller, 0, 0);
// Tremor or vigorous bash
set_ability(controller, 1, rng.gen_range(1..3));
},
HammerTactics::AttackIntermediate => {
// Scornful swipe
set_ability(controller, 0, 0);
// Tremor or vigorous bash
set_ability(controller, 1, rng.gen_range(1..3));
// Retaliate, spine cracker, or breach
set_ability(controller, 2, rng.gen_range(3..6));
},
HammerTactics::AttackAdvanced => {
// Scornful swipe, tremor, vigorous bash, retaliate, spine cracker, or breach
let mut options = vec![0, 1, 2, 3, 4, 5];
set_random(controller, 0, &mut options);
set_random(controller, 1, &mut options);
set_random(controller, 2, &mut options);
set_ability(controller, 3, rng.gen_range(6..8));
},
HammerTactics::AttackExpert => {
// Scornful swipe, tremor, vigorous bash, retaliate, spine cracker, breach, iron
// tempest, or upheaval
let mut options = vec![0, 1, 2, 3, 4, 5, 6, 7];
set_random(controller, 0, &mut options);
set_random(controller, 1, &mut options);
set_random(controller, 2, &mut options);
set_random(controller, 3, &mut options);
set_ability(controller, 4, rng.gen_range(8..10));
},
HammerTactics::SupportSimple => {
// Scornful swipe
set_ability(controller, 0, 0);
// Heavy whorl or intercept
set_ability(controller, 1, rng.gen_range(10..12));
},
HammerTactics::SupportIntermediate => {
// Scornful swipe
set_ability(controller, 0, 0);
// Heavy whorl or intercept
set_ability(controller, 1, rng.gen_range(10..12));
// Retaliate, spine cracker, or breach
set_ability(controller, 2, rng.gen_range(12..15));
},
HammerTactics::SupportAdvanced => {
// Scornful swipe, heavy whorl, intercept, pile driver, lung pummel, or helm
// crusher
let mut options = vec![0, 10, 11, 12, 13, 14];
set_random(controller, 0, &mut options);
set_random(controller, 1, &mut options);
set_random(controller, 2, &mut options);
set_ability(controller, 3, rng.gen_range(15..17));
},
HammerTactics::SupportExpert => {
// Scornful swipe, heavy whorl, intercept, pile driver, lung pummel, helm
// crusher, rampart, or tenacity
let mut options = vec![0, 10, 11, 12, 13, 14, 15, 16];
set_random(controller, 0, &mut options);
set_random(controller, 1, &mut options);
set_random(controller, 2, &mut options);
set_random(controller, 3, &mut options);
set_ability(controller, 4, rng.gen_range(17..19));
},
}
agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
ActionMode::Reckless as u8;
}
enum IntCounters {
Tactic = 0,
ActionMode = 1,
}
enum Timers {
GuardedCycle = 0,
PosTimeOut = 1,
}
enum Conditions {
GuardedDefend = 0,
RollingBreakThrough = 1,
}
enum FloatCounters {
GuardedTimer = 0,
}
enum Positions {
GuardedCover = 0,
Flee = 1,
}
let attempt_attack = handle_attack_aggression(
self,
agent,
controller,
attack_data,
tgt_data,
read_data,
rng,
Timers::PosTimeOut as usize,
Timers::GuardedCycle as usize,
FloatCounters::GuardedTimer as usize,
IntCounters::ActionMode as usize,
Conditions::GuardedDefend as usize,
Conditions::RollingBreakThrough as usize,
Positions::GuardedCover as usize,
Positions::Flee as usize,
);
let attack_failed = if attempt_attack {
let primary = self.extract_ability(AbilityInput::Primary);
let secondary = self.extract_ability(AbilityInput::Secondary);
let abilities = [
self.extract_ability(AbilityInput::Auxiliary(0)),
self.extract_ability(AbilityInput::Auxiliary(1)),
self.extract_ability(AbilityInput::Auxiliary(2)),
self.extract_ability(AbilityInput::Auxiliary(3)),
self.extract_ability(AbilityInput::Auxiliary(4)),
];
let could_use_input = |input, desired_energy| match input {
InputKind::Primary => primary.as_ref().map_or(false, |p| {
p.could_use(attack_data, self, tgt_data, read_data, desired_energy)
}),
InputKind::Secondary => secondary.as_ref().map_or(false, |s| {
s.could_use(attack_data, self, tgt_data, read_data, desired_energy)
}),
InputKind::Ability(x) => abilities[x].as_ref().map_or(false, |a| {
let ability = self.active_abilities.get_ability(
AbilityInput::Auxiliary(x),
Some(self.inventory),
Some(self.skill_set),
self.stats,
);
let additional_conditions = match ability {
Ability::MainWeaponAux(0) => self
.buffs
.map_or(false, |buffs| !buffs.contains(BuffKind::ScornfulTaunt)),
Ability::MainWeaponAux(2) => {
tgt_data.char_state.map_or(false, |cs| cs.is_stunned())
},
Ability::MainWeaponAux(4) => tgt_data.ori.map_or(false, |ori| {
ori.look_vec().angle_between(tgt_data.pos.0 - self.pos.0)
< combat::BEHIND_TARGET_ANGLE
}),
Ability::MainWeaponAux(5) => tgt_data.char_state.map_or(false, |cs| {
cs.is_block(AttackSource::Melee) || cs.is_parry(AttackSource::Melee)
}),
Ability::MainWeaponAux(7) => tgt_data
.buffs
.map_or(false, |buffs| !buffs.contains(BuffKind::Staggered)),
Ability::MainWeaponAux(12) => tgt_data
.buffs
.map_or(false, |buffs| !buffs.contains(BuffKind::Rooted)),
Ability::MainWeaponAux(13) => tgt_data
.buffs
.map_or(false, |buffs| !buffs.contains(BuffKind::Winded)),
Ability::MainWeaponAux(14) => tgt_data
.buffs
.map_or(false, |buffs| !buffs.contains(BuffKind::Concussion)),
Ability::MainWeaponAux(15) => self
.buffs
.map_or(false, |buffs| !buffs.contains(BuffKind::ProtectingWard)),
_ => true,
};
a.could_use(attack_data, self, tgt_data, read_data, desired_energy)
&& additional_conditions
}),
_ => false,
};
let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
if matches!(current_input, InputKind::Secondary) {
let charging =
matches!(self.char_state.stage_section(), Some(StageSection::Charge));
let charged = self
.char_state
.durations()
.and_then(|durs| durs.charge)
.zip(self.char_state.timer())
.map_or(false, |(dur, timer)| timer > dur);
if !(charging && charged) {
*next_input = Some(InputKind::Secondary);
}
} else {
*next_input = Some(current_input);
}
};
let current_input = self.char_state.ability_info().map(|ai| ai.input);
let ability_preferences = AbilityPreferences {
desired_energy: 40.0,
combo_scaling_buildup: 0,
};
let mut next_input = None;
if let Some(input) = current_input {
continue_current_input(input, &mut next_input);
} else {
match HammerTactics::from_u8(
agent.combat_state.int_counters[IntCounters::Tactic as usize],
) {
HammerTactics::Unskilled => {
if rng.gen_bool(0.5) {
next_input = Some(InputKind::Primary);
} else {
next_input = Some(InputKind::Secondary);
}
},
HammerTactics::Simple => {
if rng.gen_bool(0.5) {
next_input = Some(InputKind::Primary);
} else {
next_input = Some(InputKind::Secondary);
}
},
HammerTactics::AttackSimple | HammerTactics::SupportSimple => {
if could_use_input(InputKind::Ability(0), ability_preferences) {
next_input = Some(InputKind::Ability(0));
} else if rng.gen_bool(0.5) {
next_input = Some(InputKind::Primary);
} else {
next_input = Some(InputKind::Secondary);
}
},
HammerTactics::AttackIntermediate | HammerTactics::SupportIntermediate => {
let random_ability = InputKind::Ability(rng.gen_range(0..3));
if could_use_input(random_ability, ability_preferences) {
next_input = Some(random_ability);
} else if rng.gen_bool(0.5) {
next_input = Some(InputKind::Primary);
} else {
next_input = Some(InputKind::Secondary);
}
},
HammerTactics::AttackAdvanced | HammerTactics::SupportAdvanced => {
let random_ability = InputKind::Ability(rng.gen_range(0..5));
if could_use_input(random_ability, ability_preferences) {
next_input = Some(random_ability);
} else if rng.gen_bool(0.5) {
next_input = Some(InputKind::Primary);
} else {
next_input = Some(InputKind::Secondary);
}
},
HammerTactics::AttackExpert | HammerTactics::SupportExpert => {
let random_ability = InputKind::Ability(rng.gen_range(0..5));
if could_use_input(random_ability, ability_preferences) {
next_input = Some(random_ability);
} else if rng.gen_bool(0.5) {
next_input = Some(InputKind::Primary);
} else {
next_input = Some(InputKind::Secondary);
}
},
}
}
if let Some(input) = next_input {
if could_use_input(input, ability_preferences) {
controller.push_basic_input(input);
false
} else {
true
}
} else {
true
}
} else {
false
};
if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
self.path_toward_target(
agent,
controller,
tgt_data.pos.0,
read_data,
Path::Separate,
None,
);
}
}
pub fn handle_sword_attack(

View File

@ -27,6 +27,7 @@ use common::{
terrain::TerrainGrid,
uid::{IdMaps, Uid},
};
use common_base::dev_panic;
use specs::{shred, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData};
event_emitters! {
@ -72,6 +73,7 @@ pub struct AgentData<'a> {
pub struct TargetData<'a> {
pub pos: &'a Pos,
pub ori: Option<&'a Ori>,
pub body: Option<&'a Body>,
pub scale: Option<&'a Scale>,
pub char_state: Option<&'a CharacterState>,
@ -84,6 +86,7 @@ impl<'a> TargetData<'a> {
pub fn new(pos: &'a Pos, target: EcsEntity, read_data: &'a ReadData) -> Self {
Self {
pos,
ori: read_data.orientations.get(target),
body: read_data.bodies.get(target),
scale: read_data.scales.get(target),
char_state: read_data.char_states.get(target),
@ -342,6 +345,39 @@ impl AxeTactics {
}
}
#[derive(Copy, Clone, Debug)]
pub enum HammerTactics {
Unskilled = 0,
Simple = 1,
AttackSimple = 2,
SupportSimple = 3,
AttackIntermediate = 4,
SupportIntermediate = 5,
AttackAdvanced = 6,
SupportAdvanced = 7,
AttackExpert = 8,
SupportExpert = 9,
}
impl HammerTactics {
pub fn from_u8(x: u8) -> Self {
use HammerTactics::*;
match x {
0 => Unskilled,
1 => Simple,
2 => AttackSimple,
3 => SupportSimple,
4 => AttackIntermediate,
5 => SupportIntermediate,
6 => AttackAdvanced,
7 => SupportAdvanced,
8 => AttackExpert,
9 => SupportExpert,
_ => Unskilled,
}
}
}
#[derive(SystemData)]
pub struct ReadData<'a> {
pub entities: Entities<'a>,
@ -481,6 +517,17 @@ pub enum AbilityData {
angle: f32,
ori_rate: f32,
},
Shockwave {
energy: f32,
angle: f32,
range: f32,
combo: u32,
},
// Note, buff check not done as auras could be non-buff and auras could target either in or
// out of group
StaticAura {
energy: f32,
},
}
#[derive(Copy, Clone, Debug, Default)]
@ -659,7 +706,29 @@ impl AbilityData {
ori_rate: *ori_rate,
energy_drain: *energy_drain,
},
_ => return None,
Shockwave {
energy_cost,
shockwave_angle,
shockwave_speed,
shockwave_duration,
minimum_combo,
..
} => Self::Shockwave {
energy: *energy_cost,
angle: *shockwave_angle,
range: *shockwave_speed * *shockwave_duration,
combo: *minimum_combo,
},
StaticAura { energy_cost, .. } => Self::StaticAura {
energy: *energy_cost,
},
_ => {
dev_panic!(
"Agent tried to use ability with a character state they haven't learned to \
understand"
);
return None;
},
};
Some(inner)
}
@ -888,6 +957,17 @@ impl AbilityData {
angle,
ori_rate,
} => beam_check(*range, *angle, *ori_rate) && energy_check(*energy_drain * 3.0),
Shockwave {
energy,
range,
angle,
combo,
} => {
melee_check(*range, *angle, None)
&& energy_check(*energy)
&& combo_check(*combo, false)
},
StaticAura { energy } => energy_check(*energy),
}
}
}