From 8d88457434596ce81bb4393dc68570608afdb4db Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Sat, 17 Jul 2021 23:55:22 -0700 Subject: [PATCH 1/6] Restructured logic for staff AI --- .../common/entity/dungeon/tier-5/warlock.ron | 2 + .../common/skillset/dungeon/tier-5/staff.ron | 21 ++++++ .../skillset/dungeon/tier-5/warlock.ron | 5 ++ server/src/sys/agent.rs | 72 +++++++++++-------- 4 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 assets/common/skillset/dungeon/tier-5/staff.ron create mode 100644 assets/common/skillset/dungeon/tier-5/warlock.ron diff --git a/assets/common/entity/dungeon/tier-5/warlock.ron b/assets/common/entity/dungeon/tier-5/warlock.ron index 0ee439d4df..6caba2a001 100644 --- a/assets/common/entity/dungeon/tier-5/warlock.ron +++ b/assets/common/entity/dungeon/tier-5/warlock.ron @@ -11,4 +11,6 @@ EntityConfig ( ])), meta: [], + loadout_asset: None, + skillset_asset: Some("common.skillset.dungeon.tier-5.warlock"), ) diff --git a/assets/common/skillset/dungeon/tier-5/staff.ron b/assets/common/skillset/dungeon/tier-5/staff.ron new file mode 100644 index 0000000000..59d5e9ae24 --- /dev/null +++ b/assets/common/skillset/dungeon/tier-5/staff.ron @@ -0,0 +1,21 @@ +([ + Group(Weapon(Staff)), + + // Fireball + Skill((Staff(BDamage), Some(1))), + Skill((Staff(BRegen), Some(1))), + Skill((Staff(BRadius), Some(1))), + + // Flamethrower + Skill((Staff(FDamage), Some(1))), + Skill((Staff(FRange), Some(1))), + Skill((Staff(FDrain), Some(1))), + Skill((Staff(FVelocity), Some(1))), + + // Shockwave + Skill((Staff(UnlockShockwave), None)), + Skill((Staff(SDamage), Some(1))), + Skill((Staff(SKnockback), Some(1))), + Skill((Staff(SRange), Some(1))), + Skill((Staff(SCost), Some(1))), +]) diff --git a/assets/common/skillset/dungeon/tier-5/warlock.ron b/assets/common/skillset/dungeon/tier-5/warlock.ron new file mode 100644 index 0000000000..a8944845e0 --- /dev/null +++ b/assets/common/skillset/dungeon/tier-5/warlock.ron @@ -0,0 +1,5 @@ +([ + // Gather all warlock skills + Tree("common.skillset.dungeon.tier-5.bow"), + Tree("common.skillset.dungeon.tier-5.staff"), +]) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index a0ac28315e..fedd4597b0 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2443,40 +2443,37 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.in_min_range() { + //TODO: minimum energy values for skills and rolls are hard coded from + // approximate guesses + if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + && attack_data.in_min_range() + && self.energy.current() > 100 + { + // if a humanoid, have enough stamina, and in melee range, emergency roll controller .actions .push(ControlAction::basic_input(InputKind::Roll)); - } else if attack_data.dist_sqrd < (5.0 * attack_data.min_attack_dist).powi(2) - && attack_data.angle < 15.0 + } else if !matches!(self.char_state, CharacterState::Shockwave(c) if !matches!(c.stage_section, StageSection::Recover)) { - if agent.action_state.timer < 1.5 { - controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) - .xy() - .rotated_z(0.47 * PI) - .try_normalized() - .unwrap_or_else(Vec2::unit_y); - agent.action_state.timer += read_data.dt.0; - } else if agent.action_state.timer < 3.0 { - controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) - .xy() - .rotated_z(-0.47 * PI) - .try_normalized() - .unwrap_or_else(Vec2::unit_y); - agent.action_state.timer += read_data.dt.0; - } else { - agent.action_state.timer = 0.0; - } + // only try to use another ability if not already in recover and not casting + // shockwave + let target_approaching_speed = -agent + .target + .as_ref() + .map(|t| t.target) + .and_then(|e| read_data.velocities.get(e)) + .map_or(0.0, |v| v.0.dot(self.ori.look_vec())); if self .skill_set .has_skill(Skill::Staff(StaffSkill::UnlockShockwave)) - && self.energy.current() > 800 - && thread_rng().gen::() > 0.8 + && target_approaching_speed > 20.0 + && self.energy.current() > 200 { + // if enemy is closing distance quickly, use shockwave to knock back controller .actions .push(ControlAction::basic_input(InputKind::Ability(0))); - } else if self.energy.current() > 10 { + } else if self.energy.current() > 100 && attack_data.dist_sqrd < 280.0 { controller .actions .push(ControlAction::basic_input(InputKind::Secondary)); @@ -2485,7 +2482,26 @@ impl<'a> AgentData<'a> { .actions .push(ControlAction::basic_input(InputKind::Primary)); } + } + // Logic to move. Intentionally kept separate from ability logic so duplicated + // work is less necessary. + if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { + // Attempt to move away from target if too close + if let Some((bearing, speed)) = agent.chaser.chase( + &*read_data.terrain, + self.pos.0, + self.vel.0, + tgt_data.pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..self.traversal_config + }, + ) { + controller.inputs.move_dir = + -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed; + } } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { + // Else attempt to circle target if neither too close nor too far if let Some((bearing, speed)) = agent.chaser.chase( &*read_data.terrain, self.pos.0, @@ -2501,7 +2517,7 @@ impl<'a> AgentData<'a> { self.pos, tgt_data.pos, attack_data.dist_sqrd, - ) && attack_data.angle < 15.0 + ) && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() @@ -2509,25 +2525,25 @@ impl<'a> AgentData<'a> { .try_normalized() .unwrap_or_else(Vec2::zero) * speed; - controller - .actions - .push(ControlAction::basic_input(InputKind::Primary)); } else { + // Unless cannot see target, then move towards them controller.inputs.move_dir = bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed; self.jump_if(controller, bearing.z > 1.5); controller.inputs.move_z = bearing.z; } } + // Sometimes try to roll if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.dist_sqrd < 16.0f32.powi(2) - && thread_rng().gen::() < 0.02 + && thread_rng().gen::() < 0.01 { controller .actions .push(ControlAction::basic_input(InputKind::Roll)); } } else { + // If too far, move towards target self.path_toward_target(agent, controller, tgt_data, read_data, false, false, None); } } From 6913c54ab1f7a0ab5262c08e9afa5df7dd5adeab Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Wed, 21 Jul 2021 17:56:51 -0700 Subject: [PATCH 2/6] Allowed staff AI to queue shockwave if occupied by animation --- server/src/sys/agent.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index fedd4597b0..3b465692e4 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2445,14 +2445,30 @@ impl<'a> AgentData<'a> { ) { //TODO: minimum energy values for skills and rolls are hard coded from // approximate guesses + let mut flamethrower_range = 20.0_f32; + if let Ok(Some(level)) = self.skill_set.skill_level(Skill::Staff(StaffSkill::FRange)) { + flamethrower_range *= 1.25_f32.powi(level.into()); + } + let mut shockwave_cost = 600.0_f32; + if let Ok(Some(level)) = self.skill_set.skill_level(Skill::Staff(StaffSkill::SCost)) { + shockwave_cost *= 0.8_f32.powi(level.into()); + } if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.in_min_range() && self.energy.current() > 100 + && !matches!(self.char_state, CharacterState::Shockwave(_)) { // if a humanoid, have enough stamina, and in melee range, emergency roll controller .actions .push(ControlAction::basic_input(InputKind::Roll)); + } else if agent.action_state.condition + && matches!(self.char_state, CharacterState::Wielding) + { + controller + .actions + .push(ControlAction::basic_input(InputKind::Ability(0))); + agent.action_state.condition = false; } else if !matches!(self.char_state, CharacterState::Shockwave(c) if !matches!(c.stage_section, StageSection::Recover)) { // only try to use another ability if not already in recover and not casting @@ -2470,10 +2486,16 @@ impl<'a> AgentData<'a> { && self.energy.current() > 200 { // if enemy is closing distance quickly, use shockwave to knock back - controller - .actions - .push(ControlAction::basic_input(InputKind::Ability(0))); - } else if self.energy.current() > 100 && attack_data.dist_sqrd < 280.0 { + if matches!(self.char_state, CharacterState::Wielding) { + controller + .actions + .push(ControlAction::basic_input(InputKind::Ability(0))); + } else { + agent.action_state.condition = true; + } + } else if self.energy.current() as f32 > shockwave_cost + 100.0 + && attack_data.dist_sqrd < flamethrower_range.powi(2) + { controller .actions .push(ControlAction::basic_input(InputKind::Secondary)); From 7604c6794a8fcdb92badc0eeb7eaf31eedf251a4 Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Fri, 23 Jul 2021 17:59:32 -0700 Subject: [PATCH 3/6] Removed extra Warlock skills and made sceptre cultists rarer --- assets/common/entity/dungeon/tier-5/cultist.ron | 10 +++++----- assets/common/entity/dungeon/tier-5/warlock.ron | 2 -- assets/common/skillset/dungeon/tier-5/warlock.ron | 5 ----- 3 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 assets/common/skillset/dungeon/tier-5/warlock.ron diff --git a/assets/common/entity/dungeon/tier-5/cultist.ron b/assets/common/entity/dungeon/tier-5/cultist.ron index 88e29509b0..283fd9f054 100644 --- a/assets/common/entity/dungeon/tier-5/cultist.ron +++ b/assets/common/entity/dungeon/tier-5/cultist.ron @@ -6,11 +6,11 @@ EntityConfig ( loot: LootTable("common.loot_tables.dungeon.tier-5.enemy"), hands: TwoHanded(Choice([ - (1.0, Some(Item("common.items.weapons.axe_1h.orichalcum-0"))), - (2.0, Some(Item("common.items.weapons.sword.cultist"))), - (1.0, Some(Item("common.items.weapons.hammer.cultist_purp_2h-0"))), - (1.0, Some(Item("common.items.weapons.hammer_1h.orichalcum-0"))), - (1.0, Some(Item("common.items.weapons.bow.velorite"))), + (2.0, Some(Item("common.items.weapons.axe_1h.orichalcum-0"))), + (4.0, Some(Item("common.items.weapons.sword.cultist"))), + (2.0, Some(Item("common.items.weapons.hammer.cultist_purp_2h-0"))), + (2.0, Some(Item("common.items.weapons.hammer_1h.orichalcum-0"))), + (2.0, Some(Item("common.items.weapons.bow.velorite"))), (1.0, Some(Item("common.items.weapons.sceptre.sceptre_velorite_0"))), ])), diff --git a/assets/common/entity/dungeon/tier-5/warlock.ron b/assets/common/entity/dungeon/tier-5/warlock.ron index 6caba2a001..0ee439d4df 100644 --- a/assets/common/entity/dungeon/tier-5/warlock.ron +++ b/assets/common/entity/dungeon/tier-5/warlock.ron @@ -11,6 +11,4 @@ EntityConfig ( ])), meta: [], - loadout_asset: None, - skillset_asset: Some("common.skillset.dungeon.tier-5.warlock"), ) diff --git a/assets/common/skillset/dungeon/tier-5/warlock.ron b/assets/common/skillset/dungeon/tier-5/warlock.ron deleted file mode 100644 index a8944845e0..0000000000 --- a/assets/common/skillset/dungeon/tier-5/warlock.ron +++ /dev/null @@ -1,5 +0,0 @@ -([ - // Gather all warlock skills - Tree("common.skillset.dungeon.tier-5.bow"), - Tree("common.skillset.dungeon.tier-5.staff"), -]) From de55aef71d7781fc3112cc9aad82e69f01ef0b52 Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Tue, 27 Jul 2021 22:26:27 -0700 Subject: [PATCH 4/6] Used adjusted_by_skills to correctly calculate ability values --- server/src/sys/agent.rs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 3b465692e4..c0eb38100e 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -12,7 +12,7 @@ use common::{ inventory::{item::ItemTag, slot::EquipSlot}, invite::{InviteKind, InviteResponse}, item::{ - tool::{AbilitySpec, ToolKind}, + tool::{AbilityMap, AbilitySpec, ToolKind}, ConsumableKind, Item, ItemDesc, ItemKind, }, skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, @@ -2443,16 +2443,26 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - //TODO: minimum energy values for skills and rolls are hard coded from - // approximate guesses - let mut flamethrower_range = 20.0_f32; - if let Ok(Some(level)) = self.skill_set.skill_level(Skill::Staff(StaffSkill::FRange)) { - flamethrower_range *= 1.25_f32.powi(level.into()); - } - let mut shockwave_cost = 600.0_f32; - if let Ok(Some(level)) = self.skill_set.skill_level(Skill::Staff(StaffSkill::SCost)) { - shockwave_cost *= 0.8_f32.powi(level.into()); - } + let ability_map = AbilityMap::default(); + let ability_set = ability_map + .get_ability_set(&AbilitySpec::Tool(ToolKind::Staff)) + .unwrap() + .clone(); + let flamethrower = ability_set + .secondary + .adjusted_by_skills(self.skill_set, Some(ToolKind::Staff)); + let flamethrower_range = match flamethrower { + CharacterAbility::BasicBeam { range, .. } => range, + _ => 20.0_f32, + }; + let shockwave = ability_set.abilities[0] + .clone() + .1 + .adjusted_by_skills(self.skill_set, Some(ToolKind::Staff)); + let shockwave_cost = match shockwave { + CharacterAbility::Shockwave { energy_cost, .. } => energy_cost, + _ => 600.0_f32, + }; if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.in_min_range() && self.energy.current() > 100 From d12df2a88b127b51793c44d59250d984df7ba0cd Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Mon, 2 Aug 2021 16:22:22 -0700 Subject: [PATCH 5/6] Restyle logic in Staff AI --- server/src/sys/agent.rs | 55 ++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index c0eb38100e..b9f927165a 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -12,7 +12,7 @@ use common::{ inventory::{item::ItemTag, slot::EquipSlot}, invite::{InviteKind, InviteResponse}, item::{ - tool::{AbilityMap, AbilitySpec, ToolKind}, + tool::{AbilitySpec, ToolKind}, ConsumableKind, Item, ItemDesc, ItemKind, }, skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, @@ -2443,42 +2443,52 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - let ability_map = AbilityMap::default(); - let ability_set = ability_map - .get_ability_set(&AbilitySpec::Tool(ToolKind::Staff)) - .unwrap() - .clone(); - let flamethrower = ability_set - .secondary - .adjusted_by_skills(self.skill_set, Some(ToolKind::Staff)); + let extract_ability = |ability: &CharacterAbility| { + ability + .clone() + .adjusted_by_skills(self.skill_set, Some(ToolKind::Staff)) + }; + let (flamethrower, shockwave) = self + .inventory + .equipped(EquipSlot::ActiveMainhand) + .map(|i| &i.item_config_expect().abilities) + .map(|a| { + ( + Some(a.secondary.clone()), + a.abilities.get(0).map(|(_, s)| s), + ) + }) + .map_or( + (CharacterAbility::default(), CharacterAbility::default()), + |(s, a)| { + ( + extract_ability(&s.unwrap_or_default()), + extract_ability(a.unwrap_or(&CharacterAbility::default())), + ) + }, + ); let flamethrower_range = match flamethrower { CharacterAbility::BasicBeam { range, .. } => range, _ => 20.0_f32, }; - let shockwave = ability_set.abilities[0] - .clone() - .1 - .adjusted_by_skills(self.skill_set, Some(ToolKind::Staff)); - let shockwave_cost = match shockwave { - CharacterAbility::Shockwave { energy_cost, .. } => energy_cost, - _ => 600.0_f32, - }; - if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + let shockwave_cost = shockwave.get_energy_cost(); + if self.body.map_or(false, |b| b.is_humanoid()) && attack_data.in_min_range() - && self.energy.current() > 100 + && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() && !matches!(self.char_state, CharacterState::Shockwave(_)) { // if a humanoid, have enough stamina, and in melee range, emergency roll controller .actions .push(ControlAction::basic_input(InputKind::Roll)); + } else if matches!(self.char_state, CharacterState::Shockwave(_)) { + agent.action_state.condition = false; } else if agent.action_state.condition && matches!(self.char_state, CharacterState::Wielding) { controller .actions .push(ControlAction::basic_input(InputKind::Ability(0))); - agent.action_state.condition = false; } else if !matches!(self.char_state, CharacterState::Shockwave(c) if !matches!(c.stage_section, StageSection::Recover)) { // only try to use another ability if not already in recover and not casting @@ -2503,7 +2513,8 @@ impl<'a> AgentData<'a> { } else { agent.action_state.condition = true; } - } else if self.energy.current() as f32 > shockwave_cost + 100.0 + } else if self.energy.current() + > shockwave_cost + CharacterAbility::default_roll().get_energy_cost() && attack_data.dist_sqrd < flamethrower_range.powi(2) { controller @@ -2566,7 +2577,7 @@ impl<'a> AgentData<'a> { } } // Sometimes try to roll - if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + if self.body.map_or(false, |b| b.is_humanoid()) && attack_data.dist_sqrd < 16.0f32.powi(2) && thread_rng().gen::() < 0.01 { From f4bea280924401def59f70548f40ad088d1d6c5d Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Mon, 2 Aug 2021 22:38:16 -0700 Subject: [PATCH 6/6] Fix up comments relating to AI logic --- server/src/sys/agent.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index b9f927165a..64a8a0600f 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2477,7 +2477,8 @@ impl<'a> AgentData<'a> { && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() && !matches!(self.char_state, CharacterState::Shockwave(_)) { - // if a humanoid, have enough stamina, and in melee range, emergency roll + // if a humanoid, have enough stamina, not in shockwave, and in melee range, + // emergency roll controller .actions .push(ControlAction::basic_input(InputKind::Roll)); @@ -2491,8 +2492,7 @@ impl<'a> AgentData<'a> { .push(ControlAction::basic_input(InputKind::Ability(0))); } else if !matches!(self.char_state, CharacterState::Shockwave(c) if !matches!(c.stage_section, StageSection::Recover)) { - // only try to use another ability if not already in recover and not casting - // shockwave + // only try to use another ability unless in shockwave or recover let target_approaching_speed = -agent .target .as_ref() @@ -2502,8 +2502,8 @@ impl<'a> AgentData<'a> { if self .skill_set .has_skill(Skill::Staff(StaffSkill::UnlockShockwave)) - && target_approaching_speed > 20.0 - && self.energy.current() > 200 + && target_approaching_speed > 12.0 + && self.energy.current() > shockwave_cost { // if enemy is closing distance quickly, use shockwave to knock back if matches!(self.char_state, CharacterState::Wielding) { @@ -2579,7 +2579,8 @@ impl<'a> AgentData<'a> { // Sometimes try to roll if self.body.map_or(false, |b| b.is_humanoid()) && attack_data.dist_sqrd < 16.0f32.powi(2) - && thread_rng().gen::() < 0.01 + && !matches!(self.char_state, CharacterState::Shockwave(_)) + && thread_rng().gen::() < 0.02 { controller .actions