Added action timer system for agent code

This commit is contained in:
Joshua Barretto 2021-08-04 14:04:56 +01:00
parent 33e9bc79e3
commit 31a49d26a9
2 changed files with 131 additions and 36 deletions

View File

@ -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<Option<f64>>,
last_action: Option<TimerAction>,
}
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<f64> {
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<bool> {
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<AgentEvent>,
pub action_state: ActionState,
pub timer: Timer,
pub bearing: Vec2<f32>,
pub sounds_heard: Vec<Sound>,
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,

View File

@ -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<ServerEvent>| {
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::<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);
// 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::<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);
}
},
}
}
@ -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(