Merge branch 'holychowders/improve_perception_system' into 'master'

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

See merge request veloren/veloren!3307
This commit is contained in:
Samuel Keiffer 2022-05-02 22:45:14 +00:00
commit 3abba05fb8
7 changed files with 382 additions and 396 deletions

3
.gitignore vendored
View File

@ -33,6 +33,9 @@ server_settings.ron
run.sh
maps
screenshots
notes.md
notes.txt
todo.md
todo.txt
userdata
temp

View File

@ -15,9 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Admin command to reload all chunks on the server
- Furniture and waypoints in site2 towns
- text input for trading
- Themed Site CliffTown, hoodoo/arabic inspired stone structures inhabited by mountaineer NPCs.
- Themed Site CliffTown, hoodoo/arabic inspired stone structures inhabited by mountaineer NPCs.
- NPCs now have rudimentary personalities
- Added Belarusian translation
- Add FOV check for agents scanning for targets they are hostile to
### Changed
@ -37,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix agents not idling
- Fixed an error where '{amount} Exp' floater did not use existing localizations
- Fix villagers seeing cultists and familiar enemies through objects.
- Menacing agents are now less spammy with their menacing messages
## [0.12.0] - 2022-02-19
@ -49,7 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Shrubs, a system for spawning smaller tree-like plants into the world.
- Waterfalls
- Sailing boat (currently requires spawning in)
- Added a filter search function for crafting menu, use "input:_____" to search for recipe inputs
- Added a filter search function for crafting menu, use "input:______" to search for recipe inputs
- Added catalan (Catalonia) language translation
- Sneaking with weapons drawn
- Stealth stat values on (some) armors

View File

