From 9fa556b1652381925fafdc484f0aa604509eefa4 Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 3 Jun 2023 20:30:10 -0400 Subject: [PATCH] Axe AI --- assets/voxygen/i18n/en/hud/ability.ftl | 4 +- server/agent/src/attack.rs | 592 +++++++++++++++++--- server/agent/src/data.rs | 89 ++- voxygen/anim/src/character/combomelee.rs | 37 -- voxygen/anim/src/character/finishermelee.rs | 38 ++ 5 files changed, 629 insertions(+), 131 deletions(-) diff --git a/assets/voxygen/i18n/en/hud/ability.ftl b/assets/voxygen/i18n/en/hud/ability.ftl index 541cd3acb2..b7495a164a 100644 --- a/assets/voxygen/i18n/en/hud/ability.ftl +++ b/assets/voxygen/i18n/en/hud/ability.ftl @@ -306,9 +306,9 @@ common-abilities-axe-bloodfeast = Bloodfeast common-abilities-axe-fierce_raze = Fierce Raze .desc = A rapid flurry of strikes on your foe -common-abilities-axe-dueal_fierce_raze = Fierce Raze +common-abilities-axe-dual_fierce_raze = Fierce Raze .desc = - A rapid flurry of strikes on yoru foe using both of your axes + A rapid flurry of strikes on your foe using both of your axes common-abilities-axe-furor = Furor .desc = As your fury rises, your movement and attacks quicken diff --git a/server/agent/src/attack.rs b/server/agent/src/attack.rs index a6100ae340..acf59d671d 100644 --- a/server/agent/src/attack.rs +++ b/server/agent/src/attack.rs @@ -8,7 +8,7 @@ use common::{ ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, MAX_ABILITIES}, buff::BuffKind, item::tool::AbilityContext, - skills::{BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, + skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent, Controller, InputKind, }, @@ -327,18 +327,6 @@ impl<'a> AgentData<'a> { } } - pub fn handle_axe_attack( - &self, - _agent: &mut Agent, - _controller: &mut Controller, - _attack_data: &AttackData, - _tgt_data: &TargetData, - _read_data: &ReadData, - _rng: &mut impl Rng, - ) { - // TODO - } - pub fn handle_hammer_attack( &self, agent: &mut Agent, @@ -738,7 +726,10 @@ impl<'a> AgentData<'a> { agent.action_state.int_counters[IntCounters::Tactics as usize], ) { SwordTactics::Unskilled => { - let desired_energy = 15.0; + let ability_preferences = AbilityPreferences { + desired_energy: 15.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -749,7 +740,7 @@ impl<'a> AgentData<'a> { next_input = Some(InputKind::Secondary); }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -760,14 +751,17 @@ impl<'a> AgentData<'a> { } }, SwordTactics::Basic => { - let desired_energy = 25.0; + let ability_preferences = AbilityPreferences { + desired_energy: 25.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { continue_current_input(input, &mut next_input); } else { let attempt_ability = InputKind::Ability(rng.gen_range(0..5)); - if could_use_input(attempt_ability, desired_energy) { + if could_use_input(attempt_ability, ability_preferences) { next_input = Some(attempt_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); @@ -776,7 +770,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -787,7 +781,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::HeavySimple => { - let desired_energy = 35.0; + let ability_preferences = AbilityPreferences { + desired_energy: 35.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -796,16 +793,16 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(3..5)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Heavy))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -814,7 +811,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -825,7 +822,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::AgileSimple => { - let desired_energy = 35.0; + let ability_preferences = AbilityPreferences { + desired_energy: 35.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -834,16 +834,16 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(3..5)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Agile))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -852,7 +852,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -863,7 +863,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::DefensiveSimple => { - let desired_energy = 35.0; + let ability_preferences = AbilityPreferences { + desired_energy: 35.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -872,18 +875,18 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(3..5)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Defensive))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(InputKind::Ability(3), desired_energy) { + } else if could_use_input(InputKind::Ability(3), ability_preferences) { next_input = Some(InputKind::Ability(3)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -892,7 +895,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -903,7 +906,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::CripplingSimple => { - let desired_energy = 35.0; + let ability_preferences = AbilityPreferences { + desired_energy: 35.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -912,16 +918,16 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(3..5)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Crippling))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -930,7 +936,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -941,7 +947,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::CleavingSimple => { - let desired_energy = 35.0; + let ability_preferences = AbilityPreferences { + desired_energy: 35.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -950,16 +959,16 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(3..5)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Cleaving))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -968,7 +977,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -979,7 +988,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::HeavyAdvanced => { - let desired_energy = 50.0; + let ability_preferences = AbilityPreferences { + desired_energy: 50.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -988,16 +1000,16 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(1..3)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Heavy))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -1006,7 +1018,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -1017,7 +1029,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::AgileAdvanced => { - let desired_energy = 50.0; + let ability_preferences = AbilityPreferences { + desired_energy: 50.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -1026,16 +1041,16 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(1..3)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Agile))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -1044,7 +1059,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -1055,7 +1070,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::DefensiveAdvanced => { - let desired_energy = 50.0; + let ability_preferences = AbilityPreferences { + desired_energy: 50.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -1064,18 +1082,18 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(1..3)); let random_ability = InputKind::Ability(rng.gen_range(1..4)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Defensive))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else if could_use_input(random_ability, ability_preferences) { next_input = Some(random_ability); - } else if could_use_input(InputKind::Ability(4), desired_energy) + } else if could_use_input(InputKind::Ability(4), ability_preferences) && rng.gen_bool(2.0 * read_data.dt.0 as f64) { next_input = Some(InputKind::Ability(4)); @@ -1086,7 +1104,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -1097,7 +1115,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::CripplingAdvanced => { - let desired_energy = 50.0; + let ability_preferences = AbilityPreferences { + desired_energy: 50.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -1106,16 +1127,16 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(1..3)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Crippling))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -1124,7 +1145,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -1135,7 +1156,10 @@ impl<'a> AgentData<'a> { } }, SwordTactics::CleavingAdvanced => { - let desired_energy = 50.0; + let ability_preferences = AbilityPreferences { + desired_energy: 50.0, + combo_scaling_buildup: 0, + }; let current_input = self.char_state.ability_info().map(|ai| ai.input); let mut next_input = None; if let Some(input) = current_input { @@ -1144,16 +1168,16 @@ impl<'a> AgentData<'a> { let stance_ability = InputKind::Ability(rng.gen_range(1..3)); let random_ability = InputKind::Ability(rng.gen_range(1..5)); if !matches!(self.stance, Some(Stance::Sword(SwordStance::Cleaving))) { - if could_use_input(stance_ability, desired_energy) { + if could_use_input(stance_ability, ability_preferences) { next_input = Some(stance_ability); } else if rng.gen_bool(0.5) { next_input = Some(InputKind::Primary); } else { next_input = Some(InputKind::Secondary); } - } else if could_use_input(InputKind::Ability(0), desired_energy) { + } else if could_use_input(InputKind::Ability(0), ability_preferences) { next_input = Some(InputKind::Ability(0)); - } else if could_use_input(random_ability, desired_energy) { + } else 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); @@ -1162,7 +1186,7 @@ impl<'a> AgentData<'a> { } }; if let Some(input) = next_input { - if could_use_input(input, desired_energy) { + if could_use_input(input, ability_preferences) { controller.push_basic_input(input); false } else { @@ -1189,6 +1213,322 @@ impl<'a> AgentData<'a> { } } + pub fn handle_axe_attack( + &self, + agent: &mut Agent, + controller: &mut Controller, + attack_data: &AttackData, + tgt_data: &TargetData, + read_data: &ReadData, + rng: &mut impl Rng, + ) { + if !agent.action_state.initialized { + agent.action_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::Axe(skill)) { + tactics.push(tactic); + } + }; + try_tactic(AxeSkill::Execute, AxeTactics::SavageAdvanced, &mut tactics); + try_tactic( + AxeSkill::Lacerate, + AxeTactics::MercilessAdvanced, + &mut tactics, + ); + try_tactic(AxeSkill::Bulkhead, AxeTactics::RivingAdvanced, &mut tactics); + if tactics.is_empty() { + try_tactic( + AxeSkill::RisingTide, + AxeTactics::SavageIntermediate, + &mut tactics, + ); + try_tactic( + AxeSkill::FierceRaze, + AxeTactics::MercilessIntermediate, + &mut tactics, + ); + try_tactic( + AxeSkill::Plunder, + AxeTactics::RivingIntermediate, + &mut tactics, + ); + } + if tactics.is_empty() { + try_tactic( + AxeSkill::BrutalSwing, + AxeTactics::SavageSimple, + &mut tactics, + ); + try_tactic(AxeSkill::Rake, AxeTactics::MercilessSimple, &mut tactics); + try_tactic(AxeSkill::SkullBash, AxeTactics::RivingSimple, &mut tactics); + } + if tactics.is_empty() { + tactics.push(AxeTactics::Unskilled); + } + tactics + }; + + let tactic = available_tactics + .choose(rng) + .copied() + .unwrap_or(AxeTactics::Unskilled); + + agent.action_state.int_counters[IntCounters::Tactic as usize] = tactic as u8; + + let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory)); + let set_axe_ability = |controller: &mut Controller, slot, skill| { + controller.push_event(ControlEvent::ChangeAbility { + slot, + auxiliary_key, + new_ability: AuxiliaryAbility::MainWeapon(skill), + }); + }; + + match tactic { + AxeTactics::Unskilled => {}, + AxeTactics::SavageSimple => { + // Brutal swing + set_axe_ability(controller, 0, 0); + }, + AxeTactics::MercilessSimple => { + // Rake + set_axe_ability(controller, 0, 6); + }, + AxeTactics::RivingSimple => { + // Skull bash + set_axe_ability(controller, 0, 12); + }, + AxeTactics::SavageIntermediate => { + // Brutal swing + set_axe_ability(controller, 0, 0); + // Berserk + set_axe_ability(controller, 1, 1); + // Rising tide + set_axe_ability(controller, 2, 2); + }, + AxeTactics::MercilessIntermediate => { + // Rake + set_axe_ability(controller, 0, 6); + // Bloodfeast + set_axe_ability(controller, 1, 7); + // Fierce raze + set_axe_ability(controller, 2, 8); + }, + AxeTactics::RivingIntermediate => { + // Skull bash + set_axe_ability(controller, 0, 12); + // Sunder + set_axe_ability(controller, 1, 13); + // Plunder + set_axe_ability(controller, 2, 14); + }, + AxeTactics::SavageAdvanced => { + // Berserk + set_axe_ability(controller, 0, 1); + // Rising tide + set_axe_ability(controller, 1, 2); + // Savage sense + set_axe_ability(controller, 2, 3); + // Adrenaline rush + set_axe_ability(controller, 3, 4); + // Execute/maelstrom + set_axe_ability(controller, 4, 5); + }, + AxeTactics::MercilessAdvanced => { + // Bloodfeast + set_axe_ability(controller, 0, 7); + // Fierce raze + set_axe_ability(controller, 1, 8); + // Furor + set_axe_ability(controller, 2, 9); + // Fracture + set_axe_ability(controller, 3, 10); + // Lacerate/riptide + set_axe_ability(controller, 4, 11); + }, + AxeTactics::RivingAdvanced => { + // Sunder + set_axe_ability(controller, 0, 13); + // Plunder + set_axe_ability(controller, 1, 14); + // Defiance + set_axe_ability(controller, 2, 15); + // Keelhaul + set_axe_ability(controller, 3, 16); + // Bulkhead/capsize + set_axe_ability(controller, 4, 17); + }, + } + + agent.action_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| { + a.could_use(attack_data, self, tgt_data, read_data, desired_energy) + }), + _ => 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: 15, + }; + let mut next_input = None; + if let Some(input) = current_input { + continue_current_input(input, &mut next_input); + } else { + match AxeTactics::from_u8( + agent.action_state.int_counters[IntCounters::Tactic as usize], + ) { + AxeTactics::Unskilled => { + if rng.gen_bool(0.5) { + next_input = Some(InputKind::Primary); + } else { + next_input = Some(InputKind::Secondary); + } + }, + AxeTactics::SavageSimple + | AxeTactics::MercilessSimple + | AxeTactics::RivingSimple => { + 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); + } + }, + AxeTactics::SavageIntermediate + | AxeTactics::MercilessIntermediate + | AxeTactics::RivingIntermediate => { + 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); + } + }, + AxeTactics::SavageAdvanced + | AxeTactics::MercilessAdvanced + | AxeTactics::RivingAdvanced => { + 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_bow_attack( &self, agent: &mut Agent, @@ -4767,10 +5107,22 @@ impl<'a> AgentData<'a> { let secondary = self.extract_ability(AbilityInput::Secondary); let could_use_input = |input| match input { InputKind::Primary => primary.as_ref().map_or(false, |p| { - p.could_use(attack_data, self, tgt_data, read_data, 0.0) + p.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { - s.could_use(attack_data, self, tgt_data, read_data, 0.0) + s.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), _ => false, }; @@ -4823,10 +5175,22 @@ impl<'a> AgentData<'a> { let secondary = self.extract_ability(AbilityInput::Secondary); let could_use_input = |input| match input { InputKind::Primary => primary.as_ref().map_or(false, |p| { - p.could_use(attack_data, self, tgt_data, read_data, 0.0) + p.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { - s.could_use(attack_data, self, tgt_data, read_data, 0.0) + s.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), _ => false, }; @@ -4868,7 +5232,13 @@ impl<'a> AgentData<'a> { let primary = self.extract_ability(AbilityInput::Primary); let could_use_input = |input| match input { InputKind::Primary => primary.as_ref().map_or(false, |p| { - p.could_use(attack_data, self, tgt_data, read_data, 0.0) + p.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), _ => false, }; @@ -4916,13 +5286,31 @@ impl<'a> AgentData<'a> { ]; let could_use_input = |input| match input { InputKind::Primary => primary.as_ref().map_or(false, |p| { - p.could_use(attack_data, self, tgt_data, read_data, 0.0) + p.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { - s.could_use(attack_data, self, tgt_data, read_data, 0.0) + s.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), InputKind::Ability(x) => abilities[x].as_ref().map_or(false, |a| { - a.could_use(attack_data, self, tgt_data, read_data, 0.0) + a.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), _ => false, }; @@ -4984,13 +5372,31 @@ impl<'a> AgentData<'a> { ]; let could_use_input = |input| match input { InputKind::Primary => primary.as_ref().map_or(false, |p| { - p.could_use(attack_data, self, tgt_data, read_data, 0.0) + p.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { - s.could_use(attack_data, self, tgt_data, read_data, 0.0) + s.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), InputKind::Ability(x) => abilities[x].as_ref().map_or(false, |a| { - a.could_use(attack_data, self, tgt_data, read_data, 0.0) + a.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), _ => false, }; @@ -5077,13 +5483,31 @@ impl<'a> AgentData<'a> { ]; let could_use_input = |input| match input { InputKind::Primary => primary.as_ref().map_or(false, |p| { - p.could_use(attack_data, self, tgt_data, read_data, 0.0) + p.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { - s.could_use(attack_data, self, tgt_data, read_data, 0.0) + s.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), InputKind::Ability(x) => abilities[x].as_ref().map_or(false, |a| { - a.could_use(attack_data, self, tgt_data, read_data, 0.0) + a.could_use( + attack_data, + self, + tgt_data, + read_data, + AbilityPreferences::default(), + ) }), _ => false, }; diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 9b98899653..f81dd65389 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -6,7 +6,10 @@ use common::{ character_state::AttackFilters, group, inventory::{ - item::{tool::ToolKind, ItemKind, MaterialStatManifest}, + item::{ + tool::{AbilityMap, ToolKind}, + ItemKind, MaterialStatManifest, + }, slot::EquipSlot, }, ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy, Health, Inventory, @@ -246,7 +249,7 @@ pub enum Tactic { AdletElder, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub enum SwordTactics { Unskilled = 0, Basic = 1, @@ -283,6 +286,39 @@ impl SwordTactics { } } +#[derive(Copy, Clone, Debug)] +pub enum AxeTactics { + Unskilled = 0, + SavageSimple = 1, + MercilessSimple = 2, + RivingSimple = 3, + SavageIntermediate = 4, + MercilessIntermediate = 5, + RivingIntermediate = 6, + SavageAdvanced = 7, + MercilessAdvanced = 8, + RivingAdvanced = 9, +} + +impl AxeTactics { + pub fn from_u8(x: u8) -> Self { + use AxeTactics::*; + match x { + 0 => Unskilled, + 1 => SavageSimple, + 2 => MercilessSimple, + 3 => RivingSimple, + 4 => SavageIntermediate, + 5 => MercilessIntermediate, + 6 => RivingIntermediate, + 7 => SavageAdvanced, + 8 => MercilessAdvanced, + 9 => RivingAdvanced, + _ => Unskilled, + } + } +} + #[derive(SystemData)] pub struct ReadData<'a> { pub entities: Entities<'a>, @@ -323,6 +359,7 @@ pub struct ReadData<'a> { pub poises: ReadStorage<'a, Poise>, pub stances: ReadStorage<'a, Stance>, pub presences: ReadStorage<'a, Presence>, + pub ability_map: ReadExpect<'a, AbilityMap>, } impl<'a> ReadData<'a> { @@ -353,10 +390,13 @@ pub enum AbilityData { angle: f32, energy: f32, combo: u32, + combo_scales: bool, }, SelfBuff { buff: BuffKind, energy: f32, + combo: u32, + combo_scales: bool, }, DiveMelee { range: f32, @@ -420,6 +460,12 @@ pub enum AbilityData { }, } +#[derive(Copy, Clone, Debug, Default)] +pub struct AbilityPreferences { + pub desired_energy: f32, + pub combo_scaling_buildup: u32, +} + impl AbilityData { pub fn from_ability(ability: &CharacterAbility) -> Option { use CharacterAbility::*; @@ -456,20 +502,26 @@ impl AbilityData { energy_cost, melee_constructor, minimum_combo, + scaling, .. } => Self::FinisherMelee { energy: *energy_cost, range: melee_constructor.range, angle: melee_constructor.angle, combo: *minimum_combo, + combo_scales: scaling.is_some(), }, SelfBuff { buff_kind, energy_cost, + combo_cost, + combo_scaling, .. } => Self::SelfBuff { buff: *buff_kind, energy: *energy_cost, + combo: *combo_cost, + combo_scales: combo_scaling.is_some(), }, DiveMelee { energy_cost, @@ -595,7 +647,7 @@ impl AbilityData { agent_data: &AgentData, tgt_data: &TargetData, read_data: &ReadData, - desired_energy: f32, + ability_preferences: AbilityPreferences, ) -> bool { let melee_check = |range: f32, angle, forced_movement: Option| { let (range_inc, min_mult) = forced_movement.map_or((0.0, 0.0), |fm| match fm { @@ -621,9 +673,19 @@ impl AbilityData { }; let energy_check = |energy: f32| { agent_data.energy.current() >= energy - && (energy < f32::EPSILON || agent_data.energy.current() >= desired_energy) + && (energy < f32::EPSILON + || agent_data.energy.current() >= ability_preferences.desired_energy) + }; + let combo_check = |combo, scales| { + let additional_combo = if scales { + ability_preferences.combo_scaling_buildup + } else { + 0 + }; + agent_data + .combo + .map_or(false, |c| c.counter() >= combo + additional_combo) }; - let combo_check = |combo| agent_data.combo.map_or(false, |c| c.counter() >= combo); let attack_kind_check = |attacks: AttackFilters| { tgt_data .char_state @@ -673,9 +735,20 @@ impl AbilityData { angle, energy, combo, - } => melee_check(*range, *angle, None) && energy_check(*energy) && combo_check(*combo), - SelfBuff { buff, energy } => { + combo_scales, + } => { + melee_check(*range, *angle, None) + && energy_check(*energy) + && combo_check(*combo, *combo_scales) + }, + SelfBuff { + buff, + energy, + combo, + combo_scales, + } => { energy_check(*energy) + && combo_check(*combo, *combo_scales) && agent_data .buffs .map_or(false, |buffs| !buffs.contains(*buff)) @@ -716,7 +789,7 @@ impl AbilityData { } => { melee_check(*range, *angle, None) && energy_check(*energy_per_strike * *strikes as f32) - && combo_check(*combo) + && combo_check(*combo, false) }, ChargedMelee { range, diff --git a/voxygen/anim/src/character/combomelee.rs b/voxygen/anim/src/character/combomelee.rs index 109352ff4e..d4fb7be588 100644 --- a/voxygen/anim/src/character/combomelee.rs +++ b/voxygen/anim/src/character/combomelee.rs @@ -1061,43 +1061,6 @@ impl Animation for ComboAnimation { next.belt.orientation.rotate_z(move2 * -0.6); next.control.position += Vec3::new(move2 * -6.0, move2 * -20.0, move2 * -4.0); }, - Some("common.abilities.axe.fracture") => { - let (move1, move2) = match stage_section { - Some(StageSection::Buildup) => (anim_time, 0.0), - Some(StageSection::Action) => (1.0, anim_time), - Some(StageSection::Recover) => (1.0, 1.0), - _ => (0.0, 0.0), - }; - let move1 = move1 * multi_strike_pullback; - let move2 = move2 * multi_strike_pullback; - - next.hand_l.position = Vec3::new(s_a.ahl.0, s_a.ahl.1, s_a.ahl.2); - next.hand_l.orientation = - Quaternion::rotation_x(s_a.ahl.3) * Quaternion::rotation_y(s_a.ahl.4); - next.hand_r.position = Vec3::new(s_a.ahr.0, s_a.ahr.1, s_a.ahr.2); - next.hand_r.orientation = - Quaternion::rotation_x(s_a.ahr.3) * Quaternion::rotation_z(s_a.ahr.5); - - next.control.position = Vec3::new(s_a.ac.0, s_a.ac.1, s_a.ac.2); - next.control.orientation = Quaternion::rotation_x(s_a.ac.3) - * Quaternion::rotation_y(s_a.ac.4) - * Quaternion::rotation_z(s_a.ac.5 + move1 * -PI / 2.0 + move2 * -0.5); - - next.control.orientation.rotate_x(move1 * 0.0); - next.chest.orientation.rotate_x(move1 * -0.5); - next.chest.orientation.rotate_z(move1 * 0.7); - next.head.orientation.rotate_z(move1 * -0.3); - next.belt.orientation.rotate_z(move1 * -0.1); - next.shorts.orientation.rotate_z(move1 * -0.4); - - next.chest.orientation.rotate_z(move2 * -1.8); - next.head.orientation.rotate_z(move2 * 0.9); - next.shorts.orientation.rotate_z(move2 * 1.3); - next.belt.orientation.rotate_z(move2 * 0.6); - next.control.orientation.rotate_x(move2 * -0.9); - next.control.orientation.rotate_z(move2 * -3.5); - next.control.position += Vec3::new(move2 * 14.0, move2 * 6.0, 0.0); - }, Some("common.abilities.axe.skull_bash") => { let (move1, move2) = match stage_section { Some(StageSection::Buildup) => (anim_time, 0.0), diff --git a/voxygen/anim/src/character/finishermelee.rs b/voxygen/anim/src/character/finishermelee.rs index afece5c7db..05f60b3fc7 100644 --- a/voxygen/anim/src/character/finishermelee.rs +++ b/voxygen/anim/src/character/finishermelee.rs @@ -402,6 +402,44 @@ impl Animation for FinisherMeleeAnimation { next.control.position += Vec3::new(move2 * 12.0, move2 * -6.0, 0.0); next.torso.orientation.rotate_z(move2_raw * -TAU); }, + Some("common.abilities.axe.fracture") => { + let (move1, move2, move3) = match stage_section { + Some(StageSection::Buildup) => (anim_time, 0.0, 0.0), + Some(StageSection::Action) => (1.0, anim_time, 0.0), + Some(StageSection::Recover) => (1.0, 1.0, anim_time), + _ => (0.0, 0.0, 0.0), + }; + let pullback = 1.0 - move3; + let move1 = move1 * pullback; + let move2 = move2 * pullback; + + next.hand_l.position = Vec3::new(s_a.ahl.0, s_a.ahl.1, s_a.ahl.2); + next.hand_l.orientation = + Quaternion::rotation_x(s_a.ahl.3) * Quaternion::rotation_y(s_a.ahl.4); + next.hand_r.position = Vec3::new(s_a.ahr.0, s_a.ahr.1, s_a.ahr.2); + next.hand_r.orientation = + Quaternion::rotation_x(s_a.ahr.3) * Quaternion::rotation_z(s_a.ahr.5); + + next.control.position = Vec3::new(s_a.ac.0, s_a.ac.1, s_a.ac.2); + next.control.orientation = Quaternion::rotation_x(s_a.ac.3) + * Quaternion::rotation_y(s_a.ac.4) + * Quaternion::rotation_z(s_a.ac.5 + move1 * -PI / 2.0 + move2 * -0.5); + + next.control.orientation.rotate_x(move1 * 0.0); + next.chest.orientation.rotate_x(move1 * -0.5); + next.chest.orientation.rotate_z(move1 * 0.7); + next.head.orientation.rotate_z(move1 * -0.3); + next.belt.orientation.rotate_z(move1 * -0.1); + next.shorts.orientation.rotate_z(move1 * -0.4); + + next.chest.orientation.rotate_z(move2 * -1.8); + next.head.orientation.rotate_z(move2 * 0.9); + next.shorts.orientation.rotate_z(move2 * 1.3); + next.belt.orientation.rotate_z(move2 * 0.6); + next.control.orientation.rotate_x(move2 * -0.9); + next.control.orientation.rotate_z(move2 * -3.5); + next.control.position += Vec3::new(move2 * 14.0, move2 * 6.0, 0.0); + }, _ => {}, }