From df91f665d789e2d20e0f8c1e329350e4eb4ad0db Mon Sep 17 00:00:00 2001 From: holychowders Date: Mon, 7 Mar 2022 18:58:44 -0600 Subject: [PATCH 1/7] Agent Perception: Restrict when idling agents respond to sounds. - Prevent utterances and other sounds from causing undesired jitters and fleeing, such as those caused by greeting villagers. - Agents will no longer flee from quieter weapon sounds such as melee. --- CHANGELOG.md | 3 +- common/src/comp/agent.rs | 2 +- common/systems/src/beam.rs | 5 ++-- common/systems/src/melee.rs | 2 +- common/systems/src/projectile.rs | 2 +- common/systems/src/shockwave.rs | 2 +- server/src/events/entity_manipulation.rs | 2 +- server/src/sys/agent.rs | 35 ++++++++---------------- 8 files changed, 20 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c12d5a7fee..8ec4aae115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed ### Fixed -- Fixed bug that would sometimes cause taking a screenshot to panic because a buffer was mapped a the wrong time. +- Fixed bug that would sometimes cause taking a screenshot to panic because a buffer was mapped at the wrong time. - Players can no longer push waypoints around - Sites will now also be placed near the edge of the map +- Fix a bug causing NPCs to jitter on interaction and randomly run away. ## [0.12.0] - 2022-02-19 diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index b79fec525a..e7ff1ca1f8 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -347,13 +347,13 @@ impl Sound { #[derive(Copy, Clone, Debug)] pub enum SoundKind { Unknown, + Utterance(UtteranceKind, Body), Movement, Melee, Projectile, Explosion, Beam, Shockwave, - Utterance(UtteranceKind, Body), } #[derive(Clone, Copy, Debug)] diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs index 55010f688e..b073472910 100644 --- a/common/systems/src/beam.rs +++ b/common/systems/src/beam.rs @@ -100,13 +100,12 @@ impl<'a> System<'a> for Sys { 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), + sound: Sound::new(SoundKind::Beam, pos.0, 13.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 + // may have traveled and produced effects a bit before reaching its end point if end_time < time { server_events.push(ServerEvent::Delete(entity)); } diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs index 8c4be6bc4a..fe84701bc4 100644 --- a/common/systems/src/melee.rs +++ b/common/systems/src/melee.rs @@ -73,7 +73,7 @@ impl<'a> System<'a> for Sys { continue; } server_emitter.emit(ServerEvent::Sound { - sound: Sound::new(SoundKind::Melee, pos.0, 3.0, read_data.time.0), + sound: Sound::new(SoundKind::Melee, pos.0, 2.0, read_data.time.0), }); melee_attack.applied = true; diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs index 3c3f204902..d3be470e66 100644 --- a/common/systems/src/projectile.rs +++ b/common/systems/src/projectile.rs @@ -81,7 +81,7 @@ impl<'a> System<'a> for Sys { 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), + sound: Sound::new(SoundKind::Projectile, pos.0, 4.0, read_data.time.0), }); } diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs index 259756ea72..04ade59559 100644 --- a/common/systems/src/shockwave.rs +++ b/common/systems/src/shockwave.rs @@ -94,7 +94,7 @@ impl<'a> System<'a> for Sys { 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), + sound: Sound::new(SoundKind::Shockwave, pos.0, 40.0, time), }); } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 9f799aa8fd..4f373b62ac 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -597,7 +597,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o .retrieve_entity_internal(uid.into()) }); - let explosion_volume = 2.5 * explosion.radius; + let explosion_volume = 6.25 * explosion.radius; let mut emitter = server_eventbus.emitter(); emitter.emit(ServerEvent::Sound { sound: Sound::new(SoundKind::Explosion, pos, explosion_volume, time.0), diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index d339e87fb7..b1e202de7f 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -524,7 +524,7 @@ impl<'a> AgentData<'a> { match sound { Some(AgentEvent::ServerSound(sound)) => { agent.sounds_heard.push(sound); - agent.awareness += sound.vol; + agent.awareness += sound.vol / 2.0; }, Some(AgentEvent::Hurt) => { // Hurt utterances at random upon receiving damage @@ -2165,36 +2165,23 @@ impl<'a> AgentData<'a> { if let Some(agent_stats) = read_data.stats.get(*self.entity) { let sound_pos = Pos(sound.pos); let dist_sqrd = self.pos.0.distance_squared(sound_pos.0); + let close_enough_to_react = dist_sqrd < 35.0_f32.powi(2); + let too_far_to_investigate = dist_sqrd > 10.0_f32.powi(2); // 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 + // logic. The `Mark` enum from common::agent could be used to // match with `agent::Mark::Guard` let is_village_guard = agent_stats.name == *"Guard".to_string(); let is_enemy = matches!(self.alignment, Some(Alignment::Enemy)); - if is_enemy { - let far_enough = dist_sqrd > 10.0_f32.powi(2); + let sound_was_loud = sound.vol >= 10.0; + let sound_was_threatening = sound_was_loud + || matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _)); - 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, rng); - } - } else if is_village_guard { + if (is_enemy || is_village_guard) && too_far_to_investigate { self.follow(agent, controller, &read_data.terrain, &sound_pos); - } else if !is_village_guard { - let flee_health = agent.psyche.flee_health; - let close_enough = dist_sqrd < 35.0_f32.powi(2); - let sound_was_loud = sound.vol >= 10.0; - - if close_enough - && (flee_health <= 0.7 || (flee_health <= 0.5 && sound_was_loud)) - { - self.flee(agent, controller, &read_data.terrain, &sound_pos); - } else { - self.idle(agent, controller, read_data, rng); - } + } else if sound_was_threatening && close_enough_to_react { + self.flee(agent, controller, &read_data.terrain, &sound_pos); } else { // TODO: Change this to a search action instead of idle self.idle(agent, controller, read_data, rng); @@ -2354,7 +2341,7 @@ impl<'a> AgentData<'a> { sound: Sound::new( SoundKind::Utterance(UtteranceKind::Scream, *body), self.pos.0, - 100.0, + 13.0, time, ), }); From 8d98ade15ef4842a11e31b0111d432362aea5899 Mon Sep 17 00:00:00 2001 From: holychowders Date: Wed, 9 Mar 2022 11:31:35 -0600 Subject: [PATCH 2/7] Agent perception: Make handling of sounds and awareness more intuitive. When a sound was received in `idle_tree()`, awareness would be incremented, causing a call to `handle_elevated_awareness()`, which handled sounds heard. Instead, just `handle_sounds()` when they are heard and increment awareness as part of hearing them. The code more straightforwardly shows the agent first hearing a sound and then becoming more aware based on that. --- server/src/sys/agent.rs | 70 +++++++++++++++++----------------- server/src/sys/agent/consts.rs | 1 - 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index b1e202de7f..f747182986 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -7,10 +7,9 @@ use crate::{ rtsim::RtSim, sys::agent::{ consts::{ - AVG_FOLLOW_DIST, AWARENESS_INVESTIGATE_THRESHOLD, DAMAGE_MEMORY_DURATION, - DEFAULT_ATTACK_RANGE, FLEE_DURATION, HEALING_ITEM_THRESHOLD, - IDLE_HEALING_ITEM_THRESHOLD, MAX_FLEE_DIST, MAX_FOLLOW_DIST, PARTIAL_PATH_DIST, - RETARGETING_THRESHOLD_SECONDS, SEPARATION_BIAS, SEPARATION_DIST, + AVG_FOLLOW_DIST, DAMAGE_MEMORY_DURATION, DEFAULT_ATTACK_RANGE, FLEE_DURATION, + HEALING_ITEM_THRESHOLD, IDLE_HEALING_ITEM_THRESHOLD, MAX_FLEE_DIST, MAX_FOLLOW_DIST, + PARTIAL_PATH_DIST, RETARGETING_THRESHOLD_SECONDS, SEPARATION_BIAS, SEPARATION_DIST, }, data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData}, util::{ @@ -503,7 +502,6 @@ impl<'a> AgentData<'a> { rng: &mut impl Rng, ) { agent.decrement_awareness(read_data.dt.0); - agent.forget_old_sounds(read_data.time.0); let small_chance = rng.gen_bool(0.1); // Set owner if no target @@ -524,7 +522,6 @@ impl<'a> AgentData<'a> { match sound { Some(AgentEvent::ServerSound(sound)) => { agent.sounds_heard.push(sound); - agent.awareness += sound.vol / 2.0; }, Some(AgentEvent::Hurt) => { // Hurt utterances at random upon receiving damage @@ -572,10 +569,8 @@ impl<'a> AgentData<'a> { if rng.gen::() < 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, rng); } else { - self.idle(agent, controller, read_data, rng); + self.handle_sounds_heard(agent, controller, read_data, rng); } }, } @@ -2149,43 +2144,48 @@ impl<'a> AgentData<'a> { } } - fn handle_elevated_awareness( + fn handle_sounds_heard( &self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData, rng: &mut impl Rng, ) { - if is_invulnerable(*self.entity, read_data) { - self.idle(agent, controller, read_data, rng); - return; - } + agent.forget_old_sounds(read_data.time.0); if let Some(sound) = agent.sounds_heard.last() { - if let Some(agent_stats) = read_data.stats.get(*self.entity) { - let sound_pos = Pos(sound.pos); - let dist_sqrd = self.pos.0.distance_squared(sound_pos.0); - let close_enough_to_react = dist_sqrd < 35.0_f32.powi(2); - let too_far_to_investigate = dist_sqrd > 10.0_f32.powi(2); + let sound_pos = Pos(sound.pos); + let dist_sqrd = self.pos.0.distance_squared(sound_pos.0); + let close_enough_to_react = dist_sqrd < 35.0_f32.powi(2); + let too_far_to_investigate = dist_sqrd > 10.0_f32.powi(2); - // 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` - let is_village_guard = agent_stats.name == *"Guard".to_string(); - let is_enemy = matches!(self.alignment, Some(Alignment::Enemy)); + let sound_was_loud = sound.vol >= 10.0; + let sound_was_threatening = sound_was_loud + || matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _)); - let sound_was_loud = sound.vol >= 10.0; - let sound_was_threatening = sound_was_loud - || matches!(sound.kind, SoundKind::Utterance(UtteranceKind::Scream, _)); + let is_enemy = 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` + let is_village_guard = read_data + .stats + .get(*self.entity) + .map_or(false, |stats| stats.name == *"Guard".to_string()); - if (is_enemy || is_village_guard) && too_far_to_investigate { - self.follow(agent, controller, &read_data.terrain, &sound_pos); - } else if sound_was_threatening && close_enough_to_react { - self.flee(agent, controller, &read_data.terrain, &sound_pos); - } else { - // TODO: Change this to a search action instead of idle - self.idle(agent, controller, read_data, rng); - } + // TODO: Awareness currently doesn't influence anything. + agent.awareness += 0.5 * sound.vol; + + if is_invulnerable(*self.entity, read_data) { + self.idle(agent, controller, read_data, rng); + return; + } + + if (is_enemy || is_village_guard) && too_far_to_investigate { + self.follow(agent, controller, &read_data.terrain, &sound_pos); + } else if sound_was_threatening && close_enough_to_react { + self.flee(agent, controller, &read_data.terrain, &sound_pos); + } else { + self.idle(agent, controller, read_data, rng); } } } diff --git a/server/src/sys/agent/consts.rs b/server/src/sys/agent/consts.rs index 9e794e0742..c26aafcad3 100644 --- a/server/src/sys/agent/consts.rs +++ b/server/src/sys/agent/consts.rs @@ -11,4 +11,3 @@ pub const RETARGETING_THRESHOLD_SECONDS: f64 = 10.0; pub const HEALING_ITEM_THRESHOLD: f32 = 0.5; pub const IDLE_HEALING_ITEM_THRESHOLD: f32 = 0.999; pub const DEFAULT_ATTACK_RANGE: f32 = 2.0; -pub const AWARENESS_INVESTIGATE_THRESHOLD: f32 = 1.0; From 3ffb1a770631055aa601d97e22299bfc2a582315 Mon Sep 17 00:00:00 2001 From: holychowders Date: Sat, 12 Mar 2022 16:15:00 -0600 Subject: [PATCH 3/7] Also restrict when Enemy agents respond to sounds and fix potential conditional bug. --- server/src/sys/agent.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f747182986..da5caff2b0 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2171,6 +2171,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; // TODO: Awareness currently doesn't influence anything. agent.awareness += 0.5 * sound.vol; @@ -2180,10 +2181,14 @@ impl<'a> AgentData<'a> { return; } - if (is_enemy || is_village_guard) && too_far_to_investigate { - self.follow(agent, controller, &read_data.terrain, &sound_pos); - } else if sound_was_threatening && close_enough_to_react { - self.flee(agent, controller, &read_data.terrain, &sound_pos); + if sound_was_threatening { + if follows_threatening_sounds && too_far_to_investigate { + self.follow(agent, controller, &read_data.terrain, &sound_pos); + } else if !follows_threatening_sounds && close_enough_to_react { + self.flee(agent, controller, &read_data.terrain, &sound_pos); + } else { + self.idle(agent, controller, read_data, rng); + } } else { self.idle(agent, controller, read_data, rng); } From cc808251e642b5990bc435f317b08a7c5df58709 Mon Sep 17 00:00:00 2001 From: holychowders Date: Sat, 12 Mar 2022 16:50:55 -0600 Subject: [PATCH 4/7] Make Enemy agents flee from dangerous sounds instead of follow. --- server/src/sys/agent.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index da5caff2b0..4e8a29c856 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -618,8 +618,7 @@ impl<'a> AgentData<'a> { } let aggro_on = *aggro_on; - let should_flee = self.damage.min(1.0) < agent.psyche.flee_health; - if should_flee { + if self.below_flee_health(agent) { let has_opportunity_to_flee = agent.action_state.timer < FLEE_DURATION; let within_flee_distance = dist_sqrd < MAX_FLEE_DIST.powi(2); @@ -1921,8 +1920,8 @@ impl<'a> AgentData<'a> { angle_xy, }; - // Match on tactic. Each tactic has different controls - // depending on the distance from the agent to the target + // Match on tactic. Each tactic has different controls depending on the distance + // from the agent to the target. match tactic { Tactic::SimpleMelee => { self.handle_simple_melee(agent, controller, &attack_data, tgt_data, read_data, rng) @@ -2182,9 +2181,14 @@ impl<'a> AgentData<'a> { } if sound_was_threatening { - if follows_threatening_sounds && too_far_to_investigate { + if !self.below_flee_health(agent) + && follows_threatening_sounds + && too_far_to_investigate + { self.follow(agent, controller, &read_data.terrain, &sound_pos); - } else if !follows_threatening_sounds && close_enough_to_react { + } else if self.below_flee_health(agent) + || (!follows_threatening_sounds && close_enough_to_react) + { self.flee(agent, controller, &read_data.terrain, &sound_pos); } else { self.idle(agent, controller, read_data, rng); @@ -2389,4 +2393,8 @@ impl<'a> AgentData<'a> { ); } } + + fn below_flee_health(&self, agent: &Agent) -> bool { + self.damage.min(1.0) < agent.psyche.flee_health + } } From c6bc6b63ee35f9979817fe4150b52968cd559371 Mon Sep 17 00:00:00 2001 From: holychowders Date: Sat, 12 Mar 2022 18:09:22 -0600 Subject: [PATCH 5/7] Prevent enemy agents from reacting to sounds from too far away and remove an inconsequential distance check. --- server/src/sys/agent.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 4e8a29c856..23842708c5 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2155,8 +2155,11 @@ impl<'a> AgentData<'a> { 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); - let close_enough_to_react = dist_sqrd < 35.0_f32.powi(2); - let too_far_to_investigate = dist_sqrd > 10.0_f32.powi(2); + // NOTE: There is an implicit distance requirement given that sound volume + // disipates as it travels, but we will not want to flee if a sound is super + // loud but heard from a great distance, regardless of how loud it was. + // `is_close` is this limiter. + let is_close = dist_sqrd < 35.0_f32.powi(2); let sound_was_loud = sound.vol >= 10.0; let sound_was_threatening = sound_was_loud @@ -2180,15 +2183,10 @@ impl<'a> AgentData<'a> { return; } - if sound_was_threatening { - if !self.below_flee_health(agent) - && follows_threatening_sounds - && too_far_to_investigate - { + if sound_was_threatening && is_close { + 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 && close_enough_to_react) - { + } else if self.below_flee_health(agent) || !follows_threatening_sounds { self.flee(agent, controller, &read_data.terrain, &sound_pos); } else { self.idle(agent, controller, read_data, rng); From 30af95942aae7f2058462e4e94c07d1ae43d61b3 Mon Sep 17 00:00:00 2001 From: holychowders Date: Sat, 19 Mar 2022 11:46:42 -0500 Subject: [PATCH 6/7] `.gitignore`: Ignore `.vim`, not just `.vi` files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0c3b49c28a..6999b6e44e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ target **/.nvimrc **/*.vi +**/*.vim **/*.swp **/*tags From a88d8ada0fa01281ba6556b682b41d7c7d1f9c7e Mon Sep 17 00:00:00 2001 From: holychowders Date: Sat, 19 Mar 2022 12:02:29 -0500 Subject: [PATCH 7/7] Comment out unused awareness increment/decrement calls and un-nest conditional for early return. --- server/src/sys/agent.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 23842708c5..ed264fa99b 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -501,7 +501,8 @@ impl<'a> AgentData<'a> { event_emitter: &mut Emitter<'_, ServerEvent>, rng: &mut impl Rng, ) { - agent.decrement_awareness(read_data.dt.0); + // TODO: Awareness currently doesn't influence anything. + //agent.decrement_awareness(read_data.dt.0); let small_chance = rng.gen_bool(0.1); // Set owner if no target @@ -2152,6 +2153,11 @@ impl<'a> AgentData<'a> { ) { agent.forget_old_sounds(read_data.time.0); + if is_invulnerable(*self.entity, read_data) { + self.idle(agent, controller, read_data, rng); + return; + } + 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); @@ -2176,12 +2182,7 @@ impl<'a> AgentData<'a> { let follows_threatening_sounds = is_enemy || is_village_guard; // TODO: Awareness currently doesn't influence anything. - agent.awareness += 0.5 * sound.vol; - - if is_invulnerable(*self.entity, read_data) { - self.idle(agent, controller, read_data, rng); - return; - } + //agent.awareness += 0.5 * sound.vol; if sound_was_threatening && is_close { if !self.below_flee_health(agent) && follows_threatening_sounds {