From aeb887963ebfe536f04bb8a87a6bb4c8387c98f7 Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 13 Apr 2024 17:18:10 -0400 Subject: [PATCH] Hammer AI --- Cargo.lock | 2 + .../common/entity/dungeon/cultist/cultist.ron | 12 +- common/src/combat.rs | 4 +- server/agent/Cargo.toml | 2 + server/agent/src/attack.rs | 394 +++++++++++++++++- server/agent/src/data.rs | 82 +++- 6 files changed, 479 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f8473ff23..a092516c31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/assets/common/entity/dungeon/cultist/cultist.ron b/assets/common/entity/dungeon/cultist/cultist.ron index d011d4f444..2f042d911c 100644 --- a/assets/common/entity/dungeon/cultist/cultist.ron +++ b/assets/common/entity/dungeon/cultist/cultist.ron @@ -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: [ diff --git a/common/src/combat.rs b/common/src/combat.rs index 8a45a64680..0604981326 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -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 } diff --git a/server/agent/Cargo.toml b/server/agent/Cargo.toml index 71e691d709..26a643c7f0 100644 --- a/server/agent/Cargo.toml +++ b/server/agent/Cargo.toml @@ -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 } diff --git a/server/agent/src/attack.rs b/server/agent/src/attack.rs index 098d3862a2..688cae91ef 100644 --- a/server/agent/src/attack.rs +++ b/server/agent/src/attack.rs @@ -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| { + 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| { + 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| { + 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( diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 486301cc99..f020ca115b 100755 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -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), } } }