From ac2f097d80a573fc2f51e82be2989643a5b5b75c Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 25 May 2021 21:20:27 -0500 Subject: [PATCH] Tidal warrior AI. --- .../common/abilities/ability_set_manifest.ron | 2 +- .../abilities/custom/tidalwarrior/bubbles.ron | 2 +- .../abilities/custom/tidalwarrior/scuttle.ron | 2 +- .../abilities/custom/tidalwarrior/totem.ron | 32 +++-- .../items/npc_weapons/unique/tidal_claws.ron | 2 +- common/src/states/combo_melee.rs | 11 +- common/src/states/dash_melee.rs | 11 +- common/src/states/leap_melee.rs | 17 +-- common/src/states/roll.rs | 21 ++-- common/src/states/spin_melee.rs | 11 +- common/src/states/utils.rs | 14 +-- server/src/sys/agent.rs | 118 ++++++++++++++++-- 12 files changed, 150 insertions(+), 93 deletions(-) diff --git a/assets/common/abilities/ability_set_manifest.ron b/assets/common/abilities/ability_set_manifest.ron index b80f3a6685..13491aa70c 100644 --- a/assets/common/abilities/ability_set_manifest.ron +++ b/assets/common/abilities/ability_set_manifest.ron @@ -108,7 +108,7 @@ secondary: "common.abilities.custom.wendigomagic.singlestrike", abilities: [], ), - Custom("Tidal Claws"): ( + Custom("Tidal Warrior"): ( primary: "common.abilities.custom.tidalwarrior.pincer", secondary: "common.abilities.custom.tidalwarrior.scuttle", abilities: [ diff --git a/assets/common/abilities/custom/tidalwarrior/bubbles.ron b/assets/common/abilities/custom/tidalwarrior/bubbles.ron index f6a60503d2..e53f23bd9c 100644 --- a/assets/common/abilities/custom/tidalwarrior/bubbles.ron +++ b/assets/common/abilities/custom/tidalwarrior/bubbles.ron @@ -10,7 +10,7 @@ BasicBeam( kind: Wet, dur_secs: 15.0, strength: Value(4.5), - chance: 0.25, + chance: 1.0, ))), energy_regen: 0, energy_drain: 0, diff --git a/assets/common/abilities/custom/tidalwarrior/scuttle.ron b/assets/common/abilities/custom/tidalwarrior/scuttle.ron index 28552b4f46..979ce9c032 100644 --- a/assets/common/abilities/custom/tidalwarrior/scuttle.ron +++ b/assets/common/abilities/custom/tidalwarrior/scuttle.ron @@ -14,7 +14,7 @@ DashMelee( charge_duration: 2.0, swing_duration: 0.1, recover_duration: 0.5, - charge_through: false, + charge_through: true, is_interruptible: false, damage_kind: Crushing, ) diff --git a/assets/common/abilities/custom/tidalwarrior/totem.ron b/assets/common/abilities/custom/tidalwarrior/totem.ron index e7f84eac65..013a0aade2 100644 --- a/assets/common/abilities/custom/tidalwarrior/totem.ron +++ b/assets/common/abilities/custom/tidalwarrior/totem.ron @@ -1,18 +1,14 @@ -BasicMelee( - energy_cost: 0, - buildup_duration: 0.3, - swing_duration: 0.1, - recover_duration: 0.6, - base_damage: 50.0, - base_poise_damage: 100.0, - knockback: ( strength: 50.0, direction: Towards), - range: 5.0, - max_angle: 60.0, - damage_effect: Some(Buff(( - kind: Crippled, - dur_secs: 15.0, - strength: Value(0.5), - chance: 1.0, - ))), - damage_kind: Slashing, -) +BasicSummon( + buildup_duration: 0.5, + cast_duration: 1.0, + recover_duration: 0.5, + summon_amount: 6, + summon_info: ( + // If this is still HaniwaSentry, code reviewers open a comment. I'll know what to do. + body: Object(HaniwaSentry), + scale: None, + health_scaling: 20, + loadout_config: None, + skillset_config: None, + ), +) \ No newline at end of file diff --git a/assets/common/items/npc_weapons/unique/tidal_claws.ron b/assets/common/items/npc_weapons/unique/tidal_claws.ron index ac0a93a417..f2f42d6bcc 100644 --- a/assets/common/items/npc_weapons/unique/tidal_claws.ron +++ b/assets/common/items/npc_weapons/unique/tidal_claws.ron @@ -15,5 +15,5 @@ ItemDef( )), quality: Low, tags: [], - ability_spec: Some(Custom("Tidal Claws")), + ability_spec: Some(Custom("Tidal Warrior")), ) \ No newline at end of file diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs index 49d5e7c1b0..a59315043a 100644 --- a/common/src/states/combo_melee.rs +++ b/common/src/states/combo_melee.rs @@ -235,14 +235,9 @@ impl CharacterBehavior for Data { handle_orientation(data, &mut update, 0.4 * self.static_data.ori_modifier); // Forward movement - handle_forced_movement( - data, - &mut update, - ForcedMovement::Forward { - strength: self.static_data.stage_data[stage_index].forward_movement, - }, - 0.3, - ); + handle_forced_movement(data, &mut update, ForcedMovement::Forward { + strength: self.static_data.stage_data[stage_index].forward_movement, + }); // Swings update.character = CharacterState::ComboMelee(Data { diff --git a/common/src/states/dash_melee.rs b/common/src/states/dash_melee.rs index 94ff0db463..bb916ce4f4 100644 --- a/common/src/states/dash_melee.rs +++ b/common/src/states/dash_melee.rs @@ -106,14 +106,9 @@ impl CharacterBehavior for Data { .min(1.0); handle_orientation(data, &mut update, 0.6); - handle_forced_movement( - data, - &mut update, - ForcedMovement::Forward { - strength: self.static_data.forward_speed * charge_frac.sqrt(), - }, - 0.1, - ); + handle_forced_movement(data, &mut update, ForcedMovement::Forward { + strength: self.static_data.forward_speed * charge_frac.sqrt(), + }); // This logic basically just decides if a charge should end, and prevents the // character state spamming attacks while checking if it has hit something diff --git a/common/src/states/leap_melee.rs b/common/src/states/leap_melee.rs index b6b53f357f..59c6e2a49c 100644 --- a/common/src/states/leap_melee.rs +++ b/common/src/states/leap_melee.rs @@ -86,17 +86,12 @@ impl CharacterBehavior for Data { let progress = 1.0 - self.timer.as_secs_f32() / self.static_data.movement_duration.as_secs_f32(); - handle_forced_movement( - data, - &mut update, - ForcedMovement::Leap { - vertical: self.static_data.vertical_leap_strength, - forward: self.static_data.forward_leap_strength, - progress, - direction: MovementDirection::Look, - }, - 0.15, - ); + handle_forced_movement(data, &mut update, ForcedMovement::Leap { + vertical: self.static_data.vertical_leap_strength, + forward: self.static_data.forward_leap_strength, + progress, + direction: MovementDirection::Look, + }); // Increment duration // If we were to set a timeout for state, this would be diff --git a/common/src/states/roll.rs b/common/src/states/roll.rs index e8fa9ffb69..f6d83ab4f0 100644 --- a/common/src/states/roll.rs +++ b/common/src/states/roll.rs @@ -78,19 +78,14 @@ impl CharacterBehavior for Data { }, StageSection::Movement => { // Update velocity - handle_forced_movement( - data, - &mut update, - ForcedMovement::Forward { - strength: self.static_data.roll_strength - * ((1.0 - - self.timer.as_secs_f32() - / self.static_data.movement_duration.as_secs_f32()) - / 2.0 - + 0.5), - }, - 0.0, - ); + handle_forced_movement(data, &mut update, ForcedMovement::Forward { + strength: self.static_data.roll_strength + * ((1.0 + - self.timer.as_secs_f32() + / self.static_data.movement_duration.as_secs_f32()) + / 2.0 + + 0.5), + }); if self.timer < self.static_data.movement_duration { // Movement diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs index f6afd23e1a..2caf344353 100644 --- a/common/src/states/spin_melee.rs +++ b/common/src/states/spin_melee.rs @@ -167,14 +167,9 @@ impl CharacterBehavior for Data { self.static_data.movement_behavior, MovementBehavior::ForwardGround ) { - handle_forced_movement( - data, - &mut update, - ForcedMovement::Forward { - strength: self.static_data.forward_speed, - }, - 0.1, - ); + handle_forced_movement(data, &mut update, ForcedMovement::Forward { + strength: self.static_data.forward_speed, + }); } // Swings diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 5a31e27068..b95550ed1e 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -263,21 +263,15 @@ fn basic_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) { } /// Handles forced movement -pub fn handle_forced_movement( - data: &JoinData, - update: &mut StateUpdate, - movement: ForcedMovement, - efficiency: f32, -) { - let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier; - +pub fn handle_forced_movement(data: &JoinData, update: &mut StateUpdate, movement: ForcedMovement) { match movement { ForcedMovement::Forward { strength } => { + let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier; if let Some(accel) = data.physics.on_ground.then_some(data.body.base_accel()) { update.vel.0 += Vec2::broadcast(data.dt.0) * accel - * (data.inputs.move_dir + Vec2::from(update.ori) * strength) - * efficiency; + * (data.inputs.move_dir + Vec2::from(update.ori)) + * strength; } }, ForcedMovement::Leap { diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 7f26303ef6..78ab37a999 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -87,6 +87,10 @@ struct AttackData { angle: f32, } +impl AttackData { + fn in_min_range(&self) -> bool { self.dist_sqrd < self.min_attack_dist.powi(2) } +} + #[derive(Eq, PartialEq)] pub enum Tactic { Melee, @@ -113,6 +117,7 @@ pub enum Tactic { BirdLargeFire, Minotaur, ClayGolem, + TidalWarrior, } #[derive(SystemData)] @@ -1601,6 +1606,7 @@ impl<'a> AgentData<'a> { "Mindflayer" => Tactic::Mindflayer, "Minotaur" => Tactic::Minotaur, "Clay Golem" => Tactic::ClayGolem, + "Tidal Warrior" => Tactic::TidalWarrior, _ => Tactic::Melee, }, AbilitySpec::Tool(tool_kind) => tool_tactic(*tool_kind), @@ -1843,6 +1849,13 @@ impl<'a> AgentData<'a> { &tgt_data, &read_data, ), + Tactic::TidalWarrior => self.handle_tidal_warrior_attack( + agent, + controller, + &attack_data, + &tgt_data, + &read_data, + ), } } @@ -1854,7 +1867,7 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 45.0 { + if attack_data.in_min_range() && attack_data.angle < 45.0 { controller .actions .push(ControlAction::basic_input(InputKind::Primary)); @@ -1883,7 +1896,7 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 45.0 { + if attack_data.in_min_range() && attack_data.angle < 45.0 { controller.inputs.move_dir = Vec2::zero(); if agent.action_state.timer > 6.0 { controller @@ -1932,7 +1945,7 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 45.0 { + if attack_data.in_min_range() && attack_data.angle < 45.0 { controller.inputs.move_dir = Vec2::zero(); if agent.action_state.timer > 4.0 { controller @@ -2004,7 +2017,7 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 45.0 { + if attack_data.in_min_range() && attack_data.angle < 45.0 { controller.inputs.move_dir = Vec2::zero(); if self .skill_set @@ -2226,9 +2239,7 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if self.body.map(|b| b.is_humanoid()).unwrap_or(false) - && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) - { + if self.body.map(|b| b.is_humanoid()).unwrap_or(false) && attack_data.in_min_range() { controller .actions .push(ControlAction::basic_input(InputKind::Roll)); @@ -2325,7 +2336,7 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && attack_data.angle < 90.0 { + if attack_data.in_min_range() && attack_data.angle < 90.0 { controller.inputs.move_dir = Vec2::zero(); controller .actions @@ -2371,8 +2382,7 @@ impl<'a> AgentData<'a> { radius: u32, circle_time: u32, ) { - if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) && thread_rng().gen_bool(0.5) - { + if attack_data.in_min_range() && thread_rng().gen_bool(0.5) { controller.inputs.move_dir = Vec2::zero(); controller .actions @@ -2679,7 +2689,7 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if attack_data.angle < 90.0 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) { + if attack_data.angle < 90.0 && attack_data.in_min_range() { controller.inputs.move_dir = Vec2::zero(); if agent.action_state.timer < 2.0 { controller @@ -2762,7 +2772,7 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if attack_data.angle < 90.0 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) { + if attack_data.angle < 90.0 && attack_data.in_min_range() { controller.inputs.move_dir = Vec2::zero(); controller .actions @@ -3190,7 +3200,7 @@ impl<'a> AgentData<'a> { agent.action_state.timer += read_data.dt.0; } else if agent.action_state.timer < 6.0 && attack_data.angle < 90.0 - && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) + && attack_data.in_min_range() { // Triplestrike controller @@ -3355,6 +3365,88 @@ impl<'a> AgentData<'a> { self.path_toward_target(agent, controller, tgt_data, read_data, true, None); } + 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; + 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.action_state.condition { + agent.action_state.counter = 1.0 - MINION_SUMMON_THRESHOLD; + agent.action_state.condition = true; + } + + if agent.action_state.counter > health_fraction { + // Summon minions at particular thresholds of health + controller + .actions + .push(ControlAction::basic_input(InputKind::Ability(1))); + + if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover)) + { + agent.action_state.counter -= 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 + .actions + .push(ControlAction::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 + .actions + .push(ControlAction::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 + .actions + .push(ControlAction::basic_input(InputKind::Primary)); + } else if attack_data.angle < 30.0 + && can_see_tgt( + &*read_data.terrain, + self.pos, + tgt_data.pos, + attack_data.dist_sqrd, + ) + { + // Start bubbling them if not close enough to do something else and in angle and + // can see target + controller + .actions + .push(ControlAction::basic_input(InputKind::Ability(0))); + } + } else if attack_data.angle < 90.0 + && can_see_tgt( + &*read_data.terrain, + self.pos, + tgt_data.pos, + attack_data.dist_sqrd, + ) + { + // Start scuttling if not close enough to do something else and in angle and can + // see target + controller + .actions + .push(ControlAction::basic_input(InputKind::Secondary)); + } + } + // Always attempt to path towards target + self.path_toward_target(agent, controller, tgt_data, read_data, false, None); + } + fn follow( &self, agent: &mut Agent,