Tidal warrior AI.

This commit is contained in:
Sam 2021-05-25 21:20:27 -05:00
parent c81e1534f7
commit ac2f097d80
12 changed files with 150 additions and 93 deletions

View File

@ -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: [

View File

@ -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,

View File

@ -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,
)

View File

@ -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,
),
)

View File

@ -15,5 +15,5 @@ ItemDef(
)),
quality: Low,
tags: [],
ability_spec: Some(Custom("Tidal Claws")),
ability_spec: Some(Custom("Tidal Warrior")),
)

View File

@ -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 {
handle_forced_movement(data, &mut update, ForcedMovement::Forward {
strength: self.static_data.stage_data[stage_index].forward_movement,
},
0.3,
);
});
// Swings
update.character = CharacterState::ComboMelee(Data {

View File

@ -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 {
handle_forced_movement(data, &mut update, ForcedMovement::Forward {
strength: self.static_data.forward_speed * charge_frac.sqrt(),
},
0.1,
);
});
// This logic basically just decides if a charge should end, and prevents the
// character state spamming attacks while checking if it has hit something

View File

@ -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 {
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,
);
});
// Increment duration
// If we were to set a timeout for state, this would be

View File

@ -78,19 +78,14 @@ impl CharacterBehavior for Data {
},
StageSection::Movement => {
// Update velocity
handle_forced_movement(
data,
&mut update,
ForcedMovement::Forward {
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,
);
});
if self.timer < self.static_data.movement_duration {
// Movement

View File

@ -167,14 +167,9 @@ impl CharacterBehavior for Data {
self.static_data.movement_behavior,
MovementBehavior::ForwardGround
) {
handle_forced_movement(
data,
&mut update,
ForcedMovement::Forward {
handle_forced_movement(data, &mut update, ForcedMovement::Forward {
strength: self.static_data.forward_speed,
},
0.1,
);
});
}
// Swings

View File

@ -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 {

View File

@ -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,