diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index e80c6695e8..c29f412a19 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -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, pub trading: bool, + pub trading_issuer: bool, pub psyche: Psyche, pub inbox: VecDeque, pub action_timer: f32, diff --git a/common/src/comp/compass.rs b/common/src/comp/compass.rs new file mode 100644 index 0000000000..389c4fb67f --- /dev/null +++ b/common/src/comp/compass.rs @@ -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) -> 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) -> 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", + } + } +} diff --git a/common/src/comp/dialogue.rs b/common/src/comp/dialogue.rs new file mode 100644 index 0000000000..6149563dd3 --- /dev/null +++ b/common/src/comp/dialogue.rs @@ -0,0 +1,124 @@ +use vek::{Vec2, Vec3}; + +use super::Item; + +#[derive(Clone, Debug)] +pub struct AskedLocation { + pub name: String, + pub origin: Vec2, +} + +#[derive(Clone, Debug)] +pub enum PersonType { + Merchant, + Villager { name: String }, +} + +#[derive(Clone, Debug)] +pub struct AskedPerson { + pub person_type: PersonType, + pub origin: Option>, +} + +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()) + }, + } + } +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index b7c260e6ee..879a4dcee4 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -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; diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 6d9e564e0a..bc62900979 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -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) diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index 6ffc5556b2..0f40ddd4dd 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -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)); } } } diff --git a/server/src/events/invite.rs b/server/src/events/invite.rs index b67a3b0d89..dd859c910e 100644 --- a/server/src/events/invite.rs +++ b/server/src/events/invite.rs @@ -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::(); let uids = state.ecs().read_storage::(); - let agents = state.ecs().read_storage::(); + let mut agents = state.ecs().write_storage::(); let mut invites = state.ecs().write_storage::(); 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::(); 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)) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 04393d1dfa..f8ecbeb977 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -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)) } diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index d364462d9b..eb5ebc5068 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -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) { diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 47c84f8624..24fa53e1f4 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -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::() % 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_::() + - tgt_pos.0.xy() + ) + .name(), + Direction::from_dir( + location.origin.as_::() + - 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,