timing and small balance changes to chieftain

This commit is contained in:
horblegorble 2024-06-23 23:16:20 +10:00
parent f47c509c04
commit e9f2d97045
7 changed files with 176 additions and 103 deletions

View File

@ -1,6 +1,6 @@
BasicRanged( BasicRanged(
energy_cost: 0, energy_cost: 0,
buildup_duration: 0.825, buildup_duration: 0.9,
recover_duration: 0.6, recover_duration: 0.6,
projectile: ( projectile: (
kind: Explosive( kind: Explosive(

View File

@ -9,8 +9,8 @@ Shockwave(
shockwave_angle: 360, shockwave_angle: 360,
shockwave_vertical_angle: 90, shockwave_vertical_angle: 90,
shockwave_speed: 12, shockwave_speed: 12,
shockwave_duration: 1, shockwave_duration: 1.0,
dodgeable: Roll, dodgeable: Jump,
move_efficiency: 0, move_efficiency: 0,
damage_kind: Energy, damage_kind: Energy,
specifier: Fire, specifier: Fire,

View File

@ -1,7 +1,7 @@
BasicMelee( BasicMelee(
energy_cost: 0, energy_cost: 0,
buildup_duration: 0.7, buildup_duration: 0.8,
swing_duration: 0.35, swing_duration: 0.4,
hit_timing: 0.5, hit_timing: 0.5,
recover_duration: 0.6, recover_duration: 0.6,
melee_constructor: ( melee_constructor: (
@ -11,8 +11,8 @@ BasicMelee(
knockback: 0, knockback: 0,
energy_regen: 0, energy_regen: 0,
), ),
range: 4.0, range: 3.0,
angle: 60.0, angle: 50.0,
damage_effect: Some(Buff(( damage_effect: Some(Buff((
kind: Burning, kind: Burning,
dur_secs: 4.0, dur_secs: 4.0,

View File

@ -12,7 +12,7 @@ BasicAura(
), ),
( (
kind: ProtectingWard, kind: ProtectingWard,
strength: 1, strength: 0.5,
duration: Some(1), duration: Some(1),
category: Magical, category: Magical,
), ),

View File

@ -6,7 +6,7 @@ BasicAura(
auras: [ auras: [
( (
kind: Burning, kind: Burning,
strength: 1, strength: 1.2,
duration: Some(1), duration: Some(1),
category: Magical, category: Magical,
), ),

View File

@ -1273,6 +1273,7 @@ impl Body {
_ => 300, _ => 300,
}, },
Body::BipedSmall(b) => match b.species { Body::BipedSmall(b) => match b.species {
biped_small::Species::GnarlingChieftain => 130,
biped_small::Species::IronDwarf | biped_small::Species::Flamekeeper => 300, biped_small::Species::IronDwarf | biped_small::Species::Flamekeeper => 300,
_ => 100, _ => 100,
}, },

View File

@ -4796,9 +4796,9 @@ impl<'a> AgentData<'a> {
// timers // timers
enum ActionStateTimers { enum ActionStateTimers {
FirebreathCooldown = 0, Firebreath = 0,
MixupCooldown, Mixup,
FarPumpkinCooldown, FarPumpkin,
} }
// //counters // //counters
@ -4833,27 +4833,28 @@ impl<'a> AgentData<'a> {
rng.gen_range(MID_MIXUP_COOLDOWN_SPAN[0]..=MID_MIXUP_COOLDOWN_SPAN[1]); rng.gen_range(MID_MIXUP_COOLDOWN_SPAN[0]..=MID_MIXUP_COOLDOWN_SPAN[1]);
agent.combat_state.counters[ActionStateCounters::FarPumpkinCooldown as usize] = agent.combat_state.counters[ActionStateCounters::FarPumpkinCooldown as usize] =
rng.gen_range(FAR_PUMPKIN_COOLDOWN_SPAN[0]..=FAR_PUMPKIN_COOLDOWN_SPAN[1]); rng.gen_range(FAR_PUMPKIN_COOLDOWN_SPAN[0]..=FAR_PUMPKIN_COOLDOWN_SPAN[1]);
// note: if f32 rng is too expensive, could use randomised u8 multipliers of a const f32 // note: if f32 rng is too expensive, could use randomised u8
// multipliers of a const f32
} }
// timer changes // timer changes
if matches!(self.char_state, CharacterState::SpriteSummon(_)) { if matches!(self.char_state, CharacterState::SpriteSummon(_)) {
// reset all timers when summoning // reset all timers when summoning
agent.combat_state.timers[ActionStateTimers::MixupCooldown as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::Mixup as usize] = 0.0;
agent.combat_state.timers[ActionStateTimers::FarPumpkinCooldown as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::FarPumpkin as usize] = 0.0;
agent.combat_state.timers[ActionStateTimers::FirebreathCooldown as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::Firebreath as usize] = 0.0;
} else { } else {
// increment timers when not in corresponding state // increment timers when not in corresponding state
let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_)); let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_)); let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
let is_using_mixup = is_using_firebreath || is_using_pumpkin; let is_using_mixup = is_using_firebreath || is_using_pumpkin;
if !is_using_mixup { if !is_using_mixup {
agent.combat_state.timers[ActionStateTimers::MixupCooldown as usize] += read_data.dt.0; agent.combat_state.timers[ActionStateTimers::Mixup as usize] += read_data.dt.0;
} }
if !is_using_firebreath { if !is_using_firebreath {
agent.combat_state.timers[ActionStateTimers::FirebreathCooldown as usize] += read_data.dt.0; agent.combat_state.timers[ActionStateTimers::Firebreath as usize] += read_data.dt.0;
} }
if !is_using_pumpkin { if !is_using_pumpkin {
agent.combat_state.timers[ActionStateTimers::FarPumpkinCooldown as usize] += read_data.dt.0; agent.combat_state.timers[ActionStateTimers::FarPumpkin as usize] += read_data.dt.0;
} }
} }
// note: some timer resets are performerd in attack logic // note: some timer resets are performerd in attack logic
@ -4862,15 +4863,13 @@ impl<'a> AgentData<'a> {
let health_fraction = self.health.map_or(0.5, |h| h.fraction()); let health_fraction = self.health.map_or(0.5, |h| h.fraction());
// second vine summon // second vine summon
if health_fraction < SECOND_VINE_CREATION_THRESHOLD if health_fraction < SECOND_VINE_CREATION_THRESHOLD
&& !agent.combat_state.conditions && !agent.combat_state.conditions[ActionStateConditions::SecondVines as usize]
[ActionStateConditions::SecondVines as usize]
{ {
controller.push_basic_input(InputKind::Ability(2)); controller.push_basic_input(InputKind::Ability(2));
// wait till recovery before finishing // wait till recovery before finishing
if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover)) if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
{ {
agent.combat_state.conditions agent.combat_state.conditions[ActionStateConditions::SecondVines as usize] = true;
[ActionStateConditions::SecondVines as usize] = true;
} }
} }
// first vine summon // first vine summon
@ -4881,8 +4880,7 @@ impl<'a> AgentData<'a> {
// wait till recovery before finishing // wait till recovery before finishing
if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover)) if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
{ {
agent.combat_state.conditions agent.combat_state.conditions[ActionStateConditions::FirstVines as usize] = true;
[ActionStateConditions::FirstVines as usize] = true;
} }
} }
// close range // close range
@ -4893,16 +4891,16 @@ impl<'a> AgentData<'a> {
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME)) if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME))
{ {
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
agent.combat_state.timers[ActionStateTimers::FirebreathCooldown as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::Firebreath as usize] = 0.0;
} }
// on timer, randomly mixup attacks // on timer, randomly mixup attacks
else if agent.combat_state.timers[ActionStateTimers::MixupCooldown as usize] else if agent.combat_state.timers[ActionStateTimers::Mixup as usize]
> agent.combat_state.counters[ActionStateCounters::CloseMixupCooldown as usize] > agent.combat_state.counters[ActionStateCounters::CloseMixupCooldown as usize]
&& attack_data.angle < SCYTHE_AIM_ANGLE && attack_data.angle < SCYTHE_AIM_ANGLE
// for now, no line of sight check for consitency in attacks // for now, no line of sight check for consitency in attacks
{ {
// if on firebreath cooldown, throw pumpkin // if on firebreath cooldown, throw pumpkin
if agent.combat_state.timers[ActionStateTimers::FirebreathCooldown as usize] if agent.combat_state.timers[ActionStateTimers::Firebreath as usize]
< FIREBREATH_COOLDOWN < FIREBREATH_COOLDOWN
{ {
controller.push_basic_input(InputKind::Ability(0)); controller.push_basic_input(InputKind::Ability(0));
@ -4916,12 +4914,14 @@ impl<'a> AgentData<'a> {
} }
} }
// reset mixup timing if actually being used // reset mixup timing if actually being used
if matches!(self.char_state, CharacterState::BasicBeam(_) | CharacterState::BasicRanged(_)) { if matches!(
agent.combat_state.timers[ActionStateTimers::MixupCooldown as usize] = 0.0; self.char_state,
CharacterState::BasicBeam(_) | CharacterState::BasicRanged(_)
) {
agent.combat_state.timers[ActionStateTimers::Mixup as usize] = 0.0;
agent.combat_state.counters[ActionStateCounters::CloseMixupCooldown as usize] = agent.combat_state.counters[ActionStateCounters::CloseMixupCooldown as usize] =
rng.gen_range(CLOSE_MIXUP_COOLDOWN_SPAN[0]..=CLOSE_MIXUP_COOLDOWN_SPAN[1]); rng.gen_range(CLOSE_MIXUP_COOLDOWN_SPAN[0]..=CLOSE_MIXUP_COOLDOWN_SPAN[1]);
} }
} }
// default to using scythe melee // default to using scythe melee
else if attack_data.angle < SCYTHE_AIM_ANGLE { else if attack_data.angle < SCYTHE_AIM_ANGLE {
@ -4934,24 +4934,24 @@ impl<'a> AgentData<'a> {
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32(FIREBREATH_TIME)) if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32(FIREBREATH_TIME))
{ {
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
agent.combat_state.timers[ActionStateTimers::FirebreathCooldown as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::Firebreath as usize] = 0.0;
} }
// start using firebreath if close enough, in angle, and off cooldown // start using firebreath if close enough, in angle, and off cooldown
else if attack_data.dist_sqrd < (FIREBREATH_RANGE * FIREBREATH_RANGE_FACTOR).powi(2) else if attack_data.dist_sqrd < (FIREBREATH_RANGE * FIREBREATH_RANGE_FACTOR).powi(2)
&& attack_data.angle < FIREBREATH_AIM_ANGLE && attack_data.angle < FIREBREATH_AIM_ANGLE
&& agent.combat_state.timers[ActionStateTimers::FirebreathCooldown as usize] && agent.combat_state.timers[ActionStateTimers::Firebreath as usize]
> FIREBREATH_COOLDOWN > FIREBREATH_COOLDOWN
{ {
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
} }
// on mixup timer, throw a pumpkin // on mixup timer, throw a pumpkin
else if agent.combat_state.timers[ActionStateTimers::MixupCooldown as usize] else if agent.combat_state.timers[ActionStateTimers::Mixup as usize]
> agent.combat_state.counters[ActionStateCounters::MidMixupCooldown as usize] > agent.combat_state.counters[ActionStateCounters::MidMixupCooldown as usize]
{ {
controller.push_basic_input(InputKind::Ability(0)); controller.push_basic_input(InputKind::Ability(0));
// reset mixup timing if pumpkin is actually being used // reset mixup timing if pumpkin is actually being used
if matches!(self.char_state, CharacterState::BasicRanged(_)) { if matches!(self.char_state, CharacterState::BasicRanged(_)) {
agent.combat_state.timers[ActionStateTimers::MixupCooldown as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::Mixup as usize] = 0.0;
agent.combat_state.counters[ActionStateCounters::MidMixupCooldown as usize] = agent.combat_state.counters[ActionStateCounters::MidMixupCooldown as usize] =
rng.gen_range(MID_MIXUP_COOLDOWN_SPAN[0]..=MID_MIXUP_COOLDOWN_SPAN[1]); rng.gen_range(MID_MIXUP_COOLDOWN_SPAN[0]..=MID_MIXUP_COOLDOWN_SPAN[1]);
} }
@ -4959,15 +4959,15 @@ impl<'a> AgentData<'a> {
} }
// long range (with line of sight) // long range (with line of sight)
else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2) else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2)
&& line_of_sight_with_target() && agent.combat_state.timers[ActionStateTimers::FarPumpkin as usize]
&& agent.combat_state.timers[ActionStateTimers::FarPumpkinCooldown as usize]
> agent.combat_state.counters[ActionStateCounters::FarPumpkinCooldown as usize] > agent.combat_state.counters[ActionStateCounters::FarPumpkinCooldown as usize]
&& line_of_sight_with_target()
{ {
// throw pumpkin // throw pumpkin
controller.push_basic_input(InputKind::Ability(0)); controller.push_basic_input(InputKind::Ability(0));
// reset pumpkin timing if actually being used // reset pumpkin timing if actually being used
if matches!(self.char_state, CharacterState::BasicRanged(_)) { if matches!(self.char_state, CharacterState::BasicRanged(_)) {
agent.combat_state.timers[ActionStateTimers::FarPumpkinCooldown as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::FarPumpkin as usize] = 0.0;
agent.combat_state.counters[ActionStateCounters::FarPumpkinCooldown as usize] = agent.combat_state.counters[ActionStateCounters::FarPumpkinCooldown as usize] =
rng.gen_range(FAR_PUMPKIN_COOLDOWN_SPAN[0]..=FAR_PUMPKIN_COOLDOWN_SPAN[1]); rng.gen_range(FAR_PUMPKIN_COOLDOWN_SPAN[0]..=FAR_PUMPKIN_COOLDOWN_SPAN[1]);
} }
@ -4989,6 +4989,7 @@ impl<'a> AgentData<'a> {
} }
// closing angle // closing angle
else if attack_data.angle > 0.0 { else if attack_data.angle > 0.0 {
// some movement is required to trigger re-orientation
controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
.xy() .xy()
.try_normalized() .try_normalized()
@ -5927,15 +5928,16 @@ impl<'a> AgentData<'a> {
// === setup === // === setup ===
// hard-coded attack vlues (some scaled for usage) // hard-coded attack values (some scaled for usage)
const STRIKE_RANGE: f32 = 4.0; const STRIKE_RANGE: f32 = 4.0;
const STRIKE_AIM_ANGLE: f32 = 55.0 * 0.7; const STRIKE_AIM_ANGLE: f32 = 55.0 * 0.7;
const SPIN_RANGE: f32 = 5.0; const SPIN_RANGE: f32 = 5.0;
const SHOCKWAVE_AIM_ANGLE: f32 = 45.0 * 0.7; const SHOCKWAVE_AIM_ANGLE: f32 = 45.0 * 0.7;
const SHOCKWAVE_MAX_RANGE: f32 = 30.0 * 0.6; const SHOCKWAVE_MAX_RANGE: f32 = 30.0 * 0.6;
// behaviour parameters // behaviour parameters
const ATTACK_RANGE_FACTOR: f32 = 0.6; // start attack while suitably in range
const PATH_RANGE_FACTOR: f32 = 0.3; // get comfortably in range, but give player room to breathe const PATH_RANGE_FACTOR: f32 = 0.3; // get comfortably in range, but give player room to breathe
const STRIKE_RANGE_FACTOR: f32 = 0.6; // start attack while suitably in range
const SPIN_RANGE_FACTOR: f32 = 0.6; // ^
const SPIN_COOLDOWN: f32 = 1.5; const SPIN_COOLDOWN: f32 = 1.5;
const SPIN_RELAX_FACTOR: f32 = 0.2; const SPIN_RELAX_FACTOR: f32 = 0.2;
const SHOCKWAVE_COOLDOWN: f32 = 5.0; const SHOCKWAVE_COOLDOWN: f32 = 5.0;
@ -5950,11 +5952,11 @@ impl<'a> AgentData<'a> {
Mixup, Mixup,
} }
// re-used comparisons (makes separating timers and attacks easier) // re-used checks (makes separating timers and attacks easier)
let is_in_spin_range = attack_data.dist_sqrd let is_in_spin_range = attack_data.dist_sqrd
< (attack_data.body_dist + SPIN_RANGE * ATTACK_RANGE_FACTOR).powi(2); < (attack_data.body_dist + SPIN_RANGE * SPIN_RANGE_FACTOR).powi(2);
let is_in_strike_range = attack_data.dist_sqrd let is_in_strike_range = attack_data.dist_sqrd
< (attack_data.body_dist + STRIKE_RANGE * ATTACK_RANGE_FACTOR).powi(2); < (attack_data.body_dist + STRIKE_RANGE * STRIKE_RANGE_FACTOR).powi(2);
let is_in_strike_angle = attack_data.angle < STRIKE_AIM_ANGLE; let is_in_strike_angle = attack_data.angle < STRIKE_AIM_ANGLE;
// === main === // === main ===
@ -5973,7 +5975,7 @@ impl<'a> AgentData<'a> {
// relax towards zero otherwise // relax towards zero otherwise
agent.combat_state.timers[ActionStateTimers::Spin as usize] = agent.combat_state.timers[ActionStateTimers::Spin as usize] =
(agent.combat_state.timers[ActionStateTimers::Spin as usize] (agent.combat_state.timers[ActionStateTimers::Spin as usize]
- SPIN_RELAX_FACTOR * read_data.dt.0) - read_data.dt.0 * SPIN_RELAX_FACTOR)
.max(0.0); .max(0.0);
} }
// shockwave // shockwave
@ -5993,40 +5995,40 @@ impl<'a> AgentData<'a> {
// relax towards zero otherwise // relax towards zero otherwise
agent.combat_state.timers[ActionStateTimers::Mixup as usize] = agent.combat_state.timers[ActionStateTimers::Mixup as usize] =
(agent.combat_state.timers[ActionStateTimers::Mixup as usize] (agent.combat_state.timers[ActionStateTimers::Mixup as usize]
- MIXUP_RELAX_FACTOR * read_data.dt.0) - read_data.dt.0 * MIXUP_RELAX_FACTOR)
.max(0.0); .max(0.0);
} }
// --- attacks --- // --- attacks ---
// strike range and in angle // strike range and angle
if is_in_strike_range && is_in_strike_angle { if is_in_strike_range && is_in_strike_angle {
// on timer, randomly mixup between all attacks
if agent.combat_state.timers[ActionStateTimers::Mixup as usize] > MIXUP_COOLDOWN { if agent.combat_state.timers[ActionStateTimers::Mixup as usize] > MIXUP_COOLDOWN {
// randomly mix up
let randomise: u8 = rng.gen_range(1..=3); let randomise: u8 = rng.gen_range(1..=3);
match randomise { match randomise {
1 => controller.push_basic_input(InputKind::Ability(0)), // shockwave 1 => controller.push_basic_input(InputKind::Ability(0)), // shockwave
2 => controller.push_basic_input(InputKind::Primary), // strike 2 => controller.push_basic_input(InputKind::Primary), // strike
_ => controller.push_basic_input(InputKind::Secondary), // spin _ => controller.push_basic_input(InputKind::Secondary), // spin
} }
} else { }
// use strike // default to strike
else {
controller.push_basic_input(InputKind::Primary); controller.push_basic_input(InputKind::Primary);
} }
} }
// spin range (or out of angle in strike range) // spin range (or out of angle in strike range)
else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) { else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) {
// on timer, use spin attack to try and hit evasive target
if agent.combat_state.timers[ActionStateTimers::Spin as usize] > SPIN_COOLDOWN { if agent.combat_state.timers[ActionStateTimers::Spin as usize] > SPIN_COOLDOWN {
// If it's been too long since able to hit target, spin
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
} }
} }
// shockwave range // shockwave range and angle with cooldown
else if attack_data.dist_sqrd > SHOCKWAVE_MIN_RANGE.powi(2) else if attack_data.dist_sqrd > SHOCKWAVE_MIN_RANGE.powi(2)
&& attack_data.dist_sqrd < SHOCKWAVE_MAX_RANGE.powi(2) && attack_data.dist_sqrd < SHOCKWAVE_MAX_RANGE.powi(2)
&& attack_data.angle < SHOCKWAVE_AIM_ANGLE && attack_data.angle < SHOCKWAVE_AIM_ANGLE
&& agent.combat_state.timers[ActionStateTimers::Shockwave as usize] > SHOCKWAVE_COOLDOWN && agent.combat_state.timers[ActionStateTimers::Shockwave as usize] > SHOCKWAVE_COOLDOWN
{ {
// Shockwave if close enough and haven't shockwaved too recently
controller.push_basic_input(InputKind::Ability(0)); controller.push_basic_input(InputKind::Ability(0));
} }
@ -6046,7 +6048,7 @@ impl<'a> AgentData<'a> {
} }
// closing angle // closing angle
else if attack_data.angle > 0.0 { else if attack_data.angle > 0.0 {
// If not in angle, apply slight movement so golem orients itself correctly // some movement is required to trigger re-orientation
controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0) controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
.xy() .xy()
.try_normalized() .try_normalized()
@ -6076,11 +6078,24 @@ impl<'a> AgentData<'a> {
// === setup === // === setup ===
// hard-coded attack values // hard-coded attack values (some scaled for usage)
const STRIKE_RANGE: f32 = 3.0;
const STRIKE_AIM_ANGLE: f32 = 50.0 * 0.7;
const SHOCKWAVE_RANGE: f32 = 12.0 * 0.6; // attack logic assumes this is less than BARRAGE_RANGE
const BARRAGE_RANGE: f32 = 25.0 * 0.8;
// behaviour parameters // behaviour parameters
const TOTEM_TIMER: f32 = 20.0; const PATH_RANGE_FACTOR: f32 = 0.4;
const HEAVY_ATTACK_WAIT_TIME: f32 = 10.0; const STRIKE_RANGE_FACTOR: f32 = 0.7;
const BARRAGE_AIM_ANGLE: f32 = 20.0;
const TOTEM_COOLDOWN: f32 = 20.0;
const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
// conditions
enum ActionStateConditions {
FirstTotem = 0,
}
// timers // timers
enum ActionStateTimers { enum ActionStateTimers {
@ -6088,74 +6103,96 @@ impl<'a> AgentData<'a> {
HeavyAttack, HeavyAttack,
} }
// counters
enum ActionStateCounters {
HeavyAttackCooldown = 0,
}
// re-used checks
let is_in_strike_range = attack_data.dist_sqrd
< (attack_data.body_dist + STRIKE_RANGE * STRIKE_RANGE_FACTOR).powi(2);
let is_in_strike_angle = attack_data.angle < STRIKE_AIM_ANGLE;
// === main === // === main ===
// --- timers --- // --- timers ---
agent.combat_state.timers[ActionStateTimers::SummonTotem as usize] += read_data.dt.0; // initialise randomised cooldowns
if !agent.combat_state.initialized {
agent.combat_state.initialized = true;
agent.combat_state.counters[ActionStateCounters::HeavyAttackCooldown as usize] =
rng.gen_range(HEAVY_ATTACK_COOLDOWN_SPAN[0]..=HEAVY_ATTACK_COOLDOWN_SPAN[1]);
// note: if f32 rng is too expensive, could use randomised u8
// multipliers of a const f32
}
// resets
match self.char_state { match self.char_state {
CharacterState::BasicSummon(_) => { CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
// reset when summoning // reset when finished summoning
agent.combat_state.timers[ActionStateTimers::SummonTotem as usize] = 0.0 agent.combat_state.timers[ActionStateTimers::SummonTotem as usize] = 0.0;
agent.combat_state.conditions[ActionStateConditions::FirstTotem as usize] = true;
}, },
CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => { CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] = 0.0 // reset heavy attack on either ability
agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] = 0.0;
agent.combat_state.counters[ActionStateCounters::HeavyAttackCooldown as usize] =
rng.gen_range(HEAVY_ATTACK_COOLDOWN_SPAN[0]..=HEAVY_ATTACK_COOLDOWN_SPAN[1]);
}, },
_ => {}, _ => {},
} }
if attack_data.in_min_range() { // heavy attack
// Else if not time to use anything fancy agent.combat_state.timers[ActionStateTimers::SummonTotem as usize] += read_data.dt.0;
if attack_data.angle < 20.0 { if is_in_strike_range {
// recharge at standard rate in strike range and angle
if is_in_strike_angle {
agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] += agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] +=
read_data.dt.0; read_data.dt.0;
} else { } else {
// If not in angle, charge heavy attack faster // If not in angle, charge heavy attack faster
agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] += agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] +=
read_data.dt.0 * 5.0; read_data.dt.0 * HEAVY_ATTACK_FAST_CHARGE_FACTOR;
} }
} else { } else {
// If not in range, charge heavy attack faster // If not in range, charge heavy attack faster
agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] += agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] +=
read_data.dt.0 * 3.3; read_data.dt.0 * HEAVY_ATTACK_CHARGE_FACTOR;
} }
// --- attacks --- // --- attacks ---
// start out by summoning green totem
if !agent.combat_state.initialized {
// start by summoning green totem
if !agent.combat_state.conditions[ActionStateConditions::FirstTotem as usize] {
controller.push_basic_input(InputKind::Ability(2)); 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::SummonTotem as usize] // on timer, summon a new totem
> TOTEM_TIMER else if agent.combat_state.timers[ActionStateTimers::SummonTotem as usize]
> TOTEM_COOLDOWN
{ {
// If time to summon a totem, do it // randomly pick a totem to summon
let input = rng.gen_range(1..=3); let input = rng.gen_range(1..=3);
let buff_kind = match input { let buff_kind = match input {
2 => Some(BuffKind::Regeneration), 2 => Some(BuffKind::Regeneration),
3 => Some(BuffKind::Hastened), 3 => Some(BuffKind::Hastened),
_ => None, _ => None,
}; };
// if already buffed by chosen totem, skip it
if buff_kind.map_or(false, |b| self.has_buff(read_data, b)) if buff_kind.map_or(false, |b| self.has_buff(read_data, b))
&& matches!(self.char_state, CharacterState::Wielding { .. }) && matches!(self.char_state, CharacterState::Wielding { .. })
{ {
// If already under effects of buff from totem that would be summoned, don't // (doesn't work for red totems since debuff applies to enemies instead)
// summon totem (doesn't work for red totems since that applies debuff to
// enemies instead)
agent.combat_state.timers[ActionStateTimers::SummonTotem as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::SummonTotem as usize] = 0.0;
} else { }
// summon totem
else {
controller.push_basic_input(InputKind::Ability(input)); controller.push_basic_input(InputKind::Ability(input));
} }
} else if agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] }
> HEAVY_ATTACK_WAIT_TIME // on timer, use a heavy attack in range
else if agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize]
> agent.combat_state.counters[ActionStateCounters::HeavyAttackCooldown as usize]
&& attack_data.dist_sqrd < BARRAGE_RANGE.powi(2)
{ {
// Else if time for a heavy attack // has line of sight
if attack_data.in_min_range() { if entities_have_line_of_sight(
// If in range, shockwave
controller.push_basic_input(InputKind::Ability(0));
} else if entities_have_line_of_sight(
self.pos, self.pos,
self.body, self.body,
self.scale, self.scale,
@ -6164,17 +6201,43 @@ impl<'a> AgentData<'a> {
tgt_data.scale, tgt_data.scale,
read_data, read_data,
) { ) {
// Else if in sight, barrage // use shockwave out of barrage angle
if attack_data.angle > BARRAGE_AIM_ANGLE {
controller.push_basic_input(InputKind::Ability(0));
}
// randomise in shockwave range
else if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) {
let randomise: u8 = rng.gen_range(1..=2);
match randomise {
1 => controller.push_basic_input(InputKind::Ability(0)), // shockwave
_ => controller.push_basic_input(InputKind::Secondary), // barrage
}
}
// use barrage if in range and angle
else {
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
} }
} else if attack_data.in_min_range() && attack_data.angle < 20.0 { // keep pathing to target (no action required)
// Else if not time to use anything fancy, if in range and angle, strike them }
// no line of sight
else {
// use shockwave in range
if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) {
controller.push_basic_input(InputKind::Ability(0));
}
}
}
// default to flamestrike when in range and angle
else if is_in_strike_range && is_in_strike_angle {
controller.push_basic_input(InputKind::Primary); controller.push_basic_input(InputKind::Primary);
} }
// --- movement --- // --- movement ---
// close gap // closing gap
if !attack_data.in_min_range() { if attack_data.dist_sqrd
> (attack_data.body_dist + STRIKE_RANGE * PATH_RANGE_FACTOR).powi(2)
{
// TODO: pathing gap
self.path_toward_target( self.path_toward_target(
agent, agent,
controller, controller,
@ -6184,6 +6247,15 @@ impl<'a> AgentData<'a> {
None, None,
); );
} }
// closing angle
else if attack_data.angle > 0.0 {
// some movement is required to trigger re-orientation
controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
.xy()
.try_normalized()
.unwrap_or_else(Vec2::zero)
* 0.001; // scaled way down to minimise position change and keep close rotation consistent
}
} }
pub fn handle_sword_simple_attack( pub fn handle_sword_simple_attack(