mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Implement a basic dialogue system
This commit is contained in:
parent
206efcfc3a
commit
a35fa19409
@ -10,6 +10,8 @@ use specs_idvs::IdvStorage;
|
||||
use std::collections::VecDeque;
|
||||
use vek::*;
|
||||
|
||||
use super::dialogue::Subject;
|
||||
|
||||
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
|
||||
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
|
||||
|
||||
@ -179,8 +181,9 @@ impl<'a> From<&'a Body> for Psyche {
|
||||
/// Events that affect agent behavior from other entities/players/environment
|
||||
pub enum AgentEvent {
|
||||
/// Engage in conversation with entity with Uid
|
||||
Talk(Uid),
|
||||
Talk(Uid, Subject),
|
||||
TradeInvite(Uid),
|
||||
TradeAccepted(Uid),
|
||||
FinishedTrade(TradeResult),
|
||||
UpdatePendingTrade(
|
||||
// this data structure is large so box it to keep AgentEvent small
|
||||
@ -212,6 +215,7 @@ pub struct Agent {
|
||||
pub can_speak: bool,
|
||||
pub trade_for_site: Option<SiteId>,
|
||||
pub trading: bool,
|
||||
pub trading_issuer: bool,
|
||||
pub psyche: Psyche,
|
||||
pub inbox: VecDeque<AgentEvent>,
|
||||
pub action_timer: f32,
|
||||
|
90
common/src/comp/compass.rs
Normal file
90
common/src/comp/compass.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use vek::Vec2;
|
||||
|
||||
/// Cardinal directions
|
||||
pub enum Direction {
|
||||
North,
|
||||
Northeast,
|
||||
East,
|
||||
Southeast,
|
||||
South,
|
||||
Southwest,
|
||||
West,
|
||||
Northwest,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
/// Convert a direction vector to a cardinal direction
|
||||
/// Direction vector can be trivially calculated by doing (target - source)
|
||||
pub fn from_dir(dir: Vec2<f32>) -> Self {
|
||||
if let Some(dir) = dir.try_normalized() {
|
||||
let mut angle = dir.angle_between(Vec2::unit_y()).to_degrees();
|
||||
if dir.x < 0.0 {
|
||||
angle = -angle;
|
||||
}
|
||||
match angle as i32 {
|
||||
-360..=-157 => Direction::South,
|
||||
-156..=-112 => Direction::Southwest,
|
||||
-111..=-67 => Direction::West,
|
||||
-66..=-22 => Direction::Northwest,
|
||||
-21..=22 => Direction::North,
|
||||
23..=67 => Direction::Northeast,
|
||||
68..=112 => Direction::East,
|
||||
113..=157 => Direction::Southeast,
|
||||
158..=360 => Direction::South,
|
||||
_ => Direction::North, // should never happen
|
||||
}
|
||||
} else {
|
||||
Direction::North // default value, should never happen
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: localization
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Direction::North => "North",
|
||||
Direction::Northeast => "Northeast",
|
||||
Direction::East => "East",
|
||||
Direction::Southeast => "Southeast",
|
||||
Direction::South => "South",
|
||||
Direction::Southwest => "Southwest",
|
||||
Direction::West => "West",
|
||||
Direction::Northwest => "Northwest",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Arbitrarily named Distances
|
||||
pub enum Distance {
|
||||
VeryFar,
|
||||
Far,
|
||||
Ahead,
|
||||
Near,
|
||||
NextTo,
|
||||
}
|
||||
|
||||
impl Distance {
|
||||
/// Convert a length to a Distance
|
||||
pub fn from_length(length: i32) -> Self {
|
||||
match length {
|
||||
0..=100 => Distance::NextTo,
|
||||
101..=500 => Distance::Near,
|
||||
501..=3000 => Distance::Ahead,
|
||||
3001..=10000 => Distance::Far,
|
||||
_ => Distance::VeryFar,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a vector to a Distance
|
||||
pub fn from_dir(dir: Vec2<f32>) -> Self { Self::from_length(dir.magnitude() as i32) }
|
||||
|
||||
// TODO: localization
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Distance::VeryFar => "very far",
|
||||
Distance::Far => "far",
|
||||
Distance::Ahead => "ahead",
|
||||
Distance::Near => "near",
|
||||
Distance::NextTo => "just around",
|
||||
}
|
||||
}
|
||||
}
|
124
common/src/comp/dialogue.rs
Normal file
124
common/src/comp/dialogue.rs
Normal file
@ -0,0 +1,124 @@
|
||||
use vek::{Vec2, Vec3};
|
||||
|
||||
use super::Item;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AskedLocation {
|
||||
pub name: String,
|
||||
pub origin: Vec2<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PersonType {
|
||||
Merchant,
|
||||
Villager { name: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AskedPerson {
|
||||
pub person_type: PersonType,
|
||||
pub origin: Option<Vec3<f32>>,
|
||||
}
|
||||
|
||||
impl AskedPerson {
|
||||
pub fn name(&self) -> String {
|
||||
match &self.person_type {
|
||||
PersonType::Merchant => "The Merchant".to_string(),
|
||||
PersonType::Villager { name } => name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Conversation subject
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Subject {
|
||||
/// Using simple interaction with NPC
|
||||
/// This is meant to be the default behavior of talking
|
||||
/// NPC will throw a random dialogue to you
|
||||
Regular,
|
||||
/// Asking for trading
|
||||
/// Ask the person to trade with you
|
||||
/// NPC will either invite you to trade, or decline
|
||||
Trade,
|
||||
/// Inquiring the mood of the NPC
|
||||
/// NPC will explain what his mood is, and why.
|
||||
/// Can lead to potential quests if the NPC has a bad mood
|
||||
/// Else it'll just be flavor text explaining why he got this mood
|
||||
Mood,
|
||||
/// Asking for a location
|
||||
/// NPC will either know where this location is, or not
|
||||
/// It'll tell you which direction and approx what distance it is from you
|
||||
Location(AskedLocation),
|
||||
/// Asking for a person's location
|
||||
/// NPC will either know where this person is, or not
|
||||
/// It'll tell you which direction and approx what distance it is from you
|
||||
Person(AskedPerson),
|
||||
/// Asking for work
|
||||
/// NPC will give you a quest if his mood is bad enough
|
||||
/// So either it'll tell you something to do, or just say that he got
|
||||
/// nothing
|
||||
Work,
|
||||
}
|
||||
|
||||
/// Context of why a NPC has a specific mood (good, neutral, bad, ...)
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MoodContext {
|
||||
/// The weather is good, sunny, appeasing, etc...
|
||||
GoodWeather,
|
||||
/// Someone completed a quest and enlightened this NPC's day
|
||||
QuestSucceeded { hero: String, quest_desc: String },
|
||||
|
||||
/// Normal day, same as yesterday, nothing relevant to say about it, that's
|
||||
/// everyday life
|
||||
EverydayLife,
|
||||
/// Need one or more items in order to complete a personal task, or for
|
||||
/// working
|
||||
NeedItem { item: Item, quantity: u16 },
|
||||
|
||||
/// A personal good has been robbed! Gotta find a replacement
|
||||
MissingItem { item: Item },
|
||||
}
|
||||
|
||||
// Note: You can add in-between states if needed
|
||||
/// NPC mood status indicator
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MoodState {
|
||||
/// The NPC is happy!
|
||||
Good(MoodContext),
|
||||
/// The NPC is having a normal day
|
||||
Neutral(MoodContext),
|
||||
/// The NPC got a pretty bad day. He may even need player's help!
|
||||
Bad(MoodContext),
|
||||
}
|
||||
|
||||
// TODO: dialogue localization
|
||||
impl MoodState {
|
||||
pub fn describe(&self) -> String {
|
||||
match self {
|
||||
MoodState::Good(context) => format!("I'm so happy, {}", context.describe()),
|
||||
MoodState::Neutral(context) => context.describe(),
|
||||
MoodState::Bad(context) => {
|
||||
format!("I'm mad, {}", context.describe())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: dialogue localization
|
||||
impl MoodContext {
|
||||
pub fn describe(&self) -> String {
|
||||
match &self {
|
||||
MoodContext::GoodWeather => "The weather is great today!".to_string(),
|
||||
MoodContext::QuestSucceeded { hero, quest_desc } => {
|
||||
format!("{} helped me on {}", hero, quest_desc)
|
||||
},
|
||||
&MoodContext::EverydayLife => "Life's going as always.".to_string(),
|
||||
MoodContext::NeedItem { item, quantity } => {
|
||||
format!("I need {} {}!", quantity, item.name())
|
||||
},
|
||||
&MoodContext::MissingItem { item } => {
|
||||
format!("Someone robbed my {}!", item.name())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -9,8 +9,10 @@ pub mod buff;
|
||||
mod character_state;
|
||||
#[cfg(not(target_arch = "wasm32"))] pub mod chat;
|
||||
#[cfg(not(target_arch = "wasm32"))] pub mod combo;
|
||||
pub mod compass;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod controller;
|
||||
pub mod dialogue;
|
||||
#[cfg(not(target_arch = "wasm32"))] mod energy;
|
||||
#[cfg(not(target_arch = "wasm32"))] pub mod group;
|
||||
mod health;
|
||||
|
@ -7,6 +7,8 @@ use specs::Component;
|
||||
use specs_idvs::IdvStorage;
|
||||
use vek::*;
|
||||
|
||||
use crate::comp::dialogue::MoodState;
|
||||
|
||||
pub type RtSimId = usize;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@ -19,6 +21,7 @@ impl Component for RtSimEntity {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RtSimEvent {
|
||||
AddMemory(Memory),
|
||||
SetMood(Memory),
|
||||
PrintMemories,
|
||||
}
|
||||
|
||||
@ -34,6 +37,7 @@ pub enum MemoryItem {
|
||||
// such as clothing worn, weapon used, etc.
|
||||
CharacterInteraction { name: String },
|
||||
CharacterFight { name: String },
|
||||
Mood { state: MoodState },
|
||||
}
|
||||
|
||||
/// This type is the map route through which the rtsim (real-time simulation)
|
||||
|
@ -4,8 +4,8 @@ use vek::*;
|
||||
|
||||
use common::{
|
||||
comp::{
|
||||
self, agent::AgentEvent, inventory::slot::EquipSlot, item, slot::Slot, tool::ToolKind,
|
||||
Inventory, Pos,
|
||||
self, agent::AgentEvent, dialogue::Subject, inventory::slot::EquipSlot, item, slot::Slot,
|
||||
tool::ToolKind, Inventory, Pos,
|
||||
},
|
||||
consts::MAX_MOUNT_RANGE,
|
||||
outcome::Outcome,
|
||||
@ -70,7 +70,9 @@ pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_en
|
||||
.get_mut(npc_entity)
|
||||
{
|
||||
if let Some(interactor_uid) = state.ecs().uid_from_entity(interactor) {
|
||||
agent.inbox.push_front(AgentEvent::Talk(interactor_uid));
|
||||
agent
|
||||
.inbox
|
||||
.push_front(AgentEvent::Talk(interactor_uid, Subject::Regular));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
|
||||
let state = server.state_mut();
|
||||
let clients = state.ecs().read_storage::<Client>();
|
||||
let uids = state.ecs().read_storage::<Uid>();
|
||||
let agents = state.ecs().read_storage::<Agent>();
|
||||
let mut agents = state.ecs().write_storage::<Agent>();
|
||||
let mut invites = state.ecs().write_storage::<Invite>();
|
||||
if let Some((inviter, kind)) = invites.remove(entity).and_then(|invite| {
|
||||
let Invite { inviter, kind } = invite;
|
||||
@ -218,6 +218,11 @@ pub fn handle_invite_accept(server: &mut Server, entity: specs::Entity) {
|
||||
let mut trades = state.ecs().write_resource::<Trades>();
|
||||
let id = trades.begin_trade(inviter_uid, invitee_uid);
|
||||
let trade = trades.trades[&id].clone();
|
||||
if let Some(agent) = agents.get_mut(inviter) {
|
||||
agent
|
||||
.inbox
|
||||
.push_front(AgentEvent::TradeAccepted(invitee_uid));
|
||||
}
|
||||
let pricing = agents
|
||||
.get(inviter)
|
||||
.and_then(|a| index.get_site_prices(a))
|
||||
|
@ -206,6 +206,33 @@ pub struct Brain {
|
||||
impl Brain {
|
||||
pub fn add_memory(&mut self, memory: Memory) { self.memories.push(memory); }
|
||||
|
||||
pub fn remembers_mood(&self) -> bool {
|
||||
self.memories
|
||||
.iter()
|
||||
.any(|memory| matches!(&memory.item, MemoryItem::Mood { .. }))
|
||||
}
|
||||
|
||||
pub fn set_mood(&mut self, memory: Memory) {
|
||||
if let MemoryItem::Mood { .. } = memory.item {
|
||||
if self.remembers_mood() {
|
||||
while let Some(position) = self
|
||||
.memories
|
||||
.iter()
|
||||
.position(|mem| matches!(&mem.item, MemoryItem::Mood { .. }))
|
||||
{
|
||||
self.memories.remove(position);
|
||||
}
|
||||
}
|
||||
self.add_memory(memory);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_mood(&self) -> Option<&Memory> {
|
||||
self.memories
|
||||
.iter()
|
||||
.find(|memory| matches!(&memory.item, MemoryItem::Mood { .. }))
|
||||
}
|
||||
|
||||
pub fn remembers_character(&self, name_to_remember: &str) -> bool {
|
||||
self.memories.iter().any(|memory| matches!(&memory.item, MemoryItem::CharacterInteraction { name, .. } if name == name_to_remember))
|
||||
}
|
||||
|
@ -81,6 +81,12 @@ impl RtSim {
|
||||
.get_mut(entity)
|
||||
.map(|entity| entity.brain.add_memory(memory));
|
||||
}
|
||||
|
||||
pub fn set_entity_mood(&mut self, entity: RtSimId, memory: Memory) {
|
||||
self.entities
|
||||
.get_mut(entity)
|
||||
.map(|entity| entity.brain.set_mood(memory));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||
|
@ -4,9 +4,11 @@ use common::{
|
||||
self,
|
||||
agent::{AgentEvent, Tactic, Target, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME},
|
||||
buff::{BuffKind, Buffs},
|
||||
compass::{Direction, Distance},
|
||||
dialogue::{MoodContext, MoodState, Subject},
|
||||
group,
|
||||
inventory::{item::ItemTag, slot::EquipSlot, trade_pricing::TradePricing},
|
||||
invite::InviteResponse,
|
||||
invite::{InviteKind, InviteResponse},
|
||||
item::{
|
||||
tool::{ToolKind, UniqueKind},
|
||||
ItemDesc, ItemKind,
|
||||
@ -505,8 +507,14 @@ impl<'a> System<'a> for Sys {
|
||||
// Entity must be loaded in as it has an agent component :)
|
||||
// React to all events in the controller
|
||||
for event in core::mem::take(&mut agent.rtsim_controller.events) {
|
||||
if let RtSimEvent::AddMemory(memory) = event {
|
||||
rtsim.insert_entity_memory(rtsim_entity.0, memory.clone());
|
||||
match event {
|
||||
RtSimEvent::AddMemory(memory) => {
|
||||
rtsim.insert_entity_memory(rtsim_entity.0, memory.clone())
|
||||
},
|
||||
RtSimEvent::SetMood(memory) => {
|
||||
rtsim.set_entity_mood(rtsim_entity.0, memory.clone())
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -838,7 +846,7 @@ impl<'a> AgentData<'a> {
|
||||
agent.action_timer += read_data.dt.0;
|
||||
let msg = agent.inbox.pop_back();
|
||||
match msg {
|
||||
Some(AgentEvent::Talk(by)) => {
|
||||
Some(AgentEvent::Talk(by, subject)) => {
|
||||
if agent.can_speak {
|
||||
if let Some(target) = read_data.uid_allocator.retrieve_entity_internal(by.id())
|
||||
{
|
||||
@ -847,64 +855,197 @@ impl<'a> AgentData<'a> {
|
||||
hostile: false,
|
||||
selected_at: read_data.time.0,
|
||||
});
|
||||
if let Some(tgt_pos) = read_data.positions.get(target) {
|
||||
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
|
||||
let tgt_eye_offset =
|
||||
read_data.bodies.get(target).map_or(0.0, |b| b.eye_height());
|
||||
if let Some(dir) = Dir::from_unnormalized(
|
||||
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
|
||||
- Vec3::new(
|
||||
self.pos.0.x,
|
||||
self.pos.0.y,
|
||||
self.pos.0.z + eye_offset,
|
||||
),
|
||||
) {
|
||||
controller.inputs.look_dir = dir;
|
||||
}
|
||||
|
||||
if self.look_toward(controller, read_data, &target) {
|
||||
controller.actions.push(ControlAction::Talk);
|
||||
if let (Some((_travel_to, destination_name)), Some(rtsim_entity)) =
|
||||
(&agent.rtsim_controller.travel_to, &self.rtsim_entity)
|
||||
{
|
||||
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) {
|
||||
format!(
|
||||
"Greetings fair {}! It has been far too long since \
|
||||
last I saw you.",
|
||||
&tgt_stats.name
|
||||
)
|
||||
match subject {
|
||||
Subject::Regular => {
|
||||
if let (
|
||||
Some((_travel_to, destination_name)),
|
||||
Some(rtsim_entity),
|
||||
) = (&agent.rtsim_controller.travel_to, &self.rtsim_entity)
|
||||
{
|
||||
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)
|
||||
{
|
||||
format!(
|
||||
"Greetings fair {}! It has been far too \
|
||||
long since last I saw you.",
|
||||
&tgt_stats.name
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"I'm heading to {}! Want to come along?",
|
||||
destination_name
|
||||
)
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
"I'm heading to {}! Want to come along?",
|
||||
destination_name
|
||||
)
|
||||
};
|
||||
event_emitter.emit(ServerEvent::Chat(
|
||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
||||
));
|
||||
} else if agent.trade_for_site.is_some() {
|
||||
let msg = "Can I interest you in a trade?".to_string();
|
||||
event_emitter.emit(ServerEvent::Chat(
|
||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
||||
));
|
||||
} else {
|
||||
format!(
|
||||
"I'm heading to {}! Want to come along?",
|
||||
destination_name
|
||||
)
|
||||
let msg = "npc.speech.villager".to_string();
|
||||
event_emitter.emit(ServerEvent::Chat(
|
||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
"I'm heading to {}! Want to come along?",
|
||||
destination_name
|
||||
)
|
||||
};
|
||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
||||
*self.uid, msg,
|
||||
)));
|
||||
} else if agent.trade_for_site.is_some() {
|
||||
let msg = "Can I interest you in a trade?".to_string();
|
||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
||||
*self.uid, msg,
|
||||
)));
|
||||
} else {
|
||||
let msg = "npc.speech.villager".to_string();
|
||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
||||
*self.uid, msg,
|
||||
)));
|
||||
},
|
||||
Subject::Trade => {
|
||||
if agent.trade_for_site.is_some() && !agent.trading {
|
||||
controller.events.push(ControlEvent::InitiateInvite(
|
||||
by,
|
||||
InviteKind::Trade,
|
||||
));
|
||||
let msg = "Can I interest you in a trade?".to_string();
|
||||
event_emitter.emit(ServerEvent::Chat(
|
||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
||||
));
|
||||
} else {
|
||||
// TODO: maybe make some travellers willing to trade with
|
||||
// simpler goods like potions
|
||||
event_emitter.emit(ServerEvent::Chat(
|
||||
UnresolvedChatMsg::npc(
|
||||
*self.uid,
|
||||
"Sorry, I don't have anything to trade."
|
||||
.to_string(),
|
||||
),
|
||||
));
|
||||
}
|
||||
},
|
||||
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(),
|
||||
};
|
||||
event_emitter.emit(ServerEvent::Chat(
|
||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
Subject::Location(location) => {
|
||||
if let Some(tgt_pos) = read_data.positions.get(target) {
|
||||
event_emitter.emit(ServerEvent::Chat(
|
||||
UnresolvedChatMsg::npc(
|
||||
*self.uid,
|
||||
format!(
|
||||
"{} ? I think it's {} {} from here!",
|
||||
location.name,
|
||||
Distance::from_dir(
|
||||
location.origin.as_::<f32>()
|
||||
- tgt_pos.0.xy()
|
||||
)
|
||||
.name(),
|
||||
Direction::from_dir(
|
||||
location.origin.as_::<f32>()
|
||||
- tgt_pos.0.xy()
|
||||
)
|
||||
.name()
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
},
|
||||
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()
|
||||
)
|
||||
};
|
||||
event_emitter.emit(ServerEvent::Chat(
|
||||
UnresolvedChatMsg::npc(*self.uid, msg),
|
||||
));
|
||||
}
|
||||
},
|
||||
Subject::Work => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -926,12 +1067,34 @@ impl<'a> AgentData<'a> {
|
||||
controller
|
||||
.events
|
||||
.push(ControlEvent::InviteResponse(InviteResponse::Accept));
|
||||
agent.trading_issuer = false;
|
||||
agent.trading = true;
|
||||
} else {
|
||||
// TODO: Provide a hint where to find the closest merchant?
|
||||
controller
|
||||
.events
|
||||
.push(ControlEvent::InviteResponse(InviteResponse::Decline));
|
||||
if agent.can_speak {
|
||||
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
|
||||
*self.uid,
|
||||
"Sorry, I don't have anything to trade.".to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(AgentEvent::TradeAccepted(with)) => {
|
||||
if !agent.trading {
|
||||
if let Some(target) =
|
||||
read_data.uid_allocator.retrieve_entity_internal(with.id())
|
||||
{
|
||||
agent.target = Some(Target {
|
||||
target,
|
||||
hostile: false,
|
||||
selected_at: read_data.time.0,
|
||||
});
|
||||
}
|
||||
agent.trading = true;
|
||||
agent.trading_issuer = true;
|
||||
}
|
||||
},
|
||||
Some(AgentEvent::FinishedTrade(result)) => {
|
||||
@ -954,10 +1117,7 @@ impl<'a> AgentData<'a> {
|
||||
Some(AgentEvent::UpdatePendingTrade(boxval)) => {
|
||||
let (tradeid, pending, prices, inventories) = *boxval;
|
||||
if agent.trading {
|
||||
// For now, assume player is 0 and agent is 1.
|
||||
// This needs revisiting when agents can initiate trades (e.g. to offer
|
||||
// mercenary contracts as quests)
|
||||
const WHO: usize = 1;
|
||||
let who: usize = if agent.trading_issuer { 0 } else { 1 };
|
||||
let balance = |who: usize, reduce: bool| {
|
||||
pending.offers[who]
|
||||
.iter()
|
||||
@ -983,8 +1143,8 @@ impl<'a> AgentData<'a> {
|
||||
})
|
||||
.sum()
|
||||
};
|
||||
let balance0: f32 = balance(1 - WHO, true);
|
||||
let balance1: f32 = balance(WHO, false);
|
||||
let balance0: f32 = balance(1 - who, true);
|
||||
let balance1: f32 = balance(who, false);
|
||||
tracing::debug!("UpdatePendingTrade({}, {})", balance0, balance1);
|
||||
if balance0 >= balance1 {
|
||||
// If the trade is favourable to us, only send an accept message if we're
|
||||
@ -992,7 +1152,7 @@ impl<'a> AgentData<'a> {
|
||||
// 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] {
|
||||
if !pending.accept_flags[who] {
|
||||
event_emitter.emit(ServerEvent::ProcessTradeAction(
|
||||
*self.entity,
|
||||
tradeid,
|
||||
@ -1026,23 +1186,7 @@ impl<'a> AgentData<'a> {
|
||||
// no new events, continue looking towards the last interacting player for some
|
||||
// time
|
||||
if let Some(Target { target, .. }) = &agent.target {
|
||||
if let Some(tgt_pos) = read_data.positions.get(*target) {
|
||||
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
|
||||
let tgt_eye_offset = read_data
|
||||
.bodies
|
||||
.get(*target)
|
||||
.map_or(0.0, |b| b.eye_height());
|
||||
if let Some(dir) = Dir::from_unnormalized(
|
||||
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
|
||||
- Vec3::new(
|
||||
self.pos.0.x,
|
||||
self.pos.0.y,
|
||||
self.pos.0.z + eye_offset,
|
||||
),
|
||||
) {
|
||||
controller.inputs.look_dir = dir;
|
||||
}
|
||||
}
|
||||
self.look_toward(controller, read_data, target);
|
||||
} else {
|
||||
agent.action_timer = 0.0;
|
||||
}
|
||||
@ -1051,6 +1195,30 @@ impl<'a> AgentData<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn look_toward(
|
||||
&self,
|
||||
controller: &mut Controller,
|
||||
read_data: &ReadData,
|
||||
target: &EcsEntity,
|
||||
) -> bool {
|
||||
if let Some(tgt_pos) = read_data.positions.get(*target) {
|
||||
let eye_offset = self.body.map_or(0.0, |b| b.eye_height());
|
||||
let tgt_eye_offset = read_data
|
||||
.bodies
|
||||
.get(*target)
|
||||
.map_or(0.0, |b| b.eye_height());
|
||||
if let Some(dir) = Dir::from_unnormalized(
|
||||
Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset)
|
||||
- Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset),
|
||||
) {
|
||||
controller.inputs.look_dir = dir;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn flee(
|
||||
&self,
|
||||
agent: &mut Agent,
|
||||
|
Loading…
Reference in New Issue
Block a user