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/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/server/src/sys/agent.rs b/server/src/sys/agent.rs index a0ac28315e..64a8a0600f 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2443,40 +2443,80 @@ 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() { + 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_cost = shockwave.get_energy_cost(); + if self.body.map_or(false, |b| b.is_humanoid()) + && attack_data.in_min_range() + && self.energy.current() > CharacterAbility::default_roll().get_energy_cost() + && !matches!(self.char_state, CharacterState::Shockwave(_)) + { + // if a humanoid, have enough stamina, not in shockwave, 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(_)) { + agent.action_state.condition = false; + } else if agent.action_state.condition + && matches!(self.char_state, CharacterState::Wielding) { - 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; - } + controller + .actions + .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 unless in shockwave or recover + 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 > 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) { + controller + .actions + .push(ControlAction::basic_input(InputKind::Ability(0))); + } else { + agent.action_state.condition = true; + } + } else if self.energy.current() + > shockwave_cost + CharacterAbility::default_roll().get_energy_cost() + && attack_data.dist_sqrd < flamethrower_range.powi(2) { - controller - .actions - .push(ControlAction::basic_input(InputKind::Ability(0))); - } else if self.energy.current() > 10 { controller .actions .push(ControlAction::basic_input(InputKind::Secondary)); @@ -2485,7 +2525,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 +2560,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,18 +2568,18 @@ 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; } } - if self.body.map(|b| b.is_humanoid()).unwrap_or(false) + // Sometimes try to roll + if self.body.map_or(false, |b| b.is_humanoid()) && attack_data.dist_sqrd < 16.0f32.powi(2) + && !matches!(self.char_state, CharacterState::Shockwave(_)) && thread_rng().gen::() < 0.02 { controller @@ -2528,6 +2587,7 @@ impl<'a> AgentData<'a> { .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); } }