use crate::{ consts::MAX_PATH_DIST, data::*, util::{entities_have_line_of_sight, handle_attack_aggression}, }; use common::{ comp::{ ability::{ActiveAbilities, AuxiliaryAbility, Stance, SwordStance, BASE_ABILITY_LIMIT}, buff::BuffKind, item::tool::AbilityContext, skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill}, Ability, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction, ControlEvent, Controller, Fluid, InputKind, }, path::TraversalConfig, states::{ self_buff, sprite_summon::{self, SpriteSummonAnchor}, utils::StageSection, }, terrain::Block, util::Dir, vol::ReadVol, }; use rand::{prelude::SliceRandom, Rng}; use std::{f32::consts::PI, time::Duration}; use vek::*; impl<'a> AgentData<'a> { // Intended for any agent that has one attack, that attack is a melee attack, // and the agent is able to freely walk around pub fn handle_simple_melee( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { if attack_data.in_min_range() && attack_data.angle < 30.0 { controller.push_basic_input(InputKind::Primary); controller.inputs.move_dir = Vec2::zero(); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Full, None, ); if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.dist_sqrd < 16.0f32.powi(2) && rng.gen::() < 0.02 { controller.push_basic_input(InputKind::Roll); } } } // Intended for any agent that has one attack, that attack is a melee attack, // and the agent is able to freely fly around pub fn handle_simple_flying_melee( &self, _agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, _rng: &mut impl Rng, ) { // Fly to target let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0) .try_normalized() .unwrap_or_else(Vec3::zero); let speed = 1.0; controller.inputs.move_dir = dir_to_target.xy() * speed; // Always fly! If the floor can't touch you, it can't hurt you... controller.push_basic_input(InputKind::Fly); // Flee from the ground! The internet told me it was lava! // If on the ground, jump with every last ounce of energy, holding onto // all that is dear in life and straining for the wide open skies. if self.physics_state.on_ground.is_some() { controller.push_basic_input(InputKind::Jump); } else { // Use a proportional controller with a coefficient of 1.0 to // maintain altidude at the the provided set point let mut maintain_altitude = |set_point| { let alt = read_data .terrain .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0)) .until(Block::is_solid) .cast() .0; let error = set_point - alt; controller.inputs.move_z = error; }; if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) { maintain_altitude(5.0); } else { maintain_altitude(2.0); // Attack if in range if attack_data.dist_sqrd < 3.5_f32.powi(2) && attack_data.angle < 150.0 { controller.push_basic_input(InputKind::Primary); } } } } // Intended for any agent that has one attack, that attack is a melee attack, // the agent is able to freely walk around, and the agent is trying to attack // from behind its target pub fn handle_simple_backstab( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { // Handle attacking of agent if attack_data.in_min_range() && attack_data.angle < 30.0 { controller.push_basic_input(InputKind::Primary); controller.inputs.move_dir = Vec2::zero(); } // Handle movement of agent let target_ori = agent .target .and_then(|t| read_data.orientations.get(t.target)) .map(|ori| ori.look_vec()) .unwrap_or_default(); let dist = attack_data.dist_sqrd.sqrt(); let in_front_of_target = target_ori.dot(self.pos.0 - tgt_data.pos.0) > 0.0; if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { // If in front of the target, circle to try and get behind, else just make a // beeline for the back of the agent let vec_to_target = (tgt_data.pos.0 - self.pos.0).xy(); if in_front_of_target { let theta = (PI / 2. - dist * 0.1).max(0.0); // Checks both CW and CCW rotation let potential_move_dirs = [ vec_to_target .rotated_z(theta) .try_normalized() .unwrap_or_default(), vec_to_target .rotated_z(-theta) .try_normalized() .unwrap_or_default(), ]; // Finds shortest path to get behind if let Some(move_dir) = potential_move_dirs .iter() .find(|move_dir| target_ori.xy().dot(**move_dir) < 0.0) { controller.inputs.move_dir = *move_dir; } } else { // Aim for a point a given distance behind the target to prevent sideways // movement let move_target = tgt_data.pos.0.xy() - dist / 2. * target_ori.xy(); controller.inputs.move_dir = (move_target - self.pos.0) .try_normalized() .unwrap_or_default(); } } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Full, None, ); } } pub fn handle_elevated_ranged( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) }; let elevation = self.pos.0.z - tgt_data.pos.0.z; const PREF_DIST: f32 = 30_f32; if attack_data.angle_xy < 30.0 && (elevation > 10.0 || attack_data.dist_sqrd > PREF_DIST.powi(2)) && line_of_sight_with_target() { controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < (PREF_DIST / 2.).powi(2) { // Attempt to move quickly away from target if too close if let Some((bearing, _)) = 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 }, ) { let flee_dir = -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero); let pos = self.pos.0.xy().with_z(self.pos.0.z + 1.5); if read_data .terrain .ray(pos, pos + flee_dir * 2.0) .until(|b| b.is_solid() || b.get_sprite().is_none()) .cast() .0 > 1.0 { // If able to flee, flee controller.inputs.move_dir = flee_dir; if !self.char_state.is_attack() { controller.inputs.look_dir = -controller.inputs.look_dir; } } else { // Otherwise, fight to the death controller.push_basic_input(InputKind::Primary); } } } else if attack_data.dist_sqrd < PREF_DIST.powi(2) { // Attempt to move away from target if too close, while still attacking if let Some((bearing, _)) = 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 line_of_sight_with_target() { controller.push_basic_input(InputKind::Primary); } controller.inputs.move_dir = -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero); } } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Full, None, ); } } pub fn handle_hammer_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { enum ActionStateTimers { TimerHandleHammerAttack = 0, } let has_leap = || { self.skill_set .has_skill(Skill::Hammer(HammerSkill::UnlockLeap)) }; let has_energy = |need| self.energy.current() > need; let use_leap = |controller: &mut Controller| { controller.push_basic_input(InputKind::Ability(0)); }; if attack_data.in_min_range() && attack_data.angle < 45.0 { controller.inputs.move_dir = Vec2::zero(); if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 4.0 { controller.push_cancel_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] = 0.0; } else if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 3.0 { controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] += read_data.dt.0; } else if has_leap() && has_energy(50.0) && rng.gen_bool(0.9) { use_leap(controller); agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] += read_data.dt.0; } else { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] += read_data.dt.0; } } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); if attack_data.dist_sqrd < 32.0f32.powi(2) && has_leap() && has_energy(50.0) && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { use_leap(controller); } if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.dist_sqrd < 16.0f32.powi(2) && rng.gen::() < 0.02 { controller.push_basic_input(InputKind::Roll); } } } pub fn handle_sword_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { if !agent.combat_state.initialized { agent.combat_state.initialized = true; let available_tactics = { let mut tactics = Vec::new(); let try_tactic = |skill, tactic, tactics: &mut Vec| { if self.skill_set.has_skill(Skill::Sword(skill)) { tactics.push(tactic); } }; try_tactic( SwordSkill::HeavyFortitude, SwordTactics::HeavyAdvanced, &mut tactics, ); try_tactic( SwordSkill::AgileDancingEdge, SwordTactics::AgileAdvanced, &mut tactics, ); try_tactic( SwordSkill::DefensiveStalwartSword, SwordTactics::DefensiveAdvanced, &mut tactics, ); try_tactic( SwordSkill::CripplingEviscerate, SwordTactics::CripplingAdvanced, &mut tactics, ); try_tactic( SwordSkill::CleavingBladeFever, SwordTactics::CleavingAdvanced, &mut tactics, ); if tactics.is_empty() { try_tactic( SwordSkill::HeavySweep, SwordTactics::HeavySimple, &mut tactics, ); try_tactic( SwordSkill::AgileQuickDraw, SwordTactics::AgileSimple, &mut tactics, ); try_tactic( SwordSkill::DefensiveDisengage, SwordTactics::DefensiveSimple, &mut tactics, ); try_tactic( SwordSkill::CripplingGouge, SwordTactics::CripplingSimple, &mut tactics, ); try_tactic( SwordSkill::CleavingWhirlwindSlice, SwordTactics::CleavingSimple, &mut tactics, ); } if tactics.is_empty() { try_tactic(SwordSkill::CrescentSlash, SwordTactics::Basic, &mut tactics); } if tactics.is_empty() { tactics.push(SwordTactics::Unskilled); } tactics }; let tactic = available_tactics .choose(rng) .copied() .unwrap_or(SwordTactics::Unskilled); agent.combat_state.int_counters[IntCounters::Tactics as usize] = tactic as u8; let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory)); let set_sword_ability = |controller: &mut Controller, slot, skill| { controller.push_event(ControlEvent::ChangeAbility { slot, auxiliary_key, new_ability: AuxiliaryAbility::MainWeapon(skill), }); }; match tactic { SwordTactics::Unskilled => {}, SwordTactics::Basic => { // Crescent slash set_sword_ability(controller, 0, 0); // Fell strike set_sword_ability(controller, 1, 1); // Skewer set_sword_ability(controller, 2, 2); // Cascade set_sword_ability(controller, 3, 3); // Cross cut set_sword_ability(controller, 4, 4); }, SwordTactics::HeavySimple => { // Finisher set_sword_ability(controller, 0, 5); // Crescent slash set_sword_ability(controller, 1, 0); // Cascade set_sword_ability(controller, 2, 3); // Windmill slash set_sword_ability(controller, 3, 6); // Pommel strike set_sword_ability(controller, 4, 7); }, SwordTactics::AgileSimple => { // Finisher set_sword_ability(controller, 0, 5); // Skewer set_sword_ability(controller, 1, 2); // Cross cut set_sword_ability(controller, 2, 4); // Quick draw set_sword_ability(controller, 3, 8); // Feint set_sword_ability(controller, 4, 9); }, SwordTactics::DefensiveSimple => { // Finisher set_sword_ability(controller, 0, 5); // Crescent slash set_sword_ability(controller, 1, 0); // Fell strike set_sword_ability(controller, 2, 1); // Riposte set_sword_ability(controller, 3, 10); // Disengage set_sword_ability(controller, 4, 11); }, SwordTactics::CripplingSimple => { // Finisher set_sword_ability(controller, 0, 5); // Fell strike set_sword_ability(controller, 1, 1); // Skewer set_sword_ability(controller, 2, 2); // Gouge set_sword_ability(controller, 3, 12); // Hamstring set_sword_ability(controller, 4, 13); }, SwordTactics::CleavingSimple => { // Finisher set_sword_ability(controller, 0, 5); // Cascade set_sword_ability(controller, 1, 3); // Cross cut set_sword_ability(controller, 2, 4); // Whirlwind slice set_sword_ability(controller, 3, 14); // Earth splitter set_sword_ability(controller, 4, 15); }, SwordTactics::HeavyAdvanced => { // Finisher set_sword_ability(controller, 0, 5); // Windmill slash set_sword_ability(controller, 1, 6); // Pommel strike set_sword_ability(controller, 2, 7); // Fortitude set_sword_ability(controller, 3, 16); // Pillar Thrust set_sword_ability(controller, 4, 17); }, SwordTactics::AgileAdvanced => { // Finisher set_sword_ability(controller, 0, 5); // Quick draw set_sword_ability(controller, 1, 8); // Feint set_sword_ability(controller, 2, 9); // Dancing edge set_sword_ability(controller, 3, 18); // Flurry set_sword_ability(controller, 4, 19); }, SwordTactics::DefensiveAdvanced => { // Finisher set_sword_ability(controller, 0, 5); // Riposte set_sword_ability(controller, 1, 10); // Disengage set_sword_ability(controller, 2, 11); // Stalwart sword set_sword_ability(controller, 3, 20); // Deflect set_sword_ability(controller, 4, 21); }, SwordTactics::CripplingAdvanced => { // Finisher set_sword_ability(controller, 0, 5); // Gouge set_sword_ability(controller, 1, 12); // Hamstring set_sword_ability(controller, 2, 13); // Eviscerate set_sword_ability(controller, 3, 22); // Bloody gash set_sword_ability(controller, 4, 23); }, SwordTactics::CleavingAdvanced => { // Finisher set_sword_ability(controller, 0, 5); // Whirlwind slice set_sword_ability(controller, 1, 14); // Earth splitter set_sword_ability(controller, 2, 15); // Blade fever set_sword_ability(controller, 3, 24); // Sky splitter set_sword_ability(controller, 4, 25); }, } agent.combat_state.int_counters[IntCounters::ActionMode as usize] = ActionMode::Reckless as u8; } enum IntCounters { Tactics = 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); } }; match SwordTactics::from_u8( agent.combat_state.int_counters[IntCounters::Tactics as usize], ) { SwordTactics::Unskilled => { 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 { continue_current_input(input, &mut next_input); } 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 } }, SwordTactics::Basic => { 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, ability_preferences) { next_input = Some(attempt_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 } }, SwordTactics::HeavySimple => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } 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); } 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 } }, SwordTactics::AgileSimple => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } 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); } 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 } }, SwordTactics::DefensiveSimple => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } else if could_use_input(InputKind::Ability(3), ability_preferences) { next_input = Some(InputKind::Ability(3)); } 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); } 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 } }, SwordTactics::CripplingSimple => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } 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); } 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 } }, SwordTactics::CleavingSimple => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } 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); } 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 } }, SwordTactics::HeavyAdvanced => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } 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); } 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 } }, SwordTactics::AgileAdvanced => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } 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); } 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 } }, SwordTactics::DefensiveAdvanced => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } else if could_use_input(random_ability, ability_preferences) { next_input = Some(random_ability); } 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)); } 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 } }, SwordTactics::CripplingAdvanced => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } 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); } 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 } }, SwordTactics::CleavingAdvanced => { 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 { continue_current_input(input, &mut next_input); } else { 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, 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), ability_preferences) { next_input = Some(InputKind::Ability(0)); } 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); } 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_axe_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { if !agent.combat_state.initialized { agent.combat_state.initialized = true; let available_tactics = { let mut tactics = Vec::new(); let try_tactic = |skill, tactic, tactics: &mut Vec| { if self.skill_set.has_skill(Skill::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.combat_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.combat_state.int_counters[IntCounters::ActionMode as usize] = ActionMode::Reckless as u8; } enum IntCounters { Tactic = 0, ActionMode = 1, } enum Timers { GuardedCycle = 0, PosTimeOut = 1, } enum Conditions { GuardedDefend = 0, RollingBreakThrough = 1, } enum FloatCounters { GuardedTimer = 0, } enum Positions { GuardedCover = 0, Flee = 1, } let attempt_attack = handle_attack_aggression( self, agent, controller, attack_data, tgt_data, read_data, rng, Timers::PosTimeOut as usize, Timers::GuardedCycle as usize, FloatCounters::GuardedTimer as usize, IntCounters::ActionMode as usize, Conditions::GuardedDefend as usize, Conditions::RollingBreakThrough as usize, Positions::GuardedCover as usize, Positions::Flee as usize, ); let attack_failed = if attempt_attack { let primary = self.extract_ability(AbilityInput::Primary); let secondary = self.extract_ability(AbilityInput::Secondary); let abilities = [ self.extract_ability(AbilityInput::Auxiliary(0)), self.extract_ability(AbilityInput::Auxiliary(1)), self.extract_ability(AbilityInput::Auxiliary(2)), self.extract_ability(AbilityInput::Auxiliary(3)), self.extract_ability(AbilityInput::Auxiliary(4)), ]; let could_use_input = |input, desired_energy| match input { InputKind::Primary => primary.as_ref().map_or(false, |p| { p.could_use(attack_data, self, tgt_data, read_data, desired_energy) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { s.could_use(attack_data, self, tgt_data, read_data, desired_energy) }), InputKind::Ability(x) => abilities[x].as_ref().map_or(false, |a| { 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.combat_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, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { const MIN_CHARGE_FRAC: f32 = 0.5; const OPTIMAL_TARGET_VELOCITY: f32 = 5.0; const DESIRED_ENERGY_LEVEL: f32 = 50.0; let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) }; // Logic to use abilities if let CharacterState::ChargedRanged(c) = self.char_state { if !matches!(c.stage_section, StageSection::Recover) { // Don't even bother with this logic if in recover let target_speed_sqd = agent .target .as_ref() .map(|t| t.target) .and_then(|e| read_data.velocities.get(e)) .map_or(0.0, |v| v.0.magnitude_squared()); if c.charge_frac() < MIN_CHARGE_FRAC || (target_speed_sqd > OPTIMAL_TARGET_VELOCITY.powi(2) && c.charge_frac() < 1.0) { // If haven't charged to desired level, or target is moving too fast and haven't // fully charged, keep charging controller.push_basic_input(InputKind::Primary); } // Else don't send primary input to release the shot } } else if matches!(self.char_state, CharacterState::RepeaterRanged(c) if self.energy.current() > 5.0 && !matches!(c.stage_section, StageSection::Recover)) { // If in repeater ranged, have enough energy, and aren't in recovery, try to // keep firing if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2) && line_of_sight_with_target() { // Only keep firing if not in melee range or if can see target controller.push_basic_input(InputKind::Secondary); } } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { if self .skill_set .has_skill(Skill::Bow(BowSkill::UnlockShotgun)) && self.energy.current() > 45.0 && rng.gen_bool(0.5) { // Use shotgun if target close and have sufficient energy controller.push_basic_input(InputKind::Ability(0)); } else if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && self.energy.current() > CharacterAbility::default_roll(Some(self.char_state)).energy_cost() && !matches!(self.char_state, CharacterState::BasicRanged(c) if !matches!(c.stage_section, StageSection::Recover)) { // Else roll away if can roll and have enough energy, and not using shotgun // (other 2 attacks have interrupt handled above) unless in recover controller.push_basic_input(InputKind::Roll); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); if attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } } } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) && line_of_sight_with_target() { // If not really far, and can see target, attempt to shoot bow if self.energy.current() < DESIRED_ENERGY_LEVEL { // If low on energy, use primary to attempt to regen energy controller.push_basic_input(InputKind::Primary); } else { // Else we have enough energy, use repeater controller.push_basic_input(InputKind::Secondary); } } // 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 line_of_sight_with_target() && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() .rotated_z(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(bearing.z > 1.5, controller); 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) && rng.gen::() < 0.01 { controller.push_basic_input(InputKind::Roll); } } else { // If too far, move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_staff_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { enum ActionStateConditions { ConditionStaffCanShockwave = 0, } let context = AbilityContext::from(self.stance, Some(self.inventory), self.combo); let extract_ability = |input: AbilityInput| { self.active_abilities .activate_ability( input, Some(self.inventory), self.skill_set, self.body, Some(self.char_state), &context, ) .map_or(Default::default(), |a| a.0) }; let (flamethrower, shockwave) = ( extract_ability(AbilityInput::Secondary), extract_ability(AbilityInput::Auxiliary(0)), ); let flamethrower_range = match flamethrower { CharacterAbility::BasicBeam { range, .. } => range, _ => 20.0_f32, }; let shockwave_cost = shockwave.energy_cost(); if self.body.map_or(false, |b| b.is_humanoid()) && attack_data.in_min_range() && self.energy.current() > CharacterAbility::default_roll(Some(self.char_state)).energy_cost() && !matches!(self.char_state, CharacterState::Shockwave(_)) { // if a humanoid, have enough stamina, not in shockwave, and in melee range, // emergency roll controller.push_basic_input(InputKind::Roll); } else if matches!(self.char_state, CharacterState::Shockwave(_)) { agent.combat_state.conditions [ActionStateConditions::ConditionStaffCanShockwave as usize] = false; } else if agent.combat_state.conditions [ActionStateConditions::ConditionStaffCanShockwave as usize] && matches!(self.char_state, CharacterState::Wielding(_)) { controller.push_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)) && 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.push_basic_input(InputKind::Ability(0)); } else { agent.combat_state.conditions [ActionStateConditions::ConditionStaffCanShockwave as usize] = true; } } else if self.energy.current() > shockwave_cost + CharacterAbility::default_roll(Some(self.char_state)).energy_cost() && attack_data.dist_sqrd < flamethrower_range.powi(2) { controller.push_basic_input(InputKind::Secondary); } else { controller.push_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 entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() .rotated_z(rng.gen_range(-1.57..-0.5)) .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(bearing.z > 1.5, controller); controller.inputs.move_z = bearing.z; } } // 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(_)) && rng.gen::() < 0.02 { controller.push_basic_input(InputKind::Roll); } } else { // If too far, move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_sceptre_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { const DESIRED_ENERGY_LEVEL: f32 = 50.0; const DESIRED_COMBO_LEVEL: u32 = 8; let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) }; // Logic to use abilities if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2) && line_of_sight_with_target() { // If far enough away, and can see target, check which skill is appropriate to // use if self.energy.current() > DESIRED_ENERGY_LEVEL && read_data .combos .get(*self.entity) .map_or(false, |c| c.counter() >= DESIRED_COMBO_LEVEL) && !read_data.buffs.get(*self.entity).iter().any(|buff| { buff.iter_kind(BuffKind::Regeneration) .peekable() .peek() .is_some() }) { // If have enough energy and combo to use healing aura, do so controller.push_basic_input(InputKind::Secondary); } else 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) .peekable() .peek() .is_some() }) { // Use ward if target is far enough away, self is not buffed, and have // sufficient energy controller.push_basic_input(InputKind::Ability(0)); } else { // If low on energy, use primary to attempt to regen energy // Or if at desired energy level but not able/willing to ward, just attack controller.push_basic_input(InputKind::Primary); } } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { if self.body.map_or(false, |b| b.is_humanoid()) && self.energy.current() > CharacterAbility::default_roll(Some(self.char_state)).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 aura or in // recover controller.push_basic_input(InputKind::Roll); } else if attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } } // 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( &*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 line_of_sight_with_target() && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() .rotated_z(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(bearing.z > 1.5, controller); controller.inputs.move_z = bearing.z; } } // 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) && rng.gen::() < 0.01 { controller.push_basic_input(InputKind::Roll); } } else { // If too far, move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_stone_golem_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerHandleStoneGolemAttack = 0, //Timer 0 } if attack_data.in_min_range() && attack_data.angle < 90.0 { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Primary); //controller.inputs.primary.set_state(true); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { if self.vel.0.is_approx_zero() { controller.push_basic_input(InputKind::Ability(0)); } if self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ) && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) && attack_data.angle < 90.0 { if agent.combat_state.timers [ActionStateTimers::TimerHandleStoneGolemAttack as usize] > 5.0 { controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers [ActionStateTimers::TimerHandleStoneGolemAttack as usize] = 0.0; } else { agent.combat_state.timers [ActionStateTimers::TimerHandleStoneGolemAttack as usize] += read_data.dt.0; } } } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } #[allow(clippy::too_many_arguments)] pub fn handle_circle_charge_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, radius: u32, circle_time: u32, rng: &mut impl Rng, ) { enum ActionStateCountersF { CounterFHandleCircleChargeAttack = 0, } enum ActionStateCountersI { CounterIHandleCircleChargeAttack = 0, } if agent.combat_state.counters [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] >= circle_time as f32 { // if circle charge is in progress and time hasn't expired, continue charging controller.push_basic_input(InputKind::Secondary); } if attack_data.in_min_range() { if agent.combat_state.counters [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] > 0.0 { // set timer and rotation counter to zero if in minimum range agent.combat_state.counters [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] = 0.0; agent.combat_state.int_counters [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] = 0; } else { // melee attack controller.push_basic_input(InputKind::Primary); controller.inputs.move_dir = Vec2::zero(); } } else if attack_data.dist_sqrd < (radius as f32 + attack_data.min_attack_dist).powi(2) { // if in range to charge, circle, then charge if agent.combat_state.int_counters [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] == 0 { // if you haven't chosen a direction to go in, choose now agent.combat_state.int_counters [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] = 1 + rng.gen_bool(0.5) as u8; } if agent.combat_state.counters [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] < circle_time as f32 { // circle if circle timer not ready let move_dir = match agent.combat_state.int_counters [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] { 1 => // circle left if counter is 1 { (tgt_data.pos.0 - self.pos.0) .xy() .rotated_z(0.47 * PI) .try_normalized() .unwrap_or_else(Vec2::unit_y) }, 2 => // circle right if counter is 2 { (tgt_data.pos.0 - self.pos.0) .xy() .rotated_z(-0.47 * PI) .try_normalized() .unwrap_or_else(Vec2::unit_y) }, _ => // if some illegal value slipped in, get zero vector { Vec2::zero() }, }; let obstacle = read_data .terrain .ray( self.pos.0 + Vec3::unit_z(), self.pos.0 + move_dir.with_z(0.0) * 2.0 + Vec3::unit_z(), ) .until(Block::is_solid) .cast() .1 .map_or(true, |b| b.is_some()); if obstacle { // if obstacle detected, stop circling agent.combat_state.counters [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] = circle_time as f32; } controller.inputs.move_dir = move_dir; // use counter as timer since timer may be modified in other parts of the code agent.combat_state.counters [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] += read_data.dt.0; } // activating charge once circle timer expires is handled above } else { let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { // if too far away from target, move towards them Path::Separate } else { Path::Partial }; self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None); } } pub fn handle_quadlow_ranged_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerHandleQuadLowRanged = 0, } if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) && attack_data.angle < 90.0 { controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .try_normalized() .unwrap_or_else(Vec2::unit_y); controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { 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 attack_data.angle < 15.0 && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { if agent.combat_state.timers [ActionStateTimers::TimerHandleQuadLowRanged as usize] > 5.0 { agent.combat_state.timers [ActionStateTimers::TimerHandleQuadLowRanged as usize] = 0.0; } else if agent.combat_state.timers [ActionStateTimers::TimerHandleQuadLowRanged as usize] > 2.5 { controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .rotated_z(1.75 * PI) .try_normalized() .unwrap_or_else(Vec2::zero) * speed; agent.combat_state.timers [ActionStateTimers::TimerHandleQuadLowRanged as usize] += read_data.dt.0; } else { controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .rotated_z(0.25 * PI) .try_normalized() .unwrap_or_else(Vec2::zero) * speed; agent.combat_state.timers [ActionStateTimers::TimerHandleQuadLowRanged as usize] += read_data.dt.0; } controller.push_basic_input(InputKind::Secondary); self.jump_if(bearing.z > 1.5, controller); controller.inputs.move_z = bearing.z; } else { controller.inputs.move_dir = bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed; self.jump_if(bearing.z > 1.5, controller); controller.inputs.move_z = bearing.z; } } else { agent.target = None; } } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_tail_slap_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerTailSlap = 0, } if attack_data.angle < 90.0 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2) { if agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] > 4.0 { controller.push_cancel_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] = 0.0; } else if agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] > 1.0 { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] += read_data.dt.0; } else { controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] += read_data.dt.0; } controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .try_normalized() .unwrap_or_else(Vec2::unit_y) * 0.1; } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_quadlow_quick_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { if attack_data.angle < 90.0 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2) { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Secondary); } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) && attack_data.dist_sqrd > (2.0 * attack_data.min_attack_dist).powi(2) && attack_data.angle < 90.0 { controller.push_basic_input(InputKind::Primary); 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); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_quadlow_basic_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerQuadLowBasic = 0, } if attack_data.angle < 70.0 && attack_data.dist_sqrd < (1.3 * attack_data.min_attack_dist).powi(2) { controller.inputs.move_dir = Vec2::zero(); if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] > 5.0 { agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] = 0.0; } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] > 2.0 { controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] += read_data.dt.0; } else { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] += read_data.dt.0; } } else { let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { Path::Separate } else { Path::Partial }; self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None); } } pub fn handle_quadmed_jump_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { if attack_data.angle < 90.0 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2) { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Secondary); } else if attack_data.angle < 15.0 && attack_data.dist_sqrd < (5.0 * attack_data.min_attack_dist).powi(2) { controller.push_basic_input(InputKind::Ability(0)); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { if self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ) && attack_data.angle < 15.0 && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { controller.push_basic_input(InputKind::Primary); } } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_quadmed_basic_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerQuadMedBasic = 0, } if attack_data.angle < 90.0 && attack_data.in_min_range() { controller.inputs.move_dir = Vec2::zero(); if agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] < 2.0 { controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] += read_data.dt.0; } else if agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] < 3.0 { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] += read_data.dt.0; } else { agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] = 0.0; } } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_quadmed_hoof_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const HOOF_ATTACK_RANGE: f32 = 1.0; const HOOF_ATTACK_ANGLE: f32 = 50.0; if attack_data.angle < HOOF_ATTACK_ANGLE && attack_data.dist_sqrd < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.max_radius())).powi(2) { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Primary); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Full, None, ); } } pub fn handle_quadlow_beam_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerQuadLowBeam = 0, } if attack_data.angle < 90.0 && attack_data.dist_sqrd < (2.5 * attack_data.min_attack_dist).powi(2) { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Secondary); } else if attack_data.dist_sqrd < (7.0 * attack_data.min_attack_dist).powi(2) && attack_data.angle < 15.0 { if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 2.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); controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] += read_data.dt.0; } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 4.0 && attack_data.angle < 15.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); controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] += read_data.dt.0; } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 6.0 && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Ability(0)); agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] += read_data.dt.0; } else { agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] = 0.0; } } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_organ_aura_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, _tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerOrganAura = 0, } const ORGAN_AURA_DURATION: f32 = 34.75; if attack_data.dist_sqrd < (7.0 * attack_data.min_attack_dist).powi(2) { if agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] > ORGAN_AURA_DURATION { agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] = 0.0; } else if agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] < 1.0 { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] += read_data.dt.0; } else { agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] += read_data.dt.0; } } else { agent.target = None; } } pub fn handle_theropod_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { if attack_data.angle < 90.0 && attack_data.in_min_range() { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_turret_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { if entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } else { agent.target = None; } } pub fn handle_fixed_turret_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { controller.inputs.look_dir = self.ori.look_dir(); if entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } else { agent.target = None; } } pub fn handle_rotating_turret_attack( &self, agent: &mut Agent, controller: &mut Controller, tgt_data: &TargetData, read_data: &ReadData, ) { controller.inputs.look_dir = Dir::new( Quaternion::from_xyzw(self.ori.look_dir().x, self.ori.look_dir().y, 0.0, 0.0) .rotated_z(6.0 * read_data.dt.0) .into_vec3() .try_normalized() .unwrap_or_default(), ); if entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { controller.push_basic_input(InputKind::Primary); } else { agent.target = None; } } pub fn handle_radial_turret_attack(&self, controller: &mut Controller) { controller.push_basic_input(InputKind::Primary); } pub fn handle_fiery_tornado_attack(&self, agent: &mut Agent, controller: &mut Controller) { enum Conditions { AuraEmited = 0, } if matches!(self.char_state, CharacterState::BasicAura(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.conditions[Conditions::AuraEmited as usize] = true; } // 1 time use of aura if !agent.combat_state.conditions[Conditions::AuraEmited as usize] { controller.push_basic_input(InputKind::Secondary); } else { // Spin controller.push_basic_input(InputKind::Primary); } } pub fn handle_mindflayer_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { enum ActionStateFCounters { FCounterHealthThreshold = 0, } enum ActionStateICounters { ICounterNumFireballs = 0, } enum ActionStateConditions { ConditionCounterInit = 0, } const MINDFLAYER_ATTACK_DIST: f32 = 16.0; const MINION_SUMMON_THRESHOLD: f32 = 0.20; let health_fraction = self.health.map_or(0.5, |h| h.fraction()); // Sets counter at start of combat, using `condition` to keep track of whether // it was already initialized if !agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] { agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] = 1.0 - MINION_SUMMON_THRESHOLD; agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] = true; } if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] > health_fraction && (matches!(self.char_state, CharacterState::BasicSummon(_)) || entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, )) // TODO: Better check for if there's room to spawn summons { // Summon minions at particular thresholds of health controller.push_basic_input(InputKind::Ability(2)); if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.counters [ActionStateFCounters::FCounterHealthThreshold as usize] -= MINION_SUMMON_THRESHOLD; } } else if attack_data.dist_sqrd < MINDFLAYER_ATTACK_DIST.powi(2) { if entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { // If close to target, use either primary or secondary ability if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(10) && !matches!(c.stage_section, StageSection::Recover)) { // If already using primary, keep using primary until 10 consecutive seconds controller.push_basic_input(InputKind::Primary); } else if matches!(self.char_state, CharacterState::RapidMelee(c) if c.current_strike < 50 && !matches!(c.stage_section, StageSection::Recover)) { // If already using secondary, keep using secondary until 10 consecutive // seconds controller.push_basic_input(InputKind::Secondary); } else if rng.gen_bool(health_fraction.into()) { // Else if at high health, use primary controller.push_basic_input(InputKind::Primary); } else { // Else use secondary controller.push_basic_input(InputKind::Secondary); } } else { // If close in with bad line of sight, take countermeasures against cheesing // Fairly simple tactic: Punish with vortex forcing any nearby target that can // be hit to retreat. If a vortex wouldn't hit the target, just blink, with a // 50% chance to throw a single necrotic sphere, first. if matches!( read_data .terrain .ray( self.pos.0 + Vec3::unit_z() * 0.5, tgt_data.pos.0 + Vec3::unit_z() * 0.5 ) .until(|b| b.is_filled()) .cast() .1, Ok(None) ) { // Punish with vortex attack, hopefully forcing attacker to retreat controller.push_basic_input(InputKind::Secondary); } else { // Genuinely no LOS, handle it as if target is far away, but just throw 0-1 // spheres and then teleport, since they're not likely to hit. let num_fireballs = &mut agent.combat_state.int_counters [ActionStateICounters::ICounterNumFireballs as usize]; if *num_fireballs % 2 == 0 { controller.push_action(ControlAction::StartInput { input: InputKind::Ability(0), target_entity: agent .target .as_ref() .and_then(|t| read_data.uids.get(t.target)) .copied(), select_pos: None, }); if matches!(self.char_state, CharacterState::Blink(_)) { *num_fireballs = rand::random::() % 4; } } else if matches!(self.char_state, CharacterState::Wielding(_)) { *num_fireballs -= 1; controller.push_action(ControlAction::StartInput { input: InputKind::Ability(1), target_entity: agent .target .as_ref() .and_then(|t| read_data.uids.get(t.target)) .copied(), select_pos: None, }); } self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } } } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { // If too far from target, throw a random number of necrotic spheres at them and // then blink to them. let num_fireballs = &mut agent.combat_state.int_counters [ActionStateICounters::ICounterNumFireballs as usize]; if *num_fireballs == 0 { controller.push_action(ControlAction::StartInput { input: InputKind::Ability(0), target_entity: agent .target .as_ref() .and_then(|t| read_data.uids.get(t.target)) .copied(), select_pos: None, }); if matches!(self.char_state, CharacterState::Blink(_)) { *num_fireballs = rand::random::() % 4; } } else if matches!(self.char_state, CharacterState::Wielding(_)) { *num_fireballs -= 1; controller.push_action(ControlAction::StartInput { input: InputKind::Ability(1), target_entity: agent .target .as_ref() .and_then(|t| read_data.uids.get(t.target)) .copied(), select_pos: None, }); } self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_flamekeeper_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const MELEE_RANGE: f32 = 6.0; const MID_RANGE: f32 = 25.0; const SUMMON_THRESHOLD: f32 = 0.2; enum FCounters { SummonThreshold = 0, } enum Conditions { AttackToggle = 0, } enum Timers { AttackRand = 0, } if agent.combat_state.timers[Timers::AttackRand as usize] > 5.0 { agent.combat_state.timers[Timers::AttackRand as usize] = 0.0; } let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) }; let health_fraction = self.health.map_or(0.5, |h| h.fraction()); // Sets counter at start of combat, using `condition` to keep track of whether // it was already initialized if !agent.combat_state.initialized { agent.combat_state.counters[FCounters::SummonThreshold as usize] = 1.0 - SUMMON_THRESHOLD; agent.combat_state.initialized = true; } else if health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize] { // Summon Flamethrowers or Clockworks at particular thresholds of health if !agent.combat_state.conditions[Conditions::AttackToggle as usize] { // summon Flamethrowers controller.push_basic_input(InputKind::Ability(0)); } else { // summon Clockworks controller.push_basic_input(InputKind::Ability(1)); } if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.counters[FCounters::SummonThreshold as usize] -= SUMMON_THRESHOLD; agent.combat_state.conditions[Conditions::AttackToggle as usize] = !agent.combat_state.conditions[Conditions::AttackToggle as usize]; } } else { // If target is in melee range use flamecrush if attack_data.dist_sqrd < MELEE_RANGE.powi(2) { // flamecrush controller.push_basic_input(InputKind::Secondary); // If target is in mid range use mines, lavawave, flamethrower } else if attack_data.dist_sqrd < MID_RANGE.powi(2) && line_of_sight_with_target() { if agent.combat_state.timers[Timers::AttackRand as usize] > 3.5 { // lavawave controller.push_basic_input(InputKind::Ability(3)); } else if agent.combat_state.timers[Timers::AttackRand as usize] > 2.5 { // mines controller.push_basic_input(InputKind::Ability(4)); } else { // flamethrower controller.push_basic_input(InputKind::Ability(2)); } // If target is beyond mid range use lavamortar } else if attack_data.dist_sqrd > MID_RANGE.powi(2) { // lavamortar controller.push_basic_input(InputKind::Primary); } // path towards home self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); agent.combat_state.timers[Timers::AttackRand as usize] += read_data.dt.0; } } pub fn handle_birdlarge_fire_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, _rng: &mut impl Rng, ) { const PHOENIX_HEAL_THRESHOLD: f32 = 0.20; enum Conditions { Healed = 0, } enum ActionStateTimers { AttackTimer1, AttackTimer2, } let attack_timer_1 = if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 2.0 { 0 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 4.0 { 1 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 6.0 { 2 } else { 3 }; agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] += read_data.dt.0; if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] > 8.0 { // Reset timer agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] = 0.0; } let (attack_timer_2, speed) = if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 3.0 { // fly high (0, 2.0) } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 6.0 { // attack_mid_1 (1, 2.0) } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 9.0 { // fly high (0, 3.0) } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 16.0 { // attack_mid_2 (2, 1.0) } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 20.0 { // fly low (5, 20.0) } else { // attack_close (3, 1.0) }; agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] += read_data.dt.0; if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] > 28.0 { // Reset timer agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] = 0.0; } // Fly to target let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0) .try_normalized() .unwrap_or_else(Vec3::zero); controller.inputs.move_dir = dir_to_target.xy() * speed; // Always fly! If the floor can't touch you, it can't hurt you... controller.push_basic_input(InputKind::Fly); // Flee from the ground! The internet told me it was lava! // If on the ground, jump with every last ounce of energy, holding onto // all that is dear in life and straining for the wide open skies. if self.physics_state.on_ground.is_some() { controller.push_basic_input(InputKind::Jump); } else { // Use a proportional controller with a coefficient of 1.0 to // maintain altidude at the the provided set point let mut maintain_altitude = |set_point| { let alt = read_data .terrain .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0)) .until(Block::is_solid) .cast() .0; let error = set_point - alt; controller.inputs.move_z = error; }; // heal once - from_the_ashes let health_fraction = self.health.map_or(0.5, |h| h.fraction()); if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.conditions[Conditions::Healed as usize] = true; } if !agent.combat_state.conditions[Conditions::Healed as usize] && PHOENIX_HEAL_THRESHOLD > health_fraction { controller.push_basic_input(InputKind::Ability(4)); } else if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (35.0_f32).powi(2) { // heat laser maintain_altitude(2.0); controller.push_basic_input(InputKind::Ability(3)) } else { match attack_timer_2 { 0 => maintain_altitude(3.0), 1 => { //summontornados controller.push_basic_input(InputKind::Ability(1)); }, 2 => { // firerain controller.push_basic_input(InputKind::Ability(2)); }, 3 => { if attack_data.dist_sqrd < 4.0_f32.powi(2) && attack_data.angle < 150.0 { // close range attack match attack_timer_1 { 1 => { // short strike controller.push_basic_input(InputKind::Primary); }, 3 => { // long strike controller.push_basic_input(InputKind::Secondary) }, _ => { // leg strike controller.push_basic_input(InputKind::Ability(0)) }, } } else { match attack_timer_1 { 0 | 2 => { maintain_altitude(2.0); }, _ => { // heat laser controller.push_basic_input(InputKind::Ability(3)) }, } } }, _ => { maintain_altitude(2.0); }, } } } } pub fn handle_wyvern_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, _rng: &mut impl Rng, ) { enum ActionStateTimers { AttackTimer = 0, } // Set fly to false controller.push_cancel_input(InputKind::Fly); if attack_data.dist_sqrd > 30.0_f32.powi(2) { if entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } 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; if (self.pos.0.z - tgt_data.pos.0.z) < 35.0 { controller.push_basic_input(InputKind::Fly); controller.inputs.move_z = 0.2; } } } else if !read_data .terrain .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 2.0)) .until(Block::is_solid) .cast() .1 .map_or(true, |b| b.is_some()) { // Do not increment the timer during this movement // The next stage shouldn't trigger until the entity // is on the ground controller.push_basic_input(InputKind::Fly); let move_dir = tgt_data.pos.0 - self.pos.0; controller.inputs.move_dir = move_dir.xy().try_normalized().unwrap_or_else(Vec2::zero) * 2.0; controller.inputs.move_z = move_dir.z - 0.5; if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } } else if attack_data.dist_sqrd > (3.0 * attack_data.min_attack_dist).powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else if attack_data.angle < 15.0 { if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 { // beam controller.push_basic_input(InputKind::Ability(1)); } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 9.0 { // shockwave controller.push_basic_input(InputKind::Ability(0)); } else { agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0; } // Move towards the target slowly self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, Some(0.5), ); agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0; } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 9.0 && attack_data.angle < 90.0 && attack_data.in_min_range() { // Triple strike controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0; } else { // Reset timer agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0; // Target is behind us or the timer needs to be reset. Chase target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } } pub fn handle_birdlarge_breathe_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { enum ActionStateTimers { TimerBirdLargeBreathe = 0, } // Set fly to false controller.push_cancel_input(InputKind::Fly); if attack_data.dist_sqrd > 30.0_f32.powi(2) { if rng.gen_bool(0.05) && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } 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; if (self.pos.0.z - tgt_data.pos.0.z) < 20.0 { controller.push_basic_input(InputKind::Fly); controller.inputs.move_z = 1.0; } } } else if !read_data .terrain .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 2.0)) .until(Block::is_solid) .cast() .1 .map_or(true, |b| b.is_some()) { // Do not increment the timer during this movement // The next stage shouldn't trigger until the entity // is on the ground controller.push_basic_input(InputKind::Fly); let move_dir = tgt_data.pos.0 - self.pos.0; controller.inputs.move_dir = move_dir.xy().try_normalized().unwrap_or_else(Vec2::zero) * 2.0; controller.inputs.move_z = move_dir.z - 0.5; if rng.gen_bool(0.05) && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } } else if rng.gen_bool(0.05) && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } else if rng.gen_bool(0.5) && (self.pos.0.z - tgt_data.pos.0.z) < 15.0 && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) { controller.push_basic_input(InputKind::Fly); controller.inputs.move_z = 1.0; } else if attack_data.dist_sqrd > (3.0 * attack_data.min_attack_dist).powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else if self.energy.current() > 60.0 && agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] < 3.0 && attack_data.angle < 15.0 { // Fire breath attack controller.push_basic_input(InputKind::Ability(0)); // Move towards the target slowly self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, Some(0.5), ); agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] += read_data.dt.0; } else if agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] < 6.0 && attack_data.angle < 90.0 && attack_data.in_min_range() { // Triple strike controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] += read_data.dt.0; } else { // Reset timer agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] = 0.0; // Target is behind us or the timer needs to be reset. Chase target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } } pub fn handle_birdlarge_basic_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerBirdLargeBasic = 0, } enum ActionStateConditions { ConditionBirdLargeBasic = 0, /* FIXME: Not sure what this represents. This name * should be reflective of the condition... */ } const BIRD_ATTACK_RANGE: f32 = 4.0; const BIRD_CHARGE_DISTANCE: f32 = 15.0; let bird_attack_distance = self.body.map_or(0.0, |b| b.max_radius()) + BIRD_ATTACK_RANGE; // Increase action timer agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] += read_data.dt.0; if agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] > 8.0 { // If action timer higher than 8, make bird summon tornadoes controller.push_basic_input(InputKind::Secondary); if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover)) { // Reset timer agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] = 0.0; } } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover)) { // If already in dash, keep dashing if not in recover controller.push_basic_input(InputKind::Ability(0)); } else if matches!(self.char_state, CharacterState::ComboMelee(c) if matches!(c.stage_section, StageSection::Recover)) { // If already in combo keep comboing if not in recover controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd > BIRD_CHARGE_DISTANCE.powi(2) { // Charges at target if they are far enough away if attack_data.angle < 60.0 { controller.push_basic_input(InputKind::Ability(0)); } } else if attack_data.dist_sqrd < bird_attack_distance.powi(2) { // Combo melee target controller.push_basic_input(InputKind::Primary); agent.combat_state.conditions [ActionStateConditions::ConditionBirdLargeBasic as usize] = true; } // Make bird move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } pub fn handle_arthropod_ranged_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerArthropodRanged = 0, } agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0; if agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] > 6.0 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2) { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Secondary); // Reset timer if matches!(self.char_state, CharacterState::SpriteSummon(sprite_summon::Data { stage_section, .. }) | CharacterState::SelfBuff(self_buff::Data { stage_section, .. }) if matches!(stage_section, StageSection::Recover)) { agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] = 0.0; } } else if attack_data.dist_sqrd < (2.5 * attack_data.min_attack_dist).powi(2) && attack_data.angle < 90.0 { controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .try_normalized() .unwrap_or_else(Vec2::unit_y); controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { 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 attack_data.angle < 15.0 && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { if agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] > 5.0 { agent.combat_state.timers [ActionStateTimers::TimerArthropodRanged as usize] = 0.0; } else if agent.combat_state.timers [ActionStateTimers::TimerArthropodRanged as usize] > 2.5 { controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .rotated_z(1.75 * PI) .try_normalized() .unwrap_or_else(Vec2::zero) * speed; agent.combat_state.timers [ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0; } else { controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .rotated_z(0.25 * PI) .try_normalized() .unwrap_or_else(Vec2::zero) * speed; agent.combat_state.timers [ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0; } controller.push_basic_input(InputKind::Ability(0)); self.jump_if(bearing.z > 1.5, controller); controller.inputs.move_z = bearing.z; } else { controller.inputs.move_dir = bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed; self.jump_if(bearing.z > 1.5, controller); controller.inputs.move_z = bearing.z; } } else { agent.target = None; } } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_arthropod_ambush_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { enum ActionStateTimers { TimersArthropodAmbush = 0, } agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] += read_data.dt.0; if agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] > 12.0 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2) { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Secondary); // Reset timer if matches!(self.char_state, CharacterState::SpriteSummon(sprite_summon::Data { stage_section, .. }) | CharacterState::SelfBuff(self_buff::Data { stage_section, .. }) if matches!(stage_section, StageSection::Recover)) { agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] = 0.0; } } else if attack_data.angle < 90.0 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Primary); } else if rng.gen_bool(0.01) && attack_data.angle < 60.0 && attack_data.dist_sqrd > (2.0 * attack_data.min_attack_dist).powi(2) { controller.push_basic_input(InputKind::Ability(0)); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_arthropod_melee_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimersArthropodMelee = 0, } agent.combat_state.timers[ActionStateTimers::TimersArthropodMelee as usize] += read_data.dt.0; if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover)) { // If already charging, keep charging if not in recover controller.push_basic_input(InputKind::Secondary); } else if attack_data.dist_sqrd > (2.5 * attack_data.min_attack_dist).powi(2) { // Charges at target if they are far enough away if attack_data.angle < 60.0 { controller.push_basic_input(InputKind::Secondary); } } else if attack_data.angle < 90.0 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) { controller.inputs.move_dir = Vec2::zero(); controller.push_basic_input(InputKind::Primary); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_minotaur_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const MINOTAUR_FRENZY_THRESHOLD: f32 = 0.5; const MINOTAUR_ATTACK_RANGE: f32 = 5.0; const MINOTAUR_CHARGE_DISTANCE: f32 = 15.0; enum ActionStateFCounters { FCounterMinotaurAttack = 0, } enum ActionStateConditions { ConditionJustCrippledOrCleaved = 0, } let minotaur_attack_distance = self.body.map_or(0.0, |b| b.max_radius()) + MINOTAUR_ATTACK_RANGE; let health_fraction = self.health.map_or(1.0, |h| h.fraction()); // Sets action counter at start of combat if agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize] < MINOTAUR_FRENZY_THRESHOLD && health_fraction > MINOTAUR_FRENZY_THRESHOLD { agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize] = MINOTAUR_FRENZY_THRESHOLD; } if health_fraction < agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize] { // Makes minotaur buff itself with frenzy controller.push_basic_input(InputKind::Ability(1)); if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.counters [ActionStateFCounters::FCounterMinotaurAttack as usize] = 0.0; } } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover)) { // If already charging, keep charging if not in recover controller.push_basic_input(InputKind::Ability(0)); } else if matches!(self.char_state, CharacterState::ChargedMelee(c) if matches!(c.stage_section, StageSection::Charge) && c.timer < c.static_data.charge_duration) { // If already charging a melee attack, keep charging it if charging controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd > MINOTAUR_CHARGE_DISTANCE.powi(2) { // Charges at target if they are far enough away if attack_data.angle < 60.0 { controller.push_basic_input(InputKind::Ability(0)); } } else if attack_data.dist_sqrd < minotaur_attack_distance.powi(2) { if agent.combat_state.conditions [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] && !self.char_state.is_attack() { // Cripple target if not just used cripple controller.push_basic_input(InputKind::Secondary); agent.combat_state.conditions [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = false; } else if !self.char_state.is_attack() { // Cleave target if not just used cleave controller.push_basic_input(InputKind::Primary); agent.combat_state.conditions [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = true; } } // Make minotaur move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } pub fn handle_cyclops_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { // Primary const CYCLOPS_MELEE_RANGE: f32 = 9.0; // Secondary const CYCLOPS_FIRE_RANGE: f32 = 30.0; // Ability(1) const CYCLOPS_CHARGE_RANGE: f32 = 18.0; // Ability(0) - Ablity (2) const SHOCKWAVE_THRESHOLD: f32 = 0.6; enum FCounters { ShockwaveThreshold = 0, } enum Timers { AttackChange = 0, } if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 { agent.combat_state.timers[Timers::AttackChange as usize] = 0.0; } let health_fraction = self.health.map_or(0.5, |h| h.fraction()); // Sets counter at start of combat, using `condition` to keep track of whether // it was already initialized if !agent.combat_state.initialized { agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] = 1.0 - SHOCKWAVE_THRESHOLD; agent.combat_state.initialized = true; } else if health_fraction < agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] { // Scream when threshold is reached controller.push_basic_input(InputKind::Ability(2)); if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] -= SHOCKWAVE_THRESHOLD; } } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover)) { // If already AOEing, keep AOEing if not in recover controller.push_basic_input(InputKind::Ability(0)); } else if attack_data.dist_sqrd > CYCLOPS_FIRE_RANGE.powi(2) { // Chase controller.push_basic_input(InputKind::Ability(1)); } else if attack_data.dist_sqrd > CYCLOPS_CHARGE_RANGE.powi(2) { // Shoot after target if they attempt to "flee" controller.push_basic_input(InputKind::Secondary); } else if attack_data.dist_sqrd < CYCLOPS_MELEE_RANGE { if attack_data.angle < 60.0 { // Melee target if close enough and within angle controller.push_basic_input(InputKind::Primary); } else if attack_data.angle > 60.0 { // Scream if target exceeds angle but is close enough controller.push_basic_input(InputKind::Ability(0)); } } // Always attempt to path towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } pub fn handle_dullahan_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { // Primary (12 Default / Melee) const MELEE_RANGE: f32 = 9.0; // Secondary (30 Default / Range) const LONG_RANGE: f32 = 30.0; // Ability(0) (0.1 aka 10% Default / AOE) const HP_THRESHOLD: f32 = 0.1; // Ability(1) (18 Default / Dash) const MID_RANGE: f32 = 18.0; enum FCounters { HealthThreshold = 0, } enum Timers { AttackChange = 0, } if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 { agent.combat_state.timers[Timers::AttackChange as usize] = 0.0; } let health_fraction = self.health.map_or(0.5, |h| h.fraction()); // Sets counter at start of combat, using `condition` to keep track of whether // it was already initialized if !agent.combat_state.initialized { agent.combat_state.counters[FCounters::HealthThreshold as usize] = 1.0 - HP_THRESHOLD; agent.combat_state.initialized = true; } else if health_fraction < agent.combat_state.counters[FCounters::HealthThreshold as usize] { // InputKind when threshold is reached (Default is Ability(0)) controller.push_basic_input(InputKind::Ability(0)); if matches!( self.char_state.ability_info().map(|ai| ai.input), Some(InputKind::Ability(0)) ) && matches!(self.char_state.stage_section(), Some(StageSection::Recover)) { agent.combat_state.counters[FCounters::HealthThreshold as usize] -= HP_THRESHOLD; } } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover)) { // If already InputKind, keep InputKind if not in recover (Default is Shockwave) controller.push_basic_input(InputKind::Ability(0)); } else if attack_data.dist_sqrd > LONG_RANGE.powi(2) { // InputKind after target if they attempt to "flee" (>LONG) controller.push_basic_input(InputKind::Ability(1)); } else if attack_data.dist_sqrd > MID_RANGE.powi(2) { // InputKind after target if they attempt to "flee" (MID-LONG) controller.push_basic_input(InputKind::Secondary); } else if attack_data.dist_sqrd < MELEE_RANGE { if attack_data.angle < 60.0 { // InputKind target if close enough and within angle ( 60.0 { // InputKind if target exceeds angle but is close enough (FLANK/STRAFE) controller.push_basic_input(InputKind::Ability(0)); } } // Always attempt to path towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Full, None, ); } pub fn handle_clay_golem_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const GOLEM_MELEE_RANGE: f32 = 4.0; const GOLEM_LASER_RANGE: f32 = 30.0; const GOLEM_LONG_RANGE: f32 = 50.0; const GOLEM_TARGET_SPEED: f32 = 8.0; enum ActionStateFCounters { FCounterGlayGolemAttack = 0, } let golem_melee_range = self.body.map_or(0.0, |b| b.max_radius()) + GOLEM_MELEE_RANGE; // Fraction of health, used for activation of shockwave // If golem don't have health for some reason, assume it's full let health_fraction = self.health.map_or(1.0, |h| h.fraction()); // Magnitude squared of cross product of target velocity with golem orientation let target_speed_cross_sqd = agent .target .as_ref() .map(|t| t.target) .and_then(|e| read_data.velocities.get(e)) .map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared()); let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) }; if attack_data.dist_sqrd < golem_melee_range.powi(2) { if agent.combat_state.counters[ActionStateFCounters::FCounterGlayGolemAttack as usize] < 7.5 { // If target is close, whack them controller.push_basic_input(InputKind::Primary); agent.combat_state.counters [ActionStateFCounters::FCounterGlayGolemAttack as usize] += read_data.dt.0; } else { // If whacked for too long, nuke them controller.push_basic_input(InputKind::Ability(1)); if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.counters [ActionStateFCounters::FCounterGlayGolemAttack as usize] = 0.0; } } } else if attack_data.dist_sqrd < GOLEM_LASER_RANGE.powi(2) { if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5)) || target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) && line_of_sight_with_target() && attack_data.angle < 45.0 { // If target in range threshold and haven't been lasering for more than 5 // seconds already or if target is moving slow-ish, laser them controller.push_basic_input(InputKind::Secondary); } else if health_fraction < 0.7 { // Else target moving too fast for laser, shockwave time. // But only if damaged enough controller.push_basic_input(InputKind::Ability(0)); } } else if attack_data.dist_sqrd < GOLEM_LONG_RANGE.powi(2) { if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) && line_of_sight_with_target() { // If target is far-ish and moving slow-ish, rocket them controller.push_basic_input(InputKind::Ability(1)); } else if health_fraction < 0.7 { // Else target moving too fast for laser, shockwave time. // But only if damaged enough controller.push_basic_input(InputKind::Ability(0)); } } // Make clay golem move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } pub fn handle_tidal_warrior_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const SCUTTLE_RANGE: f32 = 40.0; const BUBBLE_RANGE: f32 = 20.0; const MINION_SUMMON_THRESHOLD: f32 = 0.20; enum ActionStateConditions { ConditionCounterInitialized = 0, } enum ActionStateFCounters { FCounterMinionSummonThreshold = 0, } let health_fraction = self.health.map_or(0.5, |h| h.fraction()); let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) }; // Sets counter at start of combat, using `condition` to keep track of whether // it was already initialized if !agent.combat_state.conditions [ActionStateConditions::ConditionCounterInitialized as usize] { agent.combat_state.counters [ActionStateFCounters::FCounterMinionSummonThreshold as usize] = 1.0 - MINION_SUMMON_THRESHOLD; agent.combat_state.conditions [ActionStateConditions::ConditionCounterInitialized as usize] = true; } if agent.combat_state.counters[ActionStateFCounters::FCounterMinionSummonThreshold as usize] > health_fraction { // Summon minions at particular thresholds of health controller.push_basic_input(InputKind::Ability(1)); if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.counters [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -= MINION_SUMMON_THRESHOLD; } } else if attack_data.dist_sqrd < SCUTTLE_RANGE.powi(2) { if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover)) { // Keep scuttling if already in dash melee and not in recover controller.push_basic_input(InputKind::Secondary); } else if attack_data.dist_sqrd < BUBBLE_RANGE.powi(2) { if matches!(self.char_state, CharacterState::BasicBeam(c) if !matches!(c.stage_section, StageSection::Recover) && c.timer < Duration::from_secs(10)) { // Keep shooting bubbles at them if already in basic beam and not in recover and // have not been bubbling too long controller.push_basic_input(InputKind::Ability(0)); } else if attack_data.in_min_range() && attack_data.angle < 60.0 { // Pincer them if they're in range and angle controller.push_basic_input(InputKind::Primary); } else if attack_data.angle < 30.0 && line_of_sight_with_target() { // Start bubbling them if not close enough to do something else and in angle and // can see target controller.push_basic_input(InputKind::Ability(0)); } } else if attack_data.angle < 90.0 && line_of_sight_with_target() { // Start scuttling if not close enough to do something else and in angle and can // see target controller.push_basic_input(InputKind::Secondary); } } // Always attempt to path towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } pub fn handle_yeti_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const ICE_SPIKES_RANGE: f32 = 15.0; const ICE_BREATH_RANGE: f32 = 10.0; const ICE_BREATH_TIMER: f32 = 10.0; const SNOWBALL_MAX_RANGE: f32 = 50.0; enum ActionStateFCounters { FCounterYetiAttack = 0, } agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize] += read_data.dt.0; if attack_data.dist_sqrd < ICE_BREATH_RANGE.powi(2) { if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(2)) { // Keep using ice breath for 2 second controller.push_basic_input(InputKind::Ability(0)); } else if agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize] > ICE_BREATH_TIMER { // Use ice breath if timer has gone for long enough controller.push_basic_input(InputKind::Ability(0)); if matches!(self.char_state, CharacterState::BasicBeam(_)) { // Resets action counter when using beam agent.combat_state.counters [ActionStateFCounters::FCounterYetiAttack as usize] = 0.0; } } else if attack_data.in_min_range() { // Basic attack if on top of them controller.push_basic_input(InputKind::Primary); } else { // Use ice spikes if too far for other abilities controller.push_basic_input(InputKind::Secondary); } } else if attack_data.dist_sqrd < ICE_SPIKES_RANGE.powi(2) && attack_data.angle < 60.0 { // Use ice spikes if in range controller.push_basic_input(InputKind::Secondary); } else if attack_data.dist_sqrd < SNOWBALL_MAX_RANGE.powi(2) && attack_data.angle < 60.0 { // Otherwise, chuck all the snowballs controller.push_basic_input(InputKind::Ability(1)); } // Always attempt to path towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } pub fn handle_roshwalr_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const SLOW_CHARGE_RANGE: f32 = 12.5; const SHOCKWAVE_RANGE: f32 = 12.5; const SHOCKWAVE_TIMER: f32 = 15.0; const MELEE_RANGE: f32 = 4.0; enum ActionStateFCounters { FCounterRoshwalrAttack = 0, } agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize] += read_data.dt.0; if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover)) { // If already charging, keep charging if not in recover controller.push_basic_input(InputKind::Ability(0)); } else if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 270.0 { if agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize] > SHOCKWAVE_TIMER { // Use shockwave if timer has gone for long enough controller.push_basic_input(InputKind::Ability(0)); if matches!(self.char_state, CharacterState::Shockwave(_)) { // Resets action counter when using shockwave agent.combat_state.counters [ActionStateFCounters::FCounterRoshwalrAttack as usize] = 0.0; } } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 { // Basic attack if in melee range controller.push_basic_input(InputKind::Primary); } } else if attack_data.dist_sqrd > SLOW_CHARGE_RANGE.powi(2) { // Use slow charge if outside the range controller.push_basic_input(InputKind::Secondary); } // Always attempt to path towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } pub fn handle_harvester_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const VINE_CREATION_THRESHOLD: f32 = 0.50; const FIRE_BREATH_RANGE: f32 = 20.0; const MAX_PUMPKIN_RANGE: f32 = 50.0; enum ActionStateConditions { ConditionHasSummonedVines = 0, } let health_fraction = self.health.map_or(0.5, |h| h.fraction()); let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) }; if health_fraction < VINE_CREATION_THRESHOLD && !agent.combat_state.conditions [ActionStateConditions::ConditionHasSummonedVines as usize] { // Summon vines when reach threshold of health controller.push_basic_input(InputKind::Ability(0)); if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.conditions [ActionStateConditions::ConditionHasSummonedVines as usize] = true; } } else if attack_data.dist_sqrd < FIRE_BREATH_RANGE.powi(2) { if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5)) && line_of_sight_with_target() { // Keep breathing fire if close enough, can see target, and have not been // breathing for more than 5 seconds controller.push_basic_input(InputKind::Secondary); } else if attack_data.in_min_range() && attack_data.angle < 60.0 { // Scythe them if they're in range and angle controller.push_basic_input(InputKind::Primary); } else if attack_data.angle < 30.0 && line_of_sight_with_target() { // Start breathing fire at them if close enough, in angle, and can see target controller.push_basic_input(InputKind::Secondary); } } else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2) && line_of_sight_with_target() { // Throw a pumpkin at them if close enough and can see them controller.push_basic_input(InputKind::Ability(1)); } // Always attempt to path towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } pub fn handle_frostgigas_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { const GIGAS_MELEE_RANGE: f32 = 12.0; const GIGAS_SPIKE_RANGE: f32 = 16.0; const ICEBOMB_RANGE: f32 = 70.0; const GIGAS_LEAP_RANGE: f32 = 50.0; const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.; const FLASHFREEZE_RANGE: f32 = 30.; #[allow(clippy::enum_variant_names)] enum ActionStateTimers { AttackChange, Bonk, } enum ActionStateFCounters { FCounterMinionSummonThreshold = 0, } enum ActionStateICounters { /// An ability that is forced to fully complete until moving on to /// other attacks. /// 1 = Leap shockwave, 2 = Flashfreeze, 3 = Spike summon, /// 4 = Whirlwind, 5 = Remote ice spikes, 6 = Ice bombs CurrentAbility = 0, } let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0); let remote_spikes_action = || ControlAction::StartInput { input: InputKind::Ability(5), target_entity: None, select_pos: Some(tgt_data.pos.0), }; let health_fraction = self.health.map_or(0.5, |h| h.fraction()); // Sets counter at start of combat, using `condition` to keep track of whether // it was already initialized if !agent.combat_state.initialized { agent.combat_state.counters [ActionStateFCounters::FCounterMinionSummonThreshold as usize] = 1.0 - MINION_SUMMON_THRESHOLD; agent.combat_state.initialized = true; } // Update timers if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 { agent.combat_state.timers[ActionStateTimers::AttackChange as usize] = 0.0; } else { agent.combat_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0; } agent.combat_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0; if health_fraction < agent.combat_state.counters [ActionStateFCounters::FCounterMinionSummonThreshold as usize] { // Summon minions at particular thresholds of health controller.push_basic_input(InputKind::Ability(3)); if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.counters [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -= MINION_SUMMON_THRESHOLD; } // Continue casting any attacks that are forced to complete } else if let Some(ability) = Some( &mut agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize], ) .filter(|i| **i != 0) { if *ability == 3 && should_use_targeted_spikes() { *ability = 5 }; let reset = match ability { // Must be rolled 1 => { controller.push_basic_input(InputKind::Ability(1)); matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover)) }, // Attacker will have to run away here 2 => { controller.push_basic_input(InputKind::Ability(4)); matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover)) }, // Avoid the spikes! 3 => { controller.push_basic_input(InputKind::Ability(0)); matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner))) }, // Long whirlwind attack 4 => { controller.push_basic_input(InputKind::Ability(7)); matches!(self.char_state, CharacterState::RapidMelee(c) if matches!(c.stage_section, StageSection::Recover)) }, // Remote ice spikes 5 => { controller.push_action(remote_spikes_action()); matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target))) }, // Ice bombs 6 => { controller.push_basic_input(InputKind::Ability(2)); matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover)) }, // Should never happen _ => true, }; if reset { *ability = 0; } // If our target is nearby and above us, potentially cheesing, have a // chance of summoning remote ice spikes or throwing ice bombs. // Cheesing from less than 5 blocks away is usually not possible } else if attack_data.dist_sqrd > 5f32.powi(2) // Calculate the "cheesing factor" (height of the normalized position difference) && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6 // Make it happen at about every 10 seconds! && rng.gen_bool((0.2 * read_data.dt.0).min(1.0) as f64) { agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] = rng.gen_range(5..=6); } else if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) { // Bonk the target every 10-8 s if agent.combat_state.timers[ActionStateTimers::Bonk as usize] > 10. { controller.push_basic_input(InputKind::Ability(6)); if matches!(self.char_state, CharacterState::BasicMelee(c) if matches!(c.stage_section, StageSection::Recover) && c.static_data.ability_info.ability.map_or(false, |meta| matches!(meta.ability, Ability::MainWeaponAux(6)) ) ) { agent.combat_state.timers[ActionStateTimers::Bonk as usize] = rng.gen_range(0.0..3.0); } // Have a small chance at starting a mixup attack } else if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 4.0 && rng.gen_bool(0.1 * read_data.dt.0.min(1.0) as f64) { agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] = rng.gen_range(1..=4); // Melee the target, do a whirlwind whenever he is trying to go // behind or after every 5s } else if attack_data.angle > 90.0 || agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 5.0 { // If our target is *very* behind, punish with a whirlwind if attack_data.angle > 120.0 { agent.combat_state.int_counters [ActionStateICounters::CurrentAbility as usize] = 4; } else { controller.push_basic_input(InputKind::Secondary); } } else { controller.push_basic_input(InputKind::Primary); } } else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2) && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 2.0 { if should_use_targeted_spikes() { controller.push_action(remote_spikes_action()); } else { controller.push_basic_input(InputKind::Ability(0)); } } else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2) && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 4.0 { controller.push_basic_input(InputKind::Ability(4)); // Start a leap after either every 3s or our target is not in LoS } else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2) && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 3.0 { controller.push_basic_input(InputKind::Ability(1)); } else if attack_data.dist_sqrd < ICEBOMB_RANGE.powi(2) && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 3.0 { controller.push_basic_input(InputKind::Ability(2)); // Spawn ice sprites under distant attackers } else { controller.push_action(remote_spikes_action()); } // Always attempt to path towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } pub fn handle_boreal_hammer_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { enum ActionStateTimers { TimerHandleHammerAttack = 0, } let has_leap = || { self.skill_set .has_skill(Skill::Hammer(HammerSkill::UnlockLeap)) }; let has_energy = |need| self.energy.current() > need; let use_leap = |controller: &mut Controller| { controller.push_basic_input(InputKind::Ability(0)); }; if attack_data.in_min_range() && attack_data.angle < 45.0 { controller.inputs.move_dir = Vec2::zero(); if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 4.0 { controller.push_cancel_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] = 0.0; } else if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 3.0 { controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] += read_data.dt.0; } else if has_leap() && has_energy(50.0) && rng.gen_bool(0.9) { use_leap(controller); agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] += read_data.dt.0; } else { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] += read_data.dt.0; } } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); if attack_data.dist_sqrd < 32.0f32.powi(2) && has_leap() && has_energy(50.0) && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { use_leap(controller); } if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.dist_sqrd < 16.0f32.powi(2) && rng.gen::() < 0.02 { controller.push_basic_input(InputKind::Roll); } } } pub fn handle_cardinal_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { const DESIRED_ENERGY_LEVEL: f32 = 50.0; const DESIRED_COMBO_LEVEL: u32 = 8; const MINION_SUMMON_THRESHOLD: f32 = 0.10; enum ActionStateConditions { ConditionCounterInitialized = 0, } enum ActionStateFCounters { FCounterHealthThreshold = 0, } let health_fraction = self.health.map_or(0.5, |h| h.fraction()); // Sets counter at start of combat, using `condition` to keep track of whether // it was already intitialized if !agent.combat_state.conditions [ActionStateConditions::ConditionCounterInitialized as usize] { agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] = 1.0 - MINION_SUMMON_THRESHOLD; agent.combat_state.conditions [ActionStateConditions::ConditionCounterInitialized as usize] = true; } if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] > health_fraction { // Summon minions at particular thresholds of health controller.push_basic_input(InputKind::Ability(1)); if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover)) { agent.combat_state.counters [ActionStateFCounters::FCounterHealthThreshold as usize] -= MINION_SUMMON_THRESHOLD; } } // Logic to use abilities else if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2) && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { // If far enough away, and can see target, check which skill is appropriate to // use if self.energy.current() > DESIRED_ENERGY_LEVEL && read_data .combos .get(*self.entity) .map_or(false, |c| c.counter() >= DESIRED_COMBO_LEVEL) && !read_data.buffs.get(*self.entity).iter().any(|buff| { buff.iter_kind(BuffKind::Regeneration) .peekable() .peek() .is_some() }) { // If have enough energy and combo to use healing aura, do so controller.push_basic_input(InputKind::Secondary); } else 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) .peekable() .peek() .is_some() }) { // Use steam beam if target is far enough away, self is not buffed, and have // sufficient energy controller.push_basic_input(InputKind::Ability(0)); } else { // If low on energy, use primary to attempt to regen energy // Or if at desired energy level but not able/willing to ward, just attack controller.push_basic_input(InputKind::Primary); } } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { if self.body.map_or(false, |b| b.is_humanoid()) && self.energy.current() > CharacterAbility::default_roll(Some(self.char_state)).energy_cost() && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover)) { // Else use steam beam controller.push_basic_input(InputKind::Ability(0)); } else if attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } } // 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( &*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 entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() .rotated_z(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(bearing.z > 1.5, controller); controller.inputs.move_z = bearing.z; } } // 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) && rng.gen::() < 0.01 { controller.push_basic_input(InputKind::Roll); } } else { // If too far, move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_sea_bishop_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) }; enum ActionStateTimers { TimerBeam = 0, } if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 6.0 { agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0; } else { agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0; } // When enemy in sight beam for 3 seconds, every 6 seconds if line_of_sight_with_target() && agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 3.0 { controller.push_basic_input(InputKind::Primary); } // 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( &*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 line_of_sight_with_target() && attack_data.angle < 45.0 { controller.inputs.move_dir = bearing .xy() .rotated_z(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(bearing.z > 1.5, controller); controller.inputs.move_z = bearing.z; } } } else { // If too far, move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_dagon_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { enum ActionStateTimers { TimerDagon = 0, } if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.5 { agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] = 0.0; } // if target gets very close, shoot dagon bombs and lay out sea urchins if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) { if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0; } else { controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0; } // if target in close range use steambeam and shoot dagon bombs } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) { controller.inputs.move_dir = Vec2::zero(); if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0; } else { controller.push_basic_input(InputKind::Ability(1)); } } else if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) { // if enemy is far, heal and shoot bombs if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 { controller.push_basic_input(InputKind::Primary); } else { controller.push_basic_input(InputKind::Ability(2)); } agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0; } else if entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { // if enemy in mid range shoot dagon bombs and steamwave if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 { controller.push_basic_input(InputKind::Primary); agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0; } else { controller.push_basic_input(InputKind::Ability(0)); agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0; } } // chase let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { Path::Separate } else { Path::Partial }; self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None); } pub fn handle_hermit_alligator_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, read_data: &ReadData, ) { enum Timers { TimerAttack = 0, } let attack_timer = &mut agent.combat_state.timers[Timers::TimerAttack as usize]; if *attack_timer > 2.5 { *attack_timer = 0.0; } // if target gets very close, use tail attack and shockwave if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) { if *attack_timer > 1.0 { controller.push_basic_input(InputKind::Primary); *attack_timer += read_data.dt.0; } else { controller.push_basic_input(InputKind::Secondary); *attack_timer += read_data.dt.0; } // if target in close range use beam and shoot dagon bombs } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) { controller.inputs.move_dir = Vec2::zero(); if *attack_timer > 2.0 { controller.push_basic_input(InputKind::Ability(0)); *attack_timer += read_data.dt.0; } else { controller.push_basic_input(InputKind::Ability(1)); } } else { // if target in midrange range shoot dagon bombs and heal if *attack_timer > 1.0 { controller.push_basic_input(InputKind::Ability(0)); *attack_timer += read_data.dt.0; } else { controller.push_basic_input(InputKind::Ability(2)); *attack_timer += read_data.dt.0; } } } pub fn handle_deadwood( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const BEAM_RANGE: f32 = 20.0; const BEAM_TIME: Duration = Duration::from_secs(3); // combat_state.condition controls whether or not deadwood should beam or dash if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover) { // If already dashing, keep dashing and have move_dir set to forward controller.push_basic_input(InputKind::Secondary); controller.inputs.move_dir = self.ori.look_vec().xy(); } else if attack_data.in_min_range() && attack_data.angle_xy < 10.0 { // If near target, dash at them and through them to get away controller.push_basic_input(InputKind::Secondary); } else if matches!(self.char_state, CharacterState::BasicBeam(s) if s.stage_section != StageSection::Recover && s.timer < BEAM_TIME) { // If already beaming, keep beaming if not beaming for over 5 seconds controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < BEAM_RANGE.powi(2) { // Else if in beam range, beam them if attack_data.angle_xy < 5.0 { controller.push_basic_input(InputKind::Primary); } else { // If not in angle, apply slight movement so deadwood orients itself correctly controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .try_normalized() .unwrap_or_else(Vec2::zero) * 0.01; } } else { // Otherwise too far, move towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_mandragora( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const SCREAM_RANGE: f32 = 10.0; enum ActionStateFCounters { FCounterHealthThreshold = 0, } enum ActionStateConditions { ConditionHasScreamed = 0, } if !agent.combat_state.initialized { agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] = self.health.map_or(0.0, |h| h.maximum()); agent.combat_state.initialized = true; } if !agent.combat_state.conditions[ActionStateConditions::ConditionHasScreamed as usize] { // If mandragora is still "sleeping" and hasn't screamed yet, do nothing until // target in range or until it's taken damage if self.health.map_or(false, |h| { h.current() < agent.combat_state.counters [ActionStateFCounters::FCounterHealthThreshold as usize] }) || attack_data.dist_sqrd < SCREAM_RANGE.powi(2) { agent.combat_state.conditions [ActionStateConditions::ConditionHasScreamed as usize] = true; controller.push_basic_input(InputKind::Secondary); } } else { // Once mandragora has woken, move towards target and attack if attack_data.in_min_range() { controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { // If in pathing range and can see target, move towards them self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } else { // Otherwise, go back to sleep agent.combat_state.conditions [ActionStateConditions::ConditionHasScreamed as usize] = false; agent.combat_state.counters [ActionStateFCounters::FCounterHealthThreshold as usize] = self.health.map_or(0.0, |h| h.maximum()); } } } pub fn handle_wood_golem( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const SHOCKWAVE_RANGE: f32 = 25.0; const SHOCKWAVE_WAIT_TIME: f32 = 7.5; const SPIN_WAIT_TIME: f32 = 3.0; enum ActionStateTimers { TimerSpinWait = 0, TimerShockwaveWait, } // After spinning, reset timer let current_input = self.char_state.ability_info().map(|ai| ai.input); if matches!(current_input, Some(InputKind::Secondary)) { agent.combat_state.timers[ActionStateTimers::TimerSpinWait as usize] = 0.0; } if attack_data.in_min_range() { // If in minimum range if agent.combat_state.timers[ActionStateTimers::TimerSpinWait as usize] > SPIN_WAIT_TIME { // If it's been too long since able to hit target, spin controller.push_basic_input(InputKind::Secondary); } else if attack_data.angle < 30.0 { // Else if in angle to strike, strike controller.push_basic_input(InputKind::Primary); } else { // Else increment spin timer agent.combat_state.timers[ActionStateTimers::TimerSpinWait as usize] += read_data.dt.0; // If not in angle, apply slight movement so golem orients itself correctly controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) .xy() .try_normalized() .unwrap_or_else(Vec2::zero) * 0.01; } } else { // Else if too far for melee if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 45.0 { // Shockwave if close enough and haven't shockwaved too recently if agent.combat_state.timers[ActionStateTimers::TimerSpinWait as usize] > SHOCKWAVE_WAIT_TIME { controller.push_basic_input(InputKind::Ability(0)); } if matches!(self.char_state, CharacterState::Shockwave(_)) { agent.combat_state.timers[ActionStateTimers::TimerShockwaveWait as usize] = 0.0; } else { agent.combat_state.timers[ActionStateTimers::TimerShockwaveWait as usize] += read_data.dt.0; } } // And always try to path towards target self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_gnarling_chieftain( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { const TOTEM_TIMER: f32 = 10.0; const HEAVY_ATTACK_WAIT_TIME: f32 = 15.0; enum ActionStateTimers { TimerSummonTotem = 0, TimerShockwave, } // Handle timers agent.combat_state.timers[ActionStateTimers::TimerSummonTotem as usize] += read_data.dt.0; match self.char_state { CharacterState::BasicSummon(_) => { agent.combat_state.timers[ActionStateTimers::TimerSummonTotem as usize] = 0.0 }, CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => { agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] = 0.0 }, _ => {}, } if !agent.combat_state.initialized { // If not initialized yet, start out by summoning green totem controller.push_basic_input(InputKind::Ability(2)); if matches!(self.char_state, CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover) { agent.combat_state.initialized = true; } } else if agent.combat_state.timers[ActionStateTimers::TimerSummonTotem as usize] > TOTEM_TIMER { // If time to summon a totem, do it let input = rng.gen_range(1..=3); let buff_kind = match input { 2 => Some(BuffKind::Regeneration), 3 => Some(BuffKind::Hastened), _ => None, }; if buff_kind.map_or(true, |b| self.has_buff(read_data, b)) && matches!(self.char_state, CharacterState::Wielding { .. }) { // If already under effects of buff from totem that would be summoned, don't // summon totem (doesn't work for red totems since that applies debuff to // enemies instead) agent.combat_state.timers[ActionStateTimers::TimerSummonTotem as usize] = 0.0; } else { controller.push_basic_input(InputKind::Ability(input)); } } else if agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] > HEAVY_ATTACK_WAIT_TIME { // Else if time for a heavy attack if attack_data.in_min_range() { // If in range, shockwave controller.push_basic_input(InputKind::Ability(0)); } else if entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) { // Else if in sight, barrage controller.push_basic_input(InputKind::Secondary); } } else if attack_data.in_min_range() { // Else if not time to use anything fancy, if in range and angle, strike them if attack_data.angle < 20.0 { controller.push_basic_input(InputKind::Primary); agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] += read_data.dt.0; } else { // If not in angle, charge heavy attack faster agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] += read_data.dt.0 * 5.0; } } else { // If not in range, charge heavy attack faster agent.combat_state.counters[ActionStateTimers::TimerShockwave as usize] += read_data.dt.0 * 3.3; } self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Full, None, ); } pub fn handle_sword_simple_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const DASH_TIMER: usize = 0; agent.combat_state.timers[DASH_TIMER] += read_data.dt.0; if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover)) { controller.push_basic_input(InputKind::Secondary); } else if attack_data.in_min_range() && attack_data.angle < 45.0 { if agent.combat_state.timers[DASH_TIMER] > 2.0 { agent.combat_state.timers[DASH_TIMER] = 0.0; } controller.push_basic_input(InputKind::Primary); } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) && self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ) && entities_have_line_of_sight( self.pos, self.body, self.scale, tgt_data.pos, tgt_data.body, tgt_data.scale, read_data, ) && agent.combat_state.timers[DASH_TIMER] > 4.0 && attack_data.angle < 45.0 { controller.push_basic_input(InputKind::Secondary); agent.combat_state.timers[DASH_TIMER] = 0.0; } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } pub fn handle_adlet_hunter( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { const ROTATE_TIMER: usize = 0; const ROTATE_DIR_CONDITION: usize = 0; agent.combat_state.timers[ROTATE_TIMER] -= read_data.dt.0; if agent.combat_state.timers[ROTATE_TIMER] < 0.0 { agent.combat_state.conditions[ROTATE_DIR_CONDITION] = rng.gen_bool(0.5); agent.combat_state.timers[ROTATE_TIMER] = rng.gen::() * 5.0; } let primary = self.extract_ability(AbilityInput::Primary); 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, AbilityPreferences::default(), ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { s.could_use( attack_data, self, tgt_data, read_data, AbilityPreferences::default(), ) }), _ => false, }; let move_forwards = if could_use_input(InputKind::Primary) { controller.push_basic_input(InputKind::Primary); false } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 8_f32.powi(2) { controller.push_basic_input(InputKind::Secondary); true } else { true }; if move_forwards && attack_data.dist_sqrd > 3_f32.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); let dir = if agent.combat_state.conditions[ROTATE_DIR_CONDITION] { 1.0 } else { -1.0 }; controller.inputs.move_dir.rotate_z(PI / 2.0 * dir); } } pub fn handle_adlet_icepicker( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { let primary = self.extract_ability(AbilityInput::Primary); 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, AbilityPreferences::default(), ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { s.could_use( attack_data, self, tgt_data, read_data, AbilityPreferences::default(), ) }), _ => false, }; let move_forwards = if could_use_input(InputKind::Primary) { controller.push_basic_input(InputKind::Primary); false } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 5_f32.powi(2) { controller.push_basic_input(InputKind::Secondary); false } else { true }; if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } } pub fn handle_adlet_tracker( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const TRAP_TIMER: usize = 0; agent.combat_state.timers[TRAP_TIMER] += read_data.dt.0; if agent.combat_state.timers[TRAP_TIMER] > 20.0 { agent.combat_state.timers[TRAP_TIMER] = 0.0; } 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, AbilityPreferences::default(), ) }), _ => false, }; let move_forwards = if agent.combat_state.timers[TRAP_TIMER] < 3.0 { controller.push_basic_input(InputKind::Secondary); false } else if could_use_input(InputKind::Primary) { controller.push_basic_input(InputKind::Primary); false } else { true }; if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } } pub fn handle_adlet_elder( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { const TRAP_TIMER: usize = 0; agent.combat_state.timers[TRAP_TIMER] -= read_data.dt.0; if matches!(self.char_state, CharacterState::BasicRanged(_)) { agent.combat_state.timers[TRAP_TIMER] = 15.0; } 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)), ]; 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, AbilityPreferences::default(), ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { 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, AbilityPreferences::default(), ) }), _ => false, }; let move_forwards = if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover) { controller.push_basic_input(InputKind::Secondary); false } else if agent.combat_state.timers[TRAP_TIMER] < 0.0 && !tgt_data.considered_ranged() { controller.push_basic_input(InputKind::Ability(0)); false } else if could_use_input(InputKind::Primary) { controller.push_basic_input(InputKind::Primary); false } else if could_use_input(InputKind::Secondary) && rng.gen_bool(0.5) { controller.push_basic_input(InputKind::Secondary); false } else if could_use_input(InputKind::Ability(1)) { controller.push_basic_input(InputKind::Ability(1)); false } else { true }; if matches!(self.char_state, CharacterState::LeapMelee(_)) { let tgt_vec = tgt_data.pos.0.xy() - self.pos.0.xy(); if tgt_vec.magnitude_squared() > 2_f32.powi(2) { if let Some(look_dir) = Dir::from_unnormalized(Vec3::from(tgt_vec)) { controller.inputs.look_dir = look_dir; } } } if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } } pub fn handle_icedrake( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, ) { 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)), ]; 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, AbilityPreferences::default(), ) }), InputKind::Secondary => secondary.as_ref().map_or(false, |s| { 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, AbilityPreferences::default(), ) }), _ => false, }; let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) { Some(input @ InputKind::Primary) => { if !matches!(self.char_state.stage_section(), Some(StageSection::Recover)) && could_use_input(input) { controller.push_basic_input(input); true } else { false } }, Some(input @ InputKind::Ability(1)) => { if self .char_state .timer() .map_or(false, |t| t.as_secs_f32() < 3.0) && could_use_input(input) { controller.push_basic_input(input); true } else { false } }, _ => false, }; let move_forwards = if !continued_attack { if could_use_input(InputKind::Primary) && rng.gen_bool(0.4) { controller.push_basic_input(InputKind::Primary); false } else if could_use_input(InputKind::Secondary) && rng.gen_bool(0.8) { controller.push_basic_input(InputKind::Secondary); false } else if could_use_input(InputKind::Ability(1)) && rng.gen_bool(0.9) { controller.push_basic_input(InputKind::Ability(1)); true } else if could_use_input(InputKind::Ability(0)) { controller.push_basic_input(InputKind::Ability(0)); true } else { true } } else { false }; if move_forwards { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } } pub fn handle_random_abilities( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, rng: &mut impl Rng, primary_weight: u8, secondary_weight: u8, ability_weights: [u8; BASE_ABILITY_LIMIT], ) { 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| match input { InputKind::Primary => primary.as_ref().map_or(false, |p| { 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, AbilityPreferences::default(), ) }), InputKind::Ability(x) => abilities[x].as_ref().map_or(false, |a| { a.could_use( attack_data, self, tgt_data, read_data, AbilityPreferences::default(), ) }), _ => false, }; let primary_chance = primary_weight as f64 / ((primary_weight + secondary_weight + ability_weights.iter().sum::()) as f64) .max(0.01); let secondary_chance = secondary_weight as f64 / ((secondary_weight + ability_weights.iter().sum::()) as f64).max(0.01); let ability_chances = { let mut chances = [0.0; BASE_ABILITY_LIMIT]; chances.iter_mut().enumerate().for_each(|(i, chance)| { *chance = ability_weights[i] as f64 / (ability_weights .iter() .enumerate() .filter_map(|(j, weight)| if j >= i { Some(weight) } else { None }) .sum::() as f64) .max(0.01) }); chances }; if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) { match self.char_state { CharacterState::ChargedMelee(c) => { if c.charge_frac() < 1.0 && could_use_input(input) { controller.push_basic_input(input); } }, CharacterState::ChargedRanged(c) => { if c.charge_frac() < 1.0 && could_use_input(input) { controller.push_basic_input(input); } }, _ => {}, } } let move_forwards = if could_use_input(InputKind::Primary) && rng.gen_bool(primary_chance) { controller.push_basic_input(InputKind::Primary); false } else if could_use_input(InputKind::Secondary) && rng.gen_bool(secondary_chance) { controller.push_basic_input(InputKind::Secondary); false } else if could_use_input(InputKind::Ability(0)) && rng.gen_bool(ability_chances[0]) { controller.push_basic_input(InputKind::Ability(0)); false } else if could_use_input(InputKind::Ability(1)) && rng.gen_bool(ability_chances[1]) { controller.push_basic_input(InputKind::Ability(1)); false } else if could_use_input(InputKind::Ability(2)) && rng.gen_bool(ability_chances[2]) { controller.push_basic_input(InputKind::Ability(2)); false } else if could_use_input(InputKind::Ability(3)) && rng.gen_bool(ability_chances[3]) { controller.push_basic_input(InputKind::Ability(3)); false } else if could_use_input(InputKind::Ability(4)) && rng.gen_bool(ability_chances[4]) { controller.push_basic_input(InputKind::Ability(4)); false } else { true }; if move_forwards { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } } pub fn handle_simple_double_attack( &self, agent: &mut Agent, controller: &mut Controller, attack_data: &AttackData, tgt_data: &TargetData, read_data: &ReadData, ) { const MAX_ATTACK_RANGE: f32 = 20.0; if attack_data.angle < 60.0 && attack_data.dist_sqrd < MAX_ATTACK_RANGE.powi(2) { controller.inputs.move_dir = Vec2::zero(); if attack_data.in_min_range() { controller.push_basic_input(InputKind::Primary); } else { controller.push_basic_input(InputKind::Secondary); } } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Separate, None, ); } else { self.path_toward_target( agent, controller, tgt_data.pos.0, read_data, Path::Partial, None, ); } } }