mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Move recv_interactions into BehaviorTree + cleanup functions ^& warnings
This commit is contained in:
parent
d105d7063c
commit
faf2b13ac1
@ -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::<f32>() < 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::<f32>() < 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::<u32>() % 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_::<f32>() - 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,
|
||||
|
@ -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::<u32>() % 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_::<f32>() - 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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user