@ -282,7 +282,7 @@ impl<'a> From<&'a Body> for Psyche {
},
},
sight_dist: 40.0,
listen_dist: 30.0,
listen_dist: 5.0,
aggro_dist: match body {
Body::Humanoid(_) => Some(20.0),
_ => None, // Always aggressive if detected

View File

@ -48,10 +48,10 @@ 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, entities_have_line_of_sight, get_attacker,
get_entity_by_id, is_dead, is_dead_or_invulnerable, is_dressed_as_cultist,
is_invulnerable, is_village_guard, is_villager, stop_pursuing,
},
},
};
use common::{
combat,
combat::compute_stealth_coefficient,
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},
@ -311,7 +312,7 @@ impl<'a> System<'a> for Sys {
// over the old one (i.e: because it's either close or
// because they attacked us).
if agent.target.map_or(true, |target| {
data.is_entity_more_dangerous_than_target(
data.is_more_dangerous_than_target(
attacker, target, &read_data,
)
}) {
@ -574,7 +575,6 @@ impl<'a> AgentData<'a> {
{
let target = *target;
let selected_at = *selected_at;
if let Some(tgt_pos) = read_data.positions.get(target) {
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
let origin_dist_sqrd = match agent.patrol_origin {
@ -605,13 +605,12 @@ impl<'a> AgentData<'a> {
let has_opportunity_to_flee = agent.action_state.timer < FLEE_DURATION;
let within_flee_distance = dist_sqrd < MAX_FLEE_DIST.powi(2);
// FIXME: Using the action state timer to see if an agent is allowed to speak is
// a hack.
// FIXME: Using action state timer to see if allowed to speak is a hack.
if agent.action_state.timer == 0.0 {
self.cry_out(agent, event_emitter, read_data);
agent.action_state.timer = 0.01;
} else if within_flee_distance && has_opportunity_to_flee {
self.flee(agent, controller, &read_data.terrain, tgt_pos);
self.flee(agent, controller, tgt_pos, &read_data.terrain);
agent.action_state.timer += read_data.dt.0;
} else {
agent.action_state.timer = 0.0;
@ -710,13 +709,13 @@ impl<'a> AgentData<'a> {
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
// 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 +727,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
@ -757,7 +756,7 @@ impl<'a> AgentData<'a> {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(agent.rtsim_controller.speed_factor);
self.jump_if(controller, bearing.z > 1.5 || self.traversal_config.can_fly);
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
@ -937,7 +936,7 @@ impl<'a> AgentData<'a> {
let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0);
controller.inputs.move_dir = bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(0.2 + (dist_sqrd - AVG_FOLLOW_DIST.powi(2)) / 8.0);
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
}
@ -1432,11 +1431,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 is_dressed_as_cultist(target, read_data) {
chat("npc.speech.villager_cultist_alarm");
} else {
chat("npc.speech.menacing");
@ -1445,20 +1443,29 @@ 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(
&self,
agent: &mut Agent,
controller: &mut Controller,
terrain: &TerrainGrid,
tgt_pos: &Pos,
terrain: &TerrainGrid,
) {
if let Some(body) = self.body {
if body.can_strafe() && !self.is_gliding {
controller.push_action(ControlAction::Unwield);
}
}
if let Some((bearing, speed)) = agent.chaser.chase(
&*terrain,
self.pos.0,
@ -1476,7 +1483,7 @@ impl<'a> AgentData<'a> {
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
}
@ -1543,159 +1550,59 @@ impl<'a> AgentData<'a> {
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 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_pos = |entity| read_data.positions.get(entity);
let get_enemy = |entity: EcsEntity| {
if self.is_enemy(entity, read_data) {
Some(entity)
} else if self.should_defend(entity, read_data) {
if let Some(attacker) = get_attacker(entity, read_data) {
if !self.passive_towards(attacker, read_data) {
// 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_target = |entity| {
read_data.healths.get(entity).map_or(false, |health| {
!health.is_dead && !is_invulnerable(entity, read_data)
})
};
// Search area
// TODO choose target by more than just distance
// TODO: This is a temporary hack. Remove this once footsteps are emitted and
// agents are capable of detecting when someone is directly behind them.
let within_listen_dist = |e_pos: &Pos| {
let listen_dist = agent.psyche.listen_dist;
e_pos.0.distance_squared(self.pos.0) < listen_dist.powi(2)
};
let is_detected = |entity: EcsEntity, e_pos: &Pos| {
let chance = thread_rng().gen_bool(0.3);
(within_listen_dist(e_pos) && chance)
|| self.can_see_entity(agent, controller, entity, e_pos, read_data)
};
// 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)))
.in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist())
.filter(|entity| is_valid_target(*entity))
.filter_map(get_enemy)
.filter_map(|entity| get_pos(entity).map(|pos| (entity, pos)))
.filter(|(entity, e_pos)| is_detected(*entity, e_pos))
.min_by_key(|(_, e_pos)| (e_pos.0.distance_squared(self.pos.0) * 100.0) as i32)
.map(|(e, _)| e);
.map(|(entity, _)| entity);
if agent.target.is_none() && target.is_some() {
if aggro_on {
@ -1704,7 +1611,7 @@ impl<'a> AgentData<'a> {
controller.push_utterance(UtteranceKind::Surprised);
}
}
// Change target to new target.
agent.target = target.map(|target| Target {
target,
hostile: true,
@ -2090,13 +1997,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,
@ -2142,13 +2045,7 @@ impl<'a> AgentData<'a> {
tgt_data,
read_data,
),
Tactic::RadialTurret => self.handle_radial_turret_attack(
agent,
controller,
&attack_data,
tgt_data,
read_data,
),
Tactic::RadialTurret => self.handle_radial_turret_attack(controller),
Tactic::Yeti => {
self.handle_yeti_attack(agent, controller, &attack_data, tgt_data, read_data)
},
@ -2208,7 +2105,7 @@ impl<'a> AgentData<'a> {
let sound_was_threatening = sound_was_loud
|| matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _));
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
// FIXME: We need to be able to change the name of a guard without breaking this
// logic. The `Mark` enum from common::agent could be used to match with
// `agent::Mark::Guard`
@ -2216,7 +2113,7 @@ impl<'a> AgentData<'a> {
.stats
.get(*self.entity)
.map_or(false, |stats| stats.name == *"Guard".to_string());
let follows_threatening_sounds = is_enemy || is_village_guard;
let follows_threatening_sounds = has_enemy_alignment || is_village_guard;
// TODO: Awareness currently doesn't influence anything.
//agent.awareness += 0.5 * sound.vol;
@ -2225,7 +2122,7 @@ impl<'a> AgentData<'a> {
if !self.below_flee_health(agent) && follows_threatening_sounds {
self.follow(agent, controller, &read_data.terrain, &sound_pos);
} else if self.below_flee_health(agent) || !follows_threatening_sounds {
self.flee(agent, controller, &read_data.terrain, &sound_pos);
self.flee(agent, controller, &sound_pos, &read_data.terrain);
} else {
self.idle(agent, controller, read_data, rng);
}
@ -2351,7 +2248,7 @@ impl<'a> AgentData<'a> {
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed * speed_multiplier;
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
true
} else {
@ -2373,7 +2270,7 @@ impl<'a> AgentData<'a> {
}
}
fn jump_if(&self, controller: &mut Controller, condition: bool) {
fn jump_if(&self, condition: bool, controller: &mut Controller) {
if condition {
controller.push_basic_input(InputKind::Jump);
} else {
@ -2407,9 +2304,11 @@ impl<'a> AgentData<'a> {
event_emitter: &mut Emitter<'_, ServerEvent>,
read_data: &ReadData,
) {
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
let has_enemy_alignment = matches!(self.alignment, Some(Alignment::Enemy));
if is_enemy {
if has_enemy_alignment {
// FIXME: If going to use "cultist + low health + fleeing" string, make sure
// they are each true.
self.chat_npc_if_allowed_to_speak(
"npc.speech.cultist_low_health_fleeing",
agent,
@ -2443,7 +2342,7 @@ impl<'a> AgentData<'a> {
self.damage.min(1.0) < agent.psyche.flee_health
}
fn is_entity_more_dangerous_than_target(
fn is_more_dangerous_than_target(
&self,
entity: EcsEntity,
target: Target,
@ -2475,16 +2374,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 +2383,88 @@ impl<'a> AgentData<'a> {
})
})
}
fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
let other_alignment = read_data.alignments.get(entity);
(entity != *self.entity)
&& !self.passive_towards(entity, read_data)
&& (are_our_owners_hostile(self.alignment, other_alignment, read_data)
|| self.remembers_fight_with(entity, read_data)
|| (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data)))
}
fn should_defend(&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_village_guard(*self.entity, read_data) && is_villager(entity_alignment))
|| self_owns_entity
}
fn passive_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool {
if let (Some(self_alignment), Some(other_alignment)) =
(self.alignment, read_data.alignments.get(entity))
{
self_alignment.passive_towards(*other_alignment)
} else {
false
}
}
fn can_see_entity(
&self,
agent: &Agent,
controller: &Controller,
other: EcsEntity,
other_pos: &Pos,
read_data: &ReadData,
) -> bool {
let other_stealth_coefficient = {
let is_other_stealthy = read_data
.char_states
.get(other)
.map_or(false, CharacterState::is_stealthy);
if is_other_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
}
};
let dist_sqrd = other_pos.0.distance_squared(self.pos.0);
let within_sight_dist = {
let sight_dist = agent.psyche.sight_dist / other_stealth_coefficient;
dist_sqrd < sight_dist.powi(2)
};
let within_fov = (other_pos.0 - self.pos.0)
.try_normalized()
.map_or(false, |v| v.dot(*controller.inputs.look_dir) > 0.15);
let other_body = read_data.bodies.get(other);
within_sight_dist
&& within_fov
&& entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, 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::entities_have_line_of_sight, AgentData, AttackData,
ReadData, TargetData,
};
use common::{
comp::{
@ -130,16 +130,15 @@ impl<'a> AgentData<'a> {
tgt_data: &TargetData,
read_data: &ReadData,
) {
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
};
let elevation = self.pos.0.z - tgt_data.pos.0.z;
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,
)
&& line_of_sight_with_target()
{
controller.push_basic_input(InputKind::Primary);
} else if attack_data.dist_sqrd < (PREF_DIST / 2.).powi(2) {
@ -186,12 +185,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 line_of_sight_with_target() {
controller.push_basic_input(InputKind::Primary);
}
controller.inputs.move_dir =
@ -219,12 +213,11 @@ impl<'a> AgentData<'a> {
rng: &mut impl Rng,
) {
let has_leap = || self.skill_set.has_skill(Skill::Axe(AxeSkill::UnlockLeap));
let has_energy = |need| self.energy.current() > need;
let use_leap = |controller: &mut Controller| {
controller.push_basic_input(InputKind::Ability(0));
};
if attack_data.in_min_range() && attack_data.angle < 45.0 {
controller.inputs.move_dir = Vec2::zero();
if agent.action_state.timer > 5.0 {
@ -253,11 +246,12 @@ 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,
&& entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
)
{
use_leap(controller);
@ -319,11 +313,12 @@ 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,
&& entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
)
{
use_leap(controller);
@ -370,11 +365,12 @@ impl<'a> AgentData<'a> {
read_data,
Path::Separate,
None,
) && can_see_tgt(
&*read_data.terrain,
) && entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
) {
if agent.action_state.timer > 4.0 && attack_data.angle < 45.0 {
controller.push_basic_input(InputKind::Secondary);
@ -413,6 +409,11 @@ impl<'a> AgentData<'a> {
const MIN_CHARGE_FRAC: f32 = 0.5;
const OPTIMAL_TARGET_VELOCITY: f32 = 5.0;
const DESIRED_ENERGY_LEVEL: f32 = 50.0;
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
};
// Logic to use abilities
if let CharacterState::ChargedRanged(c) = self.char_state {
if !matches!(c.stage_section, StageSection::Recover) {
@ -437,12 +438,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,
)
&& line_of_sight_with_target()
{
// Only keep firing if not in melee range or if can see target
controller.push_basic_input(InputKind::Secondary);
@ -476,14 +472,7 @@ impl<'a> AgentData<'a> {
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,
)
{
} else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) && line_of_sight_with_target() {
// If not really far, and can see target, attempt to shoot bow
if self.energy.current() < DESIRED_ENERGY_LEVEL {
// If low on energy, use primary to attempt to regen energy
@ -522,13 +511,7 @@ 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 line_of_sight_with_target() && attack_data.angle < 45.0 {
controller.inputs.move_dir = bearing
.xy()
.rotated_z(rng.gen_range(0.5..1.57))
@ -539,7 +522,7 @@ impl<'a> AgentData<'a> {
// Unless cannot see target, then move towards them
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
}
@ -660,11 +643,12 @@ impl<'a> AgentData<'a> {
..self.traversal_config
},
) {
if can_see_tgt(
&*read_data.terrain,
if entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
) && attack_data.angle < 45.0
{
controller.inputs.move_dir = bearing
@ -677,7 +661,7 @@ impl<'a> AgentData<'a> {
// Unless cannot see target, then move towards them
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
}
@ -713,14 +697,14 @@ impl<'a> AgentData<'a> {
) {
const DESIRED_ENERGY_LEVEL: f32 = 50.0;
const DESIRED_COMBO_LEVEL: u32 = 8;
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
};
// 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,
)
&& line_of_sight_with_target()
{
// If far enough away, and can see target, check which skill is appropriate to
// use
@ -798,13 +782,7 @@ 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 line_of_sight_with_target() && attack_data.angle < 45.0 {
controller.inputs.move_dir = bearing
.xy()
.rotated_z(rng.gen_range(0.5..1.57))
@ -815,7 +793,7 @@ impl<'a> AgentData<'a> {
// Unless cannot see target, then move towards them
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
}
@ -863,11 +841,12 @@ impl<'a> AgentData<'a> {
read_data,
Path::Separate,
None,
) && can_see_tgt(
&*read_data.terrain,
) && entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
) && attack_data.angle < 90.0
{
if agent.action_state.timer > 5.0 {
@ -1006,11 +985,12 @@ impl<'a> AgentData<'a> {
},
) {
if attack_data.angle < 15.0
&& can_see_tgt(
&*read_data.terrain,
&& entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
)
{
if agent.action_state.timer > 5.0 {
@ -1033,12 +1013,12 @@ impl<'a> AgentData<'a> {
agent.action_state.timer += read_data.dt.0;
}
controller.push_basic_input(InputKind::Secondary);
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
} else {
@ -1204,11 +1184,12 @@ impl<'a> AgentData<'a> {
Path::Separate,
None,
) && attack_data.angle < 15.0
&& can_see_tgt(
&*read_data.terrain,
&& entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
)
{
controller.push_basic_input(InputKind::Primary);
@ -1364,12 +1345,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 entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
&& attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
} else {
@ -1386,12 +1363,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 entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
&& attack_data.angle < 15.0
{
controller.push_basic_input(InputKind::Primary);
} else {
@ -1403,7 +1376,6 @@ impl<'a> AgentData<'a> {
&self,
agent: &mut Agent,
controller: &mut Controller,
attack_data: &AttackData,
tgt_data: &TargetData,
read_data: &ReadData,
) {
@ -1414,26 +1386,15 @@ 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 entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
{
controller.push_basic_input(InputKind::Primary);
} else {
agent.target = None;
}
}
pub fn handle_radial_turret_attack(
&self,
_agent: &mut Agent,
controller: &mut Controller,
_attack_data: &AttackData,
_tgt_data: &TargetData,
_read_data: &ReadData,
) {
pub fn handle_radial_turret_attack(&self, controller: &mut Controller) {
controller.push_basic_input(InputKind::Primary);
}
@ -1465,11 +1426,12 @@ 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,
if entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
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,11 +1522,12 @@ impl<'a> AgentData<'a> {
let small_chance = rng.gen_bool(0.05);
if small_chance
&& can_see_tgt(
&*read_data.terrain,
&& entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
)
&& attack_data.angle < 15.0
{
@ -1592,7 +1555,7 @@ impl<'a> AgentData<'a> {
controller.inputs.move_z = 1.0;
} else {
// Jump
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
}
@ -1686,11 +1649,12 @@ 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,
&& entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
)
&& attack_data.angle < 15.0
{
@ -1712,7 +1676,7 @@ impl<'a> AgentData<'a> {
controller.push_basic_input(InputKind::Fly);
controller.inputs.move_z = 1.0;
} else {
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
}
@ -1903,11 +1867,12 @@ impl<'a> AgentData<'a> {
},
) {
if attack_data.angle < 15.0
&& can_see_tgt(
&*read_data.terrain,
&& entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
)
{
if agent.action_state.timer > 5.0 {
@ -1930,12 +1895,12 @@ impl<'a> AgentData<'a> {
agent.action_state.timer += read_data.dt.0;
}
controller.push_basic_input(InputKind::Ability(0));
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
} else {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
self.jump_if(controller, bearing.z > 1.5);
self.jump_if(bearing.z > 1.5, controller);
controller.inputs.move_z = bearing.z;
}
} else {
@ -2118,6 +2083,10 @@ impl<'a> AgentData<'a> {
.map(|t| t.target)
.and_then(|e| read_data.velocities.get(e))
.map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
};
if attack_data.dist_sqrd < golem_melee_range.powi(2) {
if agent.action_state.counter < 7.5 {
// If target is close, whack them
@ -2134,12 +2103,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,
)
&& line_of_sight_with_target()
&& attack_data.angle < 45.0
{
// If target in range threshold and haven't been lasering for more than 5
@ -2151,14 +2115,7 @@ impl<'a> AgentData<'a> {
controller.push_basic_input(InputKind::Ability(0));
}
} 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,
)
{
if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) && line_of_sight_with_target() {
// If target is far-ish and moving slow-ish, rocket them
controller.push_basic_input(InputKind::Ability(1));
} else if health_fraction < 0.7 {
@ -2190,6 +2147,10 @@ impl<'a> AgentData<'a> {
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());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
};
// Sets counter at start of combat, using `condition` to keep track of whether
// it was already intitialized
if !agent.action_state.condition {
@ -2219,26 +2180,12 @@ impl<'a> AgentData<'a> {
} else if attack_data.in_min_range() && attack_data.angle < 60.0 {
// 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,
)
{
} else if attack_data.angle < 30.0 && line_of_sight_with_target() {
// 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,
)
{
} else if attack_data.angle < 90.0 && line_of_sight_with_target() {
// Start scuttling if not close enough to do something else and in angle and can
// see target
controller.push_basic_input(InputKind::Secondary);
@ -2321,6 +2268,9 @@ impl<'a> AgentData<'a> {
const FIRE_BREATH_RANGE: f32 = 20.0;
const MAX_PUMPKIN_RANGE: f32 = 50.0;
let health_fraction = self.health.map_or(0.5, |h| h.fraction());
let line_of_sight_with_target = || {
entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data)
};
if health_fraction < VINE_CREATION_THRESHOLD && !agent.action_state.condition {
// Summon vines when reach threshold of health
@ -2332,12 +2282,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,
)
&& line_of_sight_with_target()
{
// Keep breathing fire if close enough, can see target, and have not been
// breathing for more than 5 seconds
@ -2345,25 +2290,11 @@ impl<'a> AgentData<'a> {
} else if attack_data.in_min_range() && attack_data.angle < 60.0 {
// 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,
)
{
} else if attack_data.angle < 30.0 && line_of_sight_with_target() {
// 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,
)
{
} else if attack_data.dist_sqrd < MAX_PUMPKIN_RANGE.powi(2) && line_of_sight_with_target() {
// Throw a pumpkin at them if close enough and can see them
controller.push_basic_input(InputKind::Ability(1));
}
@ -2457,11 +2388,12 @@ 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,
&& entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
)
{
// If in pathing range and can see target, move towards them
@ -2594,11 +2526,12 @@ 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,
} else if entities_have_line_of_sight(
self.pos,
self.body,
tgt_data.pos,
attack_data.dist_sqrd,
tgt_data.body,
read_data,
) {
// Else if in sight, barrage
controller.push_basic_input(InputKind::Secondary);

View File

@ -1,8 +1,11 @@
use crate::sys::agent::{AgentData, ReadData};
use common::{
comp::{agent::Psyche, buff::BuffKind, Alignment, Pos},
comp::{
agent::Psyche, buff::BuffKind, inventory::item::ItemTag, item::ItemDesc, Alignment, Body,
Pos,
},
consts::GRAVITY,
terrain::{Block, TerrainGrid},
terrain::Block,
util::Dir,
vol::ReadVol,
};
@ -12,16 +15,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,7 +32,8 @@ 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
/// Gets alignment of owner if alignment given is `Owned`.
/// Returns original alignment if not owned.
pub fn try_owner_alignment<'a>(
alignment: Option<&'a Alignment>,
read_data: &'a ReadData,
@ -122,7 +116,88 @@ 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_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 {
try_owner_alignment(our_alignment, read_data).map_or(false, |our_owners_alignment| {
try_owner_alignment(their_alignment, read_data).map_or(false, |their_owners_alignment| {
our_owners_alignment.hostile_towards(*their_owners_alignment)
})
})
}
pub fn entities_have_line_of_sight(
pos: &Pos,
body: Option<&Body>,
other_pos: &Pos,
other_body: Option<&Body>,
read_data: &ReadData,
) -> bool {
let get_eye_pos = |pos: &Pos, body: Option<&Body>| {
let eye_offset = body.map_or(0.0, |b| b.eye_height());
Pos(pos.0.with_z(pos.0.z + eye_offset))
};
let eye_pos = get_eye_pos(pos, body);
let other_eye_pos = get_eye_pos(other_pos, other_body);
positions_have_line_of_sight(&eye_pos, &other_eye_pos, read_data)
}
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, pos_b.0)
.until(Block::is_opaque)
.cast()
.0
.powi(2)
>= dist_sqrd
}
pub fn is_dressed_as_cultist(entity: EcsEntity, read_data: &ReadData) -> bool {
read_data
.inventories
.get(entity)
.map_or(false, |inventory| {
inventory
.equipped_items()
.filter(|item| item.tags().contains(&ItemTag::Cultist))
.count()
> 2
})
}
pub fn get_attacker(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))
}