From 510591dee35cd5846b23223df55dd3eef8ba55c0 Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Tue, 6 Jul 2021 01:29:11 -0700 Subject: [PATCH 01/11] Added sceptre tactic and sceptre cultists --- .../common/entity/dungeon/tier-5/cultist.ron | 6 +- .../skillset/dungeon/tier-5/sceptre.ron | 21 +++ server/src/sys/agent.rs | 132 +++++++++++++++++- 3 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 assets/common/skillset/dungeon/tier-5/sceptre.ron diff --git a/assets/common/entity/dungeon/tier-5/cultist.ron b/assets/common/entity/dungeon/tier-5/cultist.ron index 8b19091d2d..6d26df32c2 100644 --- a/assets/common/entity/dungeon/tier-5/cultist.ron +++ b/assets/common/entity/dungeon/tier-5/cultist.ron @@ -5,11 +5,7 @@ EntityConfig ( loot: Some(LootTable("common.loot_tables.dungeon.tier-5.enemy")), main_tool: Some(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"))), + (1.0, Some(Item("common.items.weapons.sceptre.sceptre_velorite_0"))), ])), second_tool: None, diff --git a/assets/common/skillset/dungeon/tier-5/sceptre.ron b/assets/common/skillset/dungeon/tier-5/sceptre.ron new file mode 100644 index 0000000000..7900d41229 --- /dev/null +++ b/assets/common/skillset/dungeon/tier-5/sceptre.ron @@ -0,0 +1,21 @@ +([ + Group(Weapon(Sceptre)), + + // Beam + Skill((Sceptre(LDamage), Some(1))), + Skill((Sceptre(LRange), Some(1))), + Skill((Sceptre(LLifesteal), Some(1))), + Skill((Sceptre(LRegen), Some(1))), + + // Heal + Skill((Sceptre(HHeal), Some(1))), + Skill((Sceptre(HCost), Some(1))), + Skill((Sceptre(HRange), Some(1))), + + // Ward + Skill((Sceptre(UnlockAura), None)), + Skill((Sceptre(AStrength), Some(1))), + Skill((Sceptre(ADuration), Some(1))), + Skill((Sceptre(ARange), Some(1))), + Skill((Sceptre(ACost), Some(1))), +]) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f8696a0b87..b078f3189c 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -17,7 +17,7 @@ use common::{ tool::{AbilitySpec, ToolKind}, Item, ItemDesc, ItemKind, }, - skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill}, + skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill, SceptreSkill}, Agent, Alignment, BehaviorCapability, BehaviorState, Body, CharacterAbility, CharacterState, ControlAction, ControlEvent, Controller, Energy, Health, HealthChange, InputKind, Inventory, InventoryAction, LightEmitter, MountState, Ori, PhysicsState, Pos, @@ -99,6 +99,7 @@ pub enum Tactic { Sword, Bow, Staff, + Sceptre, StoneGolem, CircleCharge { radius: u32, circle_time: u32 }, QuadLowRanged, @@ -1613,6 +1614,7 @@ impl<'a> AgentData<'a> { let tool_tactic = |tool_kind| match tool_kind { ToolKind::Bow => Tactic::Bow, ToolKind::Staff => Tactic::Staff, + ToolKind::Sceptre => Tactic::Sceptre, ToolKind::Hammer => Tactic::Hammer, ToolKind::Sword | ToolKind::Spear => Tactic::Sword, ToolKind::Axe => Tactic::Axe, @@ -1630,6 +1632,7 @@ impl<'a> AgentData<'a> { "Axe Simple" | "Sword Simple" => Tactic::Sword, "Staff Simple" => Tactic::Staff, "Bow Simple" => Tactic::Bow, + "Sceptre Simple" => Tactic::Sceptre, "Stone Golem" => Tactic::StoneGolem, "Quad Med Quick" => Tactic::CircleCharge { radius: 3, @@ -1815,6 +1818,9 @@ impl<'a> AgentData<'a> { Tactic::Staff => { self.handle_staff_attack(agent, controller, &attack_data, &tgt_data, &read_data) }, + Tactic::Sceptre => { + self.handle_sceptre_attack(agent, controller, &attack_data, &tgt_data, &read_data) + }, Tactic::StoneGolem => self.handle_stone_golem_attack( agent, controller, @@ -2431,6 +2437,130 @@ impl<'a> AgentData<'a> { } } + fn handle_sceptre_attack( + &self, + agent: &mut Agent, + controller: &mut Controller, + attack_data: &AttackData, + tgt_data: &TargetData, + read_data: &ReadData, + ) { + const DESIRED_ENERGY_LEVEL: u32 = 500; + // Logic to use abilities + if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2) + && can_see_tgt( + &*read_data.terrain, + self.pos, + tgt_data.pos, + attack_data.dist_sqrd, + ) + { + // If far enough away, and can see target, attempt to shoot beam + if self.energy.current() < DESIRED_ENERGY_LEVEL { + // If low on energy, use primary to attempt to regen energy + controller + .actions + .push(ControlAction::basic_input(InputKind::Primary)); + } else if self + .skill_set + .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura)) + && self.energy.current() > 50 + && thread_rng().gen_bool(0.7) + { + // Use ward if target is far enough away and have sufficient energy + controller + .actions + .push(ControlAction::basic_input(InputKind::Ability(0))); + } + else { + // If at desired energy level but not able to ward, just attack + controller + .actions + .push(ControlAction::basic_input(InputKind::Primary)); + } + } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { + if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() + && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover)) + { + // Else roll away if can roll and have enough energy, and not using beam + // (other attacks have interrupt handled above) unless in recover + controller + .actions + .push(ControlAction::basic_input(InputKind::Roll)); + } else { + if attack_data.angle < 15.0 { + controller + .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, + self.vel.0, + tgt_data.pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..self.traversal_config + }, + ) { + if can_see_tgt( + &*read_data.terrain, + self.pos, + tgt_data.pos, + attack_data.dist_sqrd, + ) && attack_data.angle < 45.0 + { + controller.inputs.move_dir = bearing + .xy() + .rotated_z(thread_rng().gen_range(0.5..1.57)) + .try_normalized() + .unwrap_or_else(Vec2::zero) + * speed; + } 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.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, None); + } + } + fn handle_stone_golem_attack( &self, agent: &mut Agent, From 2d1799e6f1b240056319b9ea04c828376aff6870 Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Tue, 6 Jul 2021 01:29:11 -0700 Subject: [PATCH 02/11] Added sceptre tactic and sceptre cultists --- .../common/entity/dungeon/tier-5/cultist.ron | 6 +- .../common/skillset/dungeon/tier-5/enemy.ron | 1 + .../skillset/dungeon/tier-5/sceptre.ron | 21 +++ server/src/sys/agent.rs | 132 +++++++++++++++++- 4 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 assets/common/skillset/dungeon/tier-5/sceptre.ron diff --git a/assets/common/entity/dungeon/tier-5/cultist.ron b/assets/common/entity/dungeon/tier-5/cultist.ron index 8b19091d2d..6d26df32c2 100644 --- a/assets/common/entity/dungeon/tier-5/cultist.ron +++ b/assets/common/entity/dungeon/tier-5/cultist.ron @@ -5,11 +5,7 @@ EntityConfig ( loot: Some(LootTable("common.loot_tables.dungeon.tier-5.enemy")), main_tool: Some(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"))), + (1.0, Some(Item("common.items.weapons.sceptre.sceptre_velorite_0"))), ])), second_tool: None, diff --git a/assets/common/skillset/dungeon/tier-5/enemy.ron b/assets/common/skillset/dungeon/tier-5/enemy.ron index d0ee8c79f6..4adcb390e3 100644 --- a/assets/common/skillset/dungeon/tier-5/enemy.ron +++ b/assets/common/skillset/dungeon/tier-5/enemy.ron @@ -4,4 +4,5 @@ Tree("common.skillset.dungeon.tier-5.axe"), Tree("common.skillset.dungeon.tier-5.hammer"), Tree("common.skillset.dungeon.tier-5.bow"), + Tree("common.skillset.dungeon.tier-5.sceptre"), ]) diff --git a/assets/common/skillset/dungeon/tier-5/sceptre.ron b/assets/common/skillset/dungeon/tier-5/sceptre.ron new file mode 100644 index 0000000000..7900d41229 --- /dev/null +++ b/assets/common/skillset/dungeon/tier-5/sceptre.ron @@ -0,0 +1,21 @@ +([ + Group(Weapon(Sceptre)), + + // Beam + Skill((Sceptre(LDamage), Some(1))), + Skill((Sceptre(LRange), Some(1))), + Skill((Sceptre(LLifesteal), Some(1))), + Skill((Sceptre(LRegen), Some(1))), + + // Heal + Skill((Sceptre(HHeal), Some(1))), + Skill((Sceptre(HCost), Some(1))), + Skill((Sceptre(HRange), Some(1))), + + // Ward + Skill((Sceptre(UnlockAura), None)), + Skill((Sceptre(AStrength), Some(1))), + Skill((Sceptre(ADuration), Some(1))), + Skill((Sceptre(ARange), Some(1))), + Skill((Sceptre(ACost), Some(1))), +]) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f8696a0b87..925924cc3e 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -17,7 +17,7 @@ use common::{ tool::{AbilitySpec, ToolKind}, Item, ItemDesc, ItemKind, }, - skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill}, + skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill, SceptreSkill}, Agent, Alignment, BehaviorCapability, BehaviorState, Body, CharacterAbility, CharacterState, ControlAction, ControlEvent, Controller, Energy, Health, HealthChange, InputKind, Inventory, InventoryAction, LightEmitter, MountState, Ori, PhysicsState, Pos, @@ -99,6 +99,7 @@ pub enum Tactic { Sword, Bow, Staff, + Sceptre, StoneGolem, CircleCharge { radius: u32, circle_time: u32 }, QuadLowRanged, @@ -1613,6 +1614,7 @@ impl<'a> AgentData<'a> { let tool_tactic = |tool_kind| match tool_kind { ToolKind::Bow => Tactic::Bow, ToolKind::Staff => Tactic::Staff, + ToolKind::Sceptre => Tactic::Sceptre, ToolKind::Hammer => Tactic::Hammer, ToolKind::Sword | ToolKind::Spear => Tactic::Sword, ToolKind::Axe => Tactic::Axe, @@ -1630,6 +1632,7 @@ impl<'a> AgentData<'a> { "Axe Simple" | "Sword Simple" => Tactic::Sword, "Staff Simple" => Tactic::Staff, "Bow Simple" => Tactic::Bow, + "Sceptre Simple" => Tactic::Sceptre, "Stone Golem" => Tactic::StoneGolem, "Quad Med Quick" => Tactic::CircleCharge { radius: 3, @@ -1815,6 +1818,9 @@ impl<'a> AgentData<'a> { Tactic::Staff => { self.handle_staff_attack(agent, controller, &attack_data, &tgt_data, &read_data) }, + Tactic::Sceptre => { + self.handle_sceptre_attack(agent, controller, &attack_data, &tgt_data, &read_data) + }, Tactic::StoneGolem => self.handle_stone_golem_attack( agent, controller, @@ -2431,6 +2437,130 @@ impl<'a> AgentData<'a> { } } + fn handle_sceptre_attack( + &self, + agent: &mut Agent, + controller: &mut Controller, + attack_data: &AttackData, + tgt_data: &TargetData, + read_data: &ReadData, + ) { + const DESIRED_ENERGY_LEVEL: u32 = 500; + // Logic to use abilities + if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2) + && can_see_tgt( + &*read_data.terrain, + self.pos, + tgt_data.pos, + attack_data.dist_sqrd, + ) + { + // If far enough away, and can see target, attempt to shoot beam + if self.energy.current() < DESIRED_ENERGY_LEVEL { + // If low on energy, use primary to attempt to regen energy + controller + .actions + .push(ControlAction::basic_input(InputKind::Primary)); + } else if self + .skill_set + .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura)) + && self.energy.current() > 100 + && thread_rng().gen_bool(0.7) + { + // Use ward if target is far enough away and have sufficient energy + controller + .actions + .push(ControlAction::basic_input(InputKind::Ability(0))); + } + else { + // If at desired energy level but not able to ward, just attack + controller + .actions + .push(ControlAction::basic_input(InputKind::Primary)); + } + } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { + if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() + && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover)) + { + // Else roll away if can roll and have enough energy, and not using beam + // (other attacks have interrupt handled above) unless in recover + controller + .actions + .push(ControlAction::basic_input(InputKind::Roll)); + } else { + if attack_data.angle < 15.0 { + controller + .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, + self.vel.0, + tgt_data.pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..self.traversal_config + }, + ) { + if can_see_tgt( + &*read_data.terrain, + self.pos, + tgt_data.pos, + attack_data.dist_sqrd, + ) && attack_data.angle < 45.0 + { + controller.inputs.move_dir = bearing + .xy() + .rotated_z(thread_rng().gen_range(0.5..1.57)) + .try_normalized() + .unwrap_or_else(Vec2::zero) + * speed; + } 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.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, None); + } + } + fn handle_stone_golem_attack( &self, agent: &mut Agent, From 598d6594ff0efee4ca29bc1995dd53b2e0b86167 Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Tue, 6 Jul 2021 21:10:33 -0700 Subject: [PATCH 03/11] Add check to see if cultist already has ward buff --- server/src/sys/agent.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 925924cc3e..0869cceab5 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2455,25 +2455,27 @@ impl<'a> AgentData<'a> { attack_data.dist_sqrd, ) { - // If far enough away, and can see target, attempt to shoot beam - if self.energy.current() < DESIRED_ENERGY_LEVEL { - // If low on energy, use primary to attempt to regen energy - controller - .actions - .push(ControlAction::basic_input(InputKind::Primary)); - } else if self + // If far enough away, and can see target, check which skill is appropriate to use + if self .skill_set .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura)) - && self.energy.current() > 100 - && thread_rng().gen_bool(0.7) + && self.energy.current() > DESIRED_ENERGY_LEVEL + && !read_data.buffs.get(*self.entity) + .iter() + .any(|buff| buff.iter_kind(BuffKind::ProtectingWard) + .peekable() + .peek() + .is_some()) + && thread_rng().gen_bool(0.4) { - // Use ward if target is far enough away and have sufficient energy + // Use ward if target is far enough away, self is not buffed, and have sufficient energy controller .actions .push(ControlAction::basic_input(InputKind::Ability(0))); } else { - // If at desired energy level but not able to ward, just attack + // If low on energy, use primary to attempt to regen energy + // Or if at desired energy level but not able to ward, just attack controller .actions .push(ControlAction::basic_input(InputKind::Primary)); From 08cc63c19a6ceb593ebcff96a66b325f872356d6 Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Wed, 7 Jul 2021 17:06:02 -0700 Subject: [PATCH 04/11] Fixed formatting --- server/src/sys/agent.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 0869cceab5..3d03362f80 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -17,7 +17,7 @@ use common::{ tool::{AbilitySpec, ToolKind}, Item, ItemDesc, ItemKind, }, - skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill, SceptreSkill}, + skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, Agent, Alignment, BehaviorCapability, BehaviorState, Body, CharacterAbility, CharacterState, ControlAction, ControlEvent, Controller, Energy, Health, HealthChange, InputKind, Inventory, InventoryAction, LightEmitter, MountState, Ori, PhysicsState, Pos, @@ -2455,27 +2455,28 @@ impl<'a> AgentData<'a> { attack_data.dist_sqrd, ) { - // If far enough away, and can see target, check which skill is appropriate to use + // If far enough away, and can see target, check which skill is appropriate to + // use if self .skill_set .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura)) && self.energy.current() > DESIRED_ENERGY_LEVEL - && !read_data.buffs.get(*self.entity) - .iter() - .any(|buff| buff.iter_kind(BuffKind::ProtectingWard) + && !read_data.buffs.get(*self.entity).iter().any(|buff| { + buff.iter_kind(BuffKind::ProtectingWard) .peekable() .peek() - .is_some()) + .is_some() + }) && thread_rng().gen_bool(0.4) { - // Use ward if target is far enough away, self is not buffed, and have sufficient energy + // Use ward if target is far enough away, self is not buffed, and have + // sufficient energy controller .actions .push(ControlAction::basic_input(InputKind::Ability(0))); - } - else { + } else { // If low on energy, use primary to attempt to regen energy - // Or if at desired energy level but not able to ward, just attack + // Or if at desired energy level but not able/willing to ward, just attack controller .actions .push(ControlAction::basic_input(InputKind::Primary)); @@ -2485,17 +2486,15 @@ impl<'a> AgentData<'a> { && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover)) { - // Else roll away if can roll and have enough energy, and not using beam - // (other attacks have interrupt handled above) unless in recover + // Else roll away if can roll and have enough energy, and not using beam or in + // recover controller .actions .push(ControlAction::basic_input(InputKind::Roll)); - } else { - if attack_data.angle < 15.0 { - controller - .actions - .push(ControlAction::basic_input(InputKind::Primary)); - } + } else if attack_data.angle < 15.0 { + controller + .actions + .push(ControlAction::basic_input(InputKind::Primary)); } } // Logic to move. Intentionally kept separate from ability logic so duplicated From abe2e4d2eaab1888358cd1aea8cc27553110086e Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Wed, 7 Jul 2021 17:56:47 -0700 Subject: [PATCH 05/11] Clean up sceptre ai code before merge --- assets/common/entity/dungeon/tier-5/cultist.ron | 5 +++++ server/src/sys/agent.rs | 6 ++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/assets/common/entity/dungeon/tier-5/cultist.ron b/assets/common/entity/dungeon/tier-5/cultist.ron index 6d26df32c2..b8e7996861 100644 --- a/assets/common/entity/dungeon/tier-5/cultist.ron +++ b/assets/common/entity/dungeon/tier-5/cultist.ron @@ -5,6 +5,11 @@ EntityConfig ( loot: Some(LootTable("common.loot_tables.dungeon.tier-5.enemy")), main_tool: Some(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"))), (1.0, Some(Item("common.items.weapons.sceptre.sceptre_velorite_0"))), ])), second_tool: None, diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 3d03362f80..f7ae3302d0 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -1632,7 +1632,6 @@ impl<'a> AgentData<'a> { "Axe Simple" | "Sword Simple" => Tactic::Sword, "Staff Simple" => Tactic::Staff, "Bow Simple" => Tactic::Bow, - "Sceptre Simple" => Tactic::Sceptre, "Stone Golem" => Tactic::StoneGolem, "Quad Med Quick" => Tactic::CircleCharge { radius: 3, @@ -2467,7 +2466,6 @@ impl<'a> AgentData<'a> { .peek() .is_some() }) - && thread_rng().gen_bool(0.4) { // Use ward if target is far enough away, self is not buffed, and have // sufficient energy @@ -2482,11 +2480,11 @@ impl<'a> AgentData<'a> { .push(ControlAction::basic_input(InputKind::Primary)); } } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { - if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + if self.body.map_or(false, |b| b.is_humanoid()) && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover)) { - // Else roll away if can roll and have enough energy, and not using beam or in + // Else roll away if can roll and have enough energy, and not using aura or in // recover controller .actions From 4aa623d6c2aa0ed37241aad942aa0ffeb7b8394c Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Wed, 7 Jul 2021 19:21:30 -0700 Subject: [PATCH 06/11] Added check to prevent sceptre AI from canceling its own ward --- CHANGELOG.md | 1 + server/src/sys/agent.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df44305925..4a57352297 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ability to toggle chat visibility - Added gem rings with various stat improvements. - Animations for using consumables. +- AI for sceptre weilders and sceptre cultists in Tier 5 dungeons ### Changed diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f7ae3302d0..711fdc5d83 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2495,8 +2495,8 @@ impl<'a> AgentData<'a> { .push(ControlAction::basic_input(InputKind::Primary)); } } - // Logic to move. Intentionally kept separate from ability logic so duplicated - // work is less necessary. + // Logic to move. Intentionally kept separate from ability logic where possible + // 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( @@ -2547,6 +2547,7 @@ impl<'a> AgentData<'a> { } // Sometimes try to roll if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + && !matches!(self.char_state, CharacterState::BasicAura(_)) && attack_data.dist_sqrd < 16.0f32.powi(2) && thread_rng().gen::() < 0.01 { From 35242c71e1e81ff856ee91fc83ff8f843bb95ec7 Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Tue, 6 Jul 2021 01:29:11 -0700 Subject: [PATCH 07/11] Added sceptre tactic and sceptre cultists --- .../common/entity/dungeon/tier-5/cultist.ron | 6 +- .../common/skillset/dungeon/tier-5/enemy.ron | 1 + .../skillset/dungeon/tier-5/sceptre.ron | 21 +++ server/src/sys/agent.rs | 132 +++++++++++++++++- 4 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 assets/common/skillset/dungeon/tier-5/sceptre.ron diff --git a/assets/common/entity/dungeon/tier-5/cultist.ron b/assets/common/entity/dungeon/tier-5/cultist.ron index 8b19091d2d..6d26df32c2 100644 --- a/assets/common/entity/dungeon/tier-5/cultist.ron +++ b/assets/common/entity/dungeon/tier-5/cultist.ron @@ -5,11 +5,7 @@ EntityConfig ( loot: Some(LootTable("common.loot_tables.dungeon.tier-5.enemy")), main_tool: Some(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"))), + (1.0, Some(Item("common.items.weapons.sceptre.sceptre_velorite_0"))), ])), second_tool: None, diff --git a/assets/common/skillset/dungeon/tier-5/enemy.ron b/assets/common/skillset/dungeon/tier-5/enemy.ron index d0ee8c79f6..4adcb390e3 100644 --- a/assets/common/skillset/dungeon/tier-5/enemy.ron +++ b/assets/common/skillset/dungeon/tier-5/enemy.ron @@ -4,4 +4,5 @@ Tree("common.skillset.dungeon.tier-5.axe"), Tree("common.skillset.dungeon.tier-5.hammer"), Tree("common.skillset.dungeon.tier-5.bow"), + Tree("common.skillset.dungeon.tier-5.sceptre"), ]) diff --git a/assets/common/skillset/dungeon/tier-5/sceptre.ron b/assets/common/skillset/dungeon/tier-5/sceptre.ron new file mode 100644 index 0000000000..7900d41229 --- /dev/null +++ b/assets/common/skillset/dungeon/tier-5/sceptre.ron @@ -0,0 +1,21 @@ +([ + Group(Weapon(Sceptre)), + + // Beam + Skill((Sceptre(LDamage), Some(1))), + Skill((Sceptre(LRange), Some(1))), + Skill((Sceptre(LLifesteal), Some(1))), + Skill((Sceptre(LRegen), Some(1))), + + // Heal + Skill((Sceptre(HHeal), Some(1))), + Skill((Sceptre(HCost), Some(1))), + Skill((Sceptre(HRange), Some(1))), + + // Ward + Skill((Sceptre(UnlockAura), None)), + Skill((Sceptre(AStrength), Some(1))), + Skill((Sceptre(ADuration), Some(1))), + Skill((Sceptre(ARange), Some(1))), + Skill((Sceptre(ACost), Some(1))), +]) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f8696a0b87..925924cc3e 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -17,7 +17,7 @@ use common::{ tool::{AbilitySpec, ToolKind}, Item, ItemDesc, ItemKind, }, - skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill}, + skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill, SceptreSkill}, Agent, Alignment, BehaviorCapability, BehaviorState, Body, CharacterAbility, CharacterState, ControlAction, ControlEvent, Controller, Energy, Health, HealthChange, InputKind, Inventory, InventoryAction, LightEmitter, MountState, Ori, PhysicsState, Pos, @@ -99,6 +99,7 @@ pub enum Tactic { Sword, Bow, Staff, + Sceptre, StoneGolem, CircleCharge { radius: u32, circle_time: u32 }, QuadLowRanged, @@ -1613,6 +1614,7 @@ impl<'a> AgentData<'a> { let tool_tactic = |tool_kind| match tool_kind { ToolKind::Bow => Tactic::Bow, ToolKind::Staff => Tactic::Staff, + ToolKind::Sceptre => Tactic::Sceptre, ToolKind::Hammer => Tactic::Hammer, ToolKind::Sword | ToolKind::Spear => Tactic::Sword, ToolKind::Axe => Tactic::Axe, @@ -1630,6 +1632,7 @@ impl<'a> AgentData<'a> { "Axe Simple" | "Sword Simple" => Tactic::Sword, "Staff Simple" => Tactic::Staff, "Bow Simple" => Tactic::Bow, + "Sceptre Simple" => Tactic::Sceptre, "Stone Golem" => Tactic::StoneGolem, "Quad Med Quick" => Tactic::CircleCharge { radius: 3, @@ -1815,6 +1818,9 @@ impl<'a> AgentData<'a> { Tactic::Staff => { self.handle_staff_attack(agent, controller, &attack_data, &tgt_data, &read_data) }, + Tactic::Sceptre => { + self.handle_sceptre_attack(agent, controller, &attack_data, &tgt_data, &read_data) + }, Tactic::StoneGolem => self.handle_stone_golem_attack( agent, controller, @@ -2431,6 +2437,130 @@ impl<'a> AgentData<'a> { } } + fn handle_sceptre_attack( + &self, + agent: &mut Agent, + controller: &mut Controller, + attack_data: &AttackData, + tgt_data: &TargetData, + read_data: &ReadData, + ) { + const DESIRED_ENERGY_LEVEL: u32 = 500; + // Logic to use abilities + if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2) + && can_see_tgt( + &*read_data.terrain, + self.pos, + tgt_data.pos, + attack_data.dist_sqrd, + ) + { + // If far enough away, and can see target, attempt to shoot beam + if self.energy.current() < DESIRED_ENERGY_LEVEL { + // If low on energy, use primary to attempt to regen energy + controller + .actions + .push(ControlAction::basic_input(InputKind::Primary)); + } else if self + .skill_set + .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura)) + && self.energy.current() > 100 + && thread_rng().gen_bool(0.7) + { + // Use ward if target is far enough away and have sufficient energy + controller + .actions + .push(ControlAction::basic_input(InputKind::Ability(0))); + } + else { + // If at desired energy level but not able to ward, just attack + controller + .actions + .push(ControlAction::basic_input(InputKind::Primary)); + } + } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { + if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() + && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover)) + { + // Else roll away if can roll and have enough energy, and not using beam + // (other attacks have interrupt handled above) unless in recover + controller + .actions + .push(ControlAction::basic_input(InputKind::Roll)); + } else { + if attack_data.angle < 15.0 { + controller + .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, + self.vel.0, + tgt_data.pos.0, + TraversalConfig { + min_tgt_dist: 1.25, + ..self.traversal_config + }, + ) { + if can_see_tgt( + &*read_data.terrain, + self.pos, + tgt_data.pos, + attack_data.dist_sqrd, + ) && attack_data.angle < 45.0 + { + controller.inputs.move_dir = bearing + .xy() + .rotated_z(thread_rng().gen_range(0.5..1.57)) + .try_normalized() + .unwrap_or_else(Vec2::zero) + * speed; + } 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.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, None); + } + } + fn handle_stone_golem_attack( &self, agent: &mut Agent, From ba35aaaf9000e7e3b30abcc2fc0fbc6d99fb6b2e Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Tue, 6 Jul 2021 21:10:33 -0700 Subject: [PATCH 08/11] Add check to see if cultist already has ward buff --- server/src/sys/agent.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 925924cc3e..0869cceab5 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2455,25 +2455,27 @@ impl<'a> AgentData<'a> { attack_data.dist_sqrd, ) { - // If far enough away, and can see target, attempt to shoot beam - if self.energy.current() < DESIRED_ENERGY_LEVEL { - // If low on energy, use primary to attempt to regen energy - controller - .actions - .push(ControlAction::basic_input(InputKind::Primary)); - } else if self + // If far enough away, and can see target, check which skill is appropriate to use + if self .skill_set .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura)) - && self.energy.current() > 100 - && thread_rng().gen_bool(0.7) + && self.energy.current() > DESIRED_ENERGY_LEVEL + && !read_data.buffs.get(*self.entity) + .iter() + .any(|buff| buff.iter_kind(BuffKind::ProtectingWard) + .peekable() + .peek() + .is_some()) + && thread_rng().gen_bool(0.4) { - // Use ward if target is far enough away and have sufficient energy + // Use ward if target is far enough away, self is not buffed, and have sufficient energy controller .actions .push(ControlAction::basic_input(InputKind::Ability(0))); } else { - // If at desired energy level but not able to ward, just attack + // If low on energy, use primary to attempt to regen energy + // Or if at desired energy level but not able to ward, just attack controller .actions .push(ControlAction::basic_input(InputKind::Primary)); From 63c5d9f7dfbaa4102c36fb57a3d5d909ad5ecfda Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Wed, 7 Jul 2021 17:06:02 -0700 Subject: [PATCH 09/11] Fixed formatting --- server/src/sys/agent.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 0869cceab5..3d03362f80 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -17,7 +17,7 @@ use common::{ tool::{AbilitySpec, ToolKind}, Item, ItemDesc, ItemKind, }, - skills::{AxeSkill, BowSkill, HammerSkill, Skill, StaffSkill, SwordSkill, SceptreSkill}, + skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, Agent, Alignment, BehaviorCapability, BehaviorState, Body, CharacterAbility, CharacterState, ControlAction, ControlEvent, Controller, Energy, Health, HealthChange, InputKind, Inventory, InventoryAction, LightEmitter, MountState, Ori, PhysicsState, Pos, @@ -2455,27 +2455,28 @@ impl<'a> AgentData<'a> { attack_data.dist_sqrd, ) { - // If far enough away, and can see target, check which skill is appropriate to use + // If far enough away, and can see target, check which skill is appropriate to + // use if self .skill_set .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura)) && self.energy.current() > DESIRED_ENERGY_LEVEL - && !read_data.buffs.get(*self.entity) - .iter() - .any(|buff| buff.iter_kind(BuffKind::ProtectingWard) + && !read_data.buffs.get(*self.entity).iter().any(|buff| { + buff.iter_kind(BuffKind::ProtectingWard) .peekable() .peek() - .is_some()) + .is_some() + }) && thread_rng().gen_bool(0.4) { - // Use ward if target is far enough away, self is not buffed, and have sufficient energy + // Use ward if target is far enough away, self is not buffed, and have + // sufficient energy controller .actions .push(ControlAction::basic_input(InputKind::Ability(0))); - } - else { + } else { // If low on energy, use primary to attempt to regen energy - // Or if at desired energy level but not able to ward, just attack + // Or if at desired energy level but not able/willing to ward, just attack controller .actions .push(ControlAction::basic_input(InputKind::Primary)); @@ -2485,17 +2486,15 @@ impl<'a> AgentData<'a> { && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover)) { - // Else roll away if can roll and have enough energy, and not using beam - // (other attacks have interrupt handled above) unless in recover + // Else roll away if can roll and have enough energy, and not using beam or in + // recover controller .actions .push(ControlAction::basic_input(InputKind::Roll)); - } else { - if attack_data.angle < 15.0 { - controller - .actions - .push(ControlAction::basic_input(InputKind::Primary)); - } + } else if attack_data.angle < 15.0 { + controller + .actions + .push(ControlAction::basic_input(InputKind::Primary)); } } // Logic to move. Intentionally kept separate from ability logic so duplicated From 65d67ef781babe02d11feb658434807e79c78ff8 Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Wed, 7 Jul 2021 17:56:47 -0700 Subject: [PATCH 10/11] Clean up sceptre ai code before merge --- assets/common/entity/dungeon/tier-5/cultist.ron | 5 +++++ server/src/sys/agent.rs | 6 ++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/assets/common/entity/dungeon/tier-5/cultist.ron b/assets/common/entity/dungeon/tier-5/cultist.ron index 6d26df32c2..b8e7996861 100644 --- a/assets/common/entity/dungeon/tier-5/cultist.ron +++ b/assets/common/entity/dungeon/tier-5/cultist.ron @@ -5,6 +5,11 @@ EntityConfig ( loot: Some(LootTable("common.loot_tables.dungeon.tier-5.enemy")), main_tool: Some(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"))), (1.0, Some(Item("common.items.weapons.sceptre.sceptre_velorite_0"))), ])), second_tool: None, diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 3d03362f80..f7ae3302d0 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -1632,7 +1632,6 @@ impl<'a> AgentData<'a> { "Axe Simple" | "Sword Simple" => Tactic::Sword, "Staff Simple" => Tactic::Staff, "Bow Simple" => Tactic::Bow, - "Sceptre Simple" => Tactic::Sceptre, "Stone Golem" => Tactic::StoneGolem, "Quad Med Quick" => Tactic::CircleCharge { radius: 3, @@ -2467,7 +2466,6 @@ impl<'a> AgentData<'a> { .peek() .is_some() }) - && thread_rng().gen_bool(0.4) { // Use ward if target is far enough away, self is not buffed, and have // sufficient energy @@ -2482,11 +2480,11 @@ impl<'a> AgentData<'a> { .push(ControlAction::basic_input(InputKind::Primary)); } } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { - if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + if self.body.map_or(false, |b| b.is_humanoid()) && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover)) { - // Else roll away if can roll and have enough energy, and not using beam or in + // Else roll away if can roll and have enough energy, and not using aura or in // recover controller .actions From d0bb7004abea9737c46f904b63a35ec5fe60399b Mon Sep 17 00:00:00 2001 From: Knightress Paladin Date: Wed, 7 Jul 2021 19:21:30 -0700 Subject: [PATCH 11/11] Added check to prevent sceptre AI from canceling its own ward --- CHANGELOG.md | 1 + server/src/sys/agent.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0324bf1a1f..14c6a72f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Animations for using consumables. - New danari character customizations - Bald hairstyles for humans and danari +- AI for sceptre weilders and sceptre cultists in Tier 5 dungeons ### Changed diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f7ae3302d0..711fdc5d83 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2495,8 +2495,8 @@ impl<'a> AgentData<'a> { .push(ControlAction::basic_input(InputKind::Primary)); } } - // Logic to move. Intentionally kept separate from ability logic so duplicated - // work is less necessary. + // Logic to move. Intentionally kept separate from ability logic where possible + // 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( @@ -2547,6 +2547,7 @@ impl<'a> AgentData<'a> { } // Sometimes try to roll if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + && !matches!(self.char_state, CharacterState::BasicAura(_)) && attack_data.dist_sqrd < 16.0f32.powi(2) && thread_rng().gen::() < 0.01 {