Make NPCs Aware of Sound - See Issue #913

This commit is contained in:
holychowders 2021-05-15 19:36:27 +00:00 committed by Samuel Keiffer
parent 9060e1ea71
commit d5f3ba77d4
15 changed files with 265 additions and 63 deletions

View File

@ -60,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added timed bans and ban history.
- Added non-admin moderators with limit privileges and updated the security model to reflect this.
- Chat tabs
- NPC's now hear certain sounds
### Changed

View File

@ -14,6 +14,7 @@ use super::dialogue::Subject;
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
pub const MAX_LISTEN_DIST: f32 = 100.0;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Alignment {
@ -245,7 +246,7 @@ pub enum AgentEvent {
TradeAccepted(Uid),
FinishedTrade(TradeResult),
UpdatePendingTrade(
// this data structure is large so box it to keep AgentEvent small
// This data structure is large so box it to keep AgentEvent small
Box<(
TradeId,
PendingTrade,
@ -253,7 +254,43 @@ pub enum AgentEvent {
[Option<ReducedInventory>; 2],
)>,
),
// Add others here
ServerSound(Sound),
}
#[derive(Copy, Clone, Debug)]
pub struct Sound {
pub kind: SoundKind,
pub pos: Vec3<f32>,
pub vol: f32,
pub time: f64,
}
impl Sound {
pub fn new(kind: SoundKind, pos: Vec3<f32>, vol: f32, time: f64) -> Self {
Sound {
kind,
pos,
vol,
time,
}
}
pub fn with_new_vol(mut self, new_vol: f32) -> Self {
self.vol = new_vol;
self
}
}
#[derive(Copy, Clone, Debug)]
pub enum SoundKind {
Unknown,
Movement,
Melee,
Projectile,
Explosion,
Beam,
Shockwave,
}
#[derive(Clone, Debug)]
@ -274,6 +311,8 @@ pub struct Agent {
pub inbox: VecDeque<AgentEvent>,
pub action_state: ActionState,
pub bearing: Vec2<f32>,
pub sounds_heard: Vec<Sound>,
pub awareness: f32,
}
#[derive(Clone, Debug, Default)]

View File

@ -18,5 +18,8 @@ pub const IRON_DENSITY: f32 = 7870.0;
pub const HUMAN_DENSITY: f32 = 990.0; // value we use to make humanoids gently float
// 1 thread might be used for long-running cpu intensive tasks, like chunk
// generation. having at least 2 helps not blocking in the main tick here
pub const MIN_RECOMMENDED_RAYON_THREADS: usize = 2;
pub const MIN_RECOMMENDED_TOKIO_THREADS: usize = 2;
pub const SOUND_TRAVEL_DIST_PER_VOLUME: f32 = 3.0;

View File

@ -2,6 +2,7 @@ use crate::{
character::CharacterId,
comp::{
self,
agent::Sound,
invite::{InviteKind, InviteResponse},
item::Item,
DisconnectReason, Ori, Pos,
@ -175,6 +176,9 @@ pub enum ServerEvent {
range: Option<f32>,
pos: Pos,
},
Sound {
sound: Sound,
},
}
pub struct EventBus<E> {

View File

@ -69,7 +69,6 @@ pub trait CharacterBehavior {
ControlAction::CancelInput(input) => self.cancel_input(data, input),
}
}
// fn init(data: &JoinData) -> CharacterState;
}
/// Read-Only Data sent from Character Behavior System to behavior fn's

View File

@ -1,6 +1,7 @@
use common::{
combat::{AttackSource, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
Beam, BeamSegment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource,
Inventory, Ori, Pos, Scale, Stats,
},
@ -13,6 +14,7 @@ use common::{
GroupTarget,
};
use common_ecs::{Job, Origin, ParMode, Phase, System};
use rand::{thread_rng, Rng};
use rayon::iter::ParallelIterator;
use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, ParJoin, Read, ReadExpect,
@ -88,6 +90,14 @@ impl<'a> System<'a> for Sys {
None => return (server_events, add_hit_entities, outcomes),
};
let end_time = creation_time + beam_segment.duration.as_secs_f64();
let mut rng = thread_rng();
if rng.gen_bool(0.005) {
server_events.push(ServerEvent::Sound {
sound: Sound::new(SoundKind::Beam, pos.0, 7.0, time),
});
}
// If beam segment is out of time emit destroy event but still continue since it
// may have traveled and produced effects a bit before reaching its
// end point
@ -151,8 +161,7 @@ impl<'a> System<'a> for Sys {
let height_b = body_b.height() * scale_b;
// Check if it is a hit
let hit = entity != target
&& !health_b.is_dead
let hit = entity != target && !health_b.is_dead
// Collision shapes
&& sphere_wedge_cylinder_collision(pos.0, frame_start_dist, frame_end_dist, *ori.look_dir(), beam_segment.angle, pos_b.0, rad_b, height_b);

View File

@ -1,11 +1,13 @@
use common::{
combat::{AttackSource, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori, Pos, Scale,
Stats,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
resources::Time,
uid::Uid,
util::Dir,
GroupTarget,
@ -18,6 +20,7 @@ use vek::*;
#[derive(SystemData)]
pub struct ReadData<'a> {
time: Read<'a, Time>,
entities: Entities<'a>,
uids: ReadStorage<'a, Uid>,
positions: ReadStorage<'a, Pos>,
@ -66,6 +69,9 @@ impl<'a> System<'a> for Sys {
if melee_attack.applied {
continue;
}
server_emitter.emit(ServerEvent::Sound {
sound: Sound::new(SoundKind::Melee, pos.0, 3.0, read_data.time.0),
});
melee_attack.applied = true;
// Scales

View File

@ -1,17 +1,19 @@
use common::{
combat::{AttackSource, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
projectile, Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory,
Ori, PhysicsState, Pos, Projectile, Stats, Vel,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
resources::DeltaTime,
resources::{DeltaTime, Time},
uid::{Uid, UidAllocator},
util::Dir,
GroupTarget,
};
use common_ecs::{Job, Origin, Phase, System};
use rand::{thread_rng, Rng};
use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
World, Write, WriteStorage,
@ -21,6 +23,7 @@ use vek::*;
#[derive(SystemData)]
pub struct ReadData<'a> {
time: Read<'a, Time>,
entities: Entities<'a>,
dt: Read<'a, DeltaTime>,
uid_allocator: Read<'a, UidAllocator>,
@ -59,7 +62,6 @@ impl<'a> System<'a> for Sys {
(read_data, mut orientations, mut projectiles, mut outcomes): Self::SystemData,
) {
let mut server_emitter = read_data.server_bus.emitter();
// Attacks
'projectile_loop: for (entity, pos, physics, vel, mut projectile) in (
&read_data.entities,
@ -70,7 +72,15 @@ impl<'a> System<'a> for Sys {
)
.join()
{
let mut rng = thread_rng();
if physics.on_surface().is_none() && rng.gen_bool(0.05) {
server_emitter.emit(ServerEvent::Sound {
sound: Sound::new(SoundKind::Projectile, pos.0, 2.0, read_data.time.0),
});
}
let mut projectile_vanished: bool = false;
// Hit entity
for other in physics.touch_entities.iter().copied() {
let same_group = projectile
@ -86,16 +96,14 @@ impl<'a> System<'a> for Sys {
.and_then(|e| read_data.groups.get(e))
);
// Skip if in the same group
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
if projectile.ignore_group
// Skip if in the same group
&& same_group
{
if projectile.ignore_group && same_group {
continue;
}
@ -173,7 +181,7 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
explosion: e,
owner: projectile.owner,
})
});
},
projectile::Effect::Vanish => {
server_emitter.emit(ServerEvent::Destroy {
@ -198,8 +206,7 @@ impl<'a> System<'a> for Sys {
}
}
// Hit something solid
if physics.on_wall.is_some() || physics.on_ground || physics.on_ceiling {
if physics.on_surface().is_some() {
let projectile = &mut *projectile;
for effect in projectile.hit_solid.drain(..) {
match effect {
@ -208,7 +215,7 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
explosion: e,
owner: projectile.owner,
})
});
},
projectile::Effect::Vanish => {
server_emitter.emit(ServerEvent::Destroy {

View File

@ -1,6 +1,7 @@
use common::{
combat::{AttackSource, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory, Ori,
PhysicsState, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
},
@ -12,6 +13,7 @@ use common::{
GroupTarget,
};
use common_ecs::{Job, Origin, Phase, System};
use rand::{thread_rng, Rng};
use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
World, Write, WriteStorage,
@ -83,9 +85,15 @@ impl<'a> System<'a> for Sys {
let end_time = creation_time + shockwave.duration.as_secs_f64();
let mut rng = thread_rng();
if rng.gen_bool(0.05) {
server_emitter.emit(ServerEvent::Sound {
sound: Sound::new(SoundKind::Shockwave, pos.0, 16.0, time),
});
}
// If shockwave is out of time emit destroy event but still continue since it
// may have traveled and produced effects a bit before reaching it's
// end point
// may have traveled and produced effects a bit before reaching it's end point
if time > end_time {
server_emitter.emit(ServerEvent::Destroy {
entity,

View File

@ -1,7 +1,9 @@
use crate::{
client::Client,
comp::{
biped_large, quadruped_low, quadruped_medium, quadruped_small, skills::SkillGroupKind,
agent::{Sound, SoundKind},
biped_large, quadruped_low, quadruped_medium, quadruped_small,
skills::SkillGroupKind,
theropod, PhysicsState,
},
rtsim::RtSim,
@ -476,19 +478,22 @@ pub fn handle_delete(server: &mut Server, entity: EcsEntity) {
}
pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>) {
let state = &server.state;
let ecs = server.state.ecs();
if vel.z <= -30.0 {
let mass = state
.ecs()
let mass = ecs
.read_storage::<comp::Mass>()
.get(entity)
.copied()
.unwrap_or_default();
let inventories = state.ecs().read_storage::<Inventory>();
let falldmg = mass.0 * vel.z.powi(2) / 200.0;
let stats = state.ecs().read_storage::<Stats>();
let impact_energy = mass.0 * vel.z.powi(2) / 2.0;
let falldmg = impact_energy / 100.0;
let inventories = ecs.read_storage::<Inventory>();
let stats = ecs.read_storage::<Stats>();
// Handle health change
if let Some(mut health) = state.ecs().write_storage::<comp::Health>().get_mut(entity) {
if let Some(mut health) = ecs.write_storage::<comp::Health>().get_mut(entity) {
let damage = Damage {
source: DamageSource::Falling,
kind: DamageKind::Crushing,
@ -503,7 +508,7 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
health.change_by(change);
}
// Handle poise change
if let Some(mut poise) = state.ecs().write_storage::<comp::Poise>().get_mut(entity) {
if let Some(mut poise) = ecs.write_storage::<comp::Poise>().get_mut(entity) {
let poise_damage = PoiseChange {
amount: -(mass.0 * vel.magnitude_squared() / 1500.0) as i32,
source: PoiseSource::Falling,
@ -557,6 +562,13 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) {
pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, owner: Option<Uid>) {
// Go through all other entities
let ecs = &server.state.ecs();
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
let time = ecs.read_resource::<Time>();
let explosion_volume = 2.5 * explosion.radius;
server_eventbus.emit_now(ServerEvent::Sound {
sound: Sound::new(SoundKind::Explosion, pos, explosion_volume, time.0),
});
// Add an outcome
// Uses radius as outcome power for now
@ -751,8 +763,6 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
char_state: char_state_b_maybe,
};
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
attack.apply_attack(
target_group,
attacker_info,

View File

@ -1,13 +1,19 @@
use specs::{world::WorldExt, Builder, Entity as EcsEntity};
use specs::{world::WorldExt, Builder, Entity as EcsEntity, Join};
use tracing::error;
use vek::*;
use common::{
comp::{
self, agent::AgentEvent, dialogue::Subject, inventory::slot::EquipSlot, item, slot::Slot,
tool::ToolKind, Inventory, Pos,
self,
agent::{AgentEvent, Sound, MAX_LISTEN_DIST},
dialogue::Subject,
inventory::slot::EquipSlot,
item,
slot::Slot,
tool::ToolKind,
Inventory, Pos,
},
consts::MAX_MOUNT_RANGE,
consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
outcome::Outcome,
uid::Uid,
vol::ReadVol,
@ -72,7 +78,7 @@ pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_en
if let Some(interactor_uid) = state.ecs().uid_from_entity(interactor) {
agent
.inbox
.push_front(AgentEvent::Talk(interactor_uid, Subject::Regular));
.push_back(AgentEvent::Talk(interactor_uid, Subject::Regular));
}
}
}
@ -103,8 +109,7 @@ pub fn handle_mount(server: &mut Server, mounter: EcsEntity, mountee: EcsEntity)
state.ecs().uid_from_entity(mountee),
) {
// We know the entities must exist to be able to look up their UIDs, so these
// are guaranteed to work; hence we can ignore possible errors
// here.
// are guaranteed to work; hence we can ignore possible errors here.
state.write_component_ignore_entity_dead(
mountee,
comp::MountState::MountedBy(mounter_uid),
@ -301,3 +306,27 @@ pub fn handle_mine_block(server: &mut Server, pos: Vec3<i32>, tool: Option<ToolK
}
}
}
pub fn handle_sound(server: &mut Server, sound: &Sound) {
let ecs = &server.state.ecs();
let positions = &ecs.read_storage::<comp::Pos>();
let agents = &mut ecs.write_storage::<comp::Agent>();
for (agent, agent_pos) in (agents, positions).join() {
// TODO: Use pathfinding for more dropoff around obstacles
let agent_dist_sqrd = agent_pos.0.distance_squared(sound.pos);
let sound_travel_dist_sqrd = (sound.vol * SOUND_TRAVEL_DIST_PER_VOLUME).powi(2);
let vol_dropoff = agent_dist_sqrd / sound_travel_dist_sqrd * sound.vol;
let propagated_sound = sound.with_new_vol(sound.vol - vol_dropoff);
let can_hear_sound = propagated_sound.vol > 0.00;
let should_hear_sound = agent_dist_sqrd < MAX_LISTEN_DIST.powi(2);
if can_hear_sound && should_hear_sound {
agent
.inbox
.push_back(AgentEvent::ServerSound(propagated_sound));
}
}
}

View File

@ -132,7 +132,7 @@ pub fn handle_invite(
} else if let Some(agent) = agents.get_mut(invitee) {
if send_invite() {
if let Some(inviter) = uids.get(inviter) {
agent.inbox.push_front(AgentEvent::TradeInvite(*inviter));
agent.inbox.push_back(AgentEvent::TradeInvite(*inviter));
invite_sent = true;
}
}
@ -221,7 +221,7 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
if let Some(agent) = agents.get_mut(inviter) {
agent
.inbox
.push_front(AgentEvent::TradeAccepted(invitee_uid));
.push_back(AgentEvent::TradeAccepted(invitee_uid));
}
#[cfg(feature = "worldgen")]
let pricing = agents

View File

@ -14,7 +14,7 @@ use group_manip::handle_group;
use information::handle_site_info;
use interaction::{
handle_lantern, handle_mine_block, handle_mount, handle_npc_interaction, handle_possess,
handle_unmount,
handle_sound, handle_unmount,
};
use inventory_manip::handle_inventory;
use invite::{handle_invite, handle_invite_response};
@ -110,7 +110,6 @@ impl Server {
},
ServerEvent::InitiateInvite(interactor, target, kind) => {
handle_invite(self, interactor, target, kind)
//handle_initiate_trade(self, interactor, target)
},
ServerEvent::InviteResponse(entity, response) => {
handle_invite_response(self, entity, response)
@ -217,10 +216,11 @@ impl Server {
ServerEvent::CreateSafezone { range, pos } => {
self.state.create_safezone(range, pos).build();
},
ServerEvent::Sound { sound } => handle_sound(self, &sound),
}
}
// Generate requested chunks.
// Generate requested chunks
for (entity, key) in requested_chunks {
self.generate_chunk(entity, key);
}

View File

@ -25,7 +25,7 @@ fn notify_agent_simple(
event: AgentEvent,
) {
if let Some(agent) = agents.get_mut(entity) {
agent.inbox.push_front(event);
agent.inbox.push_back(event);
}
}
@ -42,7 +42,7 @@ fn notify_agent_prices(
// Box<(tid, pend, _, inventories)>) = event {
agent
.inbox
.push_front(AgentEvent::UpdatePendingTrade(Box::new((
.push_back(AgentEvent::UpdatePendingTrade(Box::new((
// Prefer using this Agent's price data, but use the counterparty's price
// data if we don't have price data
boxval.0,

View File

@ -2,7 +2,9 @@ use crate::rtsim::{Entity as RtSimData, RtSim};
use common::{
comp::{
self,
agent::{AgentEvent, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME},
agent::{
AgentEvent, Target, DEFAULT_INTERACTION_TIME, MAX_LISTEN_DIST, TRADE_INTERACTION_TIME,
},
buff::{BuffKind, Buffs},
compass::{Direction, Distance},
dialogue::{MoodContext, MoodState, Subject},
@ -150,7 +152,6 @@ const FLEE_DURATION: f32 = 3.0;
const MAX_FOLLOW_DIST: f32 = 12.0;
const MAX_CHASE_DIST: f32 = 250.0;
const MAX_FLEE_DIST: f32 = 20.0;
const LISTEN_DIST: f32 = 16.0;
const SEARCH_DIST: f32 = 48.0;
const SIGHT_DIST: f32 = 80.0;
const SNEAK_COEFFICIENT: f32 = 0.25;
@ -158,6 +159,9 @@ const AVG_FOLLOW_DIST: f32 = 6.0;
const RETARGETING_THRESHOLD_SECONDS: f64 = 10.0;
const HEALING_ITEM_THRESHOLD: f32 = 0.5;
const DEFAULT_ATTACK_RANGE: f32 = 2.0;
const AWARENESS_INVESTIGATE_THRESHOLD: f32 = 1.0;
const AWARENESS_DECREMENT_CONSTANT: f32 = 0.07;
const SECONDS_BEFORE_FORGET_SOUNDS: f64 = 180.0;
/// This system will allow NPCs to modify their controller
#[derive(Default)]
@ -479,8 +483,7 @@ impl<'a> System<'a> for Sys {
{
if let Some(tgt_pos) = read_data.positions.get(attacker) {
// If the target is dead or in a safezone, remove the
// target
// and idle.
// target and idle.
if should_stop_attacking(
read_data.healths.get(attacker),
read_data.buffs.get(attacker),
@ -585,6 +588,9 @@ impl<'a> AgentData<'a> {
read_data: &ReadData,
event_emitter: &mut Emitter<'_, ServerEvent>,
) {
decrement_awareness(agent);
forget_old_sounds(agent, read_data);
// Set owner if no target
if agent.target.is_none() && thread_rng().gen_bool(0.1) {
if let Some(Alignment::Owned(owner)) = self.alignment {
@ -599,7 +605,12 @@ impl<'a> AgentData<'a> {
}
// Interact if incoming messages
if !agent.inbox.is_empty() {
agent.action_state.timer = 0.1;
if !matches!(agent.inbox.front(), Some(AgentEvent::ServerSound(_))) {
agent.action_state.timer = 0.1;
} else if let Some(AgentEvent::ServerSound(sound)) = agent.inbox.pop_front() {
agent.sounds_heard.push(sound);
agent.awareness += sound.vol;
}
}
if agent.action_state.timer > 0.0 {
if agent.action_state.timer
@ -618,6 +629,8 @@ impl<'a> AgentData<'a> {
}
} else if thread_rng().gen::<f32>() < 0.1 {
self.choose_target(agent, controller, &read_data, event_emitter);
} else if agent.awareness > AWARENESS_INVESTIGATE_THRESHOLD {
self.handle_elevated_awareness(agent, controller, read_data);
} else {
self.idle(agent, controller, &read_data);
}
@ -654,13 +667,8 @@ impl<'a> AgentData<'a> {
agent.action_state.timer = 0.01;
} else if agent.action_state.timer < FLEE_DURATION || dist_sqrd < MAX_FLEE_DIST
{
self.flee(
agent,
controller,
&read_data.terrain,
tgt_pos,
&read_data.dt,
);
self.flee(agent, controller, &read_data.terrain, tgt_pos);
agent.action_state.timer += read_data.dt.0;
} else {
agent.action_state.timer = 0.0;
agent.target = None;
@ -680,12 +688,10 @@ impl<'a> AgentData<'a> {
event_emitter
.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(*self.uid, msg)));
}
agent.target = None;
// Choose a new target every 10 seconds, but only for
// enemies
// TODO: This should be more
// principled. Consider factoring
// TODO: This should be more principled. Consider factoring
// health, combat rating, wielded weapon, etc, into the
// decision to change target.
} else if read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS
@ -951,7 +957,8 @@ impl<'a> AgentData<'a> {
// .push(ControlEvent::InviteResponse(InviteResponse::Decline));
// }
agent.action_state.timer += read_data.dt.0;
let msg = agent.inbox.pop_back();
let msg = agent.inbox.pop_front();
match msg {
Some(AgentEvent::Talk(by, subject)) => {
if agent.behavior.can(BehaviorCapability::SPEAK) {
@ -1304,9 +1311,9 @@ impl<'a> AgentData<'a> {
}
}
},
None => {
_ => {
if agent.behavior.can(BehaviorCapability::SPEAK) {
// no new events, continue looking towards the last interacting player for some
// No new events, continue looking towards the last interacting player for some
// time
if let Some(Target { target, .. }) = &agent.target {
self.look_toward(controller, read_data, target);
@ -1348,7 +1355,6 @@ impl<'a> AgentData<'a> {
controller: &mut Controller,
terrain: &TerrainGrid,
tgt_pos: &Pos,
dt: &DeltaTime,
) {
if let Some(body) = self.body {
if body.can_strafe() && !self.is_gliding {
@ -1375,7 +1381,6 @@ impl<'a> AgentData<'a> {
self.jump_if(controller, bearing.z > 1.5);
controller.inputs.move_z = bearing.z;
}
agent.action_state.timer += dt.0;
}
/// Attempt to consume a healing item, and return whether any healing items
@ -1457,7 +1462,7 @@ impl<'a> AgentData<'a> {
})
.filter(|(e, e_pos, e_health, e_stats, e_inventory, e_alignment, char_state)| {
let mut search_dist = SEARCH_DIST;
let mut listen_dist = LISTEN_DIST;
let mut listen_dist = MAX_LISTEN_DIST;
if char_state.map_or(false, |c_s| c_s.is_stealthy()) {
// TODO: make sneak more effective based on a stat like e_stats.fitness
search_dist *= SNEAK_COEFFICIENT;
@ -3597,6 +3602,49 @@ impl<'a> AgentData<'a> {
controller.inputs.move_z = bearing.z;
}
}
fn handle_elevated_awareness(
&self,
agent: &mut Agent,
controller: &mut Controller,
read_data: &ReadData,
) {
// Currently this means that we are in a safezone
if invulnerability_is_in_buffs(read_data.buffs.get(*self.entity)) {
self.idle(agent, controller, &read_data);
return;
}
let is_enemy = matches!(self.alignment, Some(Alignment::Enemy));
if let Some(sound) = agent.sounds_heard.last() {
let sound_pos = Pos(sound.pos);
let dist_sqrd = self.pos.0.distance_squared(sound_pos.0);
if is_enemy {
let far_enough = dist_sqrd > 10.0_f32.powi(2);
if far_enough {
self.follow(agent, controller, &read_data.terrain, &sound_pos);
} else {
// TODO: Change this to a search action instead of idle
self.idle(agent, controller, &read_data);
}
} else if self.flees {
let aggro = agent.psyche.aggro;
let close_enough = dist_sqrd < 35.0_f32.powi(2);
let loud_sound = sound.vol >= 10.0;
if close_enough && (aggro <= 0.5 || (aggro <= 0.7 && loud_sound)) {
self.flee(agent, controller, &read_data.terrain, &sound_pos);
} else {
self.idle(agent, controller, &read_data);
}
} else {
self.idle(agent, controller, &read_data);
}
}
}
}
fn can_see_tgt(terrain: &TerrainGrid, pos: &Pos, tgt_pos: &Pos, dist_sqrd: f32) -> bool {
@ -3614,6 +3662,8 @@ fn should_stop_attacking(health: Option<&Health>, buffs: Option<&Buffs>) -> bool
health.map_or(true, |a| a.is_dead) || invulnerability_is_in_buffs(buffs)
}
// 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.
fn invulnerability_is_in_buffs(buffs: Option<&Buffs>) -> bool {
buffs.map_or(false, |b| b.kinds.contains_key(&BuffKind::Invulnerability))
}
@ -3648,3 +3698,40 @@ fn aim_projectile(speed: f32, pos: Vec3<f32>, tgt: Vec3<f32>) -> Option<Dir> {
Dir::from_unnormalized(to_tgt)
}
fn forget_old_sounds(agent: &mut Agent, read_data: &ReadData) {
if !agent.sounds_heard.is_empty() {
// Keep (retain) only newer sounds
agent
.sounds_heard
.retain(|&sound| read_data.time.0 - sound.time <= SECONDS_BEFORE_FORGET_SOUNDS);
}
}
fn decrement_awareness(agent: &mut Agent) {
let mut decrement = AWARENESS_DECREMENT_CONSTANT;
let awareness = agent.awareness;
let too_high = awareness >= 100.0;
let high = awareness >= 50.0;
let medium = awareness >= 30.0;
let low = awareness > 15.0;
let positive = awareness >= 0.0;
let negative = awareness < 0.0;
if too_high {
decrement *= 3.0;
} else if high {
decrement *= 1.0;
} else if medium {
decrement *= 2.5;
} else if low {
decrement *= 0.70;
} else if positive {
decrement *= 0.5;
} else if negative {
return;
}
agent.awareness -= decrement;
}