diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 30473b8c8f..72cd5bc641 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -9,11 +9,13 @@ use serde::Deserialize; use specs::{Component, Entity as EcsEntity}; use specs_idvs::IdvStorage; use std::{collections::VecDeque, fmt}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; use vek::*; use super::dialogue::Subject; -pub const DEFAULT_INTERACTION_TIME: f32 = 1.0; +pub const DEFAULT_INTERACTION_TIME: f32 = 3.0; pub const TRADE_INTERACTION_TIME: f32 = 300.0; #[derive(Copy, Clone, Debug, PartialEq, Deserialize)] @@ -340,6 +342,88 @@ pub struct Target { pub aggro_on: bool, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, EnumIter)] +pub enum TimerAction { + Interact, +} + +/// A time used for managing agent-related timeouts. The timer is designed to +/// keep track of the start of any number of previous actions. However, +/// starting/progressing an action will end previous actions. Therefore, the +/// timer should be used for actions that are mutually-exclusive. +#[derive(Clone, Debug)] +pub struct Timer { + action_starts: Vec>, + last_action: Option, +} + +impl Default for Timer { + fn default() -> Self { + Self { + action_starts: TimerAction::iter().map(|_| None).collect(), + last_action: None, + } + } +} + +impl Timer { + fn idx_for(action: TimerAction) -> usize { + TimerAction::iter() + .enumerate() + .find(|(_, a)| a == &action) + .unwrap() + .0 // Can't fail, EnumIter is exhaustive + } + + /// Reset the timer for the given action, returning true if the timer was + /// not already reset. + pub fn reset(&mut self, action: TimerAction) -> bool { + std::mem::replace(&mut self.action_starts[Self::idx_for(action)], None).is_some() + } + + /// Start the timer for the given action, even if it was already started. + pub fn start(&mut self, time: f64, action: TimerAction) { + self.action_starts[Self::idx_for(action)] = Some(time); + self.last_action = Some(action); + } + + /// Continue timing the given action, starting it if it was not already + /// started. + pub fn progress(&mut self, time: f64, action: TimerAction) { + if self.last_action != Some(action) { + self.start(time, action); + } + } + + /// Return the time that the given action was last performed at. + pub fn time_of_last(&self, action: TimerAction) -> Option { + self.action_starts[Self::idx_for(action)] + } + + /// Return `true` if the time since the action was last started exceeds the + /// given timeout. + pub fn time_since_exceeds(&self, time: f64, action: TimerAction, timeout: f64) -> bool { + self.time_of_last(action) + .map_or(true, |last_time| (time - last_time).max(0.0) > timeout) + } + + /// Return `true` while the time since the action was last started is less + /// than the given period. Once the time has elapsed, reset the timer. + pub fn timeout_elapsed( + &mut self, + time: f64, + action: TimerAction, + timeout: f64, + ) -> Option { + if self.time_since_exceeds(time, action, timeout) { + Some(self.reset(action)) + } else { + self.progress(time, action); + None + } + } +} + #[allow(clippy::type_complexity)] #[derive(Clone, Debug)] pub struct Agent { @@ -351,6 +435,7 @@ pub struct Agent { pub psyche: Psyche, pub inbox: VecDeque, pub action_state: ActionState, + pub timer: Timer, pub bearing: Vec2, pub sounds_heard: Vec, pub awareness: f32, @@ -376,6 +461,7 @@ impl Agent { psyche: Psyche::from(body), inbox: VecDeque::new(), action_state: ActionState::default(), + timer: Timer::default(), bearing: Vec2::zero(), sounds_heard: Vec::new(), awareness: 0.0, diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 64a8a0600f..5e27677d9a 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -3,7 +3,8 @@ use common::{ comp::{ self, agent::{ - AgentEvent, Sound, SoundKind, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, + AgentEvent, Sound, SoundKind, Target, TimerAction, DEFAULT_INTERACTION_TIME, + TRADE_INTERACTION_TIME, }, buff::{BuffKind, Buffs}, compass::{Direction, Distance}, @@ -383,7 +384,6 @@ impl<'a> System<'a> for Sys { |agent: &mut Agent, controller: &mut Controller, event_emitter: &mut Emitter| { - agent.target = None; data.idle_tree(agent, controller, &read_data, event_emitter); }; @@ -608,27 +608,43 @@ impl<'a> AgentData<'a> { agent.action_state.timer = 0.1; } } - if agent.action_state.timer > 0.0 { - if agent.action_state.timer - < (if agent.behavior.is(BehaviorState::TRADING) { - TRADE_INTERACTION_TIME - } else { - DEFAULT_INTERACTION_TIME - }) - { - self.interact(agent, controller, read_data, event_emitter); - } else { - agent.action_state.timer = 0.0; - agent.target = None; - controller.actions.push(ControlAction::Stand); - self.idle(agent, controller, read_data); - } - } else if thread_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); + + // If we receive a new interaction, start the interaction timer + if can_speak(agent) && self.recv_interaction(agent, controller, read_data, event_emitter) { + agent.timer.start(read_data.time.0, TimerAction::Interact); + } + + let timeout = if agent.behavior.is(BehaviorState::TRADING) { + TRADE_INTERACTION_TIME } else { - self.idle(agent, controller, read_data); + DEFAULT_INTERACTION_TIME + }; + + match agent + .timer + .timeout_elapsed(read_data.time.0, TimerAction::Interact, timeout as f64) + { + None => { + // Look toward the interacting entity for a while + if let Some(Target { target, .. }) = &agent.target { + self.look_toward(controller, read_data, *target); + controller.actions.push(ControlAction::Talk); + } + }, + Some(just_ended) => { + if just_ended { + agent.target = None; + controller.actions.push(ControlAction::Stand); + } + + if thread_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); + } else { + self.idle(agent, controller, read_data); + } + }, } } @@ -967,13 +983,13 @@ impl<'a> AgentData<'a> { } } - fn interact( + fn recv_interaction( &self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData, event_emitter: &mut Emitter<'_, ServerEvent>, - ) { + ) -> bool { // TODO: Process group invites // TODO: Add Group AgentEvent // let accept = false; // set back to "matches!(alignment, Alignment::Npc)" @@ -1302,18 +1318,11 @@ impl<'a> AgentData<'a> { } } }, - _ => { - if can_speak(agent) { - // 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); - } else { - agent.action_state.timer = 0.0; - } - } - }, + Some(AgentEvent::ServerSound(_)) => {}, + Some(AgentEvent::Hurt) => {}, + None => return false, } + true } fn look_toward(