diff --git a/assets/common/abilities/custom/harvester/explodingpumpkin.ron b/assets/common/abilities/custom/harvester/explodingpumpkin.ron index 6bb4a6bc09..3af60d04c7 100644 --- a/assets/common/abilities/custom/harvester/explodingpumpkin.ron +++ b/assets/common/abilities/custom/harvester/explodingpumpkin.ron @@ -4,7 +4,7 @@ BasicRanged( recover_duration: 1.6, projectile: ( kind: Explosive( - radius: 7.5, + radius: 7.2, min_falloff: 0.6, reagent: Some(Red), terrain: Some((5, Black)), diff --git a/assets/common/abilities/custom/harvester/scythe.ron b/assets/common/abilities/custom/harvester/scythe.ron index 5c6e63dff7..c9e76cd098 100644 --- a/assets/common/abilities/custom/harvester/scythe.ron +++ b/assets/common/abilities/custom/harvester/scythe.ron @@ -11,7 +11,7 @@ BasicMelee( knockback: 10.0, energy_regen: 0.0, ), - range: 4.0, + range: 4.5, angle: 60.0, multi_target: Some(Normal), ), diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 3fe487f938..c88f18ff35 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -1204,9 +1204,13 @@ impl<'a> AgentData<'a> { // Wield the weapon as running towards the target controller.push_action(ControlAction::Wield); - let min_attack_dist = (self.body.map_or(0.5, |b| b.max_radius()) + DEFAULT_ATTACK_RANGE) - * self.scale - + tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0); + let self_radius = self.body.map_or(0.5, |b| b.max_radius()) * self.scale; + let self_attack_range = + (self.body.map_or(0.5, |b| b.max_radius()) + DEFAULT_ATTACK_RANGE) * self.scale; + let tgt_radius = + tgt_data.body.map_or(0.5, |b| b.max_radius()) * tgt_data.scale.map_or(1.0, |s| s.0); + let min_attack_dist = self_attack_range + tgt_radius; + let body_dist = self_radius + tgt_radius; let dist_sqrd = self.pos.0.distance_squared(tgt_data.pos.0); let angle = self .ori @@ -1358,6 +1362,7 @@ impl<'a> AgentData<'a> { } let attack_data = AttackData { + body_dist, min_attack_dist, dist_sqrd, angle, diff --git a/server/agent/src/attack.rs b/server/agent/src/attack.rs index 6ad5abcebd..318931d739 100644 --- a/server/agent/src/attack.rs +++ b/server/agent/src/attack.rs @@ -4758,40 +4758,34 @@ impl<'a> AgentData<'a> { read_data: &ReadData, rng: &mut impl Rng, ) { + // --- setup --- + + // behaviour parameters const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60; const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30; + const MELEE_RANGE: f32 = 5.0; // hard coded from scythe attack const MAX_PUMPKIN_RANGE: f32 = 50.0; - const FIRE_BREATH_RANGE: f32 = 20.0; - const FIRE_BREATH_TIME: f32 = 4.0; - const FIRE_BREATH_SHORT_TIME: f32 = 2.0; - const FIRE_BREATH_COOLDOWN: f32 = 3.0; - const CLOSE_MIXUP_COOLDOWN: f32 = 6.0; + const FIREBREATH_RANGE: f32 = 20.0; // hard coded from firebreath attack + const FIREBREATH_TIME: f32 = 4.0; + const FIREBREATH_SHORT_TIME: f32 = 2.5; // cutoff sooner at close range + const FIREBREATH_COOLDOWN: f32 = 3.0; + const CLOSE_MIXUP_COOLDOWN: f32 = 5.0; // variation in attacks at close range + const FAR_PUMPKIN_COOLDOWN: f32 = 1.0; // allows for pathing to player between throws + // conditions enum ActionStateConditions { - ConditionHasSummonedFirstVines = 0, - ConditionHasSummonedSecondVines, + HasSummonedFirstVines = 0, + HasSummonedSecondVines, } + // timers enum ActionStateTimers { - TimerFire = 0, - TimerCloseMixup, + SinceFirebreath = 0, + SinceCloseMixup, + SinceFarPumpkin, } - match self.char_state { - CharacterState::BasicBeam(_) => { - agent.combat_state.timers[ActionStateTimers::TimerFire as usize] = 0.0; - }, - CharacterState::BasicRanged(_) => { - agent.combat_state.timers[ActionStateTimers::TimerCloseMixup as usize] = 0.0; - }, - _ => { - agent.combat_state.timers[ActionStateTimers::TimerFire as usize] += read_data.dt.0; - agent.combat_state.timers[ActionStateTimers::TimerCloseMixup as usize] += - read_data.dt.0; - }, - } - - let health_fraction = self.health.map_or(0.5, |h| h.fraction()); + // setup line of sight check let line_of_sight_with_target = || { entities_have_line_of_sight( self.pos, @@ -4804,95 +4798,159 @@ impl<'a> AgentData<'a> { ) }; - if (health_fraction < FIRST_VINE_CREATION_THRESHOLD - && !agent.combat_state.conditions - [ActionStateConditions::ConditionHasSummonedFirstVines as usize]) + // --- readability macros --- + + // actions + macro_rules! reset_timer { + ($timer:ident) => { + agent.combat_state.timers[ActionStateTimers::$timer as usize] = 0.0 + }; + } + macro_rules! increment_timer { + ($timer:ident) => { + agent.combat_state.timers[ActionStateTimers::$timer as usize] += read_data.dt.0 + }; + } + macro_rules! use_scythe { + () => { + controller.push_basic_input(InputKind::Primary) + }; + } + macro_rules! use_fire_breath { + () => { + controller.push_basic_input(InputKind::Secondary) + }; + } + macro_rules! use_pumpkin { + () => { + controller.push_basic_input(InputKind::Ability(0)) + }; + } + macro_rules! use_first_vines { + () => { + controller.push_basic_input(InputKind::Ability(1)) + }; + } + macro_rules! use_second_vines { + () => { + controller.push_basic_input(InputKind::Ability(2)) + }; + } + macro_rules! move_to_target { + () => { + self.path_toward_target( + agent, + controller, + tgt_data.pos.0, + read_data, + Path::Partial, + None, + ) + }; + } + + // shortcuts + macro_rules! conditions { + ($condition:ident) => { + agent.combat_state.conditions[ActionStateConditions::$condition as usize] + }; + } + macro_rules! timers { + ($timer:ident) => { + agent.combat_state.timers[ActionStateTimers::$timer as usize] + }; + } + macro_rules! is_in_vines_recovery { + () => ( + matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover)) + ) + } + macro_rules! is_using_firebreath { + // currently using firebreath and under time limit + ($time_limit:ident) => ( + matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32($time_limit)) + ) + } + + // --- main --- + + // handle timers + match self.char_state { + CharacterState::BasicBeam(_) => { + reset_timer!(SinceFirebreath); + }, + CharacterState::BasicRanged(_) => { + reset_timer!(SinceCloseMixup); + reset_timer!(SinceFarPumpkin); + }, + _ => { + increment_timer!(SinceFirebreath); + increment_timer!(SinceCloseMixup); + increment_timer!(SinceFarPumpkin); + }, + } + + // vine summoning + let health_fraction = self.health.map_or(0.5, |h| h.fraction()); + + if (health_fraction < FIRST_VINE_CREATION_THRESHOLD && !conditions!(HasSummonedFirstVines)) || (health_fraction < SECOND_VINE_CREATION_THRESHOLD - && !agent.combat_state.conditions - [ActionStateConditions::ConditionHasSummonedSecondVines as usize]) + && !conditions!(HasSummonedSecondVines)) { if health_fraction < SECOND_VINE_CREATION_THRESHOLD { - // Summon second vines when reach threshold of health - controller.push_basic_input(InputKind::Ability(2)); - - if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover)) - { - agent.combat_state.conditions - [ActionStateConditions::ConditionHasSummonedSecondVines as usize] = true; + use_second_vines!(); + if is_in_vines_recovery!() { + conditions!(HasSummonedSecondVines) = true; } } else { - // Summon first vines when reach threshold of health - controller.push_basic_input(InputKind::Ability(1)); - - if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover)) - { - agent.combat_state.conditions - [ActionStateConditions::ConditionHasSummonedFirstVines as usize] = true; + use_first_vines!(); + if is_in_vines_recovery!() { + conditions!(HasSummonedFirstVines) = true; } } - } else if attack_data.in_min_range() { - if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32(FIRE_BREATH_SHORT_TIME)) + } + // close range + else if attack_data.dist_sqrd < (attack_data.body_dist + 0.75 * MELEE_RANGE).powi(2) { + if is_using_firebreath!(FIREBREATH_SHORT_TIME) { + use_fire_breath!() + } else if timers!(SinceCloseMixup) > CLOSE_MIXUP_COOLDOWN + // for now, no line of sight check for consitency in attacks { - // Keep breathing fire if close enough, can see target, and have not been - // breathing for more than short limit - controller.push_basic_input(InputKind::Secondary); - } else if agent.combat_state.timers[ActionStateTimers::TimerCloseMixup as usize] - > CLOSE_MIXUP_COOLDOWN - && line_of_sight_with_target() - { - if agent.combat_state.timers[ActionStateTimers::TimerFire as usize] - < FIRE_BREATH_COOLDOWN - { - // Throw a close range pumpkin to knock back player - controller.push_basic_input(InputKind::Ability(0)); + if timers!(SinceFirebreath) < FIREBREATH_COOLDOWN { + use_pumpkin!(); } else { - let randomise = rng.gen_range(1..=3); + let randomise = rng.gen_range(1..=2); match randomise { - 1 => controller.push_basic_input(InputKind::Secondary), /* start fire - * breath */ - _ => controller.push_basic_input(InputKind::Ability(0)), /* close range - * pumpkin - * _ => controller.push_basic_input(InputKind::Primary), // scythe */ + 1 => use_fire_breath!(), + _ => use_pumpkin!(), } } } else if attack_data.angle < 60.0 { - // Scythe them if they're in range and angle - controller.push_basic_input(InputKind::Primary); + use_scythe!(); } - } else if attack_data.dist_sqrd < FIRE_BREATH_RANGE.powi(2) { - if line_of_sight_with_target() { - if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32(FIRE_BREATH_TIME)) - { - // Keep breathing fire if close enough, can see target, and have not been - // breathing for more than upper limit - controller.push_basic_input(InputKind::Secondary); - } else if attack_data.angle < 30.0 - && agent.combat_state.timers[ActionStateTimers::TimerFire as usize] - > FIRE_BREATH_COOLDOWN - { - // Start breathing fire at them if close enough, in angle, and can see target - controller.push_basic_input(InputKind::Secondary); - } else if agent.combat_state.timers[ActionStateTimers::TimerCloseMixup as usize] - > CLOSE_MIXUP_COOLDOWN - { - // Throw a close range pumpkin to knock back player - controller.push_basic_input(InputKind::Ability(0)); - } + } + // mid range (with line of sight) + else if attack_data.dist_sqrd < FIREBREATH_RANGE.powi(2) && line_of_sight_with_target() { + if is_using_firebreath!(FIREBREATH_TIME) { + use_fire_breath!() + } else if attack_data.angle < 30.0 && timers!(SinceFirebreath) > FIREBREATH_COOLDOWN { + use_fire_breath!() + } else if timers!(SinceCloseMixup) > CLOSE_MIXUP_COOLDOWN { + use_pumpkin!(); } - } 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(0)); + } + // long range (with line of sight) + else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2) + && line_of_sight_with_target() + && timers!(SinceFarPumpkin) > FAR_PUMPKIN_COOLDOWN + { + use_pumpkin!(); } - // Always attempt to path towards target - self.path_toward_target( - agent, - controller, - tgt_data.pos.0, - read_data, - Path::Partial, - None, - ); + // closing gap + if !(attack_data.dist_sqrd < (attack_data.body_dist + 0.4 * MELEE_RANGE).powi(2)) { + move_to_target!(); + } } pub fn handle_frostgigas_attack( diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 0df1bdcc39..9be063076c 100755 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -145,6 +145,7 @@ impl<'a> TargetData<'a> { } pub struct AttackData { + pub body_dist: f32, pub min_attack_dist: f32, pub dist_sqrd: f32, pub angle: f32,