2022-02-13 01:52:01 +00:00
|
|
|
use crate::sys::agent::{AgentData, ReadData};
|
2022-01-18 03:02:43 +00:00
|
|
|
use common::{
|
2022-03-30 00:04:20 +00:00
|
|
|
combat::compute_stealth_coefficient,
|
|
|
|
comp::{
|
|
|
|
agent::Psyche, buff::BuffKind, inventory::item::ItemTag, item::ItemDesc, Agent, Alignment,
|
|
|
|
CharacterState, Controller, Pos,
|
|
|
|
},
|
2022-01-18 03:02:43 +00:00
|
|
|
consts::GRAVITY,
|
2022-03-30 00:04:20 +00:00
|
|
|
terrain::Block,
|
2022-01-18 03:02:43 +00:00
|
|
|
util::Dir,
|
|
|
|
vol::ReadVol,
|
|
|
|
};
|
|
|
|
use specs::{
|
|
|
|
saveload::{Marker, MarkerAllocator},
|
|
|
|
Entity as EcsEntity,
|
|
|
|
};
|
|
|
|
use vek::*;
|
|
|
|
|
|
|
|
pub fn is_dead_or_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool {
|
|
|
|
is_dead(entity, read_data) || is_invulnerable(entity, read_data)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_dead(entity: EcsEntity, read_data: &ReadData) -> bool {
|
|
|
|
let health = read_data.healths.get(entity);
|
|
|
|
health.map_or(false, |a| a.is_dead)
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: The logic that is used in this function and throughout the code
|
|
|
|
// shouldn't be used to mean that a character is in a safezone.
|
|
|
|
pub fn is_invulnerable(entity: EcsEntity, read_data: &ReadData) -> bool {
|
|
|
|
let buffs = read_data.buffs.get(entity);
|
|
|
|
|
|
|
|
buffs.map_or(false, |b| b.kinds.contains_key(&BuffKind::Invulnerability))
|
|
|
|
}
|
|
|
|
|
2022-03-30 00:04:20 +00:00
|
|
|
/// Gets alignment of owner if alignment given is `Owned`.
|
|
|
|
/// Returns original alignment if not owned.
|
|
|
|
pub fn owner_alignment<'a>(
|
2022-01-18 03:02:43 +00:00
|
|
|
alignment: Option<&'a Alignment>,
|
|
|
|
read_data: &'a ReadData,
|
|
|
|
) -> Option<&'a Alignment> {
|
|
|
|
if let Some(Alignment::Owned(owner_uid)) = alignment {
|
|
|
|
if let Some(owner) = get_entity_by_id(owner_uid.id(), read_data) {
|
|
|
|
return read_data.alignments.get(owner);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
alignment
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Projectile motion: Returns the direction to aim for the projectile to reach
|
|
|
|
/// target position. Does not take any forces but gravity into account.
|
|
|
|
pub fn aim_projectile(speed: f32, pos: Vec3<f32>, tgt: Vec3<f32>) -> Option<Dir> {
|
|
|
|
let mut to_tgt = tgt - pos;
|
|
|
|
let dist_sqrd = to_tgt.xy().magnitude_squared();
|
|
|
|
let u_sqrd = speed.powi(2);
|
|
|
|
to_tgt.z = (u_sqrd
|
|
|
|
- (u_sqrd.powi(2) - GRAVITY * (GRAVITY * dist_sqrd + 2.0 * to_tgt.z * u_sqrd))
|
|
|
|
.sqrt()
|
|
|
|
.max(0.0))
|
|
|
|
/ GRAVITY;
|
|
|
|
|
|
|
|
Dir::from_unnormalized(to_tgt)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_entity_by_id(id: u64, read_data: &ReadData) -> Option<EcsEntity> {
|
|
|
|
read_data.uid_allocator.retrieve_entity_internal(id)
|
|
|
|
}
|
2022-02-13 01:52:01 +00:00
|
|
|
|
|
|
|
impl<'a> AgentData<'a> {
|
|
|
|
pub fn has_buff(&self, read_data: &ReadData, buff: BuffKind) -> bool {
|
|
|
|
read_data
|
|
|
|
.buffs
|
|
|
|
.get(*self.entity)
|
|
|
|
.map_or(false, |b| b.kinds.contains_key(&buff))
|
|
|
|
}
|
|
|
|
}
|
2022-02-27 13:30:31 +00:00
|
|
|
|
|
|
|
/// Calculates whether the agent should continue chase or let the target escape.
|
|
|
|
///
|
|
|
|
/// Will return true when score of letting target escape is higher then the
|
|
|
|
/// score of continuing the pursue, false otherwise.
|
|
|
|
pub fn stop_pursuing(
|
|
|
|
dist_to_target_sqrd: f32,
|
|
|
|
dist_to_home_sqrd: f32,
|
|
|
|
own_health_fraction: f32,
|
|
|
|
target_health_fraction: f32,
|
|
|
|
dur_since_last_attacked: f64,
|
|
|
|
psyche: &Psyche,
|
|
|
|
) -> bool {
|
|
|
|
should_let_target_escape(
|
|
|
|
dist_to_home_sqrd,
|
|
|
|
dur_since_last_attacked,
|
|
|
|
own_health_fraction,
|
|
|
|
) > should_continue_to_pursue(dist_to_target_sqrd, psyche, target_health_fraction)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Scores the benefit of continuing the pursue in value from 0 to infinity.
|
|
|
|
fn should_continue_to_pursue(
|
|
|
|
dist_to_target_sqrd: f32,
|
|
|
|
psyche: &Psyche,
|
|
|
|
target_health_fraction: f32,
|
|
|
|
) -> f32 {
|
|
|
|
let aggression_score = (1.0 / psyche.flee_health.max(0.25))
|
|
|
|
* psyche.aggro_dist.unwrap_or(psyche.sight_dist)
|
|
|
|
* psyche.sight_dist;
|
|
|
|
|
|
|
|
(100.0 * aggression_score) / (dist_to_target_sqrd * target_health_fraction)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Scores the benefit of letting the target escape in a value from 0 to
|
|
|
|
/// infinity.
|
|
|
|
fn should_let_target_escape(
|
|
|
|
dist_to_home_sqrd: f32,
|
|
|
|
dur_since_last_attacked: f64,
|
|
|
|
own_health_fraction: f32,
|
|
|
|
) -> f32 {
|
|
|
|
(dist_to_home_sqrd / own_health_fraction) * dur_since_last_attacked as f32 * 0.005
|
|
|
|
}
|
2022-05-01 15:06:43 +00:00
|
|
|
|
2022-03-30 00:04:20 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-05-01 15:06:43 +00:00
|
|
|
// 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))
|
|
|
|
}
|
2022-03-30 00:04:20 +00:00
|
|
|
|
|
|
|
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))
|
|
|
|
}
|