From 6fae4efd453ca593bc5e805fdc925cd0775dfa69 Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Thu, 28 Jul 2022 23:31:44 +0200 Subject: [PATCH 01/10] PoC: Refactor agent's behavior tree --- server/src/sys/agent.rs | 235 +++++----- server/src/sys/agent/behavior_tree.rs | 632 ++++++++++++++++++++++++++ 2 files changed, 742 insertions(+), 125 deletions(-) create mode 100644 server/src/sys/agent/behavior_tree.rs diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 652ee01d3a..8abd5df579 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -1,4 +1,5 @@ pub mod attack; +pub mod behavior; pub mod consts; pub mod data; pub mod util; @@ -6,6 +7,7 @@ pub mod util; use crate::{ rtsim::{entity::PersonalityTrait, RtSim}, sys::agent::{ + behavior::BehaviorTree, consts::{ AVG_FOLLOW_DIST, DAMAGE_MEMORY_DURATION, DEFAULT_ATTACK_RANGE, FLEE_DURATION, HEALING_ITEM_THRESHOLD, IDLE_HEALING_ITEM_THRESHOLD, MAX_FLEE_DIST, MAX_FOLLOW_DIST, @@ -259,137 +261,120 @@ impl<'a> System<'a> for Sys { // Falling damage starts from 30.0 as of time of writing // But keep in mind our 25 m/s gravity - let is_falling_dangerous = data.vel.0.z < -20.0; - let is_on_fire = read_data - .buffs - .get(entity) - .map_or(false, |b| b.kinds.contains_key(&BuffKind::Burning)); + BehaviorTree::root().run( + agent, + data, + &read_data, + &mut event_emitter, + controller, + ); - // If falling velocity is critical, throw everything - // and save yourself! + // let is_falling_dangerous = data.vel.0.z < -20.0; + + // let is_on_fire = read_data + // .buffs + // .get(entity) + // .map_or(false, |b| b.kinds.contains_key(&BuffKind::Burning)); + + // // If falling velocity is critical, throw everything + // // and save yourself! + // // + // // If can fly - fly. + // // If have glider - glide. + // // Else, rest in peace. + // if is_falling_dangerous && data.traversal_config.can_fly { + // data.fly_upward(controller) + // } else if is_falling_dangerous && data.glider_equipped { + // data.glider_fall(controller); + // // If on fire and able, stop, drop, and roll + // } else if is_on_fire + // && data.body.map_or(false, |b| b.is_humanoid()) + // && data.physics_state.on_ground.is_some() + // && rng.gen_bool((2.0 * read_data.dt.0).clamp(0.0, 1.0) as f64) + // { + // controller.inputs.move_dir = ori + // .look_vec() + // .xy() + // .try_normalized() + // .unwrap_or_else(Vec2::zero); + // controller.push_basic_input(InputKind::Roll); + // } else { + // // Target an entity that's attacking us if the attack was recent and we + // have // a health component + // match health { + // Some(health) + // if read_data.time.0 - health.last_change.time.0 + // < DAMAGE_MEMORY_DURATION => + // { + // if let Some(by) = health.last_change.damage_by() { + // if let Some(attacker) = // - // If can fly - fly. - // If have glider - glide. - // Else, rest in peace. - if is_falling_dangerous && data.traversal_config.can_fly { - data.fly_upward(controller) - } else if is_falling_dangerous && data.glider_equipped { - data.glider_fall(controller); - // If on fire and able, stop, drop, and roll - } else if is_on_fire - && data.body.map_or(false, |b| b.is_humanoid()) - && data.physics_state.on_ground.is_some() - && rng.gen_bool((2.0 * read_data.dt.0).clamp(0.0, 1.0) as f64) - { - controller.inputs.move_dir = ori - .look_vec() - .xy() - .try_normalized() - .unwrap_or_else(Vec2::zero); - controller.push_basic_input(InputKind::Roll); - } else { - // Target an entity that's attacking us if the attack was recent and we have - // a health component - match health { - Some(health) - if read_data.time.0 - health.last_change.time.0 - < DAMAGE_MEMORY_DURATION => - { - if let Some(by) = health.last_change.damage_by() { - if let Some(attacker) = - read_data.uid_allocator.retrieve_entity_internal(by.uid().0) - { - // If target is dead or invulnerable (for now, this only - // means safezone), untarget them and idle. - if is_dead_or_invulnerable(attacker, &read_data) { - agent.target = None; - } else { - if agent.target.is_none() { - controller.push_event(ControlEvent::Utterance( - UtteranceKind::Angry, - )); - } + // read_data.uid_allocator.retrieve_entity_internal(by.uid().0) + // { + // // If target is dead or invulnerable (for now, this only + // // means safezone), untarget them and idle. + // if is_dead_or_invulnerable(attacker, &read_data) { + // agent.target = None; + // } else { + // if agent.target.is_none() { + // controller.push_event(ControlEvent::Utterance( + // UtteranceKind::Angry, + // )); + // } - // 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). - if agent.target.map_or(true, |target| { - data.is_more_dangerous_than_target( - attacker, target, &read_data, - ) - }) { - agent.target = Some(Target { - target: attacker, - hostile: true, - selected_at: read_data.time.0, - aggro_on: true, - }); - } + // // 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). if + // agent.target.map_or(true, |target| { + // data.is_more_dangerous_than_target( + // attacker, target, &read_data, + // ) }) { + // agent.target = Some(Target { + // target: attacker, + // hostile: true, + // selected_at: read_data.time.0, + // aggro_on: true, + // }); + // } - // Remember this attack if we're an RtSim entity - if let Some(attacker_stats) = - data.rtsim_entity.and(read_data.stats.get(attacker)) - { - agent.add_fight_to_memory( - &attacker_stats.name, - read_data.time.0, - ); - } - } - } - } - }, - _ => {}, - } + // // Remember this attack if we're an RtSim entity + // if let Some(attacker_stats) = + // + // data.rtsim_entity.and(read_data.stats.get(attacker)) + // { + // agent.add_fight_to_memory( + // &attacker_stats.name, + // read_data.time.0, + // ); + // } + // } + // } + // } + // }, + // _ => {}, + // } - if let Some(target_info) = agent.target { - data.react_to_target( - agent, - controller, - &read_data, - &mut event_emitter, - target_info, - &mut rng, - ); - } else { - data.idle_tree(agent, controller, &read_data, &mut rng); - } - if agent.allowed_to_speak() - && data.recv_interaction( - agent, - controller, - &read_data, - &mut event_emitter, - ) - { - agent.timer.start(read_data.time.0, TimerAction::Interact); - } - // Interact if incoming messages - if !agent.inbox.is_empty() { - if matches!( - agent.inbox.front(), - Some(AgentEvent::ServerSound(_)) | Some(AgentEvent::Hurt) - ) { - let sound = agent.inbox.pop_front(); - match sound { - Some(AgentEvent::ServerSound(sound)) => { - agent.sounds_heard.push(sound); - }, - Some(AgentEvent::Hurt) => { - // Hurt utterances at random upon receiving damage - if rng.gen::() < 0.4 { - controller.push_utterance(UtteranceKind::Hurt); - } - }, - //Note: this should be unreachable - Some(_) | None => return, - } - } else { - agent.action_state.timer = 0.1; - } - } - } + // if let Some(target_info) = agent.target { + // data.react_to_target( + // agent, + // controller, + // &read_data, + // &mut event_emitter, + // target_info, + // &mut rng, + // ); + // } else { + // data.idle_tree( + // agent, + // controller, + // &read_data, + // &mut event_emitter, + // &mut rng, + // ); + // } + // } debug_assert!(controller.inputs.move_dir.map(|e| !e.is_nan()).reduce_and()); debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs new file mode 100644 index 0000000000..d4c778cfe8 --- /dev/null +++ b/server/src/sys/agent/behavior_tree.rs @@ -0,0 +1,632 @@ +use common::{ + comp::{ + agent::{ + AgentEvent, Target, TimerAction, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, + }, + Agent, Alignment, BehaviorState, Body, BuffKind, ControlAction, ControlEvent, Controller, + InputKind, InventoryEvent, UtteranceKind, + }, + event::{Emitter, ServerEvent}, + path::TraversalConfig, +}; +use common_base::prof_span; +use rand::{prelude::ThreadRng, thread_rng, Rng}; +use specs::saveload::{Marker, MarkerAllocator}; +use vek::Vec2; + +use super::{ + consts::{ + DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_FLEE_DIST, + MAX_FOLLOW_DIST, NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS, + }, + data::{AgentData, ReadData, TargetData}, + util::{get_entity_by_id, is_dead, is_dead_or_invulnerable, is_invulnerable, stop_pursuing}, +}; + +/// Struct containing essential data for running a behavior tree +struct BehaviorData<'a, 'b, 'c> { + agent: &'a mut Agent, + agent_data: AgentData<'a>, + read_data: &'a ReadData<'a>, + event_emitter: &'a mut Emitter<'c, ServerEvent>, + controller: &'a mut Controller, + rng: &'b mut ThreadRng, +} + +/// Behavior function +/// Determines if the current situation can be handled and act accordingly +/// Returns true if an action has been taken, stopping the tree execution +type BehaviorFn = fn(&mut BehaviorData) -> bool; + +/// ~~list~~ ""tree"" of behavior functions +/// This struct will allow you to run through multiple behavior function until +/// one finally handles an event +pub struct BehaviorTree { + tree: Vec, +} + +impl BehaviorTree { + pub fn root() -> Self { + Self { + tree: vec![ + react_on_dangerous_fall, + react_if_on_fire, + target_if_attacked, + do_target_tree_if_target, + do_idle_tree, + ], + } + } + + pub fn target() -> Self { + Self { + tree: vec![ + untarget_if_dead, + do_hostile_tree_if_hostile, + do_pet_tree_if_owned, + do_pickup_loot, + untarget, + do_idle_tree, + ], + } + } + + pub fn pet() -> Self { + Self { + tree: vec![follow_if_far_away, attack_if_owner_hurt, do_idle_tree], + } + } + + pub fn hostile() -> Self { + Self { + tree: vec![heal_self_if_hurt, hurt_utterance, do_combat], + } + } + + pub fn idle() -> Self { + Self { + tree: vec![ + set_owner_if_no_target, + process_inbox_sound_and_hurt, + process_inbox_interaction, + handle_timer, + ], + } + } + + /// Run the behavior tree until an event has been handled + pub fn run<'a, 'b>( + &self, + agent: &'a mut Agent, + agent_data: AgentData<'a>, + read_data: &'a ReadData, + event_emitter: &'a mut Emitter<'b, ServerEvent>, + controller: &'a mut Controller, + ) -> bool { + let mut behavior_data = BehaviorData { + agent, + agent_data, + read_data, + event_emitter, + controller, + rng: &mut thread_rng(), + }; + + self.run_with_behavior_data(&mut behavior_data) + } + + fn run_with_behavior_data(&self, bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "Behavior Tree"); + for behavior_fn in self.tree.iter() { + if behavior_fn(bdata) { + return true; + } + } + false + } +} + +/// If falling velocity is critical, throw everything +/// and save yourself! +/// +/// If can fly - fly. +/// If have glider - glide. +/// Else, rest in peace. +fn react_on_dangerous_fall(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT react_on_dangerous_fall"); + // Falling damage starts from 30.0 as of time of writing + // But keep in mind our 25 m/s gravity + let is_falling_dangerous = bdata.agent_data.vel.0.z < -20.0; + + if is_falling_dangerous && bdata.agent_data.traversal_config.can_fly { + bdata.agent_data.fly_upward(bdata.controller); + return true; + } else if is_falling_dangerous && bdata.agent_data.glider_equipped { + bdata.agent_data.glider_fall(bdata.controller); + return true; + } + false +} + +/// If on fire and able, stop, drop, and roll +fn react_if_on_fire(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT react_if_on_fire"); + + let is_on_fire = bdata + .read_data + .buffs + .get(*bdata.agent_data.entity) + .map_or(false, |b| b.kinds.contains_key(&BuffKind::Burning)); + + if is_on_fire + && bdata.agent_data.body.map_or(false, |b| b.is_humanoid()) + && bdata.agent_data.physics_state.on_ground.is_some() + && bdata + .rng + .gen_bool((2.0 * bdata.read_data.dt.0).clamp(0.0, 1.0) as f64) + { + bdata.controller.inputs.move_dir = bdata + .agent_data + .ori + .look_vec() + .xy() + .try_normalized() + .unwrap_or_else(Vec2::zero); + bdata.controller.push_basic_input(InputKind::Roll); + return true; + } + false +} + +/// Target an entity that's attacking us if the attack was recent and we have +/// a health component +fn target_if_attacked(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT target_if_attacked"); + match bdata.agent_data.health { + Some(health) + if bdata.read_data.time.0 - health.last_change.time.0 < DAMAGE_MEMORY_DURATION => + { + if let Some(by) = health.last_change.damage_by() { + if let Some(attacker) = bdata + .read_data + .uid_allocator + .retrieve_entity_internal(by.uid().0) + { + // If target is dead or invulnerable (for now, this only + // means safezone), untarget them and idle. + if is_dead_or_invulnerable(attacker, bdata.read_data) { + bdata.agent.target = None; + } else { + if bdata.agent.target.is_none() { + bdata + .controller + .push_event(ControlEvent::Utterance(UtteranceKind::Angry)); + } + + // 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). + if bdata.agent.target.map_or(true, |target| { + bdata.agent_data.is_more_dangerous_than_target( + attacker, + target, + bdata.read_data, + ) + }) { + bdata.agent.target = Some(Target { + target: attacker, + hostile: true, + selected_at: bdata.read_data.time.0, + aggro_on: true, + }); + } + + // Remember this attack if we're an RtSim entity + if let Some(attacker_stats) = bdata + .agent_data + .rtsim_entity + .and(bdata.read_data.stats.get(attacker)) + { + bdata + .agent + .add_fight_to_memory(&attacker_stats.name, bdata.read_data.time.0); + } + } + } + } + }, + _ => {}, + } + false +} + +fn do_target_tree_if_target(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT do_target_tree_if_target"); + if bdata.agent.target.is_some() { + BehaviorTree::target().run_with_behavior_data(bdata); + return true; + } + false +} + +fn do_idle_tree(bdata: &mut BehaviorData) -> bool { + BehaviorTree::idle().run_with_behavior_data(bdata); + true +} + +/// If target is dead, forget them +fn untarget_if_dead(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT untarget_if_dead"); + if let Some(Target { target, .. }) = bdata.agent.target { + if let Some(tgt_health) = bdata.read_data.healths.get(target) { + // If target is dead, forget them + if tgt_health.is_dead { + if let Some(tgt_stats) = bdata + .agent_data + .rtsim_entity + .and(bdata.read_data.stats.get(target)) + { + bdata.agent.forget_enemy(&tgt_stats.name); + } + bdata.agent.target = None; + return true; + } + } + } + false +} + +/// If target is hostile, hostile tree +fn do_hostile_tree_if_hostile(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT do_hostile_tree_if_hostile"); + if let Some(Target { hostile, .. }) = bdata.agent.target { + if hostile { + BehaviorTree::hostile().run_with_behavior_data(bdata); + return true; + } + } + false +} + +/// if owned, act as pet to them +fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT do_pet_tree_if_owned"); + if let (Some(Target { target, .. }), Some(Alignment::Owned(uid))) = + (bdata.agent.target, bdata.agent_data.alignment) + { + if bdata.read_data.uids.get(target) == Some(uid) { + BehaviorTree::pet().run_with_behavior_data(bdata); + } else { + bdata.agent.target = None; + BehaviorTree::idle().run_with_behavior_data(bdata); + } + return true; + } + false +} + +fn do_pickup_loot(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT do_pickup_loot"); + if let Some(Target { target, .. }) = bdata.agent.target { + if matches!(bdata.read_data.bodies.get(target), Some(Body::ItemDrop(_))) { + if let Some(tgt_pos) = bdata.read_data.positions.get(target) { + let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0); + if dist_sqrd < NPC_PICKUP_RANGE.powi(2) { + if let Some(uid) = bdata.read_data.uids.get(target) { + bdata + .controller + .push_event(ControlEvent::InventoryEvent(InventoryEvent::Pickup(*uid))); + } + } else if let Some((bearing, speed)) = bdata.agent.chaser.chase( + &*bdata.read_data.terrain, + bdata.agent_data.pos.0, + bdata.agent_data.vel.0, + tgt_pos.0, + TraversalConfig { + min_tgt_dist: NPC_PICKUP_RANGE - 1.0, + ..bdata.agent_data.traversal_config + }, + ) { + bdata.controller.inputs.move_dir = + bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) + * speed.min(0.2 + (dist_sqrd - (NPC_PICKUP_RANGE - 1.5).powi(2)) / 8.0); + bdata.agent_data.jump_if(bearing.z > 1.5, bdata.controller); + bdata.controller.inputs.move_z = bearing.z; + } + } + return true; + } + } + false +} + +fn untarget(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT untarget"); + bdata.agent.target = None; + false +} + +// If too far away, then follow +fn follow_if_far_away(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT follow_if_far_away"); + if let Some(Target { target, .. }) = bdata.agent.target { + if let Some(tgt_pos) = bdata.read_data.positions.get(target) { + let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0); + + if dist_sqrd > (MAX_FOLLOW_DIST).powi(2) { + bdata.agent_data.follow( + bdata.agent, + bdata.controller, + &bdata.read_data.terrain, + tgt_pos, + ); + return true; + } + } + } + false +} + +/// Attack target's attacker (if there is one) +/// Target is the owner in this case +fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT attack_if_owner_hurt"); + if let Some(Target { target, .. }) = bdata.agent.target { + if bdata.read_data.positions.get(target).is_some() { + let owner_recently_attacked = + if let Some(target_health) = bdata.read_data.healths.get(target) { + bdata.read_data.time.0 - target_health.last_change.time.0 < 5.0 + && target_health.last_change.amount < 0.0 + } else { + false + }; + + if owner_recently_attacked { + bdata.agent_data.attack_target_attacker( + bdata.agent, + bdata.read_data, + bdata.controller, + bdata.rng, + ); + return true; + } + } + } + false +} + +/// Set owner if no target +fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT set_owner_if_no_target"); + let small_chance = bdata.rng.gen_bool(0.1); + + if bdata.agent.target.is_none() && small_chance { + if let Some(Alignment::Owned(owner)) = bdata.agent_data.alignment { + if let Some(owner) = get_entity_by_id(owner.id(), bdata.read_data) { + bdata.agent.target = Some(Target::new(owner, false, bdata.read_data.time.0, false)); + } + } + } + false +} + +/// Interact if incoming messages +fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT process_inbox_sound_and_hurt"); + if !bdata.agent.inbox.is_empty() { + if matches!( + bdata.agent.inbox.front(), + Some(AgentEvent::ServerSound(_)) | Some(AgentEvent::Hurt) + ) { + let sound = bdata.agent.inbox.pop_front(); + match sound { + Some(AgentEvent::ServerSound(sound)) => { + bdata.agent.sounds_heard.push(sound); + }, + Some(AgentEvent::Hurt) => { + // Hurt utterances at random upon receiving damage + if bdata.rng.gen::() < 0.4 { + bdata.controller.push_utterance(UtteranceKind::Hurt); + } + }, + //Note: this should be unreachable + Some(_) | None => {}, + } + } else { + bdata.agent.action_state.timer = 0.1; + } + } + false +} + +/// If we receive a new interaction, start the interaction timer +fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT process_inbox_interaction"); + if bdata.agent.allowed_to_speak() + && bdata.agent_data.recv_interaction( + bdata.agent, + bdata.controller, + bdata.read_data, + bdata.event_emitter, + ) + { + bdata + .agent + .timer + .start(bdata.read_data.time.0, TimerAction::Interact); + } + false +} + +fn handle_timer(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT handle_timer"); + let timeout = if bdata.agent.behavior.is(BehaviorState::TRADING) { + TRADE_INTERACTION_TIME + } else { + DEFAULT_INTERACTION_TIME + }; + + match bdata.agent.timer.timeout_elapsed( + bdata.read_data.time.0, + TimerAction::Interact, + timeout as f64, + ) { + None => { + // Look toward the interacting entity for a while + if let Some(Target { target, .. }) = &bdata.agent.target { + bdata + .agent_data + .look_toward(bdata.controller, bdata.read_data, *target); + bdata.controller.push_action(ControlAction::Talk); + } + }, + Some(just_ended) => { + if just_ended { + bdata.agent.target = None; + bdata.controller.push_action(ControlAction::Stand); + } + + if bdata.rng.gen::() < 0.1 { + bdata + .agent_data + .choose_target(bdata.agent, bdata.controller, bdata.read_data); + } else { + bdata.agent_data.handle_sounds_heard( + bdata.agent, + bdata.controller, + bdata.read_data, + bdata.rng, + ); + } + }, + } + false +} + +fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT heal_self_if_hurt"); + if bdata.agent_data.damage < HEALING_ITEM_THRESHOLD + && bdata + .agent_data + .heal_self(bdata.agent, bdata.controller, false) + { + bdata.agent.action_state.timer = 0.01; + return true; + } + false +} + +fn hurt_utterance(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT hurt_utterance"); + if let Some(AgentEvent::Hurt) = bdata.agent.inbox.pop_front() { + // Hurt utterances at random upon receiving damage + if bdata.rng.gen::() < 0.4 { + bdata.controller.push_utterance(UtteranceKind::Hurt); + } + } + false +} + +fn do_combat(bdata: &mut BehaviorData) -> bool { + prof_span!(guard, "BT do_combat"); + let BehaviorData { + agent, + agent_data, + read_data, + event_emitter, + controller, + rng, + } = bdata; + if let Some(Target { + target, + selected_at, + aggro_on, + .. + }) = &mut agent.target + { + let target = *target; + let selected_at = *selected_at; + if let Some(tgt_pos) = read_data.positions.get(target) { + let dist_sqrd = agent_data.pos.0.distance_squared(tgt_pos.0); + let origin_dist_sqrd = match agent.patrol_origin { + Some(pos) => pos.distance_squared(agent_data.pos.0), + None => 1.0, + }; + + let own_health_fraction = match agent_data.health { + Some(val) => val.fraction(), + None => 1.0, + }; + let target_health_fraction = match read_data.healths.get(target) { + Some(val) => val.fraction(), + None => 1.0, + }; + + let in_aggro_range = agent + .psyche + .aggro_dist + .map_or(true, |ad| dist_sqrd < ad.powi(2)); + + if in_aggro_range { + *aggro_on = true; + } + let aggro_on = *aggro_on; + + if agent_data.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); + + // FIXME: Using action state timer to see if allowed to speak is a hack. + if agent.action_state.timer == 0.0 { + agent_data.cry_out(agent, event_emitter, read_data); + agent.action_state.timer = 0.01; + } else if within_flee_distance && has_opportunity_to_flee { + agent_data.flee(agent, controller, tgt_pos, &read_data.terrain); + agent.action_state.timer += read_data.dt.0; + } else { + agent.action_state.timer = 0.0; + agent.target = None; + agent_data.idle(agent, controller, read_data, rng); + } + } else if is_dead(target, read_data) { + agent_data.exclaim_relief_about_enemy_dead(agent, event_emitter); + agent.target = None; + agent_data.idle(agent, controller, read_data, rng); + } else if is_invulnerable(target, read_data) + || stop_pursuing( + dist_sqrd, + origin_dist_sqrd, + own_health_fraction, + target_health_fraction, + read_data.time.0 - selected_at, + &agent.psyche, + ) + { + agent.target = None; + agent_data.idle(agent, controller, read_data, rng); + } else { + let is_time_to_retarget = + read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS; + + if !in_aggro_range && is_time_to_retarget { + agent_data.choose_target(agent, controller, read_data); + } + + if aggro_on { + let target_data = TargetData::new( + tgt_pos, + read_data.bodies.get(target), + read_data.scales.get(target), + ); + let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone()); + + tgt_name.map(|tgt_name| agent.add_fight_to_memory(&tgt_name, read_data.time.0)); + agent_data.attack(agent, controller, &target_data, read_data, rng); + } else { + agent_data.menacing(agent, controller, target, read_data, event_emitter, rng); + } + } + } + } + false +} From d105d7063c7d9bdb72bc8476cda3aead2f2d6d09 Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Fri, 29 Jul 2022 12:13:00 +0200 Subject: [PATCH 02/10] Cleanup prof_span + finish unfinished module rename --- server/src/sys/agent.rs | 4 ++-- server/src/sys/agent/behavior_tree.rs | 21 --------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 8abd5df579..6db65d7274 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -1,5 +1,5 @@ pub mod attack; -pub mod behavior; +pub mod behavior_tree; pub mod consts; pub mod data; pub mod util; @@ -7,7 +7,7 @@ pub mod util; use crate::{ rtsim::{entity::PersonalityTrait, RtSim}, sys::agent::{ - behavior::BehaviorTree, + behavior_tree::BehaviorTree, consts::{ AVG_FOLLOW_DIST, DAMAGE_MEMORY_DURATION, DEFAULT_ATTACK_RANGE, FLEE_DURATION, HEALING_ITEM_THRESHOLD, IDLE_HEALING_ITEM_THRESHOLD, MAX_FLEE_DIST, MAX_FOLLOW_DIST, diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index d4c778cfe8..303a9c7551 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -9,7 +9,6 @@ use common::{ event::{Emitter, ServerEvent}, path::TraversalConfig, }; -use common_base::prof_span; use rand::{prelude::ThreadRng, thread_rng, Rng}; use specs::saveload::{Marker, MarkerAllocator}; use vek::Vec2; @@ -116,7 +115,6 @@ impl BehaviorTree { } fn run_with_behavior_data(&self, bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "Behavior Tree"); for behavior_fn in self.tree.iter() { if behavior_fn(bdata) { return true; @@ -133,7 +131,6 @@ impl BehaviorTree { /// If have glider - glide. /// Else, rest in peace. fn react_on_dangerous_fall(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT react_on_dangerous_fall"); // Falling damage starts from 30.0 as of time of writing // But keep in mind our 25 m/s gravity let is_falling_dangerous = bdata.agent_data.vel.0.z < -20.0; @@ -150,8 +147,6 @@ fn react_on_dangerous_fall(bdata: &mut BehaviorData) -> bool { /// If on fire and able, stop, drop, and roll fn react_if_on_fire(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT react_if_on_fire"); - let is_on_fire = bdata .read_data .buffs @@ -181,7 +176,6 @@ fn react_if_on_fire(bdata: &mut BehaviorData) -> bool { /// Target an entity that's attacking us if the attack was recent and we have /// a health component fn target_if_attacked(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT target_if_attacked"); match bdata.agent_data.health { Some(health) if bdata.read_data.time.0 - health.last_change.time.0 < DAMAGE_MEMORY_DURATION => @@ -241,7 +235,6 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool { } fn do_target_tree_if_target(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT do_target_tree_if_target"); if bdata.agent.target.is_some() { BehaviorTree::target().run_with_behavior_data(bdata); return true; @@ -256,7 +249,6 @@ fn do_idle_tree(bdata: &mut BehaviorData) -> bool { /// If target is dead, forget them fn untarget_if_dead(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT untarget_if_dead"); if let Some(Target { target, .. }) = bdata.agent.target { if let Some(tgt_health) = bdata.read_data.healths.get(target) { // If target is dead, forget them @@ -278,7 +270,6 @@ fn untarget_if_dead(bdata: &mut BehaviorData) -> bool { /// If target is hostile, hostile tree fn do_hostile_tree_if_hostile(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT do_hostile_tree_if_hostile"); if let Some(Target { hostile, .. }) = bdata.agent.target { if hostile { BehaviorTree::hostile().run_with_behavior_data(bdata); @@ -290,7 +281,6 @@ fn do_hostile_tree_if_hostile(bdata: &mut BehaviorData) -> bool { /// if owned, act as pet to them fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT do_pet_tree_if_owned"); if let (Some(Target { target, .. }), Some(Alignment::Owned(uid))) = (bdata.agent.target, bdata.agent_data.alignment) { @@ -306,7 +296,6 @@ fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool { } fn do_pickup_loot(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT do_pickup_loot"); if let Some(Target { target, .. }) = bdata.agent.target { if matches!(bdata.read_data.bodies.get(target), Some(Body::ItemDrop(_))) { if let Some(tgt_pos) = bdata.read_data.positions.get(target) { @@ -341,14 +330,12 @@ fn do_pickup_loot(bdata: &mut BehaviorData) -> bool { } fn untarget(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT untarget"); bdata.agent.target = None; false } // If too far away, then follow fn follow_if_far_away(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT follow_if_far_away"); if let Some(Target { target, .. }) = bdata.agent.target { if let Some(tgt_pos) = bdata.read_data.positions.get(target) { let dist_sqrd = bdata.agent_data.pos.0.distance_squared(tgt_pos.0); @@ -370,7 +357,6 @@ fn follow_if_far_away(bdata: &mut BehaviorData) -> bool { /// Attack target's attacker (if there is one) /// Target is the owner in this case fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT attack_if_owner_hurt"); if let Some(Target { target, .. }) = bdata.agent.target { if bdata.read_data.positions.get(target).is_some() { let owner_recently_attacked = @@ -397,7 +383,6 @@ fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool { /// Set owner if no target fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT set_owner_if_no_target"); let small_chance = bdata.rng.gen_bool(0.1); if bdata.agent.target.is_none() && small_chance { @@ -412,7 +397,6 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool { /// Interact if incoming messages fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT process_inbox_sound_and_hurt"); if !bdata.agent.inbox.is_empty() { if matches!( bdata.agent.inbox.front(), @@ -441,7 +425,6 @@ fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool { /// If we receive a new interaction, start the interaction timer fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT process_inbox_interaction"); if bdata.agent.allowed_to_speak() && bdata.agent_data.recv_interaction( bdata.agent, @@ -459,7 +442,6 @@ fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { } fn handle_timer(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT handle_timer"); let timeout = if bdata.agent.behavior.is(BehaviorState::TRADING) { TRADE_INTERACTION_TIME } else { @@ -504,7 +486,6 @@ fn handle_timer(bdata: &mut BehaviorData) -> bool { } fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT heal_self_if_hurt"); if bdata.agent_data.damage < HEALING_ITEM_THRESHOLD && bdata .agent_data @@ -517,7 +498,6 @@ fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool { } fn hurt_utterance(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT hurt_utterance"); if let Some(AgentEvent::Hurt) = bdata.agent.inbox.pop_front() { // Hurt utterances at random upon receiving damage if bdata.rng.gen::() < 0.4 { @@ -528,7 +508,6 @@ fn hurt_utterance(bdata: &mut BehaviorData) -> bool { } fn do_combat(bdata: &mut BehaviorData) -> bool { - prof_span!(guard, "BT do_combat"); let BehaviorData { agent, agent_data, From faf2b13ac105a00f428b1d6e7614a47fdfaa95aa Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Fri, 29 Jul 2022 18:33:34 +0200 Subject: [PATCH 03/10] Move recv_interactions into BehaviorTree + cleanup functions ^& warnings --- server/src/sys/agent.rs | 927 +------------------------- server/src/sys/agent/behavior_tree.rs | 495 +++++++++++++- 2 files changed, 497 insertions(+), 925 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 6db65d7274..e7271be2b3 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -5,20 +5,18 @@ pub mod data; pub mod util; use crate::{ - rtsim::{entity::PersonalityTrait, RtSim}, + rtsim::RtSim, sys::agent::{ behavior_tree::BehaviorTree, consts::{ - AVG_FOLLOW_DIST, DAMAGE_MEMORY_DURATION, DEFAULT_ATTACK_RANGE, FLEE_DURATION, - HEALING_ITEM_THRESHOLD, IDLE_HEALING_ITEM_THRESHOLD, MAX_FLEE_DIST, MAX_FOLLOW_DIST, - NPC_PICKUP_RANGE, PARTIAL_PATH_DIST, RETARGETING_THRESHOLD_SECONDS, SEPARATION_BIAS, - SEPARATION_DIST, + AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, PARTIAL_PATH_DIST, + SEPARATION_BIAS, SEPARATION_DIST, }, data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData}, util::{ aim_projectile, are_our_owners_hostile, entities_have_line_of_sight, get_attacker, - get_entity_by_id, is_dead, is_dead_or_invulnerable, is_dressed_as_cultist, - is_invulnerable, is_village_guard, is_villager, stop_pursuing, + get_entity_by_id, is_dead_or_invulnerable, is_dressed_as_cultist, is_invulnerable, + is_village_guard, is_villager, }, }, }; @@ -26,33 +24,25 @@ use common::{ combat::perception_dist_multiplier_from_stealth, comp::{ self, - agent::{ - AgentEvent, Sound, SoundKind, Target, TimerAction, DEFAULT_INTERACTION_TIME, - TRADE_INTERACTION_TIME, - }, + agent::{Sound, SoundKind, Target}, buff::BuffKind, - compass::{Direction, Distance}, - dialogue::{MoodContext, MoodState, Subject}, inventory::slot::EquipSlot, - invite::{InviteKind, InviteResponse}, item::{ tool::{AbilitySpec, ToolKind}, ConsumableKind, Item, ItemDesc, ItemKind, }, item_drop, projectile::ProjectileConstructor, - Agent, Alignment, BehaviorState, Body, CharacterState, ControlAction, ControlEvent, - Controller, Health, HealthChange, InputKind, InventoryAction, InventoryEvent, Pos, Scale, - UnresolvedChatMsg, UtteranceKind, + Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Health, + HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind, }, effect::{BuffEffect, Effect}, event::{Emitter, EventBus, ServerEvent}, path::TraversalConfig, - rtsim::{Memory, MemoryItem, RtSimEvent}, + rtsim::RtSimEvent, states::basic_beam, terrain::{Block, TerrainGrid}, time::DayPeriod, - trade::{TradeAction, TradePhase, TradeResult}, util::Dir, vol::ReadVol, }; @@ -61,8 +51,7 @@ use common_ecs::{Job, Origin, ParMode, Phase, System}; use rand::{thread_rng, Rng}; use rayon::iter::ParallelIterator; use specs::{ - saveload::{Marker, MarkerAllocator}, - Entity as EcsEntity, Join, ParJoin, Read, WriteExpect, WriteStorage, + saveload::Marker, Entity as EcsEntity, Join, ParJoin, Read, WriteExpect, WriteStorage, }; use vek::*; @@ -135,7 +124,7 @@ impl<'a> System<'a> for Sys { _, )| { let mut event_emitter = event_bus.emitter(); - let mut rng = thread_rng(); + // let mut rng = thread_rng(); // Hack, replace with better system when groups are more sophisticated // Override alignment if in a group unless entity is owned already @@ -259,9 +248,6 @@ impl<'a> System<'a> for Sys { // are the only parts of this tree that should provide // inputs. - // Falling damage starts from 30.0 as of time of writing - // But keep in mind our 25 m/s gravity - BehaviorTree::root().run( agent, data, @@ -270,112 +256,6 @@ impl<'a> System<'a> for Sys { controller, ); - // let is_falling_dangerous = data.vel.0.z < -20.0; - - // let is_on_fire = read_data - // .buffs - // .get(entity) - // .map_or(false, |b| b.kinds.contains_key(&BuffKind::Burning)); - - // // If falling velocity is critical, throw everything - // // and save yourself! - // // - // // If can fly - fly. - // // If have glider - glide. - // // Else, rest in peace. - // if is_falling_dangerous && data.traversal_config.can_fly { - // data.fly_upward(controller) - // } else if is_falling_dangerous && data.glider_equipped { - // data.glider_fall(controller); - // // If on fire and able, stop, drop, and roll - // } else if is_on_fire - // && data.body.map_or(false, |b| b.is_humanoid()) - // && data.physics_state.on_ground.is_some() - // && rng.gen_bool((2.0 * read_data.dt.0).clamp(0.0, 1.0) as f64) - // { - // controller.inputs.move_dir = ori - // .look_vec() - // .xy() - // .try_normalized() - // .unwrap_or_else(Vec2::zero); - // controller.push_basic_input(InputKind::Roll); - // } else { - // // Target an entity that's attacking us if the attack was recent and we - // have // a health component - // match health { - // Some(health) - // if read_data.time.0 - health.last_change.time.0 - // < DAMAGE_MEMORY_DURATION => - // { - // if let Some(by) = health.last_change.damage_by() { - // if let Some(attacker) = - // - // read_data.uid_allocator.retrieve_entity_internal(by.uid().0) - // { - // // If target is dead or invulnerable (for now, this only - // // means safezone), untarget them and idle. - // if is_dead_or_invulnerable(attacker, &read_data) { - // agent.target = None; - // } else { - // if agent.target.is_none() { - // controller.push_event(ControlEvent::Utterance( - // UtteranceKind::Angry, - // )); - // } - - // // 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). if - // agent.target.map_or(true, |target| { - // data.is_more_dangerous_than_target( - // attacker, target, &read_data, - // ) }) { - // agent.target = Some(Target { - // target: attacker, - // hostile: true, - // selected_at: read_data.time.0, - // aggro_on: true, - // }); - // } - - // // Remember this attack if we're an RtSim entity - // if let Some(attacker_stats) = - // - // data.rtsim_entity.and(read_data.stats.get(attacker)) - // { - // agent.add_fight_to_memory( - // &attacker_stats.name, - // read_data.time.0, - // ); - // } - // } - // } - // } - // }, - // _ => {}, - // } - - // if let Some(target_info) = agent.target { - // data.react_to_target( - // agent, - // controller, - // &read_data, - // &mut event_emitter, - // target_info, - // &mut rng, - // ); - // } else { - // data.idle_tree( - // agent, - // controller, - // &read_data, - // &mut event_emitter, - // &mut rng, - // ); - // } - // } - debug_assert!(controller.inputs.move_dir.map(|e| !e.is_nan()).reduce_and()); debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); }, @@ -402,275 +282,6 @@ impl<'a> System<'a> for Sys { } impl<'a> AgentData<'a> { - //////////////////////////////////////// - // Subtrees - //////////////////////////////////////// - fn react_to_target( - &self, - agent: &mut Agent, - controller: &mut Controller, - read_data: &ReadData, - event_emitter: &mut Emitter<'_, ServerEvent>, - target_info: Target, - rng: &mut impl Rng, - ) { - let Target { - target, hostile, .. - } = target_info; - - if let Some(tgt_health) = read_data.healths.get(target) { - // If target is dead, forget them - if tgt_health.is_dead { - if let Some(tgt_stats) = self.rtsim_entity.and(read_data.stats.get(target)) { - agent.forget_enemy(&tgt_stats.name); - } - agent.target = None; - // Else, if target is hostile, hostile tree - } else if hostile { - self.cancel_interaction(agent, controller, event_emitter); - self.hostile_tree(agent, controller, read_data, event_emitter, rng); - // Else, if owned, act as pet to them - } else if let Some(Alignment::Owned(uid)) = self.alignment { - if read_data.uids.get(target) == Some(uid) { - self.react_as_pet(agent, controller, read_data, target, rng); - } else { - agent.target = None; - self.idle_tree(agent, controller, read_data, rng); - }; - } else { - self.idle_tree(agent, controller, read_data, rng); - } - } else if matches!(read_data.bodies.get(target), Some(Body::ItemDrop(_))) { - if let Some(tgt_pos) = read_data.positions.get(target) { - let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0); - if dist_sqrd < NPC_PICKUP_RANGE.powi(2) { - if let Some(uid) = read_data.uids.get(target) { - controller - .push_event(ControlEvent::InventoryEvent(InventoryEvent::Pickup(*uid))); - } - } else if let Some((bearing, speed)) = agent.chaser.chase( - &*read_data.terrain, - self.pos.0, - self.vel.0, - tgt_pos.0, - TraversalConfig { - min_tgt_dist: NPC_PICKUP_RANGE - 1.0, - ..self.traversal_config - }, - ) { - controller.inputs.move_dir = - bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) - * speed.min(0.2 + (dist_sqrd - (NPC_PICKUP_RANGE - 1.5).powi(2)) / 8.0); - self.jump_if(bearing.z > 1.5, controller); - controller.inputs.move_z = bearing.z; - } - } - } else { - agent.target = None; - self.idle_tree(agent, controller, read_data, rng); - } - } - - fn react_as_pet( - &self, - agent: &mut Agent, - controller: &mut Controller, - read_data: &ReadData, - target: EcsEntity, - rng: &mut impl Rng, - ) { - if let Some(tgt_pos) = read_data.positions.get(target) { - let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0); - - let owner_recently_attacked = if let Some(target_health) = read_data.healths.get(target) - { - read_data.time.0 - target_health.last_change.time.0 < 5.0 - && target_health.last_change.amount < 0.0 - } else { - false - }; - - // If too far away, then follow - if dist_sqrd > (MAX_FOLLOW_DIST).powi(2) { - self.follow(agent, controller, &read_data.terrain, tgt_pos); - // Else, attack target's attacker (if there is one) - // Target is the owner in this case - } else if owner_recently_attacked { - self.attack_target_attacker(agent, read_data, controller, rng); - // Otherwise, just idle - } else { - self.idle_tree(agent, controller, read_data, rng); - } - } - } - - fn idle_tree( - &self, - agent: &mut Agent, - controller: &mut Controller, - read_data: &ReadData, - rng: &mut impl Rng, - ) { - // 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 - if agent.target.is_none() && small_chance { - if let Some(Alignment::Owned(owner)) = self.alignment { - if let Some(owner) = get_entity_by_id(owner.id(), read_data) { - agent.target = Some(Target::new(owner, false, read_data.time.0, false)); - } - } - } - - let timeout = if agent.behavior.is(BehaviorState::TRADING) { - TRADE_INTERACTION_TIME - } else { - 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.push_action(ControlAction::Talk); - } - }, - Some(just_ended) => { - if just_ended { - agent.target = None; - controller.push_action(ControlAction::Stand); - } - - if rng.gen::() < 0.1 { - self.choose_target(agent, controller, read_data); - } else { - self.handle_sounds_heard(agent, controller, read_data, rng); - } - }, - } - } - - fn hostile_tree( - &self, - agent: &mut Agent, - controller: &mut Controller, - read_data: &ReadData, - event_emitter: &mut Emitter<'_, ServerEvent>, - rng: &mut impl Rng, - ) { - if self.damage < HEALING_ITEM_THRESHOLD && self.heal_self(agent, controller, false) { - agent.action_state.timer = 0.01; - return; - } - - if matches!(agent.inbox.front(), Some(AgentEvent::Hurt)) { - // Hurt utterances at random upon receiving damage - if rng.gen::() < 0.4 { - controller.push_utterance(UtteranceKind::Hurt); - } - agent.inbox.pop_front(); - } - - if let Some(Target { - target, - selected_at, - aggro_on, - .. - }) = &mut agent.target - { - let target = *target; - let selected_at = *selected_at; - if let Some(tgt_pos) = read_data.positions.get(target) { - let dist_sqrd = self.pos.0.distance_squared(tgt_pos.0); - let origin_dist_sqrd = match agent.patrol_origin { - Some(pos) => pos.distance_squared(self.pos.0), - None => 1.0, - }; - - let own_health_fraction = match self.health { - Some(val) => val.fraction(), - None => 1.0, - }; - let target_health_fraction = match read_data.healths.get(target) { - Some(val) => val.fraction(), - None => 1.0, - }; - - let in_aggro_range = agent - .psyche - .aggro_dist - .map_or(true, |ad| dist_sqrd < ad.powi(2)); - - if in_aggro_range { - *aggro_on = true; - } - let aggro_on = *aggro_on; - - 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); - - // FIXME: Using action state timer to see if allowed to speak is a hack. - if agent.action_state.timer == 0.0 { - self.cry_out(agent, event_emitter, read_data); - agent.action_state.timer = 0.01; - } else if within_flee_distance && has_opportunity_to_flee { - self.flee(agent, controller, tgt_pos, &read_data.terrain); - agent.action_state.timer += read_data.dt.0; - } else { - agent.action_state.timer = 0.0; - agent.target = None; - self.idle(agent, controller, read_data, rng); - } - } else if is_dead(target, read_data) { - self.exclaim_relief_about_enemy_dead(agent, event_emitter); - agent.target = None; - self.idle(agent, controller, read_data, rng); - } else if is_invulnerable(target, read_data) - || stop_pursuing( - dist_sqrd, - origin_dist_sqrd, - own_health_fraction, - target_health_fraction, - read_data.time.0 - selected_at, - &agent.psyche, - ) - { - agent.target = None; - self.idle(agent, controller, read_data, rng); - } else { - let is_time_to_retarget = - read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS; - - if !in_aggro_range && is_time_to_retarget { - self.choose_target(agent, controller, read_data); - } - - if aggro_on { - let target_data = TargetData::new( - tgt_pos, - read_data.bodies.get(target), - read_data.scales.get(target), - ); - let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone()); - - tgt_name - .map(|tgt_name| agent.add_fight_to_memory(&tgt_name, read_data.time.0)); - self.attack(agent, controller, &target_data, read_data, rng); - } else { - self.menacing(agent, controller, target, read_data, event_emitter, rng); - } - } - } - } - } - //////////////////////////////////////// // Action Nodes //////////////////////////////////////// @@ -962,522 +573,6 @@ impl<'a> AgentData<'a> { } } - /// deny any interaction whenever possible - fn cancel_interaction( - &self, - agent: &mut Agent, - controller: &mut Controller, - event_emitter: &mut Emitter<'_, ServerEvent>, - ) -> bool { - if let Some(msg) = agent.inbox.front() { - let used = match msg { - AgentEvent::Talk(..) | AgentEvent::TradeAccepted(_) => { - self.chat_npc_if_allowed_to_speak( - "npc-speech-villager_busy", - agent, - event_emitter, - ); - true - }, - AgentEvent::TradeInvite(_) => { - controller.push_invite_response(InviteResponse::Decline); - if agent.behavior.can_trade() { - self.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_busy", - agent, - event_emitter, - ); - } else { - self.chat_npc_if_allowed_to_speak( - "npc-speech-villager_busy", - agent, - event_emitter, - ); - } - true - }, - AgentEvent::FinishedTrade(result) => { - // copy pasted from recv_interaction - // because the trade is not cancellable in this state - if agent.behavior.is(BehaviorState::TRADING) { - match result { - TradeResult::Completed => { - self.chat_npc( - "npc-speech-merchant_trade_successful", - event_emitter, - ); - }, - _ => { - self.chat_npc("npc-speech-merchant_trade_declined", event_emitter); - }, - } - agent.behavior.unset(BehaviorState::TRADING); - } - true - }, - AgentEvent::UpdatePendingTrade(boxval) => { - // immediately cancel the trade - let (tradeid, _pending, _prices, _inventories) = &**boxval; - agent.behavior.unset(BehaviorState::TRADING); - event_emitter.emit(ServerEvent::ProcessTradeAction( - *self.entity, - *tradeid, - TradeAction::Decline, - )); - self.chat_npc("npc-speech-merchant_trade_cancelled_hostile", event_emitter); - true - }, - AgentEvent::ServerSound(_) | AgentEvent::Hurt => false, - }; - if used { - agent.inbox.pop_front(); - } - return used; - } - false - } - - 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)" - // when we got better NPC recruitment mechanics if accept { - // // Clear agent comp - // //*agent = Agent::default(); - // controller - // .push_event(ControlEvent::InviteResponse(InviteResponse::Accept)); - // } else { - // controller - // .push_event(ControlEvent::InviteResponse(InviteResponse::Decline)); - // } - agent.action_state.timer += read_data.dt.0; - - let msg = agent.inbox.front(); - let used = match msg { - Some(AgentEvent::Talk(by, subject)) => { - if agent.allowed_to_speak() { - if let Some(target) = get_entity_by_id(by.id(), read_data) { - agent.target = Some(Target::new(target, false, read_data.time.0, false)); - - if self.look_toward(controller, read_data, target) { - controller.push_action(ControlAction::Stand); - controller.push_action(ControlAction::Talk); - controller.push_utterance(UtteranceKind::Greeting); - - match subject { - Subject::Regular => { - if let ( - Some((_travel_to, destination_name)), - Some(rtsim_entity), - ) = (&agent.rtsim_controller.travel_to, &self.rtsim_entity) - { - let personality = &rtsim_entity.brain.personality; - let standard_response_msg = || -> String { - if personality - .personality_traits - .contains(PersonalityTrait::Extroverted) - { - format!( - "I'm heading to {}! Want to come along?", - destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Hrm.".to_string() - } else { - "Hello!".to_string() - } - }; - let msg = - if let Some(tgt_stats) = read_data.stats.get(target) { - agent.rtsim_controller.events.push( - RtSimEvent::AddMemory(Memory { - item: MemoryItem::CharacterInteraction { - name: tgt_stats.name.clone(), - }, - time_to_forget: read_data.time.0 + 600.0, - }), - ); - if rtsim_entity - .brain - .remembers_character(&tgt_stats.name) - { - if personality - .personality_traits - .contains(PersonalityTrait::Extroverted) - { - format!( - "Greetings fair {}! It has been far \ - too long since last I saw you. I'm \ - going to {} right now.", - &tgt_stats.name, destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Oh. It's you again.".to_string() - } else { - format!( - "Hi again {}! Unfortunately I'm in a \ - hurry right now. See you!", - &tgt_stats.name - ) - } - } else { - standard_response_msg() - } - } else { - standard_response_msg() - }; - self.chat_npc(msg, event_emitter); - } else if agent.behavior.can_trade() { - if !agent.behavior.is(BehaviorState::TRADING) { - controller.push_initiate_invite(*by, InviteKind::Trade); - self.chat_npc( - "npc-speech-merchant_advertisement", - event_emitter, - ); - } else { - let default_msg = "npc-speech-merchant_busy"; - let msg = self.rtsim_entity.map_or(default_msg, |e| { - if e.brain - .personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "npc-speech-merchant_busy_rude" - } else { - default_msg - } - }); - self.chat_npc(msg, event_emitter); - } - } else { - let mut rng = thread_rng(); - if let Some(extreme_trait) = - self.rtsim_entity.and_then(|e| { - e.brain.personality.random_chat_trait(&mut rng) - }) - { - let msg = match extreme_trait { - PersonalityTrait::Open => { - "npc-speech-villager_open" - }, - PersonalityTrait::Adventurous => { - "npc-speech-villager_adventurous" - }, - PersonalityTrait::Closed => { - "npc-speech-villager_closed" - }, - PersonalityTrait::Conscientious => { - "npc-speech-villager_conscientious" - }, - PersonalityTrait::Busybody => { - "npc-speech-villager_busybody" - }, - PersonalityTrait::Unconscientious => { - "npc-speech-villager_unconscientious" - }, - PersonalityTrait::Extroverted => { - "npc-speech-villager_extroverted" - }, - PersonalityTrait::Introverted => { - "npc-speech-villager_introverted" - }, - PersonalityTrait::Agreeable => { - "npc-speech-villager_agreeable" - }, - PersonalityTrait::Sociable => { - "npc-speech-villager_sociable" - }, - PersonalityTrait::Disagreeable => { - "npc-speech-villager_disagreeable" - }, - PersonalityTrait::Neurotic => { - "npc-speech-villager_neurotic" - }, - PersonalityTrait::Seeker => { - "npc-speech-villager_seeker" - }, - PersonalityTrait::SadLoner => { - "npc-speech-villager_sad_loner" - }, - PersonalityTrait::Worried => { - "npc-speech-villager_worried" - }, - PersonalityTrait::Stable => { - "npc-speech-villager_stable" - }, - }; - self.chat_npc(msg, event_emitter); - } else { - self.chat_npc("npc-speech-villager", event_emitter); - } - } - }, - Subject::Trade => { - if agent.behavior.can_trade() { - if !agent.behavior.is(BehaviorState::TRADING) { - controller.push_initiate_invite(*by, InviteKind::Trade); - self.chat_npc( - "npc-speech-merchant_advertisement", - event_emitter, - ); - } else { - self.chat_npc( - "npc-speech-merchant_busy", - event_emitter, - ); - } - } else { - // TODO: maybe make some travellers willing to trade with - // simpler goods like potions - self.chat_npc( - "npc-speech-villager_decline_trade", - event_emitter, - ); - } - }, - Subject::Mood => { - if let Some(rtsim_entity) = self.rtsim_entity { - if !rtsim_entity.brain.remembers_mood() { - // TODO: the following code will need a rework to - // implement more mood contexts - // This require that town NPCs becomes rtsim_entities to - // work fully. - match rand::random::() % 3 { - 0 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Good( - MoodContext::GoodWeather, - ), - }, - time_to_forget: read_data.time.0 + 21200.0, - }), - ), - 1 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Neutral( - MoodContext::EverydayLife, - ), - }, - time_to_forget: read_data.time.0 + 21200.0, - }), - ), - 2 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Bad( - MoodContext::GoodWeather, - ), - }, - time_to_forget: read_data.time.0 + 86400.0, - }), - ), - _ => {}, // will never happen - } - } - if let Some(memory) = rtsim_entity.brain.get_mood() { - let msg = match &memory.item { - MemoryItem::Mood { state } => state.describe(), - _ => "".to_string(), - }; - self.chat_npc(msg, event_emitter); - } - } - }, - Subject::Location(location) => { - if let Some(tgt_pos) = read_data.positions.get(target) { - let raw_dir = location.origin.as_::() - tgt_pos.0.xy(); - let dist = Distance::from_dir(raw_dir).name(); - let dir = Direction::from_dir(raw_dir).name(); - - let msg = format!( - "{} ? I think it's {} {} from here!", - location.name, dist, dir - ); - self.chat_npc(msg, event_emitter); - } - }, - Subject::Person(person) => { - if let Some(src_pos) = read_data.positions.get(target) { - let msg = if let Some(person_pos) = person.origin { - let distance = Distance::from_dir( - person_pos.xy() - src_pos.0.xy(), - ); - match distance { - Distance::NextTo | Distance::Near => { - format!( - "{} ? I think he's {} {} from here!", - person.name(), - distance.name(), - Direction::from_dir( - person_pos.xy() - src_pos.0.xy(), - ) - .name() - ) - }, - _ => { - format!( - "{} ? I think he's gone visiting another \ - town. Come back later!", - person.name() - ) - }, - } - } else { - format!( - "{} ? Sorry, I don't know where you can find him.", - person.name() - ) - }; - self.chat_npc(msg, event_emitter); - } - }, - Subject::Work => {}, - } - } - } - } - true - }, - Some(AgentEvent::TradeInvite(with)) => { - if agent.behavior.can_trade() { - if !agent.behavior.is(BehaviorState::TRADING) { - // stand still and looking towards the trading player - controller.push_action(ControlAction::Stand); - controller.push_action(ControlAction::Talk); - if let Some(target) = get_entity_by_id(with.id(), read_data) { - agent.target = - Some(Target::new(target, false, read_data.time.0, false)); - } - controller.push_invite_response(InviteResponse::Accept); - agent.behavior.unset(BehaviorState::TRADING_ISSUER); - agent.behavior.set(BehaviorState::TRADING); - } else { - controller.push_invite_response(InviteResponse::Decline); - self.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_busy", - agent, - event_emitter, - ); - } - } else { - // TODO: Provide a hint where to find the closest merchant? - controller.push_invite_response(InviteResponse::Decline); - self.chat_npc_if_allowed_to_speak( - "npc-speech-villager_decline_trade", - agent, - event_emitter, - ); - } - true - }, - Some(AgentEvent::TradeAccepted(with)) => { - if !agent.behavior.is(BehaviorState::TRADING) { - if let Some(target) = get_entity_by_id(with.id(), read_data) { - agent.target = Some(Target::new(target, false, read_data.time.0, false)); - } - agent.behavior.set(BehaviorState::TRADING); - agent.behavior.set(BehaviorState::TRADING_ISSUER); - } - true - }, - Some(AgentEvent::FinishedTrade(result)) => { - if agent.behavior.is(BehaviorState::TRADING) { - match result { - TradeResult::Completed => { - self.chat_npc("npc-speech-merchant_trade_successful", event_emitter); - }, - _ => { - self.chat_npc("npc-speech-merchant_trade_declined", event_emitter); - }, - } - agent.behavior.unset(BehaviorState::TRADING); - } - true - }, - Some(AgentEvent::UpdatePendingTrade(boxval)) => { - let (tradeid, pending, prices, inventories) = &**boxval; - if agent.behavior.is(BehaviorState::TRADING) { - let who: usize = if agent.behavior.is(BehaviorState::TRADING_ISSUER) { - 0 - } else { - 1 - }; - let balance0: f32 = prices.balance(&pending.offers, inventories, 1 - who, true); - let balance1: f32 = prices.balance(&pending.offers, inventories, who, false); - if balance0 >= balance1 { - // If the trade is favourable to us, only send an accept message if we're - // not already accepting (since otherwise, spam-clicking the accept button - // results in lagging and moving to the review phase of an unfavorable trade - // (although since the phase is included in the message, this shouldn't - // result in fully accepting an unfavourable trade)) - if !pending.accept_flags[who] && !pending.is_empty_trade() { - event_emitter.emit(ServerEvent::ProcessTradeAction( - *self.entity, - *tradeid, - TradeAction::Accept(pending.phase), - )); - tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade"); - } - } else { - if balance1 > 0.0 { - let msg = format!( - "That only covers {:.0}% of my costs!", - (balance0 / balance1 * 100.0).floor() - ); - if let Some(tgt_data) = &agent.target { - // If talking with someone in particular, "tell" it only to them - if let Some(with) = read_data.uids.get(tgt_data.target) { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc_tell(*self.uid, *with, msg), - )); - } else { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc_say(*self.uid, msg), - )); - } - } else { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( - *self.uid, msg, - ))); - } - } - if pending.phase != TradePhase::Mutate { - // we got into the review phase but without balanced goods, decline - agent.behavior.unset(BehaviorState::TRADING); - event_emitter.emit(ServerEvent::ProcessTradeAction( - *self.entity, - *tradeid, - TradeAction::Decline, - )); - } - } - } - true - }, - Some(AgentEvent::ServerSound(_)) => false, - Some(AgentEvent::Hurt) => false, - None => false, - }; - if used { - agent.inbox.pop_front(); - } - used - } - fn look_toward( &self, controller: &mut Controller, diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 303a9c7551..f85373aa58 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -3,16 +3,23 @@ use common::{ agent::{ AgentEvent, Target, TimerAction, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, }, + compass::{Direction, Distance}, + dialogue::{MoodContext, MoodState, Subject}, + invite::{InviteKind, InviteResponse}, Agent, Alignment, BehaviorState, Body, BuffKind, ControlAction, ControlEvent, Controller, - InputKind, InventoryEvent, UtteranceKind, + InputKind, InventoryEvent, UnresolvedChatMsg, UtteranceKind, }, event::{Emitter, ServerEvent}, path::TraversalConfig, + rtsim::{Memory, MemoryItem, RtSimEvent}, + trade::{TradeAction, TradePhase, TradeResult}, }; use rand::{prelude::ThreadRng, thread_rng, Rng}; use specs::saveload::{Marker, MarkerAllocator}; use vek::Vec2; +use crate::rtsim::entity::PersonalityTrait; + use super::{ consts::{ DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_FLEE_DIST, @@ -76,6 +83,19 @@ impl BehaviorTree { } } + pub fn interaction() -> Self { + Self { + tree: vec![ + increment_timer_deltatime, + handle_inbox_talk, + handle_inbox_trade_invite, + handle_inbox_trade_accepted, + handle_inbox_finished_trade, + handle_inbox_update_pending_trade, + ], + } + } + pub fn hostile() -> Self { Self { tree: vec![heal_self_if_hurt, hurt_utterance, do_combat], @@ -425,14 +445,7 @@ fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool { /// If we receive a new interaction, start the interaction timer fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { - if bdata.agent.allowed_to_speak() - && bdata.agent_data.recv_interaction( - bdata.agent, - bdata.controller, - bdata.read_data, - bdata.event_emitter, - ) - { + if bdata.agent.allowed_to_speak() && BehaviorTree::interaction().run_with_behavior_data(bdata) { bdata .agent .timer @@ -516,6 +529,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { controller, rng, } = bdata; + if let Some(Target { target, selected_at, @@ -609,3 +623,466 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { } false } + +fn increment_timer_deltatime(bdata: &mut BehaviorData) -> bool { + bdata.agent.action_state.timer += bdata.read_data.dt.0; + false +} + +/// Handles Talk event if the front of the agent's inbox contains one +fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + read_data, + event_emitter, + controller, + .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::Talk(_, _))) { + return false; + } + + if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() { + if agent.allowed_to_speak() { + if let Some(target) = get_entity_by_id(by.id(), read_data) { + agent.target = Some(Target::new(target, false, read_data.time.0, false)); + + if agent_data.look_toward(controller, read_data, target) { + controller.push_action(ControlAction::Stand); + controller.push_action(ControlAction::Talk); + controller.push_utterance(UtteranceKind::Greeting); + + match subject { + Subject::Regular => { + if let (Some((_travel_to, destination_name)), Some(rtsim_entity)) = + (&agent.rtsim_controller.travel_to, &agent_data.rtsim_entity) + { + let personality = &rtsim_entity.brain.personality; + let standard_response_msg = || -> String { + if personality + .personality_traits + .contains(PersonalityTrait::Extroverted) + { + format!( + "I'm heading to {}! Want to come along?", + destination_name + ) + } else if personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "Hrm.".to_string() + } else { + "Hello!".to_string() + } + }; + let msg = if let Some(tgt_stats) = read_data.stats.get(target) { + agent.rtsim_controller.events.push(RtSimEvent::AddMemory( + Memory { + item: MemoryItem::CharacterInteraction { + name: tgt_stats.name.clone(), + }, + time_to_forget: read_data.time.0 + 600.0, + }, + )); + if rtsim_entity.brain.remembers_character(&tgt_stats.name) { + if personality + .personality_traits + .contains(PersonalityTrait::Extroverted) + { + format!( + "Greetings fair {}! It has been far too long \ + since last I saw you. I'm going to {} right now.", + &tgt_stats.name, destination_name + ) + } else if personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "Oh. It's you again.".to_string() + } else { + format!( + "Hi again {}! Unfortunately I'm in a hurry right \ + now. See you!", + &tgt_stats.name + ) + } + } else { + standard_response_msg() + } + } else { + standard_response_msg() + }; + agent_data.chat_npc(msg, event_emitter); + } else if agent.behavior.can_trade() { + if !agent.behavior.is(BehaviorState::TRADING) { + controller.push_initiate_invite(by, InviteKind::Trade); + agent_data.chat_npc( + "npc.speech.merchant_advertisement", + event_emitter, + ); + } else { + let default_msg = "npc.speech.merchant_busy"; + let msg = agent_data.rtsim_entity.map_or(default_msg, |e| { + if e.brain + .personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "npc.speech.merchant_busy_rude" + } else { + default_msg + } + }); + agent_data.chat_npc(msg, event_emitter); + } + } else { + let mut rng = thread_rng(); + if let Some(extreme_trait) = agent_data + .rtsim_entity + .and_then(|e| e.brain.personality.random_chat_trait(&mut rng)) + { + let msg = match extreme_trait { + PersonalityTrait::Open => "npc.speech.villager_open", + PersonalityTrait::Adventurous => { + "npc.speech.villager_adventurous" + }, + PersonalityTrait::Closed => "npc.speech.villager_closed", + PersonalityTrait::Conscientious => { + "npc.speech.villager_conscientious" + }, + PersonalityTrait::Busybody => { + "npc.speech.villager_busybody" + }, + PersonalityTrait::Unconscientious => { + "npc.speech.villager_unconscientious" + }, + PersonalityTrait::Extroverted => { + "npc.speech.villager_extroverted" + }, + PersonalityTrait::Introverted => { + "npc.speech.villager_introverted" + }, + PersonalityTrait::Agreeable => { + "npc.speech.villager_agreeable" + }, + PersonalityTrait::Sociable => { + "npc.speech.villager_sociable" + }, + PersonalityTrait::Disagreeable => { + "npc.speech.villager_disagreeable" + }, + PersonalityTrait::Neurotic => { + "npc.speech.villager_neurotic" + }, + PersonalityTrait::Seeker => "npc.speech.villager_seeker", + PersonalityTrait::SadLoner => { + "npc.speech.villager_sad_loner" + }, + PersonalityTrait::Worried => "npc.speech.villager_worried", + PersonalityTrait::Stable => "npc.speech.villager_stable", + }; + agent_data.chat_npc(msg, event_emitter); + } else { + agent_data.chat_npc("npc.speech.villager", event_emitter); + } + } + }, + Subject::Trade => { + if agent.behavior.can_trade() { + if !agent.behavior.is(BehaviorState::TRADING) { + controller.push_initiate_invite(by, InviteKind::Trade); + agent_data.chat_npc( + "npc.speech.merchant_advertisement", + event_emitter, + ); + } else { + agent_data.chat_npc("npc.speech.merchant_busy", event_emitter); + } + } else { + // TODO: maybe make some travellers willing to trade with + // simpler goods like potions + agent_data + .chat_npc("npc.speech.villager_decline_trade", event_emitter); + } + }, + Subject::Mood => { + if let Some(rtsim_entity) = agent_data.rtsim_entity { + if !rtsim_entity.brain.remembers_mood() { + // TODO: the following code will need a rework to + // implement more mood contexts + // This require that town NPCs becomes rtsim_entities to + // work fully. + match rand::random::() % 3 { + 0 => agent.rtsim_controller.events.push( + RtSimEvent::SetMood(Memory { + item: MemoryItem::Mood { + state: MoodState::Good( + MoodContext::GoodWeather, + ), + }, + time_to_forget: read_data.time.0 + 21200.0, + }), + ), + 1 => agent.rtsim_controller.events.push( + RtSimEvent::SetMood(Memory { + item: MemoryItem::Mood { + state: MoodState::Neutral( + MoodContext::EverydayLife, + ), + }, + time_to_forget: read_data.time.0 + 21200.0, + }), + ), + 2 => agent.rtsim_controller.events.push( + RtSimEvent::SetMood(Memory { + item: MemoryItem::Mood { + state: MoodState::Bad(MoodContext::GoodWeather), + }, + time_to_forget: read_data.time.0 + 86400.0, + }), + ), + _ => {}, // will never happen + } + } + if let Some(memory) = rtsim_entity.brain.get_mood() { + let msg = match &memory.item { + MemoryItem::Mood { state } => state.describe(), + _ => "".to_string(), + }; + agent_data.chat_npc(msg, event_emitter); + } + } + }, + Subject::Location(location) => { + if let Some(tgt_pos) = read_data.positions.get(target) { + let raw_dir = location.origin.as_::() - tgt_pos.0.xy(); + let dist = Distance::from_dir(raw_dir).name(); + let dir = Direction::from_dir(raw_dir).name(); + + let msg = format!( + "{} ? I think it's {} {} from here!", + location.name, dist, dir + ); + agent_data.chat_npc(msg, event_emitter); + } + }, + Subject::Person(person) => { + if let Some(src_pos) = read_data.positions.get(target) { + let msg = if let Some(person_pos) = person.origin { + let distance = + Distance::from_dir(person_pos.xy() - src_pos.0.xy()); + match distance { + Distance::NextTo | Distance::Near => { + format!( + "{} ? I think he's {} {} from here!", + person.name(), + distance.name(), + Direction::from_dir( + person_pos.xy() - src_pos.0.xy(), + ) + .name() + ) + }, + _ => { + format!( + "{} ? I think he's gone visiting another town. \ + Come back later!", + person.name() + ) + }, + } + } else { + format!( + "{} ? Sorry, I don't know where you can find him.", + person.name() + ) + }; + agent_data.chat_npc(msg, event_emitter); + } + }, + Subject::Work => {}, + } + } + } + } + } + true +} + +fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + read_data, + event_emitter, + controller, + .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::TradeInvite(_))) { + return false; + } + + if let Some(AgentEvent::TradeInvite(with)) = agent.inbox.pop_front() { + if agent.behavior.can_trade() { + if !agent.behavior.is(BehaviorState::TRADING) { + // stand still and looking towards the trading player + controller.push_action(ControlAction::Stand); + controller.push_action(ControlAction::Talk); + if let Some(target) = get_entity_by_id(with.id(), read_data) { + agent.target = Some(Target::new(target, false, read_data.time.0, false)); + } + controller.push_invite_response(InviteResponse::Accept); + agent.behavior.unset(BehaviorState::TRADING_ISSUER); + agent.behavior.set(BehaviorState::TRADING); + } else { + controller.push_invite_response(InviteResponse::Decline); + agent_data.chat_npc_if_allowed_to_speak( + "npc.speech.merchant_busy", + agent, + event_emitter, + ); + } + } else { + // TODO: Provide a hint where to find the closest merchant? + controller.push_invite_response(InviteResponse::Decline); + agent_data.chat_npc_if_allowed_to_speak( + "npc.speech.villager_decline_trade", + agent, + event_emitter, + ); + } + } + true +} + +fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, read_data, .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::TradeAccepted(_))) { + return false; + } + + if let Some(AgentEvent::TradeAccepted(with)) = agent.inbox.pop_front() { + if !agent.behavior.is(BehaviorState::TRADING) { + if let Some(target) = get_entity_by_id(with.id(), read_data) { + agent.target = Some(Target::new(target, false, read_data.time.0, false)); + } + agent.behavior.set(BehaviorState::TRADING); + agent.behavior.set(BehaviorState::TRADING_ISSUER); + } + } + true +} + +fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + event_emitter, + .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::FinishedTrade(_))) { + return false; + } + + if let Some(AgentEvent::FinishedTrade(result)) = agent.inbox.pop_front() { + if agent.behavior.is(BehaviorState::TRADING) { + match result { + TradeResult::Completed => { + agent_data.chat_npc("npc.speech.merchant_trade_successful", event_emitter); + }, + _ => { + agent_data.chat_npc("npc.speech.merchant_trade_declined", event_emitter); + }, + } + agent.behavior.unset(BehaviorState::TRADING); + } + } + true +} + +fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + read_data, + event_emitter, + .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::UpdatePendingTrade(_))) { + return false; + } + + if let Some(AgentEvent::UpdatePendingTrade(boxval)) = agent.inbox.pop_front() { + let (tradeid, pending, prices, inventories) = *boxval; + if agent.behavior.is(BehaviorState::TRADING) { + let who: usize = if agent.behavior.is(BehaviorState::TRADING_ISSUER) { + 0 + } else { + 1 + }; + let balance0: f32 = prices.balance(&pending.offers, &inventories, 1 - who, true); + let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); + if balance0 >= balance1 { + // If the trade is favourable to us, only send an accept message if we're + // not already accepting (since otherwise, spam-clicking the accept button + // results in lagging and moving to the review phase of an unfavorable trade + // (although since the phase is included in the message, this shouldn't + // result in fully accepting an unfavourable trade)) + if !pending.accept_flags[who] && !pending.is_empty_trade() { + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Accept(pending.phase), + )); + tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade"); + } + } else { + if balance1 > 0.0 { + let msg = format!( + "That only covers {:.0}% of my costs!", + (balance0 / balance1 * 100.0).floor() + ); + if let Some(tgt_data) = &agent.target { + // If talking with someone in particular, "tell" it only to them + if let Some(with) = read_data.uids.get(tgt_data.target) { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell( + *agent_data.uid, + *with, + msg, + ))); + } else { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *agent_data.uid, + msg, + ))); + } + } else { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *agent_data.uid, + msg, + ))); + } + } + if pending.phase != TradePhase::Mutate { + // we got into the review phase but without balanced goods, decline + agent.behavior.unset(BehaviorState::TRADING); + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Decline, + )); + } + } + } + } + true +} From 6994b026b1ae0c98e96ee0bb12f8be4007425010 Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Fri, 29 Jul 2022 19:06:08 +0200 Subject: [PATCH 04/10] Remove unused import --- server/src/sys/agent.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index e7271be2b3..ac0d34f8e6 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -50,9 +50,7 @@ use common_base::prof_span; use common_ecs::{Job, Origin, ParMode, Phase, System}; use rand::{thread_rng, Rng}; use rayon::iter::ParallelIterator; -use specs::{ - saveload::Marker, Entity as EcsEntity, Join, ParJoin, Read, WriteExpect, WriteStorage, -}; +use specs::{Entity as EcsEntity, Join, ParJoin, Read, WriteExpect, WriteStorage}; use vek::*; /// This system will allow NPCs to modify their controller From 4f53d055deef1d54a42b7156d83e78e9ed99cb84 Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Sat, 6 Aug 2022 13:05:36 +0200 Subject: [PATCH 05/10] Reimplement !3467 + fix event being pushed in front of agent inbox --- server/src/events/entity_manipulation.rs | 2 +- server/src/sys/agent/behavior_tree.rs | 566 ++-------------- .../sys/agent/behavior_tree/interaction.rs | 611 ++++++++++++++++++ 3 files changed, 651 insertions(+), 528 deletions(-) create mode 100644 server/src/sys/agent/behavior_tree/interaction.rs diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 32fb838add..e7a398e94f 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -94,7 +94,7 @@ pub fn handle_health_change(server: &Server, entity: EcsEntity, change: HealthCh let damage = -change.amount; if damage > 5.0 { if let Some(agent) = ecs.write_storage::().get_mut(entity) { - agent.inbox.push_front(AgentEvent::Hurt); + agent.inbox.push_back(AgentEvent::Hurt); } } } diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index f85373aa58..16d042dda4 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -3,22 +3,21 @@ use common::{ agent::{ AgentEvent, Target, TimerAction, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, }, - compass::{Direction, Distance}, - dialogue::{MoodContext, MoodState, Subject}, - invite::{InviteKind, InviteResponse}, - Agent, Alignment, BehaviorState, Body, BuffKind, ControlAction, ControlEvent, Controller, - InputKind, InventoryEvent, UnresolvedChatMsg, UtteranceKind, + Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, Body, BuffKind, + ControlAction, ControlEvent, Controller, InputKind, InventoryEvent, UtteranceKind, }, event::{Emitter, ServerEvent}, path::TraversalConfig, - rtsim::{Memory, MemoryItem, RtSimEvent}, - trade::{TradeAction, TradePhase, TradeResult}, }; use rand::{prelude::ThreadRng, thread_rng, Rng}; use specs::saveload::{Marker, MarkerAllocator}; use vek::Vec2; -use crate::rtsim::entity::PersonalityTrait; +use self::interaction::{ + handle_inbox_cancel_interactions, handle_inbox_finished_trade, handle_inbox_talk, + handle_inbox_trade_accepted, handle_inbox_trade_invite, handle_inbox_update_pending_trade, + increment_timer_deltatime, process_inbox_interaction, process_inbox_sound_and_hurt, +}; use super::{ consts::{ @@ -29,8 +28,10 @@ use super::{ util::{get_entity_by_id, is_dead, is_dead_or_invulnerable, is_invulnerable, stop_pursuing}, }; +mod interaction; + /// Struct containing essential data for running a behavior tree -struct BehaviorData<'a, 'b, 'c> { +pub struct BehaviorData<'a, 'b, 'c> { agent: &'a mut Agent, agent_data: AgentData<'a>, read_data: &'a ReadData<'a>, @@ -58,8 +59,8 @@ impl BehaviorTree { react_on_dangerous_fall, react_if_on_fire, target_if_attacked, - do_target_tree_if_target, - do_idle_tree, + do_target_tree_if_target_else_do_idle_tree, + process_inbox_interaction, ], } } @@ -83,22 +84,33 @@ impl BehaviorTree { } } - pub fn interaction() -> Self { - Self { - tree: vec![ - increment_timer_deltatime, - handle_inbox_talk, - handle_inbox_trade_invite, - handle_inbox_trade_accepted, - handle_inbox_finished_trade, - handle_inbox_update_pending_trade, - ], + pub fn interaction(behavior: Behavior) -> Self { + if behavior.can(BehaviorCapability::SPEAK) { + Self { + tree: vec![ + increment_timer_deltatime, + handle_inbox_talk, + handle_inbox_trade_invite, + handle_inbox_trade_accepted, + handle_inbox_finished_trade, + handle_inbox_update_pending_trade, + ], + } + } else { + Self { + tree: vec![handle_inbox_cancel_interactions], + } } } pub fn hostile() -> Self { Self { - tree: vec![heal_self_if_hurt, hurt_utterance, do_combat], + tree: vec![ + handle_inbox_cancel_interactions, + heal_self_if_hurt, + hurt_utterance, + do_combat, + ], } } @@ -107,7 +119,6 @@ impl BehaviorTree { tree: vec![ set_owner_if_no_target, process_inbox_sound_and_hurt, - process_inbox_interaction, handle_timer, ], } @@ -254,10 +265,13 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool { false } -fn do_target_tree_if_target(bdata: &mut BehaviorData) -> bool { +/// If the agent has a target, do the target tree, else do the idle tree +/// This function won't stop the behavior tree +fn do_target_tree_if_target_else_do_idle_tree(bdata: &mut BehaviorData) -> bool { if bdata.agent.target.is_some() { BehaviorTree::target().run_with_behavior_data(bdata); - return true; + } else { + BehaviorTree::idle().run_with_behavior_data(bdata); } false } @@ -415,45 +429,6 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool { false } -/// Interact if incoming messages -fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool { - if !bdata.agent.inbox.is_empty() { - if matches!( - bdata.agent.inbox.front(), - Some(AgentEvent::ServerSound(_)) | Some(AgentEvent::Hurt) - ) { - let sound = bdata.agent.inbox.pop_front(); - match sound { - Some(AgentEvent::ServerSound(sound)) => { - bdata.agent.sounds_heard.push(sound); - }, - Some(AgentEvent::Hurt) => { - // Hurt utterances at random upon receiving damage - if bdata.rng.gen::() < 0.4 { - bdata.controller.push_utterance(UtteranceKind::Hurt); - } - }, - //Note: this should be unreachable - Some(_) | None => {}, - } - } else { - bdata.agent.action_state.timer = 0.1; - } - } - false -} - -/// If we receive a new interaction, start the interaction timer -fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { - if bdata.agent.allowed_to_speak() && BehaviorTree::interaction().run_with_behavior_data(bdata) { - bdata - .agent - .timer - .start(bdata.read_data.time.0, TimerAction::Interact); - } - false -} - fn handle_timer(bdata: &mut BehaviorData) -> bool { let timeout = if bdata.agent.behavior.is(BehaviorState::TRADING) { TRADE_INTERACTION_TIME @@ -623,466 +598,3 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { } false } - -fn increment_timer_deltatime(bdata: &mut BehaviorData) -> bool { - bdata.agent.action_state.timer += bdata.read_data.dt.0; - false -} - -/// Handles Talk event if the front of the agent's inbox contains one -fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { - let BehaviorData { - agent, - agent_data, - read_data, - event_emitter, - controller, - .. - } = bdata; - - if !matches!(agent.inbox.front(), Some(AgentEvent::Talk(_, _))) { - return false; - } - - if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() { - if agent.allowed_to_speak() { - if let Some(target) = get_entity_by_id(by.id(), read_data) { - agent.target = Some(Target::new(target, false, read_data.time.0, false)); - - if agent_data.look_toward(controller, read_data, target) { - controller.push_action(ControlAction::Stand); - controller.push_action(ControlAction::Talk); - controller.push_utterance(UtteranceKind::Greeting); - - match subject { - Subject::Regular => { - if let (Some((_travel_to, destination_name)), Some(rtsim_entity)) = - (&agent.rtsim_controller.travel_to, &agent_data.rtsim_entity) - { - let personality = &rtsim_entity.brain.personality; - let standard_response_msg = || -> String { - if personality - .personality_traits - .contains(PersonalityTrait::Extroverted) - { - format!( - "I'm heading to {}! Want to come along?", - destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Hrm.".to_string() - } else { - "Hello!".to_string() - } - }; - let msg = if let Some(tgt_stats) = read_data.stats.get(target) { - agent.rtsim_controller.events.push(RtSimEvent::AddMemory( - Memory { - item: MemoryItem::CharacterInteraction { - name: tgt_stats.name.clone(), - }, - time_to_forget: read_data.time.0 + 600.0, - }, - )); - if rtsim_entity.brain.remembers_character(&tgt_stats.name) { - if personality - .personality_traits - .contains(PersonalityTrait::Extroverted) - { - format!( - "Greetings fair {}! It has been far too long \ - since last I saw you. I'm going to {} right now.", - &tgt_stats.name, destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Oh. It's you again.".to_string() - } else { - format!( - "Hi again {}! Unfortunately I'm in a hurry right \ - now. See you!", - &tgt_stats.name - ) - } - } else { - standard_response_msg() - } - } else { - standard_response_msg() - }; - agent_data.chat_npc(msg, event_emitter); - } else if agent.behavior.can_trade() { - if !agent.behavior.is(BehaviorState::TRADING) { - controller.push_initiate_invite(by, InviteKind::Trade); - agent_data.chat_npc( - "npc.speech.merchant_advertisement", - event_emitter, - ); - } else { - let default_msg = "npc.speech.merchant_busy"; - let msg = agent_data.rtsim_entity.map_or(default_msg, |e| { - if e.brain - .personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "npc.speech.merchant_busy_rude" - } else { - default_msg - } - }); - agent_data.chat_npc(msg, event_emitter); - } - } else { - let mut rng = thread_rng(); - if let Some(extreme_trait) = agent_data - .rtsim_entity - .and_then(|e| e.brain.personality.random_chat_trait(&mut rng)) - { - let msg = match extreme_trait { - PersonalityTrait::Open => "npc.speech.villager_open", - PersonalityTrait::Adventurous => { - "npc.speech.villager_adventurous" - }, - PersonalityTrait::Closed => "npc.speech.villager_closed", - PersonalityTrait::Conscientious => { - "npc.speech.villager_conscientious" - }, - PersonalityTrait::Busybody => { - "npc.speech.villager_busybody" - }, - PersonalityTrait::Unconscientious => { - "npc.speech.villager_unconscientious" - }, - PersonalityTrait::Extroverted => { - "npc.speech.villager_extroverted" - }, - PersonalityTrait::Introverted => { - "npc.speech.villager_introverted" - }, - PersonalityTrait::Agreeable => { - "npc.speech.villager_agreeable" - }, - PersonalityTrait::Sociable => { - "npc.speech.villager_sociable" - }, - PersonalityTrait::Disagreeable => { - "npc.speech.villager_disagreeable" - }, - PersonalityTrait::Neurotic => { - "npc.speech.villager_neurotic" - }, - PersonalityTrait::Seeker => "npc.speech.villager_seeker", - PersonalityTrait::SadLoner => { - "npc.speech.villager_sad_loner" - }, - PersonalityTrait::Worried => "npc.speech.villager_worried", - PersonalityTrait::Stable => "npc.speech.villager_stable", - }; - agent_data.chat_npc(msg, event_emitter); - } else { - agent_data.chat_npc("npc.speech.villager", event_emitter); - } - } - }, - Subject::Trade => { - if agent.behavior.can_trade() { - if !agent.behavior.is(BehaviorState::TRADING) { - controller.push_initiate_invite(by, InviteKind::Trade); - agent_data.chat_npc( - "npc.speech.merchant_advertisement", - event_emitter, - ); - } else { - agent_data.chat_npc("npc.speech.merchant_busy", event_emitter); - } - } else { - // TODO: maybe make some travellers willing to trade with - // simpler goods like potions - agent_data - .chat_npc("npc.speech.villager_decline_trade", event_emitter); - } - }, - Subject::Mood => { - if let Some(rtsim_entity) = agent_data.rtsim_entity { - if !rtsim_entity.brain.remembers_mood() { - // TODO: the following code will need a rework to - // implement more mood contexts - // This require that town NPCs becomes rtsim_entities to - // work fully. - match rand::random::() % 3 { - 0 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Good( - MoodContext::GoodWeather, - ), - }, - time_to_forget: read_data.time.0 + 21200.0, - }), - ), - 1 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Neutral( - MoodContext::EverydayLife, - ), - }, - time_to_forget: read_data.time.0 + 21200.0, - }), - ), - 2 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Bad(MoodContext::GoodWeather), - }, - time_to_forget: read_data.time.0 + 86400.0, - }), - ), - _ => {}, // will never happen - } - } - if let Some(memory) = rtsim_entity.brain.get_mood() { - let msg = match &memory.item { - MemoryItem::Mood { state } => state.describe(), - _ => "".to_string(), - }; - agent_data.chat_npc(msg, event_emitter); - } - } - }, - Subject::Location(location) => { - if let Some(tgt_pos) = read_data.positions.get(target) { - let raw_dir = location.origin.as_::() - tgt_pos.0.xy(); - let dist = Distance::from_dir(raw_dir).name(); - let dir = Direction::from_dir(raw_dir).name(); - - let msg = format!( - "{} ? I think it's {} {} from here!", - location.name, dist, dir - ); - agent_data.chat_npc(msg, event_emitter); - } - }, - Subject::Person(person) => { - if let Some(src_pos) = read_data.positions.get(target) { - let msg = if let Some(person_pos) = person.origin { - let distance = - Distance::from_dir(person_pos.xy() - src_pos.0.xy()); - match distance { - Distance::NextTo | Distance::Near => { - format!( - "{} ? I think he's {} {} from here!", - person.name(), - distance.name(), - Direction::from_dir( - person_pos.xy() - src_pos.0.xy(), - ) - .name() - ) - }, - _ => { - format!( - "{} ? I think he's gone visiting another town. \ - Come back later!", - person.name() - ) - }, - } - } else { - format!( - "{} ? Sorry, I don't know where you can find him.", - person.name() - ) - }; - agent_data.chat_npc(msg, event_emitter); - } - }, - Subject::Work => {}, - } - } - } - } - } - true -} - -fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { - let BehaviorData { - agent, - agent_data, - read_data, - event_emitter, - controller, - .. - } = bdata; - - if !matches!(agent.inbox.front(), Some(AgentEvent::TradeInvite(_))) { - return false; - } - - if let Some(AgentEvent::TradeInvite(with)) = agent.inbox.pop_front() { - if agent.behavior.can_trade() { - if !agent.behavior.is(BehaviorState::TRADING) { - // stand still and looking towards the trading player - controller.push_action(ControlAction::Stand); - controller.push_action(ControlAction::Talk); - if let Some(target) = get_entity_by_id(with.id(), read_data) { - agent.target = Some(Target::new(target, false, read_data.time.0, false)); - } - controller.push_invite_response(InviteResponse::Accept); - agent.behavior.unset(BehaviorState::TRADING_ISSUER); - agent.behavior.set(BehaviorState::TRADING); - } else { - controller.push_invite_response(InviteResponse::Decline); - agent_data.chat_npc_if_allowed_to_speak( - "npc.speech.merchant_busy", - agent, - event_emitter, - ); - } - } else { - // TODO: Provide a hint where to find the closest merchant? - controller.push_invite_response(InviteResponse::Decline); - agent_data.chat_npc_if_allowed_to_speak( - "npc.speech.villager_decline_trade", - agent, - event_emitter, - ); - } - } - true -} - -fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool { - let BehaviorData { - agent, read_data, .. - } = bdata; - - if !matches!(agent.inbox.front(), Some(AgentEvent::TradeAccepted(_))) { - return false; - } - - if let Some(AgentEvent::TradeAccepted(with)) = agent.inbox.pop_front() { - if !agent.behavior.is(BehaviorState::TRADING) { - if let Some(target) = get_entity_by_id(with.id(), read_data) { - agent.target = Some(Target::new(target, false, read_data.time.0, false)); - } - agent.behavior.set(BehaviorState::TRADING); - agent.behavior.set(BehaviorState::TRADING_ISSUER); - } - } - true -} - -fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool { - let BehaviorData { - agent, - agent_data, - event_emitter, - .. - } = bdata; - - if !matches!(agent.inbox.front(), Some(AgentEvent::FinishedTrade(_))) { - return false; - } - - if let Some(AgentEvent::FinishedTrade(result)) = agent.inbox.pop_front() { - if agent.behavior.is(BehaviorState::TRADING) { - match result { - TradeResult::Completed => { - agent_data.chat_npc("npc.speech.merchant_trade_successful", event_emitter); - }, - _ => { - agent_data.chat_npc("npc.speech.merchant_trade_declined", event_emitter); - }, - } - agent.behavior.unset(BehaviorState::TRADING); - } - } - true -} - -fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { - let BehaviorData { - agent, - agent_data, - read_data, - event_emitter, - .. - } = bdata; - - if !matches!(agent.inbox.front(), Some(AgentEvent::UpdatePendingTrade(_))) { - return false; - } - - if let Some(AgentEvent::UpdatePendingTrade(boxval)) = agent.inbox.pop_front() { - let (tradeid, pending, prices, inventories) = *boxval; - if agent.behavior.is(BehaviorState::TRADING) { - let who: usize = if agent.behavior.is(BehaviorState::TRADING_ISSUER) { - 0 - } else { - 1 - }; - let balance0: f32 = prices.balance(&pending.offers, &inventories, 1 - who, true); - let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); - if balance0 >= balance1 { - // If the trade is favourable to us, only send an accept message if we're - // not already accepting (since otherwise, spam-clicking the accept button - // results in lagging and moving to the review phase of an unfavorable trade - // (although since the phase is included in the message, this shouldn't - // result in fully accepting an unfavourable trade)) - if !pending.accept_flags[who] && !pending.is_empty_trade() { - event_emitter.emit(ServerEvent::ProcessTradeAction( - *agent_data.entity, - tradeid, - TradeAction::Accept(pending.phase), - )); - tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade"); - } - } else { - if balance1 > 0.0 { - let msg = format!( - "That only covers {:.0}% of my costs!", - (balance0 / balance1 * 100.0).floor() - ); - if let Some(tgt_data) = &agent.target { - // If talking with someone in particular, "tell" it only to them - if let Some(with) = read_data.uids.get(tgt_data.target) { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell( - *agent_data.uid, - *with, - msg, - ))); - } else { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( - *agent_data.uid, - msg, - ))); - } - } else { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( - *agent_data.uid, - msg, - ))); - } - } - if pending.phase != TradePhase::Mutate { - // we got into the review phase but without balanced goods, decline - agent.behavior.unset(BehaviorState::TRADING); - event_emitter.emit(ServerEvent::ProcessTradeAction( - *agent_data.entity, - tradeid, - TradeAction::Decline, - )); - } - } - } - } - true -} diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs new file mode 100644 index 0000000000..9b05d92c1c --- /dev/null +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -0,0 +1,611 @@ +use common::{ + comp::{ + agent::{AgentEvent, Target, TimerAction}, + compass::{Direction, Distance}, + dialogue::{MoodContext, MoodState, Subject}, + invite::{InviteKind, InviteResponse}, + BehaviorState, ControlAction, UnresolvedChatMsg, UtteranceKind, + }, + event::ServerEvent, + rtsim::{Memory, MemoryItem, RtSimEvent}, + trade::{TradeAction, TradePhase, TradeResult}, +}; +use rand::{thread_rng, Rng}; +use specs::saveload::Marker; + +use crate::{rtsim::entity::PersonalityTrait, sys::agent::util::get_entity_by_id}; + +use super::{BehaviorData, BehaviorTree}; + +/// Interact if incoming messages +pub fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool { + if !bdata.agent.inbox.is_empty() { + if matches!( + bdata.agent.inbox.front(), + Some(AgentEvent::ServerSound(_)) | Some(AgentEvent::Hurt) + ) { + let sound = bdata.agent.inbox.pop_front(); + match sound { + Some(AgentEvent::ServerSound(sound)) => { + bdata.agent.sounds_heard.push(sound); + }, + Some(AgentEvent::Hurt) => { + // Hurt utterances at random upon receiving damage + if bdata.rng.gen::() < 0.4 { + bdata.controller.push_utterance(UtteranceKind::Hurt); + } + }, + //Note: this should be unreachable + Some(_) | None => {}, + } + } else { + bdata.agent.action_state.timer = 0.1; + } + } + false +} + +/// If we receive a new interaction, start the interaction timer +pub fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { + if BehaviorTree::interaction(bdata.agent.behavior).run_with_behavior_data(bdata) { + bdata + .agent + .timer + .start(bdata.read_data.time.0, TimerAction::Interact); + } + false +} + +pub fn increment_timer_deltatime(bdata: &mut BehaviorData) -> bool { + bdata.agent.action_state.timer += bdata.read_data.dt.0; + false +} + +/// Handles Talk event if the front of the agent's inbox contains one +pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + read_data, + event_emitter, + controller, + .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::Talk(_, _))) { + return false; + } + + if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() { + if agent.allowed_to_speak() { + if let Some(target) = get_entity_by_id(by.id(), read_data) { + agent.target = Some(Target::new(target, false, read_data.time.0, false)); + + if agent_data.look_toward(controller, read_data, target) { + controller.push_action(ControlAction::Stand); + controller.push_action(ControlAction::Talk); + controller.push_utterance(UtteranceKind::Greeting); + + match subject { + Subject::Regular => { + if let (Some((_travel_to, destination_name)), Some(rtsim_entity)) = + (&agent.rtsim_controller.travel_to, &agent_data.rtsim_entity) + { + let personality = &rtsim_entity.brain.personality; + let standard_response_msg = || -> String { + if personality + .personality_traits + .contains(PersonalityTrait::Extroverted) + { + format!( + "I'm heading to {}! Want to come along?", + destination_name + ) + } else if personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "Hrm.".to_string() + } else { + "Hello!".to_string() + } + }; + let msg = if let Some(tgt_stats) = read_data.stats.get(target) { + agent.rtsim_controller.events.push(RtSimEvent::AddMemory( + Memory { + item: MemoryItem::CharacterInteraction { + name: tgt_stats.name.clone(), + }, + time_to_forget: read_data.time.0 + 600.0, + }, + )); + if rtsim_entity.brain.remembers_character(&tgt_stats.name) { + if personality + .personality_traits + .contains(PersonalityTrait::Extroverted) + { + format!( + "Greetings fair {}! It has been far too long \ + since last I saw you. I'm going to {} right now.", + &tgt_stats.name, destination_name + ) + } else if personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "Oh. It's you again.".to_string() + } else { + format!( + "Hi again {}! Unfortunately I'm in a hurry right \ + now. See you!", + &tgt_stats.name + ) + } + } else { + standard_response_msg() + } + } else { + standard_response_msg() + }; + agent_data.chat_npc(msg, event_emitter); + } else if agent.behavior.can_trade() { + if !agent.behavior.is(BehaviorState::TRADING) { + controller.push_initiate_invite(by, InviteKind::Trade); + agent_data.chat_npc( + "npc.speech.merchant_advertisement", + event_emitter, + ); + } else { + let default_msg = "npc.speech.merchant_busy"; + let msg = agent_data.rtsim_entity.map_or(default_msg, |e| { + if e.brain + .personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "npc.speech.merchant_busy_rude" + } else { + default_msg + } + }); + agent_data.chat_npc(msg, event_emitter); + } + } else { + let mut rng = thread_rng(); + if let Some(extreme_trait) = agent_data + .rtsim_entity + .and_then(|e| e.brain.personality.random_chat_trait(&mut rng)) + { + let msg = match extreme_trait { + PersonalityTrait::Open => "npc.speech.villager_open", + PersonalityTrait::Adventurous => { + "npc.speech.villager_adventurous" + }, + PersonalityTrait::Closed => "npc.speech.villager_closed", + PersonalityTrait::Conscientious => { + "npc.speech.villager_conscientious" + }, + PersonalityTrait::Busybody => { + "npc.speech.villager_busybody" + }, + PersonalityTrait::Unconscientious => { + "npc.speech.villager_unconscientious" + }, + PersonalityTrait::Extroverted => { + "npc.speech.villager_extroverted" + }, + PersonalityTrait::Introverted => { + "npc.speech.villager_introverted" + }, + PersonalityTrait::Agreeable => { + "npc.speech.villager_agreeable" + }, + PersonalityTrait::Sociable => { + "npc.speech.villager_sociable" + }, + PersonalityTrait::Disagreeable => { + "npc.speech.villager_disagreeable" + }, + PersonalityTrait::Neurotic => { + "npc.speech.villager_neurotic" + }, + PersonalityTrait::Seeker => "npc.speech.villager_seeker", + PersonalityTrait::SadLoner => { + "npc.speech.villager_sad_loner" + }, + PersonalityTrait::Worried => "npc.speech.villager_worried", + PersonalityTrait::Stable => "npc.speech.villager_stable", + }; + agent_data.chat_npc(msg, event_emitter); + } else { + agent_data.chat_npc("npc.speech.villager", event_emitter); + } + } + }, + Subject::Trade => { + if agent.behavior.can_trade() { + if !agent.behavior.is(BehaviorState::TRADING) { + controller.push_initiate_invite(by, InviteKind::Trade); + agent_data.chat_npc( + "npc.speech.merchant_advertisement", + event_emitter, + ); + } else { + agent_data.chat_npc("npc.speech.merchant_busy", event_emitter); + } + } else { + // TODO: maybe make some travellers willing to trade with + // simpler goods like potions + agent_data + .chat_npc("npc.speech.villager_decline_trade", event_emitter); + } + }, + Subject::Mood => { + if let Some(rtsim_entity) = agent_data.rtsim_entity { + if !rtsim_entity.brain.remembers_mood() { + // TODO: the following code will need a rework to + // implement more mood contexts + // This require that town NPCs becomes rtsim_entities to + // work fully. + match rand::random::() % 3 { + 0 => agent.rtsim_controller.events.push( + RtSimEvent::SetMood(Memory { + item: MemoryItem::Mood { + state: MoodState::Good( + MoodContext::GoodWeather, + ), + }, + time_to_forget: read_data.time.0 + 21200.0, + }), + ), + 1 => agent.rtsim_controller.events.push( + RtSimEvent::SetMood(Memory { + item: MemoryItem::Mood { + state: MoodState::Neutral( + MoodContext::EverydayLife, + ), + }, + time_to_forget: read_data.time.0 + 21200.0, + }), + ), + 2 => agent.rtsim_controller.events.push( + RtSimEvent::SetMood(Memory { + item: MemoryItem::Mood { + state: MoodState::Bad(MoodContext::GoodWeather), + }, + time_to_forget: read_data.time.0 + 86400.0, + }), + ), + _ => {}, // will never happen + } + } + if let Some(memory) = rtsim_entity.brain.get_mood() { + let msg = match &memory.item { + MemoryItem::Mood { state } => state.describe(), + _ => "".to_string(), + }; + agent_data.chat_npc(msg, event_emitter); + } + } + }, + Subject::Location(location) => { + if let Some(tgt_pos) = read_data.positions.get(target) { + let raw_dir = location.origin.as_::() - tgt_pos.0.xy(); + let dist = Distance::from_dir(raw_dir).name(); + let dir = Direction::from_dir(raw_dir).name(); + + let msg = format!( + "{} ? I think it's {} {} from here!", + location.name, dist, dir + ); + agent_data.chat_npc(msg, event_emitter); + } + }, + Subject::Person(person) => { + if let Some(src_pos) = read_data.positions.get(target) { + let msg = if let Some(person_pos) = person.origin { + let distance = + Distance::from_dir(person_pos.xy() - src_pos.0.xy()); + match distance { + Distance::NextTo | Distance::Near => { + format!( + "{} ? I think he's {} {} from here!", + person.name(), + distance.name(), + Direction::from_dir( + person_pos.xy() - src_pos.0.xy(), + ) + .name() + ) + }, + _ => { + format!( + "{} ? I think he's gone visiting another town. \ + Come back later!", + person.name() + ) + }, + } + } else { + format!( + "{} ? Sorry, I don't know where you can find him.", + person.name() + ) + }; + agent_data.chat_npc(msg, event_emitter); + } + }, + Subject::Work => {}, + } + } + } + } + } + true +} + +pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + read_data, + event_emitter, + controller, + .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::TradeInvite(_))) { + return false; + } + + if let Some(AgentEvent::TradeInvite(with)) = agent.inbox.pop_front() { + if agent.behavior.can_trade() { + if !agent.behavior.is(BehaviorState::TRADING) { + // stand still and looking towards the trading player + controller.push_action(ControlAction::Stand); + controller.push_action(ControlAction::Talk); + if let Some(target) = get_entity_by_id(with.id(), read_data) { + agent.target = Some(Target::new(target, false, read_data.time.0, false)); + } + controller.push_invite_response(InviteResponse::Accept); + agent.behavior.unset(BehaviorState::TRADING_ISSUER); + agent.behavior.set(BehaviorState::TRADING); + } else { + controller.push_invite_response(InviteResponse::Decline); + agent_data.chat_npc_if_allowed_to_speak( + "npc.speech.merchant_busy", + agent, + event_emitter, + ); + } + } else { + // TODO: Provide a hint where to find the closest merchant? + controller.push_invite_response(InviteResponse::Decline); + agent_data.chat_npc_if_allowed_to_speak( + "npc.speech.villager_decline_trade", + agent, + event_emitter, + ); + } + } + true +} + +pub fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, read_data, .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::TradeAccepted(_))) { + return false; + } + + if let Some(AgentEvent::TradeAccepted(with)) = agent.inbox.pop_front() { + if !agent.behavior.is(BehaviorState::TRADING) { + if let Some(target) = get_entity_by_id(with.id(), read_data) { + agent.target = Some(Target::new(target, false, read_data.time.0, false)); + } + agent.behavior.set(BehaviorState::TRADING); + agent.behavior.set(BehaviorState::TRADING_ISSUER); + } + } + true +} + +pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + event_emitter, + .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::FinishedTrade(_))) { + return false; + } + + if let Some(AgentEvent::FinishedTrade(result)) = agent.inbox.pop_front() { + if agent.behavior.is(BehaviorState::TRADING) { + match result { + TradeResult::Completed => { + agent_data.chat_npc("npc.speech.merchant_trade_successful", event_emitter); + }, + _ => { + agent_data.chat_npc("npc.speech.merchant_trade_declined", event_emitter); + }, + } + agent.behavior.unset(BehaviorState::TRADING); + } + } + true +} + +pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + read_data, + event_emitter, + .. + } = bdata; + + if !matches!(agent.inbox.front(), Some(AgentEvent::UpdatePendingTrade(_))) { + return false; + } + + if let Some(AgentEvent::UpdatePendingTrade(boxval)) = agent.inbox.pop_front() { + let (tradeid, pending, prices, inventories) = *boxval; + if agent.behavior.is(BehaviorState::TRADING) { + let who: usize = if agent.behavior.is(BehaviorState::TRADING_ISSUER) { + 0 + } else { + 1 + }; + let balance0: f32 = prices.balance(&pending.offers, &inventories, 1 - who, true); + let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); + if balance0 >= balance1 { + // If the trade is favourable to us, only send an accept message if we're + // not already accepting (since otherwise, spam-clicking the accept button + // results in lagging and moving to the review phase of an unfavorable trade + // (although since the phase is included in the message, this shouldn't + // result in fully accepting an unfavourable trade)) + if !pending.accept_flags[who] && !pending.is_empty_trade() { + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Accept(pending.phase), + )); + tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade"); + } + } else { + if balance1 > 0.0 { + let msg = format!( + "That only covers {:.0}% of my costs!", + (balance0 / balance1 * 100.0).floor() + ); + if let Some(tgt_data) = &agent.target { + // If talking with someone in particular, "tell" it only to them + if let Some(with) = read_data.uids.get(tgt_data.target) { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell( + *agent_data.uid, + *with, + msg, + ))); + } else { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *agent_data.uid, + msg, + ))); + } + } else { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *agent_data.uid, + msg, + ))); + } + } + if pending.phase != TradePhase::Mutate { + // we got into the review phase but without balanced goods, decline + agent.behavior.unset(BehaviorState::TRADING); + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Decline, + )); + } + } + } + } + true +} + +pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + event_emitter, + controller, + .. + } = bdata; + + if let Some(msg) = agent.inbox.front() { + let used = match msg { + AgentEvent::Talk(by, _) | AgentEvent::TradeAccepted(by) => { + if let (Some(target), Some(speaker)) = + (agent.target, get_entity_by_id(by.id(), bdata.read_data)) + { + // in combat, speak to players that aren't the current target + if !target.hostile || target.target != speaker { + agent_data.chat_npc_if_allowed_to_speak( + "npc.speech.villager_busy", + agent, + event_emitter, + ); + } + } + + true + }, + AgentEvent::TradeInvite(by) => { + controller.push_invite_response(InviteResponse::Decline); + if let (Some(target), Some(speaker)) = + (agent.target, get_entity_by_id(by.id(), bdata.read_data)) + { + // in combat, speak to players that aren't the current target + if !target.hostile || target.target != speaker { + if agent.behavior.can_trade() { + agent_data.chat_npc_if_allowed_to_speak( + "npc.speech.merchant_busy", + agent, + event_emitter, + ); + } else { + agent_data.chat_npc_if_allowed_to_speak( + "npc.speech.villager_busy", + agent, + event_emitter, + ); + } + } + } + true + }, + AgentEvent::FinishedTrade(result) => { + // copy pasted from recv_interaction + // because the trade is not cancellable in this state + if agent.behavior.is(BehaviorState::TRADING) { + match result { + TradeResult::Completed => { + agent_data + .chat_npc("npc.speech.merchant_trade_successful", event_emitter); + }, + _ => { + agent_data + .chat_npc("npc.speech.merchant_trade_declined", event_emitter); + }, + } + agent.behavior.unset(BehaviorState::TRADING); + } + true + }, + AgentEvent::UpdatePendingTrade(boxval) => { + // immediately cancel the trade + let (tradeid, _pending, _prices, _inventories) = &**boxval; + agent.behavior.unset(BehaviorState::TRADING); + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + *tradeid, + TradeAction::Decline, + )); + agent_data.chat_npc("npc.speech.merchant_trade_cancelled_hostile", event_emitter); + true + }, + AgentEvent::ServerSound(_) | AgentEvent::Hurt => false, + }; + if used { + agent.inbox.pop_front(); + } + return used; + } + false +} From 3203051fc3061148b9ec460ea49fadc3d284d90e Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Sat, 6 Aug 2022 14:03:09 +0200 Subject: [PATCH 06/10] Move interaction-cancelling code in interaction behavior + includes missing fix from !3467 --- server/src/sys/agent/behavior_tree.rs | 32 +++++++++---------- .../sys/agent/behavior_tree/interaction.rs | 3 +- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 16d042dda4..421009924d 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -3,8 +3,8 @@ use common::{ agent::{ AgentEvent, Target, TimerAction, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, }, - Agent, Alignment, Behavior, BehaviorCapability, BehaviorState, Body, BuffKind, - ControlAction, ControlEvent, Controller, InputKind, InventoryEvent, UtteranceKind, + Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, ControlAction, + ControlEvent, Controller, InputKind, InventoryEvent, UtteranceKind, }, event::{Emitter, ServerEvent}, path::TraversalConfig, @@ -59,8 +59,9 @@ impl BehaviorTree { react_on_dangerous_fall, react_if_on_fire, target_if_attacked, - do_target_tree_if_target_else_do_idle_tree, + process_inbox_sound_and_hurt, process_inbox_interaction, + do_target_tree_if_target_else_do_idle_tree, ], } } @@ -84,8 +85,13 @@ impl BehaviorTree { } } - pub fn interaction(behavior: Behavior) -> Self { - if behavior.can(BehaviorCapability::SPEAK) { + pub fn interaction(agent: &Agent) -> Self { + let is_in_combat = if let Some(Target { hostile, .. }) = agent.target { + hostile + } else { + false + }; + if !is_in_combat && agent.behavior.can(BehaviorCapability::SPEAK) { Self { tree: vec![ increment_timer_deltatime, @@ -105,22 +111,13 @@ impl BehaviorTree { pub fn hostile() -> Self { Self { - tree: vec![ - handle_inbox_cancel_interactions, - heal_self_if_hurt, - hurt_utterance, - do_combat, - ], + tree: vec![heal_self_if_hurt, hurt_utterance, do_combat], } } pub fn idle() -> Self { Self { - tree: vec![ - set_owner_if_no_target, - process_inbox_sound_and_hurt, - handle_timer, - ], + tree: vec![set_owner_if_no_target, handle_timer], } } @@ -486,11 +483,12 @@ fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool { } fn hurt_utterance(bdata: &mut BehaviorData) -> bool { - if let Some(AgentEvent::Hurt) = bdata.agent.inbox.pop_front() { + if matches!(bdata.agent.inbox.front(), Some(AgentEvent::Hurt)) { // Hurt utterances at random upon receiving damage if bdata.rng.gen::() < 0.4 { bdata.controller.push_utterance(UtteranceKind::Hurt); } + bdata.agent.inbox.pop_front(); } false } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 9b05d92c1c..44271f4f13 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -47,7 +47,7 @@ pub fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool { /// If we receive a new interaction, start the interaction timer pub fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { - if BehaviorTree::interaction(bdata.agent.behavior).run_with_behavior_data(bdata) { + if BehaviorTree::interaction(bdata.agent).run_with_behavior_data(bdata) { bdata .agent .timer @@ -605,7 +605,6 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { if used { agent.inbox.pop_front(); } - return used; } false } From 62568d2229fd0ed46460f7fc59ff2a62885e8526 Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Sat, 6 Aug 2022 14:30:32 +0200 Subject: [PATCH 07/10] Add documentation --- server/src/sys/agent/behavior_tree.rs | 46 +++++++++++++++---- .../sys/agent/behavior_tree/interaction.rs | 12 +++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 421009924d..e5879c0fe2 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -53,6 +53,9 @@ pub struct BehaviorTree { } impl BehaviorTree { + /// Base BehaviorTree + /// + /// React to immediate dangers (fire, fall & attacks) then call subtrees pub fn root() -> Self { Self { tree: vec![ @@ -66,6 +69,10 @@ impl BehaviorTree { } } + /// Target BehaviorTree + /// + /// React to the agent's target. + /// Either redirect to hostile or pet tree pub fn target() -> Self { Self { tree: vec![ @@ -79,12 +86,20 @@ impl BehaviorTree { } } + /// Pet BehaviorTree + /// + /// Follow the owner and attack ennemies pub fn pet() -> Self { Self { tree: vec![follow_if_far_away, attack_if_owner_hurt, do_idle_tree], } } + /// Interaction BehaviorTree + /// + /// Either process the inbox for talk and trade events if the agent can + /// talk. If not, or if we are in combat, deny all talk and trade + /// events. pub fn interaction(agent: &Agent) -> Self { let is_in_combat = if let Some(Target { hostile, .. }) = agent.target { hostile @@ -109,15 +124,19 @@ impl BehaviorTree { } } + /// Hostile BehaviorTree + /// + /// Attack the target, and heal self if applicable pub fn hostile() -> Self { Self { tree: vec![heal_self_if_hurt, hurt_utterance, do_combat], } } + /// Idle BehaviorTree pub fn idle() -> Self { Self { - tree: vec![set_owner_if_no_target, handle_timer], + tree: vec![set_owner_if_no_target, handle_timed_events], } } @@ -263,7 +282,8 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool { } /// If the agent has a target, do the target tree, else do the idle tree -/// This function won't stop the behavior tree +/// +/// This function will never stop the BehaviorTree fn do_target_tree_if_target_else_do_idle_tree(bdata: &mut BehaviorData) -> bool { if bdata.agent.target.is_some() { BehaviorTree::target().run_with_behavior_data(bdata); @@ -273,9 +293,11 @@ fn do_target_tree_if_target_else_do_idle_tree(bdata: &mut BehaviorData) -> bool false } +/// Run the Idle BehaviorTree +/// +/// This function can stop the BehaviorTree fn do_idle_tree(bdata: &mut BehaviorData) -> bool { - BehaviorTree::idle().run_with_behavior_data(bdata); - true + BehaviorTree::idle().run_with_behavior_data(bdata) } /// If target is dead, forget them @@ -299,7 +321,7 @@ fn untarget_if_dead(bdata: &mut BehaviorData) -> bool { false } -/// If target is hostile, hostile tree +/// If target is hostile, do the hostile tree and stop the current BehaviorTree fn do_hostile_tree_if_hostile(bdata: &mut BehaviorData) -> bool { if let Some(Target { hostile, .. }) = bdata.agent.target { if hostile { @@ -310,7 +332,7 @@ fn do_hostile_tree_if_hostile(bdata: &mut BehaviorData) -> bool { false } -/// if owned, act as pet to them +/// if owned, do the pet tree and stop the current BehaviorTree fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool { if let (Some(Target { target, .. }), Some(Alignment::Owned(uid))) = (bdata.agent.target, bdata.agent_data.alignment) @@ -326,6 +348,7 @@ fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool { false } +/// If the target is an ItemDrop, go pick it up fn do_pickup_loot(bdata: &mut BehaviorData) -> bool { if let Some(Target { target, .. }) = bdata.agent.target { if matches!(bdata.read_data.bodies.get(target), Some(Body::ItemDrop(_))) { @@ -360,12 +383,15 @@ fn do_pickup_loot(bdata: &mut BehaviorData) -> bool { false } +/// Reset the agent's target +/// +/// This function will never stop the BehaviorTree fn untarget(bdata: &mut BehaviorData) -> bool { bdata.agent.target = None; false } -// If too far away, then follow +// If too far away, then follow the target fn follow_if_far_away(bdata: &mut BehaviorData) -> bool { if let Some(Target { target, .. }) = bdata.agent.target { if let Some(tgt_pos) = bdata.read_data.positions.get(target) { @@ -426,7 +452,8 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool { false } -fn handle_timer(bdata: &mut BehaviorData) -> bool { +/// Handle timed events, like looking at the player we are talking to +fn handle_timed_events(bdata: &mut BehaviorData) -> bool { let timeout = if bdata.agent.behavior.is(BehaviorState::TRADING) { TRADE_INTERACTION_TIME } else { @@ -470,6 +497,7 @@ fn handle_timer(bdata: &mut BehaviorData) -> bool { false } +/// Try to heal self if our damage went below a certain threshold fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool { if bdata.agent_data.damage < HEALING_ITEM_THRESHOLD && bdata @@ -482,9 +510,9 @@ fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool { false } +/// Hurt utterances at random upon receiving damage fn hurt_utterance(bdata: &mut BehaviorData) -> bool { if matches!(bdata.agent.inbox.front(), Some(AgentEvent::Hurt)) { - // Hurt utterances at random upon receiving damage if bdata.rng.gen::() < 0.4 { bdata.controller.push_utterance(UtteranceKind::Hurt); } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 44271f4f13..f04d237d99 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -56,6 +56,7 @@ pub fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { false } +/// Increment agent's action_state timer pub fn increment_timer_deltatime(bdata: &mut BehaviorData) -> bool { bdata.agent.action_state.timer += bdata.read_data.dt.0; false @@ -344,6 +345,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { true } +/// Handles TradeInvite event if the front of the agent's inbox contains one pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, @@ -391,6 +393,7 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { true } +/// Handles TradeAccepted event if the front of the agent's inbox contains one pub fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, read_data, .. @@ -412,6 +415,7 @@ pub fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool { true } +/// Handles TradeFinished event if the front of the agent's inbox contains one pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, @@ -440,6 +444,8 @@ pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool { true } +/// Handles UpdatePendingTrade event if the front of the agent's inbox contains +/// one pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, @@ -519,6 +525,12 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { true } +/// Deny any received interaction: +/// - `AgentEvent::Talk` and `AgentEvent::TradeAccepted` are cut short by an +/// "I'm busy" message +/// - `AgentEvent::TradeInvite` are denied +/// - `AgentEvent::FinishedTrade` are still handled +/// - `AgentEvent::UpdatePendingTrade` will immediately close the trade pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, From b82da6acdb77c6bd5225d2ad1377ec4e515ce04a Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Sun, 7 Aug 2022 21:11:58 +0200 Subject: [PATCH 08/10] Reimplement !3508 --- .../sys/agent/behavior_tree/interaction.rs | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index f04d237d99..7f64015100 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -153,18 +153,18 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { if !agent.behavior.is(BehaviorState::TRADING) { controller.push_initiate_invite(by, InviteKind::Trade); agent_data.chat_npc( - "npc.speech.merchant_advertisement", + "npc-speech-merchant_advertisement", event_emitter, ); } else { - let default_msg = "npc.speech.merchant_busy"; + let default_msg = "npc-speech-merchant_busy"; let msg = agent_data.rtsim_entity.map_or(default_msg, |e| { if e.brain .personality .personality_traits .contains(PersonalityTrait::Disagreeable) { - "npc.speech.merchant_busy_rude" + "npc-speech-merchant_busy_rude" } else { default_msg } @@ -178,48 +178,48 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { .and_then(|e| e.brain.personality.random_chat_trait(&mut rng)) { let msg = match extreme_trait { - PersonalityTrait::Open => "npc.speech.villager_open", + PersonalityTrait::Open => "npc-speech-villager_open", PersonalityTrait::Adventurous => { - "npc.speech.villager_adventurous" + "npc-speech-villager_adventurous" }, PersonalityTrait::Closed => "npc.speech.villager_closed", PersonalityTrait::Conscientious => { - "npc.speech.villager_conscientious" + "npc-speech-villager_conscientious" }, PersonalityTrait::Busybody => { - "npc.speech.villager_busybody" + "npc-speech-villager_busybody" }, PersonalityTrait::Unconscientious => { - "npc.speech.villager_unconscientious" + "npc-speech-villager_unconscientious" }, PersonalityTrait::Extroverted => { - "npc.speech.villager_extroverted" + "npc-speech-villager_extroverted" }, PersonalityTrait::Introverted => { - "npc.speech.villager_introverted" + "npc-speech-villager_introverted" }, PersonalityTrait::Agreeable => { - "npc.speech.villager_agreeable" + "npc-speech-villager_agreeable" }, PersonalityTrait::Sociable => { - "npc.speech.villager_sociable" + "npc-speech-villager_sociable" }, PersonalityTrait::Disagreeable => { - "npc.speech.villager_disagreeable" + "npc-speech-villager_disagreeable" }, PersonalityTrait::Neurotic => { - "npc.speech.villager_neurotic" + "npc-speech-villager_neurotic" }, - PersonalityTrait::Seeker => "npc.speech.villager_seeker", + PersonalityTrait::Seeker => "npc-speech-villager_seeker", PersonalityTrait::SadLoner => { - "npc.speech.villager_sad_loner" + "npc-speech-villager_sad_loner" }, - PersonalityTrait::Worried => "npc.speech.villager_worried", - PersonalityTrait::Stable => "npc.speech.villager_stable", + PersonalityTrait::Worried => "npc-speech-villager_worried", + PersonalityTrait::Stable => "npc-speech-villager_stable", }; agent_data.chat_npc(msg, event_emitter); } else { - agent_data.chat_npc("npc.speech.villager", event_emitter); + agent_data.chat_npc("npc-speech-villager", event_emitter); } } }, @@ -228,17 +228,17 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { if !agent.behavior.is(BehaviorState::TRADING) { controller.push_initiate_invite(by, InviteKind::Trade); agent_data.chat_npc( - "npc.speech.merchant_advertisement", + "npc-speech-merchant_advertisement", event_emitter, ); } else { - agent_data.chat_npc("npc.speech.merchant_busy", event_emitter); + agent_data.chat_npc("npc-speech-merchant_busy", event_emitter); } } else { // TODO: maybe make some travellers willing to trade with // simpler goods like potions agent_data - .chat_npc("npc.speech.villager_decline_trade", event_emitter); + .chat_npc("npc-speech-villager_decline_trade", event_emitter); } }, Subject::Mood => { @@ -375,7 +375,7 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { } else { controller.push_invite_response(InviteResponse::Decline); agent_data.chat_npc_if_allowed_to_speak( - "npc.speech.merchant_busy", + "npc-speech-merchant_busy", agent, event_emitter, ); @@ -384,7 +384,7 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { // TODO: Provide a hint where to find the closest merchant? controller.push_invite_response(InviteResponse::Decline); agent_data.chat_npc_if_allowed_to_speak( - "npc.speech.villager_decline_trade", + "npc-speech-villager_decline_trade", agent, event_emitter, ); @@ -432,10 +432,10 @@ pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool { if agent.behavior.is(BehaviorState::TRADING) { match result { TradeResult::Completed => { - agent_data.chat_npc("npc.speech.merchant_trade_successful", event_emitter); + agent_data.chat_npc("npc-speech-merchant_trade_successful", event_emitter); }, _ => { - agent_data.chat_npc("npc.speech.merchant_trade_declined", event_emitter); + agent_data.chat_npc("npc-speech-merchant_trade_declined", event_emitter); }, } agent.behavior.unset(BehaviorState::TRADING); @@ -549,7 +549,7 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { // in combat, speak to players that aren't the current target if !target.hostile || target.target != speaker { agent_data.chat_npc_if_allowed_to_speak( - "npc.speech.villager_busy", + "npc-speech-villager_busy", agent, event_emitter, ); @@ -567,13 +567,13 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { if !target.hostile || target.target != speaker { if agent.behavior.can_trade() { agent_data.chat_npc_if_allowed_to_speak( - "npc.speech.merchant_busy", + "npc-speech-merchant_busy", agent, event_emitter, ); } else { agent_data.chat_npc_if_allowed_to_speak( - "npc.speech.villager_busy", + "npc-speech-villager_busy", agent, event_emitter, ); @@ -589,11 +589,11 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { match result { TradeResult::Completed => { agent_data - .chat_npc("npc.speech.merchant_trade_successful", event_emitter); + .chat_npc("npc-speech-merchant_trade_successful", event_emitter); }, _ => { agent_data - .chat_npc("npc.speech.merchant_trade_declined", event_emitter); + .chat_npc("npc-speech-merchant_trade_declined", event_emitter); }, } agent.behavior.unset(BehaviorState::TRADING); @@ -609,7 +609,7 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { *tradeid, TradeAction::Decline, )); - agent_data.chat_npc("npc.speech.merchant_trade_cancelled_hostile", event_emitter); + agent_data.chat_npc("npc-speech-merchant_trade_cancelled_hostile", event_emitter); true }, AgentEvent::ServerSound(_) | AgentEvent::Hurt => false, From c1dcb6e4122fc30328785a85e27e4144efd0c716 Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Sun, 7 Aug 2022 21:16:31 +0200 Subject: [PATCH 09/10] Fix missing translation key in interaction --- server/src/sys/agent/behavior_tree/interaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 7f64015100..042a7f4ece 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -182,7 +182,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { PersonalityTrait::Adventurous => { "npc-speech-villager_adventurous" }, - PersonalityTrait::Closed => "npc.speech.villager_closed", + PersonalityTrait::Closed => "npc-speech-villager_closed", PersonalityTrait::Conscientious => { "npc-speech-villager_conscientious" }, From b1baa09815ccd4e641289b3a434bea48c80b7203 Mon Sep 17 00:00:00 2001 From: Vincent Foulon Date: Thu, 11 Aug 2022 21:15:46 +0200 Subject: [PATCH 10/10] Address review comments --- server/src/sys/agent.rs | 18 +++--- server/src/sys/agent/behavior_tree.rs | 60 ++++++------------- .../sys/agent/behavior_tree/interaction.rs | 2 +- 3 files changed, 28 insertions(+), 52 deletions(-) diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index ac0d34f8e6..490a03b495 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -7,7 +7,7 @@ pub mod util; use crate::{ rtsim::RtSim, sys::agent::{ - behavior_tree::BehaviorTree, + behavior_tree::{BehaviorData, BehaviorTree}, consts::{ AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, PARTIAL_PATH_DIST, SEPARATION_BIAS, SEPARATION_DIST, @@ -122,7 +122,7 @@ impl<'a> System<'a> for Sys { _, )| { let mut event_emitter = event_bus.emitter(); - // let mut rng = thread_rng(); + let mut rng = thread_rng(); // Hack, replace with better system when groups are more sophisticated // Override alignment if in a group unless entity is owned already @@ -245,14 +245,16 @@ impl<'a> System<'a> for Sys { // also methods on the `AgentData` struct. Action nodes // are the only parts of this tree that should provide // inputs. - - BehaviorTree::root().run( + let mut behavior_data = BehaviorData { agent, - data, - &read_data, - &mut event_emitter, + agent_data: data, + read_data: &read_data, + event_emitter: &mut event_emitter, controller, - ); + rng: &mut rng, + }; + + BehaviorTree::root().run(&mut behavior_data); debug_assert!(controller.inputs.move_dir.map(|e| !e.is_nan()).reduce_and()); debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index e5879c0fe2..e4b6dc79ca 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -9,7 +9,7 @@ use common::{ event::{Emitter, ServerEvent}, path::TraversalConfig, }; -use rand::{prelude::ThreadRng, thread_rng, Rng}; +use rand::{prelude::ThreadRng, Rng}; use specs::saveload::{Marker, MarkerAllocator}; use vek::Vec2; @@ -32,12 +32,12 @@ mod interaction; /// Struct containing essential data for running a behavior tree pub struct BehaviorData<'a, 'b, 'c> { - agent: &'a mut Agent, - agent_data: AgentData<'a>, - read_data: &'a ReadData<'a>, - event_emitter: &'a mut Emitter<'c, ServerEvent>, - controller: &'a mut Controller, - rng: &'b mut ThreadRng, + pub agent: &'a mut Agent, + pub agent_data: AgentData<'a>, + pub read_data: &'a ReadData<'a>, + pub event_emitter: &'a mut Emitter<'c, ServerEvent>, + pub controller: &'a mut Controller, + pub rng: &'b mut ThreadRng, } /// Behavior function @@ -88,7 +88,7 @@ impl BehaviorTree { /// Pet BehaviorTree /// - /// Follow the owner and attack ennemies + /// Follow the owner and attack enemies pub fn pet() -> Self { Self { tree: vec![follow_if_far_away, attack_if_owner_hurt, do_idle_tree], @@ -101,11 +101,7 @@ impl BehaviorTree { /// talk. If not, or if we are in combat, deny all talk and trade /// events. pub fn interaction(agent: &Agent) -> Self { - let is_in_combat = if let Some(Target { hostile, .. }) = agent.target { - hostile - } else { - false - }; + let is_in_combat = agent.target.map_or(false, |t| t.hostile); if !is_in_combat && agent.behavior.can(BehaviorCapability::SPEAK) { Self { tree: vec![ @@ -141,29 +137,9 @@ impl BehaviorTree { } /// Run the behavior tree until an event has been handled - pub fn run<'a, 'b>( - &self, - agent: &'a mut Agent, - agent_data: AgentData<'a>, - read_data: &'a ReadData, - event_emitter: &'a mut Emitter<'b, ServerEvent>, - controller: &'a mut Controller, - ) -> bool { - let mut behavior_data = BehaviorData { - agent, - agent_data, - read_data, - event_emitter, - controller, - rng: &mut thread_rng(), - }; - - self.run_with_behavior_data(&mut behavior_data) - } - - fn run_with_behavior_data(&self, bdata: &mut BehaviorData) -> bool { + pub fn run(&self, behavior_data: &mut BehaviorData) -> bool { for behavior_fn in self.tree.iter() { - if behavior_fn(bdata) { + if behavior_fn(behavior_data) { return true; } } @@ -286,9 +262,9 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool { /// This function will never stop the BehaviorTree fn do_target_tree_if_target_else_do_idle_tree(bdata: &mut BehaviorData) -> bool { if bdata.agent.target.is_some() { - BehaviorTree::target().run_with_behavior_data(bdata); + BehaviorTree::target().run(bdata); } else { - BehaviorTree::idle().run_with_behavior_data(bdata); + BehaviorTree::idle().run(bdata); } false } @@ -296,9 +272,7 @@ fn do_target_tree_if_target_else_do_idle_tree(bdata: &mut BehaviorData) -> bool /// Run the Idle BehaviorTree /// /// This function can stop the BehaviorTree -fn do_idle_tree(bdata: &mut BehaviorData) -> bool { - BehaviorTree::idle().run_with_behavior_data(bdata) -} +fn do_idle_tree(bdata: &mut BehaviorData) -> bool { BehaviorTree::idle().run(bdata) } /// If target is dead, forget them fn untarget_if_dead(bdata: &mut BehaviorData) -> bool { @@ -325,7 +299,7 @@ fn untarget_if_dead(bdata: &mut BehaviorData) -> bool { fn do_hostile_tree_if_hostile(bdata: &mut BehaviorData) -> bool { if let Some(Target { hostile, .. }) = bdata.agent.target { if hostile { - BehaviorTree::hostile().run_with_behavior_data(bdata); + BehaviorTree::hostile().run(bdata); return true; } } @@ -338,10 +312,10 @@ fn do_pet_tree_if_owned(bdata: &mut BehaviorData) -> bool { (bdata.agent.target, bdata.agent_data.alignment) { if bdata.read_data.uids.get(target) == Some(uid) { - BehaviorTree::pet().run_with_behavior_data(bdata); + BehaviorTree::pet().run(bdata); } else { bdata.agent.target = None; - BehaviorTree::idle().run_with_behavior_data(bdata); + BehaviorTree::idle().run(bdata); } return true; } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 042a7f4ece..f0216ab0b6 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -47,7 +47,7 @@ pub fn process_inbox_sound_and_hurt(bdata: &mut BehaviorData) -> bool { /// If we receive a new interaction, start the interaction timer pub fn process_inbox_interaction(bdata: &mut BehaviorData) -> bool { - if BehaviorTree::interaction(bdata.agent).run_with_behavior_data(bdata) { + if BehaviorTree::interaction(bdata.agent).run(bdata) { bdata .agent .timer