read & use attack data from AbilityData: added default usage to AbilityData, expanded AbilityData::BasicRanged; tweaked chieftain flamestrike; removed existing buff check for cheiftain totem & increased cooldown; more comments in attack functions

This commit is contained in:
horblegorble 2024-07-02 23:00:11 +10:00
parent 674a9e1d6c
commit 93b336cd20
3 changed files with 331 additions and 163 deletions

View File

@ -1,6 +1,6 @@
BasicMelee( BasicMelee(
energy_cost: 0, energy_cost: 0,
buildup_duration: 0.8, buildup_duration: 0.75,
swing_duration: 0.4, swing_duration: 0.4,
hit_timing: 0.5, hit_timing: 0.5,
recover_duration: 0.6, recover_duration: 0.6,
@ -12,7 +12,7 @@ BasicMelee(
energy_regen: 0, energy_regen: 0,
), ),
range: 3.0, range: 3.0,
angle: 50.0, angle: 40.0,
damage_effect: Some(Buff(( damage_effect: Some(Buff((
kind: Burning, kind: Burning,
dur_secs: 4.0, dur_secs: 4.0,

View File

@ -4769,21 +4769,18 @@ impl<'a> AgentData<'a> {
// === setup === // === setup ===
// hard-coded attack values // --- static ---
const SCYTHE_RANGE: f32 = 4.5; const MAX_PUMPKIN_RANGE: f32 = 50.0;
const SCYTHE_AIM_ANGLE: f32 = 50.0;
const FIREBREATH_RANGE: f32 = 20.0;
// behaviour parameters // behaviour parameters
const PATH_RANGE_FACTOR: f32 = 0.4; // get comfortably in range, but give player room to breathe
const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60; const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60;
const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30; const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30;
const PATH_RANGE_FACTOR: f32 = 0.4; // get comfortably in range, but give player room to breathe
const SCYTHE_RANGE_FACTOR: f32 = 0.75; // start attack while suitably in range const SCYTHE_RANGE_FACTOR: f32 = 0.75; // start attack while suitably in range
const SCYTHE_AIM_FACTOR: f32 = 0.7; const SCYTHE_AIM_FACTOR: f32 = 0.7;
const MAX_PUMPKIN_RANGE: f32 = 50.0;
const FIREBREATH_RANGE_FACTOR: f32 = 0.7; const FIREBREATH_RANGE_FACTOR: f32 = 0.7;
const FIREBREATH_AIM_ANGLE: f32 = 30.0; const FIREBREATH_AIM_FACTOR: f32 = 0.8;
const FIREBREATH_TIME: f32 = 4.0; const FIREBREATH_TIME_LIMIT: f32 = 4.0;
const FIREBREATH_SHORT_TIME: f32 = 2.5; // cutoff sooner at close range const FIREBREATH_SHORT_TIME_LIMIT: f32 = 2.5; // cutoff sooner at close range
const FIREBREATH_COOLDOWN: f32 = 3.5; const FIREBREATH_COOLDOWN: f32 = 3.5;
const CLOSE_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 7.0]; // variation in attacks at close range const CLOSE_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 7.0]; // variation in attacks at close range
const MID_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 4.5]; // ^ mid const MID_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 4.5]; // ^ mid
@ -4791,13 +4788,13 @@ impl<'a> AgentData<'a> {
// conditions // conditions
enum ActionStateConditions { enum ActionStateConditions {
HasSummonedFirstVines = 0, HasSummonedFirstVines,
HasSummonedSecondVines, HasSummonedSecondVines,
} }
// timers // timers
enum ActionStateTimers { enum ActionStateTimers {
Firebreath = 0, Firebreath,
Mixup, Mixup,
FarPumpkin, FarPumpkin,
} }
@ -4805,7 +4802,7 @@ impl<'a> AgentData<'a> {
// //counters // //counters
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
enum ActionStateCounters { enum ActionStateCounters {
CloseMixupCooldown = 0, CloseMixupCooldown,
MidMixupCooldown, MidMixupCooldown,
FarPumpkinCooldown, FarPumpkinCooldown,
} }
@ -4823,9 +4820,78 @@ impl<'a> AgentData<'a> {
) )
}; };
// === main === // --- dynamic ---
// attack data (with fallback values)
let scythe_range;
let scythe_angle;
match self
.extract_ability(AbilityInput::Primary)
.unwrap_or(AbilityData::default())
{
AbilityData::BasicMelee { range, angle, .. } => {
scythe_range = range;
scythe_angle = angle;
},
_ => {
scythe_range = 4.5;
scythe_angle = 50.0;
},
};
let firebreath_range;
let firebreath_angle;
match self
.extract_ability(AbilityInput::Secondary)
.unwrap_or(AbilityData::default())
{
AbilityData::BasicBeam { range, angle, .. } => {
firebreath_range = range;
firebreath_angle = angle;
},
_ => {
firebreath_range = 20.0;
firebreath_angle = 15.0;
},
};
let pumpkin_speed;
match self
.extract_ability(AbilityInput::Auxiliary(0))
.unwrap_or(AbilityData::default())
{
AbilityData::BasicRanged {
projectile_speed, ..
} => {
pumpkin_speed = projectile_speed;
},
_ => {
pumpkin_speed = 30.0;
},
};
// TODO: calculate ground-level max pumpkin range from projectile speed
// character state info
let mut is_using_firebreath = false;
let mut is_using_pumpkin = false;
let mut is_in_summon_recovery = false;
let mut firebreath_timer = Duration::from_secs(0);
match self.char_state {
CharacterState::BasicBeam(data) => {
is_using_firebreath = true;
firebreath_timer = data.timer;
},
CharacterState::BasicRanged(_) => {
is_using_pumpkin = true;
},
CharacterState::SpriteSummon(data)
if matches!(data.stage_section, StageSection::Recover) =>
{
is_in_summon_recovery = true;
},
_ => {},
}
let is_using_mixup = is_using_firebreath || is_using_pumpkin;
// --- timers ---
// initialise randomised cooldowns // initialise randomised cooldowns
if !agent.combat_state.initialized { if !agent.combat_state.initialized {
agent.combat_state.initialized = true; agent.combat_state.initialized = true;
@ -4835,31 +4901,34 @@ 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
} }
// timer changes
if matches!(self.char_state, CharacterState::SpriteSummon(_)) { // === main ===
// reset all timers when summoning
// --- timers ---
if is_in_summon_recovery {
// reset all timers when done summoning
agent.combat_state.timers[ActionStateTimers::Firebreath as usize] = 0.0;
agent.combat_state.timers[ActionStateTimers::Mixup as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::Mixup as usize] = 0.0;
agent.combat_state.timers[ActionStateTimers::FarPumpkin as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::FarPumpkin as usize] = 0.0;
} else {
// handle state timers
if is_using_firebreath {
agent.combat_state.timers[ActionStateTimers::Firebreath as usize] = 0.0; agent.combat_state.timers[ActionStateTimers::Firebreath as usize] = 0.0;
} else { } else {
// increment timers when not in corresponding state
let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
let is_using_mixup = is_using_firebreath || is_using_pumpkin;
if !is_using_mixup {
agent.combat_state.timers[ActionStateTimers::Mixup as usize] += read_data.dt.0;
}
if !is_using_firebreath {
agent.combat_state.timers[ActionStateTimers::Firebreath 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_mixup {
agent.combat_state.timers[ActionStateTimers::Mixup as usize] = 0.0;
} else {
agent.combat_state.timers[ActionStateTimers::Mixup as usize] += read_data.dt.0;
}
if is_using_pumpkin {
agent.combat_state.timers[ActionStateTimers::FarPumpkin as usize] = 0.0;
} else {
agent.combat_state.timers[ActionStateTimers::FarPumpkin 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
// --- attacks --- // --- attacks ---
let health_fraction = self.health.map_or(0.5, |h| h.fraction()); let health_fraction = self.health.map_or(0.5, |h| h.fraction());
@ -4870,8 +4939,7 @@ impl<'a> AgentData<'a> {
{ {
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 is_in_summon_recovery {
{
agent.combat_state.conditions agent.combat_state.conditions
[ActionStateConditions::HasSummonedSecondVines as usize] = true; [ActionStateConditions::HasSummonedSecondVines as usize] = true;
} }
@ -4882,26 +4950,26 @@ impl<'a> AgentData<'a> {
{ {
controller.push_basic_input(InputKind::Ability(1)); controller.push_basic_input(InputKind::Ability(1));
// 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 is_in_summon_recovery {
{
agent.combat_state.conditions agent.combat_state.conditions
[ActionStateConditions::HasSummonedFirstVines as usize] = true; [ActionStateConditions::HasSummonedFirstVines as usize] = true;
} }
} }
// close range // close range
else if attack_data.dist_sqrd else if attack_data.dist_sqrd
< (attack_data.body_dist + SCYTHE_RANGE * SCYTHE_RANGE_FACTOR).powi(2) < (attack_data.body_dist + scythe_range * SCYTHE_RANGE_FACTOR).powi(2)
{ {
// if using firebreath, keep going under short time limit // if using firebreath, keep going under short time limit
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME)) if is_using_firebreath
&& firebreath_timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME_LIMIT)
{ {
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
agent.combat_state.timers[ActionStateTimers::Firebreath as usize] = 0.0;
} }
// in scythe angle
if attack_data.angle < scythe_angle * SCYTHE_AIM_FACTOR {
// on timer, randomly mixup attacks // on timer, randomly mixup attacks
else if agent.combat_state.timers[ActionStateTimers::Mixup as usize] 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 * SCYTHE_AIM_FACTOR
// 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
@ -4911,38 +4979,34 @@ impl<'a> AgentData<'a> {
controller.push_basic_input(InputKind::Ability(0)); controller.push_basic_input(InputKind::Ability(0));
} }
// otherwise, randomise between firebreath and pumpkin // otherwise, randomise between firebreath and pumpkin
else { else if rng.gen_bool(0.5) {
match rng.gen_bool(0.5) { controller.push_basic_input(InputKind::Secondary);
true => controller.push_basic_input(InputKind::Secondary), // firebreath } else {
_ => controller.push_basic_input(InputKind::Ability(0)), // pumpkin controller.push_basic_input(InputKind::Ability(0));
} }
} // reset mixup cooldown if actually being used
// reset mixup timing if actually being used if is_using_mixup {
if matches!( agent.combat_state.counters
self.char_state, [ActionStateCounters::CloseMixupCooldown as usize] = rng
CharacterState::BasicBeam(_) | CharacterState::BasicRanged(_) .gen_range(CLOSE_MIXUP_COOLDOWN_SPAN[0]..=CLOSE_MIXUP_COOLDOWN_SPAN[1]);
) {
agent.combat_state.timers[ActionStateTimers::Mixup as usize] = 0.0;
agent.combat_state.counters[ActionStateCounters::CloseMixupCooldown as usize] =
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 * SCYTHE_AIM_FACTOR { else {
controller.push_basic_input(InputKind::Primary); controller.push_basic_input(InputKind::Primary);
} }
} }
// mid range (line of sight not needed for these 'suppressing' attacks) // mid range (line of sight not needed for these 'suppressing' attacks)
else if attack_data.dist_sqrd < FIREBREATH_RANGE.powi(2) { } else if attack_data.dist_sqrd < firebreath_range.powi(2) {
// if using firebreath, keep going under full time limit // if using firebreath, keep going under full time limit
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs_f32(FIREBREATH_TIME)) if is_using_firebreath
&& firebreath_timer < Duration::from_secs_f32(FIREBREATH_TIME_LIMIT)
{ {
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
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_angle * FIREBREATH_AIM_FACTOR
&& agent.combat_state.timers[ActionStateTimers::Firebreath as usize] && agent.combat_state.timers[ActionStateTimers::Firebreath as usize]
> FIREBREATH_COOLDOWN > FIREBREATH_COOLDOWN
{ {
@ -4953,9 +5017,8 @@ impl<'a> AgentData<'a> {
> 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 cooldown if pumpkin is actually being used
if matches!(self.char_state, CharacterState::BasicRanged(_)) { if is_using_pumpkin {
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]);
} }
@ -4969,9 +5032,8 @@ impl<'a> AgentData<'a> {
{ {
// 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 cooldown if actually being used
if matches!(self.char_state, CharacterState::BasicRanged(_)) { if is_using_pumpkin {
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]);
} }
@ -4980,7 +5042,7 @@ impl<'a> AgentData<'a> {
// --- movement --- // --- movement ---
// closing gap // closing gap
if attack_data.dist_sqrd if attack_data.dist_sqrd
> (attack_data.body_dist + SCYTHE_RANGE * PATH_RANGE_FACTOR).powi(2) > (attack_data.body_dist + scythe_range * PATH_RANGE_FACTOR).powi(2)
{ {
self.path_toward_target( self.path_toward_target(
agent, agent,
@ -5932,39 +5994,83 @@ impl<'a> AgentData<'a> {
// === setup === // === setup ===
// hard-coded attack values (some scaled for usage) // --- static ---
const STRIKE_RANGE: f32 = 4.0;
const STRIKE_AIM_ANGLE: f32 = 55.0;
const SPIN_RANGE: f32 = 5.0;
const SHOCKWAVE_AIM_ANGLE: f32 = 45.0;
const SHOCKWAVE_MAX_RANGE: f32 = 30.0;
// behaviour parameters // behaviour parameters
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 STRIKE_RANGE_FACTOR: f32 = 0.6; // start attack while suitably in range
const STRIKE_AIM_FACTOR: f32 = 0.7; const STRIKE_AIM_FACTOR: f32 = 0.7;
const SPIN_RANGE_FACTOR: f32 = 0.6; // ^ 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_AIM_FACTOR: f32 = 0.7;
const SHOCKWAVE_COOLDOWN: f32 = 5.0;
const SHOCKWAVE_MIN_RANGE: f32 = 8.0;
const SHOCKWAVE_RANGE_FACTOR: f32 = 0.6; const SHOCKWAVE_RANGE_FACTOR: f32 = 0.6;
const SHOCKWAVE_AIM_FACTOR: f32 = 0.4;
const SHOCKWAVE_COOLDOWN: f32 = 5.0;
const MIXUP_COOLDOWN: f32 = 2.5; const MIXUP_COOLDOWN: f32 = 2.5;
const MIXUP_RELAX_FACTOR: f32 = 0.3; const MIXUP_RELAX_FACTOR: f32 = 0.3;
// timers // timers
enum ActionStateTimers { enum ActionStateTimers {
Spin = 0, Spin,
Shockwave, Shockwave,
Mixup, Mixup,
} }
// --- dynamic ---
// behaviour parameters
let shockwave_min_range = self.body.map_or(8.0, |b| b.height() * 1.1);
// attack data (with fallback values)
let strike_range;
let strike_angle;
match self
.extract_ability(AbilityInput::Primary)
.unwrap_or(AbilityData::default())
{
AbilityData::BasicMelee { range, angle, .. } => {
strike_range = range;
strike_angle = angle;
},
_ => {
strike_range = 4.0;
strike_angle = 55.0;
},
};
let spin_range;
match self
.extract_ability(AbilityInput::Secondary)
.unwrap_or(AbilityData::default())
{
AbilityData::BasicMelee { range, .. } => {
spin_range = range;
},
_ => {
spin_range = 5.0;
},
};
let shockwave_max_range;
let shockwave_angle;
match self
.extract_ability(AbilityInput::Auxiliary(0))
.unwrap_or(AbilityData::default())
{
AbilityData::Shockwave { angle, range, .. } => {
shockwave_max_range = range;
shockwave_angle = angle;
},
_ => {
shockwave_max_range = 15.0 * 2.0;
shockwave_angle = 90.0;
},
};
// re-used checks (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 * SPIN_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 * STRIKE_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 * STRIKE_AIM_FACTOR; let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
// === main === // === main ===
@ -6029,20 +6135,25 @@ impl<'a> AgentData<'a> {
if agent.combat_state.timers[ActionStateTimers::Spin as usize] > SPIN_COOLDOWN { if agent.combat_state.timers[ActionStateTimers::Spin as usize] > SPIN_COOLDOWN {
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
} }
// otherwise, close angle (no action required)
} }
// shockwave range and angle with cooldown // shockwave range and angle
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 * SHOCKWAVE_RANGE_FACTOR).powi(2) && attack_data.dist_sqrd < (shockwave_max_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
&& attack_data.angle < SHOCKWAVE_AIM_ANGLE * SHOCKWAVE_AIM_FACTOR && attack_data.angle < shockwave_angle * SHOCKWAVE_AIM_FACTOR
&& agent.combat_state.timers[ActionStateTimers::Shockwave as usize] > SHOCKWAVE_COOLDOWN {
// on timer, use shockwave
if agent.combat_state.timers[ActionStateTimers::Shockwave as usize] > SHOCKWAVE_COOLDOWN
{ {
controller.push_basic_input(InputKind::Ability(0)); controller.push_basic_input(InputKind::Ability(0));
} }
// otherwise, close gap and/or angle (no action required)
}
// --- movement --- // --- movement ---
// closing gap // closing gap
if attack_data.dist_sqrd if attack_data.dist_sqrd
> (attack_data.body_dist + STRIKE_RANGE * PATH_RANGE_FACTOR).powi(2) > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
{ {
self.path_toward_target( self.path_toward_target(
agent, agent,
@ -6085,55 +6196,124 @@ impl<'a> AgentData<'a> {
// === setup === // === setup ===
// hard-coded attack values (some scaled for usage) // --- static ---
const STRIKE_RANGE: f32 = 3.0; // hard-coded attack values
const STRIKE_AIM_ANGLE: f32 = 50.0; const BARRAGE_RANGE: f32 = 25.0; // attack logic assumes this is greater than shockwave_range
const SHOCKWAVE_RANGE: f32 = 12.0; // attack logic assumes this is less than BARRAGE_RANGE const BARRAGE_ANGLE: f32 = 20.0;
const BARRAGE_RANGE: f32 = 25.0;
// behaviour parameters // behaviour parameters
const PATH_RANGE_FACTOR: f32 = 0.4; const PATH_RANGE_FACTOR: f32 = 0.4;
const STRIKE_RANGE_FACTOR: f32 = 0.7; const STRIKE_RANGE_FACTOR: f32 = 0.7;
const STRIKE_AIM_FACTOR: f32 = 0.7; const STRIKE_AIM_FACTOR: f32 = 0.8;
const BARRAGE_RANGE_FACTOR: f32 = 0.8; const BARRAGE_RANGE_FACTOR: f32 = 0.8;
const BARRAGE_AIM_ANGLE: f32 = 20.0; const BARRAGE_AIM_FACTOR: f32 = 1.0;
const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75; const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75;
const TOTEM_COOLDOWN: f32 = 20.0; const TOTEM_COOLDOWN: f32 = 25.0;
const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0]; const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3; const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0; const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
// conditions // conditions
enum ActionStateConditions { enum ActionStateConditions {
HasSummonedFirstTotem = 0, HasSummonedFirstTotem,
} }
// timers // timers
enum ActionStateTimers { enum ActionStateTimers {
SummonTotem = 0, SummonTotem,
HeavyAttack, HeavyAttack,
} }
// counters // counters
enum ActionStateCounters { enum ActionStateCounters {
HeavyAttackCooldown = 0, HeavyAttackCooldown,
} }
// line of sight check
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,
)
};
// --- dynamic ---
// attack data (with fallback values)
let strike_range;
let strike_angle;
match self
.extract_ability(AbilityInput::Primary)
.unwrap_or(AbilityData::default())
{
AbilityData::BasicMelee { range, angle, .. } => {
strike_range = range;
strike_angle = angle;
},
_ => {
strike_range = 3.0;
strike_angle = 40.0;
},
};
let barrage_speed;
let barrage_spread;
let barrage_count;
match self
.extract_ability(AbilityInput::Secondary)
.unwrap_or(AbilityData::default())
{
AbilityData::BasicRanged {
projectile_speed,
projectile_spread,
num_projectiles,
..
} => {
barrage_speed = projectile_speed;
barrage_spread = projectile_spread;
barrage_count = num_projectiles;
},
_ => {
barrage_speed = 25.0;
barrage_spread = 0.125;
barrage_count = 5;
},
};
// TODO: calculate ground-level max barrage range from projectile_speed
// TODO: calculaate approx. aiming angle from projectile_spread and
// num_projectiles
let shockwave_range;
match self
.extract_ability(AbilityInput::Auxiliary(0))
.unwrap_or(AbilityData::default())
{
AbilityData::Shockwave { range, .. } => {
shockwave_range = range;
},
_ => {
shockwave_range = 12.0 * 1.0;
},
};
// re-used checks // re-used checks
let is_in_strike_range = attack_data.dist_sqrd let is_in_strike_range = attack_data.dist_sqrd
< (attack_data.body_dist + STRIKE_RANGE * STRIKE_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 * STRIKE_AIM_FACTOR; let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
// === main ===
// --- timers ---
// initialise randomised cooldowns // initialise randomised cooldowns
if !agent.combat_state.initialized { if !agent.combat_state.initialized {
agent.combat_state.initialized = true; agent.combat_state.initialized = true;
agent.combat_state.counters[ActionStateCounters::HeavyAttackCooldown as usize] = agent.combat_state.counters[ActionStateCounters::HeavyAttackCooldown as usize] =
rng.gen_range(HEAVY_ATTACK_COOLDOWN_SPAN[0]..=HEAVY_ATTACK_COOLDOWN_SPAN[1]); 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
} }
// === main ===
// --- timers ---
// resets // resets
match self.char_state { match self.char_state {
CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => { CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
@ -6170,84 +6350,61 @@ impl<'a> AgentData<'a> {
} }
// --- attacks --- // --- attacks ---
// start by summoning green totem // start by summoning green totem
if !agent.combat_state.conditions[ActionStateConditions::HasSummonedFirstTotem as usize] { if !agent.combat_state.conditions[ActionStateConditions::HasSummonedFirstTotem as usize] {
controller.push_basic_input(InputKind::Ability(2)); controller.push_basic_input(InputKind::Ability(2));
} }
// on timer, summon a new totem // on timer, summon a new random totem
else if agent.combat_state.timers[ActionStateTimers::SummonTotem as usize] else if agent.combat_state.timers[ActionStateTimers::SummonTotem as usize]
> TOTEM_COOLDOWN > TOTEM_COOLDOWN
{ {
// randomly pick a totem to summon controller.push_basic_input(InputKind::Ability(rng.gen_range(1..=3)));
let input = rng.gen_range(1..=3);
let buff_kind = match input {
2 => Some(BuffKind::Regeneration),
3 => Some(BuffKind::Hastened),
_ => None,
};
// if already buffed by chosen totem, skip it
if buff_kind.map_or(false, |b| self.has_buff(read_data, b))
&& matches!(self.char_state, CharacterState::Wielding { .. })
{
// (doesn't work for red totems since debuff applies to enemies instead)
agent.combat_state.timers[ActionStateTimers::SummonTotem as usize] = 0.0;
} }
// summon totem // on timer and in range, use a heavy attack
else {
controller.push_basic_input(InputKind::Ability(input));
}
}
// on timer, use a heavy attack in range
else if agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize] else if agent.combat_state.counters[ActionStateTimers::HeavyAttack as usize]
> agent.combat_state.counters[ActionStateCounters::HeavyAttackCooldown as usize] > agent.combat_state.counters[ActionStateCounters::HeavyAttackCooldown as usize]
&& attack_data.dist_sqrd < (BARRAGE_RANGE * BARRAGE_RANGE_FACTOR).powi(2) && attack_data.dist_sqrd < (BARRAGE_RANGE * BARRAGE_RANGE_FACTOR).powi(2)
{ {
// has line of sight // has line of sight
if entities_have_line_of_sight( if line_of_sight_with_target() {
self.pos, // out of barrage angle, use shockwave
self.body, if attack_data.angle > BARRAGE_ANGLE * BARRAGE_AIM_FACTOR {
self.scale,
tgt_data.pos,
tgt_data.body,
tgt_data.scale,
read_data,
) {
// use shockwave out of barrage angle
if attack_data.angle > BARRAGE_AIM_ANGLE {
controller.push_basic_input(InputKind::Ability(0)); controller.push_basic_input(InputKind::Ability(0));
} }
// randomise in shockwave range // in shockwave range, randomise between barrage and shockwave
else if attack_data.dist_sqrd < (SHOCKWAVE_RANGE * SHOCKWAVE_RANGE_FACTOR).powi(2) else if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
{ {
match rng.gen_bool(0.5) { if rng.gen_bool(0.5) {
true => controller.push_basic_input(InputKind::Secondary), // barrage controller.push_basic_input(InputKind::Secondary);
_ => controller.push_basic_input(InputKind::Ability(0)), // shockwave } else {
controller.push_basic_input(InputKind::Ability(0));
} }
} }
// use barrage if in range and angle // in range and angle, use barrage
else { else {
controller.push_basic_input(InputKind::Secondary); controller.push_basic_input(InputKind::Secondary);
} }
// keep pathing to target (no action required) // otherwise, close gap and/or angle (no action required)
} }
// no line of sight // no line of sight
else { else {
// use shockwave in range // in range, use shockwave
if attack_data.dist_sqrd < (SHOCKWAVE_RANGE * SHOCKWAVE_RANGE_FACTOR).powi(2) { if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2) {
controller.push_basic_input(InputKind::Ability(0)); controller.push_basic_input(InputKind::Ability(0));
} }
// otherwise, close gap (no action required)
} }
} }
// default to flamestrike when in range and angle // if viable, default to flamestrike
else if is_in_strike_range && is_in_strike_angle { else if is_in_strike_range && is_in_strike_angle {
controller.push_basic_input(InputKind::Primary); controller.push_basic_input(InputKind::Primary);
} }
// otherwise, close gap and/or angle (no action required)
// --- movement --- // --- movement ---
// closing gap // closing gap
if attack_data.dist_sqrd if attack_data.dist_sqrd
> (attack_data.body_dist + STRIKE_RANGE * PATH_RANGE_FACTOR).powi(2) > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
{ {
self.path_toward_target( self.path_toward_target(
agent, agent,

View File

@ -438,8 +438,10 @@ pub enum Path {
Partial, Partial,
} }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, Default)]
pub enum AbilityData { pub enum AbilityData {
#[default]
Default,
ComboMelee { ComboMelee {
range: f32, range: f32,
angle: f32, angle: f32,
@ -499,6 +501,8 @@ pub enum AbilityData {
BasicRanged { BasicRanged {
energy: f32, energy: f32,
projectile_speed: f32, projectile_speed: f32,
projectile_spread: f32,
num_projectiles: u32,
}, },
BasicMelee { BasicMelee {
energy: f32, energy: f32,
@ -667,10 +671,14 @@ impl AbilityData {
BasicRanged { BasicRanged {
energy_cost, energy_cost,
projectile_speed, projectile_speed,
projectile_spread,
num_projectiles,
.. ..
} => Self::BasicRanged { } => Self::BasicRanged {
energy: *energy_cost, energy: *energy_cost,
projectile_speed: *projectile_speed, projectile_speed: *projectile_speed,
projectile_spread: *projectile_spread,
num_projectiles: *num_projectiles,
}, },
BasicMelee { BasicMelee {
energy_cost, energy_cost,
@ -818,6 +826,7 @@ impl AbilityData {
}; };
use AbilityData::*; use AbilityData::*;
match self { match self {
Default => false,
ComboMelee { ComboMelee {
range, range,
angle, angle,
@ -930,6 +939,8 @@ impl AbilityData {
BasicRanged { BasicRanged {
energy, energy,
projectile_speed, projectile_speed,
projectile_spread: _,
num_projectiles: _,
} => ranged_check(*projectile_speed) && energy_check(*energy), } => ranged_check(*projectile_speed) && energy_check(*energy),
BasicMelee { BasicMelee {
energy, energy,