Agent: Use FOV when scanning for hostile targets and refactor choose_target().

- Refactors `choose_target()`, renaming it and extracting functions with more meaningful names and more correct behavior.
- Adds FOV for agents scanning for hostile targets.
This commit is contained in:
holychowders 2022-03-29 19:04:20 -05:00
parent 0fd20ba00e
commit d3873e357e
4 changed files with 260 additions and 381 deletions

View File

@ -48,10 +48,9 @@ impl SpatialGrid {
}
}
/// Get an iterator over the entities overlapping the
/// provided axis aligned bounding region
/// NOTE: for best optimization of the iterator use `for_each` rather than a
/// for loop
/// Get an iterator over the entities overlapping the provided axis aligned
/// bounding region. NOTE: for best optimization of the iterator use
/// `for_each` rather than a for loop.
pub fn in_aabr<'a>(&'a self, aabr: Aabr<i32>) -> impl Iterator<Item = specs::Entity> + 'a {
let iter = |max_entity_radius, grid: &'a hashbrown::HashMap<_, _>, lg2_cell_size| {
// Add buffer for other entity radius

View File

@ -13,13 +13,14 @@ use crate::{
},
data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData},
util::{
aim_projectile, can_see_tgt, get_entity_by_id, is_dead, is_dead_or_invulnerable,
is_invulnerable, is_villager, stop_pursuing, try_owner_alignment,
aim_projectile, are_our_owners_hostile, does_entity_see_other,
entity_looks_like_cultist, get_attacker_of_entity, get_entity_by_id, is_dead,
is_dead_or_invulnerable, is_entity_a_village_guard, is_invulnerable, is_villager,
stop_pursuing,
},
},
};
use common::{
combat,
comp::{
self,
agent::{
@ -29,7 +30,7 @@ use common::{
buff::BuffKind,
compass::{Direction, Distance},
dialogue::{MoodContext, MoodState, Subject},
inventory::{item::ItemTag, slot::EquipSlot},
inventory::slot::EquipSlot,
invite::{InviteKind, InviteResponse},
item::{
tool::{AbilitySpec, ToolKind},
@ -37,7 +38,7 @@ use common::{
},
projectile::ProjectileConstructor,
Agent, Alignment, BehaviorState, Body, CharacterState, ControlAction, ControlEvent,
Controller, Health, HealthChange, InputKind, Inventory, InventoryAction, Pos, Scale,
Controller, Health, HealthChange, InputKind, InventoryAction, Pos, Scale,
UnresolvedChatMsg, UtteranceKind,
},
effect::{BuffEffect, Effect},
@ -537,7 +538,7 @@ impl<'a> AgentData<'a> {
}
if rng.gen::<f32>() < 0.1 {
self.choose_target(agent, controller, read_data);
self.scan_for_new_hostile_target(agent, controller, read_data);
} else {
self.handle_sounds_heard(agent, controller, read_data, rng);
}
@ -639,7 +640,7 @@ impl<'a> AgentData<'a> {
read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS;
if !in_aggro_range && is_time_to_retarget {
self.choose_target(agent, controller, read_data);
self.scan_for_new_hostile_target(agent, controller, read_data);
}
if aggro_on {
@ -709,14 +710,14 @@ impl<'a> AgentData<'a> {
// Only emit event for agents that have a lantern equipped
if lantern_equipped && rng.gen_bool(0.001) {
if day_period.is_dark() && !lantern_turned_on {
// Agents with turned off lanterns turn them on randomly once it's
// nighttime and keep them on
// Agents with turned off lanterns turn them on randomly once it's nighttime and
// keep them on.
// Only emit event for agents that sill need to
// turn on their lantern
// turn on their lantern.
controller.push_event(ControlEvent::EnableLantern)
} else if lantern_turned_on && day_period.is_light() {
// agents with turned on lanterns turn them off randomly once it's
// daytime and keep them off
// daytime and keep them off.
controller.push_event(ControlEvent::DisableLantern)
}
};
@ -728,8 +729,8 @@ impl<'a> AgentData<'a> {
agent.action_state.timer = 0.0;
if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to {
// if it has an rtsim destination and can fly then it should
// if it is flying and bumps something above it then it should move down
// If it has an rtsim destination and can fly, then it should.
// If it is flying and bumps something above it, then it should move down.
if self.traversal_config.can_fly
&& !read_data
.terrain
@ -1432,11 +1433,10 @@ impl<'a> AgentData<'a> {
if small_chance {
controller.push_utterance(UtteranceKind::Angry);
if is_villager(self.alignment) {
if self.remembers_fight_with(target, read_data) {
chat_villager_remembers_fighting();
} else if self.entity_looks_like_cultist(target, read_data) {
} else if entity_looks_like_cultist(target, read_data) {
chat("npc.speech.villager_cultist_alarm");
} else {
chat("npc.speech.menacing");
@ -1445,6 +1445,14 @@ impl<'a> AgentData<'a> {
chat("npc.speech.menacing");
}
}
// Remember target.
self.rtsim_entity.is_some().then(|| {
read_data
.stats
.get(target)
.map(|stats| agent.add_fight_to_memory(&stats.name, read_data.time.0))
});
}
fn flee(
@ -1539,172 +1547,76 @@ impl<'a> AgentData<'a> {
}
}
fn choose_target(&self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData) {
fn scan_for_new_hostile_target(
&self,
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
) {
agent.action_state.timer = 0.0;
let mut aggro_on = false;
let worth_choosing = |entity| {
read_data.positions.get(entity).and_then(|pos| {
Some((
entity,
pos,
read_data.healths.get(entity)?,
read_data.inventories.get(entity)?,
read_data.alignments.get(entity),
read_data.char_states.get(entity),
read_data.bodies.get(entity),
))
let get_pos = |entity| read_data.positions.get(entity);
let is_alignment_passive_towards_entity = |entity| {
self.alignment.map_or(false, |alignment| {
read_data
.alignments
.get(entity)
.map_or(false, |entity_alignment| {
alignment.passive_towards(*entity_alignment)
})
})
};
let max_search_dist = agent.psyche.search_dist();
let max_sight_dist = agent.psyche.sight_dist;
let max_listen_dist = agent.psyche.listen_dist;
let in_sight_dist =
|e_pos: &Pos, e_char_state: Option<&CharacterState>, inventory: &Inventory| {
let search_dist = max_sight_dist
/ if e_char_state.map_or(false, CharacterState::is_stealthy) {
combat::compute_stealth_coefficient(Some(inventory))
let get_enemy = |entity: EcsEntity| {
if self.is_entity_an_enemy(entity, read_data) {
Some(entity)
} else if self.defends_entity(entity, read_data) {
if let Some(attacker) = get_attacker_of_entity(entity, read_data) {
if !is_alignment_passive_towards_entity(attacker) {
// aggro_on: attack immediately, do not warn/menace.
aggro_on = true;
Some(attacker)
} else {
1.0
};
e_pos.0.distance_squared(self.pos.0) < search_dist.powi(2)
};
let within_fov = |e_pos: &Pos| {
(e_pos.0 - self.pos.0)
.try_normalized()
.map_or(true, |v| v.dot(*controller.inputs.look_dir) > 0.15)
};
let in_listen_dist =
|e_pos: &Pos, e_char_state: Option<&CharacterState>, inventory: &Inventory| {
let listen_dist = max_listen_dist
/ if e_char_state.map_or(false, CharacterState::is_stealthy) {
combat::compute_stealth_coefficient(Some(inventory))
} else {
1.0
};
// TODO implement proper sound system for agents
e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)
};
let within_reach =
|e_pos: &Pos, e_char_state: Option<&CharacterState>, e_inventory: &Inventory| {
(in_sight_dist(e_pos, e_char_state, e_inventory) && within_fov(e_pos))
|| in_listen_dist(e_pos, e_char_state, e_inventory)
};
let is_owner_hostile = |e_alignment: Option<&Alignment>| {
try_owner_alignment(self.alignment, read_data).map_or(false, |owner_alignment| {
try_owner_alignment(e_alignment, read_data).map_or(false, |e_owner_alignment| {
owner_alignment.hostile_towards(*e_owner_alignment)
})
})
};
let guard_other =
|e_health: &Health, e_body: Option<&Body>, e_alignment: Option<&Alignment>| {
let i_am_a_guard = read_data
.stats
.get(*self.entity)
.map_or(false, |stats| stats.name == "Guard");
let we_are_friendly: bool = self.alignment.map_or(false, |ma| {
e_alignment.map_or(false, |ea| !ea.hostile_towards(*ma))
});
let we_share_species: bool = self.body.map_or(false, |mb| {
e_body.map_or(false, |eb| {
eb.is_same_species_as(mb) || (eb.is_humanoid() && mb.is_humanoid())
})
});
let i_own_other =
matches!(e_alignment, Some(Alignment::Owned(ouid)) if self.uid == ouid);
let other_has_taken_damage = read_data.time.0 - e_health.last_change.time.0 < 5.0;
let attacker_of = |health: &Health| health.last_change.damage_by();
let i_should_defend = other_has_taken_damage
&& ((we_are_friendly && we_share_species)
|| (i_am_a_guard && is_villager(e_alignment))
|| i_own_other);
i_should_defend
.then(|| {
attacker_of(e_health)
.and_then(|damage_contributor| {
get_entity_by_id(damage_contributor.uid().0, read_data)
})
.and_then(|attacker| {
read_data.alignments.get(attacker).and_then(|aa| {
self.alignment.and_then({
|ma| {
if !ma.passive_towards(*aa) {
read_data
.positions
.get(attacker)
.map(|a_pos| (attacker, *a_pos))
} else {
None
}
}
})
})
})
})
.flatten()
};
let possible_target =
|(entity, e_pos, e_health, e_inventory, e_alignment, e_char_state, e_body): (
EcsEntity,
&Pos,
&Health,
&Inventory,
Option<&Alignment>,
Option<&CharacterState>,
Option<&Body>,
)| {
let can_target = within_reach(e_pos, e_char_state, e_inventory)
&& entity != *self.entity
&& !e_health.is_dead
&& !is_invulnerable(entity, read_data);
if !can_target {
None
} else if is_owner_hostile(e_alignment) {
Some((entity, *e_pos))
} else if let Some(villain_info) = guard_other(e_health, e_body, e_alignment) {
aggro_on = true;
Some(villain_info)
} else if self.remembers_fight_with(entity, read_data)
|| is_villager(self.alignment)
&& self.entity_looks_like_cultist(entity, read_data)
{
Some((entity, *e_pos))
None
}
} else {
None
}
};
} else {
None
}
};
let is_valid = |entity| {
read_data.healths.get(entity).map_or(false, |health| {
!health.is_dead && !is_invulnerable(entity, read_data)
})
};
let lost_target = |target: Option<EcsEntity>| agent.target.is_none() && target.is_some();
// Search area
// TODO choose target by more than just distance
// Search the area.
// TODO: choose target by more than just distance
let common::CachedSpatialGrid(grid) = self.cached_spatial_grid;
let target = grid
.in_circle_aabr(self.pos.0.xy(), max_search_dist)
.filter_map(worth_choosing)
.filter_map(possible_target)
// TODO: This seems expensive. Cache this to avoid recomputing each tick
.filter(|(_, e_pos)| can_see_tgt(&read_data.terrain, self.pos, e_pos, e_pos.0.distance_squared(self.pos.0)))
.min_by_key(|(_, e_pos)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32)
.map(|(e, _)| e);
if agent.target.is_none() && target.is_some() {
let target = grid
.in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist())
.filter(|entity| {
does_entity_see_other(agent, *self.entity, *entity, controller, read_data)
&& is_valid(*entity)
})
.filter_map(get_enemy)
.min_by_key(|entity| {
// TODO: This seems expensive. Cache this to avoid recomputing each tick
get_pos(*entity).map(|pos| (pos.0.distance_squared(self.pos.0) * 100.0) as i32)
});
if lost_target(target) {
if aggro_on {
controller.push_utterance(UtteranceKind::Angry);
} else {
controller.push_utterance(UtteranceKind::Surprised);
}
}
// Change target to new target.
agent.target = target.map(|target| Target {
target,
hostile: true,
@ -2090,13 +2002,9 @@ impl<'a> AgentData<'a> {
tgt_data,
read_data,
),
Tactic::RotatingTurret => self.handle_rotating_turret_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
),
Tactic::RotatingTurret => {
self.handle_rotating_turret_attack(agent, controller, tgt_data, read_data)
},
Tactic::Mindflayer => self.handle_mindflayer_attack(
agent,
controller,
@ -2475,16 +2383,6 @@ impl<'a> AgentData<'a> {
})
}
fn entity_looks_like_cultist(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let number_of_cultist_items_equipped = read_data.inventories.get(entity).map_or(0, |inv| {
inv.equipped_items()
.filter(|item| item.tags().contains(&ItemTag::Cultist))
.count()
});
number_of_cultist_items_equipped > 2
}
fn remembers_fight_with(&self, other: EcsEntity, read_data: &ReadData) -> bool {
let name = || read_data.stats.get(other).map(|stats| stats.name.clone());
@ -2494,4 +2392,39 @@ impl<'a> AgentData<'a> {
})
})
}
fn is_entity_an_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let alignment = read_data.alignments.get(entity);
(entity != *self.entity)
&& (are_our_owners_hostile(self.alignment, alignment, read_data)
|| self.remembers_fight_with(entity, read_data)
|| self.is_villager_and_entity_is_cultist(entity, read_data))
}
fn defends_entity(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let entity_alignment = read_data.alignments.get(entity);
let we_are_friendly = entity_alignment.map_or(false, |entity_alignment| {
self.alignment.map_or(false, |alignment| {
!alignment.hostile_towards(*entity_alignment)
})
});
let we_share_species = read_data.bodies.get(entity).map_or(false, |entity_body| {
self.body.map_or(false, |body| {
entity_body.is_same_species_as(body)
|| (entity_body.is_humanoid() && body.is_humanoid())
})
});
let self_owns_entity =
matches!(entity_alignment, Some(Alignment::Owned(ouid)) if *self.uid == *ouid);
(we_are_friendly && we_share_species)
|| (is_entity_a_village_guard(*self.entity, read_data) && is_villager(entity_alignment))
|| self_owns_entity
}
fn is_villager_and_entity_is_cultist(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
is_villager(self.alignment) && entity_looks_like_cultist(entity, read_data)
}
}

View File

@ -1,6 +1,6 @@
use crate::sys::agent::{
consts::MAX_PATH_DIST, data::Path, util::can_see_tgt, AgentData, AttackData, ReadData,
TargetData,
consts::MAX_PATH_DIST, data::Path, util::positions_have_line_of_sight, AgentData, AttackData,
ReadData, TargetData,
};
use common::{
comp::{
@ -134,12 +134,7 @@ impl<'a> AgentData<'a> {
const PREF_DIST: f32 = 30_f32;
if attack_data.angle_xy < 30.0
&& (elevation > 10.0 || attack_data.dist_sqrd > PREF_DIST.powi(2))
&& can_see_tgt(
&read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
controller.push_basic_input(InputKind::Primary);
} else if attack_data.dist_sqrd < (PREF_DIST / 2.).powi(2) {
@ -186,12 +181,7 @@ impl<'a> AgentData<'a> {
..self.traversal_config
},
) {
if can_see_tgt(
&read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) {
if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) {
controller.push_basic_input(InputKind::Primary);
}
controller.inputs.move_dir =
@ -253,12 +243,7 @@ impl<'a> AgentData<'a> {
if attack_data.dist_sqrd < 32.0f32.powi(2)
&& has_leap()
&& has_energy(50.0)
&& can_see_tgt(
&read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
use_leap(controller);
}
@ -319,12 +304,7 @@ impl<'a> AgentData<'a> {
if attack_data.dist_sqrd < 32.0f32.powi(2)
&& has_leap()
&& has_energy(50.0)
&& can_see_tgt(
&read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
use_leap(controller);
}
@ -370,12 +350,8 @@ impl<'a> AgentData<'a> {
read_data,
Path::Separate,
None,
) && can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) {
) && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
if agent.action_state.timer > 4.0 && attack_data.angle < 45.0 {
controller.push_basic_input(InputKind::Secondary);
agent.action_state.timer = 0.0;
@ -437,12 +413,7 @@ impl<'a> AgentData<'a> {
// If in repeater ranged, have enough energy, and aren't in recovery, try to
// keep firing
if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// Only keep firing if not in melee range or if can see target
controller.push_basic_input(InputKind::Secondary);
@ -477,12 +448,7 @@ impl<'a> AgentData<'a> {
}
}
} else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// If not really far, and can see target, attempt to shoot bow
if self.energy.current() < DESIRED_ENERGY_LEVEL {
@ -522,12 +488,8 @@ impl<'a> AgentData<'a> {
..self.traversal_config
},
) {
if can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) && attack_data.angle < 45.0
if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 45.0
{
controller.inputs.move_dir = bearing
.xy()
@ -660,12 +622,8 @@ impl<'a> AgentData<'a> {
..self.traversal_config
},
) {
if can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) && attack_data.angle < 45.0
if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 45.0
{
controller.inputs.move_dir = bearing
.xy()
@ -715,12 +673,7 @@ impl<'a> AgentData<'a> {
const DESIRED_COMBO_LEVEL: u32 = 8;
// Logic to use abilities
if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// If far enough away, and can see target, check which skill is appropriate to
// use
@ -798,12 +751,8 @@ impl<'a> AgentData<'a> {
..self.traversal_config
},
) {
if can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) && attack_data.angle < 45.0
if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 45.0
{
controller.inputs.move_dir = bearing
.xy()
@ -863,12 +812,8 @@ impl<'a> AgentData<'a> {
read_data,
Path::Separate,
None,
) && can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) && attack_data.angle < 90.0
) && positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 90.0
{
if agent.action_state.timer > 5.0 {
controller.push_basic_input(InputKind::Secondary);
@ -1006,12 +951,7 @@ impl<'a> AgentData<'a> {
},
) {
if attack_data.angle < 15.0
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
if agent.action_state.timer > 5.0 {
agent.action_state.timer = 0.0;
@ -1204,12 +1144,7 @@ impl<'a> AgentData<'a> {
Path::Separate,
None,
) && attack_data.angle < 15.0
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
controller.push_basic_input(InputKind::Primary);
}
@ -1364,12 +1299,8 @@ impl<'a> AgentData<'a> {
tgt_data: &TargetData,
read_data: &ReadData,
) {
if can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) && attack_data.angle < 15.0
if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
} else {
@ -1386,12 +1317,8 @@ impl<'a> AgentData<'a> {
read_data: &ReadData,
) {
controller.inputs.look_dir = self.ori.look_dir();
if can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) && attack_data.angle < 15.0
if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
} else {
@ -1403,7 +1330,6 @@ impl<'a> AgentData<'a> {
&self,
agent: &mut Agent,
controller: &mut Controller,
attack_data: &AttackData,
tgt_data: &TargetData,
read_data: &ReadData,
) {
@ -1414,12 +1340,7 @@ impl<'a> AgentData<'a> {
.try_normalized()
.unwrap_or_default(),
);
if can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) {
if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) {
controller.push_basic_input(InputKind::Primary);
} else {
agent.target = None;
@ -1465,12 +1386,7 @@ impl<'a> AgentData<'a> {
agent.action_state.counter -= MINION_SUMMON_THRESHOLD;
}
} else if attack_data.dist_sqrd < MINDFLAYER_ATTACK_DIST.powi(2) {
if can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) {
if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) {
// If close to target, use either primary or secondary ability
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(10) && !matches!(c.stage_section, StageSection::Recover))
{
@ -1560,12 +1476,7 @@ impl<'a> AgentData<'a> {
let small_chance = rng.gen_bool(0.05);
if small_chance
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 15.0
{
// Fireball
@ -1686,12 +1597,7 @@ impl<'a> AgentData<'a> {
controller.push_cancel_input(InputKind::Fly);
if attack_data.dist_sqrd > 30.0_f32.powi(2) {
if rng.gen_bool(0.05)
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
@ -1903,12 +1809,7 @@ impl<'a> AgentData<'a> {
},
) {
if attack_data.angle < 15.0
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
if agent.action_state.timer > 5.0 {
agent.action_state.timer = 0.0;
@ -2134,12 +2035,7 @@ impl<'a> AgentData<'a> {
} else if attack_data.dist_sqrd < GOLEM_LASER_RANGE.powi(2) {
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5))
|| target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2)
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
&& attack_data.angle < 45.0
{
// If target in range threshold and haven't been lasering for more than 5
@ -2152,12 +2048,7 @@ impl<'a> AgentData<'a> {
}
} else if attack_data.dist_sqrd < GOLEM_LONG_RANGE.powi(2) {
if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2)
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// If target is far-ish and moving slow-ish, rocket them
controller.push_basic_input(InputKind::Ability(1));
@ -2220,24 +2111,14 @@ impl<'a> AgentData<'a> {
// Pincer them if they're in range and angle
controller.push_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,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// Start bubbling them if not close enough to do something else and in angle and
// can see target
controller.push_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,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// Start scuttling if not close enough to do something else and in angle and can
// see target
@ -2332,12 +2213,7 @@ impl<'a> AgentData<'a> {
}
} else if attack_data.dist_sqrd < FIRE_BREATH_RANGE.powi(2) {
if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5))
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// Keep breathing fire if close enough, can see target, and have not been
// breathing for more than 5 seconds
@ -2346,23 +2222,13 @@ impl<'a> AgentData<'a> {
// Scythe them if they're in range and angle
controller.push_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,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// Start breathing fire at them if close enough, in angle, and can see target
controller.push_basic_input(InputKind::Secondary);
}
} else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2)
&& can_see_tgt(
&*read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// Throw a pumpkin at them if close enough and can see them
controller.push_basic_input(InputKind::Ability(1));
@ -2457,12 +2323,7 @@ impl<'a> AgentData<'a> {
if attack_data.in_min_range() {
controller.push_basic_input(InputKind::Primary);
} else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
&& can_see_tgt(
&read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
)
&& positions_have_line_of_sight(self.pos, tgt_data.pos, read_data)
{
// If in pathing range and can see target, move towards them
self.path_toward_target(
@ -2594,12 +2455,7 @@ impl<'a> AgentData<'a> {
if attack_data.in_min_range() {
// If in range, shockwave
controller.push_basic_input(InputKind::Ability(0));
} else if can_see_tgt(
&read_data.terrain,
self.pos,
tgt_data.pos,
attack_data.dist_sqrd,
) {
} else if positions_have_line_of_sight(self.pos, tgt_data.pos, read_data) {
// Else if in sight, barrage
controller.push_basic_input(InputKind::Secondary);
}

View File

@ -1,8 +1,12 @@
use crate::sys::agent::{AgentData, ReadData};
use common::{
comp::{agent::Psyche, buff::BuffKind, Alignment, Pos},
combat::compute_stealth_coefficient,
comp::{
agent::Psyche, buff::BuffKind, inventory::item::ItemTag, item::ItemDesc, Agent, Alignment,
CharacterState, Controller, Pos,
},
consts::GRAVITY,
terrain::{Block, TerrainGrid},
terrain::Block,
util::Dir,
vol::ReadVol,
};
@ -12,16 +16,6 @@ use specs::{
};
use vek::*;
pub fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool {
terrain
.ray(pos.0 + Vec3::unit_z(), tgt_pos.0 + Vec3::unit_z())
.until(Block::is_opaque)
.cast()
.0
.powi(2)
>= dist_sqrd
}
pub fn is_dead_or_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool {
is_dead(entity, read_data) || is_invulnerable(entity, read_data)
}
@ -39,8 +33,9 @@ pub fn is_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool {
buffs.map_or(false, |b| b.kinds.contains_key(&BuffKind::Invulnerability))
}
/// Attempts to get alignment of owner if entity has Owned alignment
pub fn try_owner_alignment<'a>(
/// Gets alignment of owner if alignment given is `Owned`.
/// Returns original alignment if not owned.
pub fn owner_alignment<'a>(
alignment: Option<&'a Alignment>,
read_data: &'a ReadData,
) -> Option<&'a Alignment> {
@ -122,7 +117,103 @@ fn should_let_target_escape(
(dist_to_home_sqrd / own_health_fraction) * dur_since_last_attacked as f32 * 0.005
}
pub fn entity_looks_like_cultist(entity: EcsEntity, read_data: &ReadData) -> bool {
let number_of_cultist_items_equipped = read_data.inventories.get(entity).map_or(0, |inv| {
inv.equipped_items()
.filter(|item| item.tags().contains(&ItemTag::Cultist))
.count()
});
number_of_cultist_items_equipped > 2
}
// FIXME: `Alignment::Npc` doesn't necessarily mean villager.
pub fn is_villager(alignment: Option<&Alignment>) -> bool {
alignment.map_or(false, |alignment| matches!(alignment, Alignment::Npc))
}
pub fn is_entity_a_village_guard(entity: EcsEntity, read_data: &ReadData) -> bool {
read_data
.stats
.get(entity)
.map_or(false, |stats| stats.name == "Guard")
}
pub fn are_our_owners_hostile(
our_alignment: Option<&Alignment>,
their_alignment: Option<&Alignment>,
read_data: &ReadData,
) -> bool {
owner_alignment(our_alignment, read_data).map_or(false, |our_owners_alignment| {
owner_alignment(their_alignment, read_data).map_or(false, |their_owners_alignment| {
our_owners_alignment.hostile_towards(*their_owners_alignment)
})
})
}
pub fn positions_have_line_of_sight(pos_a: &Pos, pos_b: &Pos, read_data: &ReadData) -> bool {
let dist_sqrd = pos_b.0.distance_squared(pos_a.0);
read_data
.terrain
.ray(pos_a.0 + Vec3::unit_z(), pos_b.0 + Vec3::unit_z())
.until(Block::is_opaque)
.cast()
.0
.powi(2)
>= dist_sqrd
}
pub fn does_entity_see_other(
agent: &Agent,
entity: EcsEntity,
other: EcsEntity,
controller: &Controller,
read_data: &ReadData,
) -> bool {
let stealth_coefficient = {
let is_other_being_stealthy = read_data
.char_states
.get(other)
.map_or(false, CharacterState::is_stealthy);
if is_other_being_stealthy {
// TODO: We shouldn't have to check CharacterState. This should be factored in
// by the function (such as the one we're calling below) that supposedly
// computes a coefficient given stealthy-ness.
compute_stealth_coefficient(read_data.inventories.get(other))
} else {
1.0
}
};
if let (Some(pos), Some(other_pos)) = (
read_data.positions.get(entity),
read_data.positions.get(other),
) {
let dist = other_pos.0 - pos.0;
let dist_sqrd = other_pos.0.distance_squared(pos.0);
let within_sight_dist = {
let sight_dist = agent.psyche.sight_dist / stealth_coefficient;
dist_sqrd < sight_dist.powi(2)
};
let within_fov = dist
.try_normalized()
// FIXME: Should this be map_or(false)?
.map_or(true, |v| v.dot(*controller.inputs.look_dir) > 0.15);
within_sight_dist && positions_have_line_of_sight(pos, other_pos, read_data) && within_fov
} else {
false
}
}
pub fn get_attacker_of_entity(entity: EcsEntity, read_data: &ReadData) -> Option<EcsEntity> {
read_data
.healths
.get(entity)
.and_then(|health| health.last_change.damage_by())
.and_then(|damage_contributor| get_entity_by_id(damage_contributor.uid().0, read_data))
}