diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index a874aa02e2..1b53c776b3 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -18,7 +18,6 @@ use super::{dialogue::Subject, Pos}; pub const DEFAULT_INTERACTION_TIME: f32 = 3.0; pub const TRADE_INTERACTION_TIME: f32 = 300.0; -const AWARENESS_DECREMENT_CONSTANT: f32 = 2.1; const SECONDS_BEFORE_FORGET_SOUNDS: f64 = 180.0; //intentionally very few concurrent action state variables are allowed. This is @@ -529,12 +528,77 @@ pub struct Agent { pub timer: Timer, pub bearing: Vec2, pub sounds_heard: Vec, - pub awareness: f32, pub position_pid_controller: Option, Vec3) -> f32, 16>>, /// Position from which to flee. Intended to be the agent's position plus a /// random position offset, to be used when a random flee direction is /// required and reset each time the flee timer is reset. pub flee_from_pos: Option, + pub awareness: Awareness, +} + +#[derive(Clone, Debug)] +/// Always clamped between `0.0` and `1.0`. +pub struct Awareness { + level: f32, + reached: bool, +} +impl fmt::Display for Awareness { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}", self.level) } +} +impl Awareness { + const ALERT: f32 = 1.0; + const HIGH: f32 = 0.6; + const LOW: f32 = 0.1; + const MEDIUM: f32 = 0.3; + const UNAWARE: f32 = 0.0; + + pub fn new(level: f32) -> Self { + Self { + level: level.clamp(Self::UNAWARE, Self::ALERT), + reached: false, + } + } + + /// The level of awareness as a decimal. + pub fn level(&self) -> f32 { self.level } + + /// The level of awareness in English. To see if awareness has been fully + /// reached, use `self.reached()`. + pub fn state(&self) -> AwarenessState { + if self.level == Self::ALERT { + AwarenessState::Alert + } else if self.level.is_between(Self::HIGH, Self::ALERT) { + AwarenessState::High + } else if self.level.is_between(Self::MEDIUM, Self::HIGH) { + AwarenessState::Medium + } else if self.level.is_between(Self::LOW, Self::MEDIUM) { + AwarenessState::Low + } else { + AwarenessState::Unaware + } + } + + /// Awareness was reached at some point and has not been reset. + pub fn reached(&self) -> bool { self.reached } + + pub fn change_by(&mut self, amount: f32) { + self.level = (self.level + amount).clamp(Self::UNAWARE, Self::ALERT); + + if self.state() == AwarenessState::Alert { + self.reached = true; + } else if self.state() == AwarenessState::Unaware { + self.reached = false; + } + } +} + +#[derive(Clone, Debug, PartialOrd, PartialEq, Eq)] +pub enum AwarenessState { + Unaware = 0, + Low = 1, + Medium = 2, + High = 3, + Alert = 4, } /// State persistence object for the behavior tree @@ -565,9 +629,9 @@ impl Agent { timer: Timer::default(), bearing: Vec2::zero(), sounds_heard: Vec::new(), - awareness: 0.0, position_pid_controller: None, flee_from_pos: None, + awareness: Awareness::new(0.0), } } @@ -618,34 +682,6 @@ impl Agent { self } - pub fn decrement_awareness(&mut self, dt: f32) { - let mut decrement = dt * AWARENESS_DECREMENT_CONSTANT; - let awareness = self.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; - } - - self.awareness -= decrement; - } - pub fn forget_old_sounds(&mut self, time: f64) { if !self.sounds_heard.is_empty() { // Keep (retain) only newer sounds diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index d1fb00c29f..b9c66324d9 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -1,7 +1,7 @@ use crate::{ consts::{ AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, PARTIAL_PATH_DIST, - SEPARATION_BIAS, SEPARATION_DIST, + SEPARATION_BIAS, SEPARATION_DIST, STD_AWARENESS_DECAY_RATE, }, data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData}, util::{ @@ -166,6 +166,11 @@ impl<'a> AgentData<'a> { enum ActionTimers { TimerIdle = 0, } + + agent + .awareness + .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0); + // Light lanterns at night // TODO Add a method to turn on NPC lanterns underground let lantern_equipped = self @@ -696,13 +701,8 @@ impl<'a> AgentData<'a> { }, }; - let can_sense_directly_near = - { |e_pos: &Pos| e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) }; - let is_detected = |entity: EcsEntity, e_pos: &Pos| { - let chance = thread_rng().gen_bool(0.3); - - (can_sense_directly_near(e_pos) && chance) + self.can_sense_directly_near(e_pos) || self.can_see_entity(agent, controller, entity, e_pos, read_data) }; @@ -1306,9 +1306,6 @@ impl<'a> AgentData<'a> { .map_or(false, |stats| stats.name == *"Guard".to_string()); let follows_threatening_sounds = has_enemy_alignment || is_village_guard; - // TODO: Awareness currently doesn't influence anything. - //agent.awareness += 0.5 * sound.vol; - if sound_was_threatening && is_close { if !self.below_flee_health(agent) && follows_threatening_sounds { self.follow(agent, controller, &read_data.terrain, &sound_pos); @@ -1517,7 +1514,7 @@ impl<'a> AgentData<'a> { } } - fn can_see_entity( + pub fn can_see_entity( &self, agent: &Agent, controller: &Controller, @@ -1550,6 +1547,11 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data) } + pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool { + let chance = thread_rng().gen_bool(0.3); + e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) && chance + } + pub fn menacing( &self, agent: &mut Agent, diff --git a/server/agent/src/consts.rs b/server/agent/src/consts.rs index ac5722121f..e49f11af89 100644 --- a/server/agent/src/consts.rs +++ b/server/agent/src/consts.rs @@ -14,4 +14,4 @@ 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; +pub const STD_AWARENESS_DECAY_RATE: f32 = -0.05; diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index eb1cc42c03..f2035841cf 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -203,6 +203,7 @@ impl<'a> System<'a> for Sys { msm: &read_data.msm, poise: read_data.poises.get(entity), }; + /////////////////////////////////////////////////////////// // Behavior tree /////////////////////////////////////////////////////////// diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 1a507230e5..18a34b5372 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -2,7 +2,8 @@ use crate::rtsim::Entity as RtSimEntity; use common::{ comp::{ agent::{ - AgentEvent, Target, TimerAction, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, + AgentEvent, AwarenessState, Target, TimerAction, DEFAULT_INTERACTION_TIME, + TRADE_INTERACTION_TIME, }, Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, ControlAction, ControlEvent, Controller, InputKind, InventoryEvent, Pos, UtteranceKind, @@ -11,7 +12,6 @@ use common::{ path::TraversalConfig, }; use rand::{prelude::ThreadRng, thread_rng, Rng}; -use server_agent::consts::NORMAL_FLEE_DIR_DIST; use specs::{ saveload::{Marker, MarkerAllocator}, Entity as EcsEntity, @@ -27,7 +27,8 @@ use self::interaction::{ use super::{ consts::{ DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_FOLLOW_DIST, - NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS, + NORMAL_FLEE_DIR_DIST, NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS, + STD_AWARENESS_DECAY_RATE, }, data::{AgentData, ReadData, TargetData}, util::{get_entity_by_id, is_dead, is_dead_or_invulnerable, is_invulnerable, stop_pursuing}, @@ -100,7 +101,8 @@ impl BehaviorTree { Self { tree: vec![ untarget_if_dead, - do_hostile_tree_if_hostile, + update_target_awareness, + do_hostile_tree_if_hostile_and_aware, do_pet_tree_if_owned, do_pickup_loot, do_idle_tree, @@ -247,6 +249,8 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool { .push_event(ControlEvent::Utterance(UtteranceKind::Angry)); } + bdata.agent.awareness.change_by(1.0); + // Determine whether the new target should be a priority // over the old one (i.e: because it's either close or // because they attacked us). @@ -316,10 +320,13 @@ fn untarget_if_dead(bdata: &mut BehaviorData) -> bool { false } -/// If target is hostile, do the hostile tree and stop the current BehaviorTree -fn do_hostile_tree_if_hostile(bdata: &mut BehaviorData) -> bool { +/// If target is hostile and agent is aware of target, do the hostile tree and +/// stop the current BehaviorTree +fn do_hostile_tree_if_hostile_and_aware(bdata: &mut BehaviorData) -> bool { + let alert = bdata.agent.awareness.reached(); + if let Some(Target { hostile, .. }) = bdata.agent.target { - if hostile { + if alert && hostile { BehaviorTree::hostile().run(bdata); return true; } @@ -514,6 +521,41 @@ fn hurt_utterance(bdata: &mut BehaviorData) -> bool { false } +fn update_target_awareness(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + read_data, + controller, + .. + } = bdata; + + let target = agent.target.map(|t| t.target); + let tgt_pos = target.and_then(|t| read_data.positions.get(t)); + + if let (Some(target), Some(tgt_pos)) = (target, tgt_pos) { + if agent_data.can_see_entity(agent, controller, target, tgt_pos, read_data) { + agent.awareness.change_by(1.75 * read_data.dt.0); + } else if agent_data.can_sense_directly_near(tgt_pos) { + agent.awareness.change_by(0.25); + } else { + agent + .awareness + .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0); + } + } else { + agent + .awareness + .change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0); + } + + if bdata.agent.awareness.state() == AwarenessState::Unaware { + bdata.agent.target = None; + } + + false +} + fn do_combat(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent,