Make NPC interaction go via rtsim

This commit is contained in:
Joshua Barretto 2023-05-04 11:23:46 +01:00
parent 2ff0118df0
commit a5b1e41d8b
19 changed files with 156 additions and 72 deletions

View File

@ -23,6 +23,7 @@ use common::{
self, self,
chat::KillSource, chat::KillSource,
controller::CraftEvent, controller::CraftEvent,
dialogue::Subject,
group, group,
inventory::item::{modular, tool, ItemKind}, inventory::item::{modular, tool, ItemKind},
invite::{InviteKind, InviteResponse}, invite::{InviteKind, InviteResponse},
@ -1132,14 +1133,16 @@ impl Client {
} }
} }
pub fn npc_interact(&mut self, npc_entity: EcsEntity) { pub fn npc_interact(&mut self, npc_entity: EcsEntity, subject: Subject) {
// If we're dead, exit before sending message // If we're dead, exit before sending message
if self.is_dead() { if self.is_dead() {
return; return;
} }
if let Some(uid) = self.state.read_component_copied(npc_entity) { if let Some(uid) = self.state.read_component_copied(npc_entity) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(uid))); self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Interact(
uid, subject,
)));
} }
} }

View File

@ -4,7 +4,7 @@ use crate::{
quadruped_small, ship, Body, UtteranceKind, quadruped_small, ship, Body, UtteranceKind,
}, },
path::Chaser, path::Chaser,
rtsim::RtSimController, rtsim::{NpcInput, RtSimController},
trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult}, trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
uid::Uid, uid::Uid,
}; };
@ -569,6 +569,8 @@ pub struct Agent {
/// required and reset each time the flee timer is reset. /// required and reset each time the flee timer is reset.
pub flee_from_pos: Option<Pos>, pub flee_from_pos: Option<Pos>,
pub awareness: Awareness, pub awareness: Awareness,
/// Inputs sent up to rtsim
pub rtsim_outbox: Option<VecDeque<NpcInput>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -673,6 +675,7 @@ impl Agent {
position_pid_controller: None, position_pid_controller: None,
flee_from_pos: None, flee_from_pos: None,
awareness: Awareness::new(0.0), awareness: Awareness::new(0.0),
rtsim_outbox: None,
} }
} }

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
comp::{ comp::{
ability, ability,
dialogue::Subject,
inventory::{ inventory::{
item::tool::ToolKind, item::tool::ToolKind,
slot::{EquipSlot, InvSlotId, Slot}, slot::{EquipSlot, InvSlotId, Slot},
@ -137,7 +138,7 @@ pub enum ControlEvent {
//ToggleLantern, //ToggleLantern,
EnableLantern, EnableLantern,
DisableLantern, DisableLantern,
Interact(Uid), Interact(Uid, Subject),
InitiateInvite(Uid, InviteKind), InitiateInvite(Uid, InviteKind),
InviteResponse(InviteResponse), InviteResponse(InviteResponse),
PerformTradeAction(TradeId, TradeAction), PerformTradeAction(TradeId, TradeAction),

View File

@ -1,23 +1,24 @@
use serde::{Deserialize, Serialize};
use vek::{Vec2, Vec3}; use vek::{Vec2, Vec3};
use super::Item; use super::Item;
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AskedLocation { pub struct AskedLocation {
pub name: String, pub name: String,
pub origin: Vec2<i32>, pub origin: Vec2<i32>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PersonType { pub enum PersonType {
Merchant, Merchant,
Villager { name: String }, Villager { name: String },
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AskedPerson { pub struct AskedPerson {
pub person_type: PersonType, pub person_type: PersonType,
pub origin: Option<Vec3<f32>>, pub origin: Option<Vec3<i32>>,
} }
impl AskedPerson { impl AskedPerson {
@ -30,7 +31,7 @@ impl AskedPerson {
} }
/// Conversation subject /// Conversation subject
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Subject { pub enum Subject {
/// Using simple interaction with NPC /// Using simple interaction with NPC
/// This is meant to be the default behavior of talking /// This is meant to be the default behavior of talking

View File

@ -40,6 +40,14 @@ impl PresenceKind {
/// certain in-game messages from the client such as control inputs /// certain in-game messages from the client such as control inputs
/// should be handled. /// should be handled.
pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) } pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) }
pub fn character_id(&self) -> Option<CharacterId> {
if let Self::Character(character_id) = self {
Some(*character_id)
} else {
None
}
}
} }
#[derive(PartialEq, Debug, Clone, Copy)] #[derive(PartialEq, Debug, Clone, Copy)]

View File

@ -3,6 +3,7 @@ use crate::{
comp::{ comp::{
self, self,
agent::Sound, agent::Sound,
dialogue::Subject,
invite::{InviteKind, InviteResponse}, invite::{InviteKind, InviteResponse},
DisconnectReason, Ori, Pos, DisconnectReason, Ori, Pos,
}, },
@ -189,7 +190,7 @@ pub enum ServerEvent {
}, },
EnableLantern(EcsEntity), EnableLantern(EcsEntity),
DisableLantern(EcsEntity), DisableLantern(EcsEntity),
NpcInteract(EcsEntity, EcsEntity), NpcInteract(EcsEntity, EcsEntity, Subject),
InviteResponse(EcsEntity, InviteResponse), InviteResponse(EcsEntity, InviteResponse),
InitiateInvite(EcsEntity, Uid, InviteKind), InitiateInvite(EcsEntity, Uid, InviteKind),
ProcessTradeAction(EcsEntity, TradeId, TradeAction), ProcessTradeAction(EcsEntity, TradeId, TradeAction),

View File

@ -3,7 +3,10 @@
// `Agent`). When possible, this should be moved to the `rtsim` // `Agent`). When possible, this should be moved to the `rtsim`
// module in `server`. // module in `server`.
use crate::{character::CharacterId, comp::Content}; use crate::{
character::CharacterId,
comp::{dialogue::Subject, Content},
};
use rand::{seq::IteratorRandom, Rng}; use rand::{seq::IteratorRandom, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::Component; use specs::Component;
@ -19,6 +22,8 @@ slotmap::new_key_type! { pub struct SiteId; }
slotmap::new_key_type! { pub struct FactionId; } slotmap::new_key_type! { pub struct FactionId; }
slotmap::new_key_type! { pub struct ReportId; }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct RtSimEntity(pub NpcId); pub struct RtSimEntity(pub NpcId);
@ -258,6 +263,13 @@ pub enum NpcAction {
Attack(Actor), Attack(Actor),
} }
// Represents a message passed back to rtsim from an agent's brain
#[derive(Clone, Debug)]
pub enum NpcInput {
Report(ReportId),
Interaction(Actor, Subject),
}
// Note: the `serde(name = "...")` is to minimise the length of field // Note: the `serde(name = "...")` is to minimise the length of field
// identifiers for the sake of rtsim persistence // identifiers for the sake of rtsim persistence
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, enum_map::Enum)]

View File

@ -66,12 +66,13 @@ impl<'a> System<'a> for Sys {
ControlEvent::DisableLantern => { ControlEvent::DisableLantern => {
server_emitter.emit(ServerEvent::DisableLantern(entity)) server_emitter.emit(ServerEvent::DisableLantern(entity))
}, },
ControlEvent::Interact(npc_uid) => { ControlEvent::Interact(npc_uid, subject) => {
if let Some(npc_entity) = read_data if let Some(npc_entity) = read_data
.uid_allocator .uid_allocator
.retrieve_entity_internal(npc_uid.id()) .retrieve_entity_internal(npc_uid.id())
{ {
server_emitter.emit(ServerEvent::NpcInteract(entity, npc_entity)); server_emitter
.emit(ServerEvent::NpcInteract(entity, npc_entity, subject));
} }
}, },
ControlEvent::InitiateInvite(inviter_uid, kind) => { ControlEvent::InitiateInvite(inviter_uid, kind) => {

View File

@ -5,7 +5,10 @@ use crate::{
}, },
RtState, RtState,
}; };
use common::resources::{Time, TimeOfDay}; use common::{
resources::{Time, TimeOfDay},
rtsim::NpcInput,
};
use hashbrown::HashSet; use hashbrown::HashSet;
use itertools::Either; use itertools::Either;
use rand_chacha::ChaChaRng; use rand_chacha::ChaChaRng;
@ -26,7 +29,7 @@ pub struct NpcCtx<'a> {
pub npc_id: NpcId, pub npc_id: NpcId,
pub npc: &'a Npc, pub npc: &'a Npc,
pub controller: &'a mut Controller, pub controller: &'a mut Controller,
pub inbox: &'a mut VecDeque<ReportId>, // TODO: Allow more inbox items pub inbox: &'a mut VecDeque<NpcInput>, // TODO: Allow more inbox items
pub sentiments: &'a mut Sentiments, pub sentiments: &'a mut Sentiments,
pub known_reports: &'a mut HashSet<ReportId>, pub known_reports: &'a mut HashSet<ReportId>,

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
ai::Action, ai::Action,
data::{ReportId, Reports, Sentiments}, data::{Reports, Sentiments},
gen::name, gen::name,
}; };
pub use common::rtsim::{NpcId, Profession}; pub use common::rtsim::{NpcId, Profession};
@ -9,8 +9,8 @@ use common::{
comp, comp,
grid::Grid, grid::Grid,
rtsim::{ rtsim::{
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, Role, SiteId, Actor, ChunkResource, FactionId, NpcAction, NpcActivity, NpcInput, Personality, ReportId,
VehicleId, Role, SiteId, VehicleId,
}, },
store::Id, store::Id,
terrain::CoordinateConversions, terrain::CoordinateConversions,
@ -121,7 +121,7 @@ pub struct Npc {
#[serde(skip)] #[serde(skip)]
pub controller: Controller, pub controller: Controller,
#[serde(skip)] #[serde(skip)]
pub inbox: VecDeque<ReportId>, pub inbox: VecDeque<NpcInput>,
/// Whether the NPC is in simulated or loaded mode (when rtsim is run on the /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the
/// server, loaded corresponds to being within a loaded chunk). When in /// server, loaded corresponds to being within a loaded chunk). When in

View File

@ -4,7 +4,7 @@ use slotmap::HopSlotMap;
use std::ops::Deref; use std::ops::Deref;
use vek::*; use vek::*;
slotmap::new_key_type! { pub struct ReportId; } pub use common::rtsim::ReportId;
/// Represents a single piece of information known by an rtsim entity. /// Represents a single piece of information known by an rtsim entity.
/// ///

View File

@ -13,10 +13,11 @@ use common::{
astar::{Astar, PathResult}, astar::{Astar, PathResult},
comp::{ comp::{
compass::{Direction, Distance}, compass::{Direction, Distance},
dialogue::Subject,
Content, Content,
}, },
path::Path, path::Path,
rtsim::{Actor, ChunkResource, Profession, Role, SiteId}, rtsim::{Actor, ChunkResource, NpcInput, Profession, Role, SiteId},
spiral::Spiral2d, spiral::Spiral2d,
store::Id, store::Id,
terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize}, terrain::{CoordinateConversions, SiteKindMeta, TerrainChunkSize},
@ -542,27 +543,8 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time) move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time)
} }
fn socialize() -> impl Action { fn talk_to(tgt: Actor, subject: Option<Subject>) -> impl Action {
now(|ctx| { now(move |ctx| {
// Skip most socialising actions if we're not loaded
if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) {
// Sometimes dance
if ctx.rng.gen_bool(0.15) {
return just(|ctx| ctx.controller.do_dance())
.repeat()
.stop_if(timeout(6.0))
.debug(|| "dancing")
.map(|_| ())
.l()
.l();
// Talk to nearby NPCs
} else if let Some(other) = ctx
.state
.data()
.npcs
.nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0)
.choose(&mut ctx.rng)
{
// Mention nearby sites // Mention nearby sites
let comment = if ctx.rng.gen_bool(0.3) let comment = if ctx.rng.gen_bool(0.3)
&& let Some(current_site) = ctx.npc.current_site && let Some(current_site) = ctx.npc.current_site
@ -592,7 +574,32 @@ fn socialize() -> impl Action {
} else { } else {
ctx.npc.personality.get_generic_comment(&mut ctx.rng) ctx.npc.personality.get_generic_comment(&mut ctx.rng)
}; };
return just(move |ctx| ctx.controller.say(other, comment.clone())) just(move |ctx| ctx.controller.say(tgt, comment.clone()))
})
}
fn socialize() -> impl Action {
now(|ctx| {
// Skip most socialising actions if we're not loaded
if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) {
// Sometimes dance
if ctx.rng.gen_bool(0.15) {
return just(|ctx| ctx.controller.do_dance())
.repeat()
.stop_if(timeout(6.0))
.debug(|| "dancing")
.map(|_| ())
.l()
.l();
// Talk to nearby NPCs
} else if let Some(other) = ctx
.state
.data()
.npcs
.nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0)
.choose(&mut ctx.rng)
{
return talk_to(other, None)
// After talking, wait for a while // After talking, wait for a while
.then(idle().repeat().stop_if(timeout(4.0))) .then(idle().repeat().stop_if(timeout(4.0)))
.map(|_| ()) .map(|_| ())
@ -972,7 +979,7 @@ fn captain() -> impl Action {
fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> { fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
loop { loop {
match ctx.inbox.pop_front() { match ctx.inbox.pop_front() {
Some(report_id) if !ctx.known_reports.contains(&report_id) => { Some(NpcInput::Report(report_id)) if !ctx.known_reports.contains(&report_id) => {
#[allow(clippy::single_match)] #[allow(clippy::single_match)]
match ctx.state.data().reports.get(report_id).map(|r| r.kind) { match ctx.state.data().reports.get(report_id).map(|r| r.kind) {
Some(ReportKind::Death { killer, actor, .. }) Some(ReportKind::Death { killer, actor, .. })
@ -1013,15 +1020,17 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
"npc-speech-witness_death" "npc-speech-witness_death"
}; };
ctx.known_reports.insert(report_id); ctx.known_reports.insert(report_id);
break Some(just(move |ctx| { break Some(
ctx.controller.say(killer, Content::localized(phrase)) just(move |ctx| ctx.controller.say(killer, Content::localized(phrase)))
})); .l(),
);
}, },
Some(ReportKind::Death { .. }) => {}, // We don't care about death Some(ReportKind::Death { .. }) => {}, // We don't care about death
None => {}, // Stale report, ignore None => {}, // Stale report, ignore
} }
}, },
Some(_) => {}, // Reports we already know of are ignored Some(NpcInput::Report(_)) => {}, // Reports we already know of are ignored
Some(NpcInput::Interaction(by, subject)) => break Some(talk_to(by, Some(subject)).r()),
None => break None, None => break None,
} }
} }

View File

@ -3,6 +3,7 @@ use crate::{
event::{EventCtx, OnDeath}, event::{EventCtx, OnDeath},
RtState, Rule, RuleError, RtState, Rule, RuleError,
}; };
use common::rtsim::NpcInput;
pub struct ReportEvents; pub struct ReportEvents;
@ -38,7 +39,7 @@ fn on_death(ctx: EventCtx<ReportEvents, OnDeath>) {
// data structure in their own time. // data structure in their own time.
for npc_id in nearby { for npc_id in nearby {
if let Some(npc) = data.npcs.get_mut(npc_id) { if let Some(npc) = data.npcs.get_mut(npc_id) {
npc.inbox.push_back(report); npc.inbox.push_back(NpcInput::Report(report));
} }
} }
} }

View File

@ -2,7 +2,11 @@ use crate::{
event::{EventCtx, OnDeath, OnSetup, OnTick}, event::{EventCtx, OnDeath, OnSetup, OnTick},
RtState, Rule, RuleError, RtState, Rule, RuleError,
}; };
use common::{grid::Grid, rtsim::Actor, terrain::CoordinateConversions}; use common::{
grid::Grid,
rtsim::{Actor, NpcInput},
terrain::CoordinateConversions,
};
pub struct SyncNpcs; pub struct SyncNpcs;
@ -124,7 +128,8 @@ fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
npc.inbox.extend(site.known_reports npc.inbox.extend(site.known_reports
.iter() .iter()
.copied() .copied()
.filter(|report| !npc.known_reports.contains(report))); .filter(|report| !npc.known_reports.contains(report))
.map(NpcInput::Report));
} }
} }

View File

@ -78,7 +78,12 @@ pub fn handle_lantern(server: &mut Server, entity: EcsEntity, enable: bool) {
} }
} }
pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_entity: EcsEntity) { pub fn handle_npc_interaction(
server: &mut Server,
interactor: EcsEntity,
npc_entity: EcsEntity,
subject: Subject,
) {
let state = server.state_mut(); let state = server.state_mut();
if let Some(agent) = state if let Some(agent) = state
.ecs() .ecs()
@ -89,7 +94,7 @@ pub fn handle_npc_interaction(server: &mut Server, interactor: EcsEntity, npc_en
if let Some(interactor_uid) = state.ecs().uid_from_entity(interactor) { if let Some(interactor_uid) = state.ecs().uid_from_entity(interactor) {
agent agent
.inbox .inbox
.push_back(AgentEvent::Talk(interactor_uid, Subject::Regular)); .push_back(AgentEvent::Talk(interactor_uid, subject));
} }
} }
} }

View File

@ -123,8 +123,8 @@ impl Server {
}, },
ServerEvent::EnableLantern(entity) => handle_lantern(self, entity, true), ServerEvent::EnableLantern(entity) => handle_lantern(self, entity, true),
ServerEvent::DisableLantern(entity) => handle_lantern(self, entity, false), ServerEvent::DisableLantern(entity) => handle_lantern(self, entity, false),
ServerEvent::NpcInteract(interactor, target) => { ServerEvent::NpcInteract(interactor, target, subject) => {
handle_npc_interaction(self, interactor, target) handle_npc_interaction(self, interactor, target, subject)
}, },
ServerEvent::InitiateInvite(interactor, target, kind) => { ServerEvent::InitiateInvite(interactor, target, kind) => {
handle_invite(self, interactor, target, kind) handle_invite(self, interactor, target, kind)

View File

@ -3,7 +3,7 @@
use super::*; use super::*;
use crate::sys::terrain::NpcData; use crate::sys::terrain::NpcData;
use common::{ use common::{
comp::{self, Body, Presence, PresenceKind}, comp::{self, Agent, Body, Presence, PresenceKind},
event::{EventBus, NpcBuilder, ServerEvent}, event::{EventBus, NpcBuilder, ServerEvent},
generation::{BodyBuilder, EntityConfig, EntityInfo}, generation::{BodyBuilder, EntityConfig, EntityInfo},
resources::{DeltaTime, Time, TimeOfDay}, resources::{DeltaTime, Time, TimeOfDay},
@ -314,7 +314,10 @@ impl<'a> System<'a> for Sys {
.with_health(health) .with_health(health)
.with_poise(poise) .with_poise(poise)
.with_inventory(inventory) .with_inventory(inventory)
.with_agent(agent) .with_agent(agent.map(|agent| Agent {
rtsim_outbox: Some(Default::default()),
..agent
}))
.with_scale(scale) .with_scale(scale)
.with_loot(loot) .with_loot(loot)
.with_rtsim(RtSimEntity(npc_id)), .with_rtsim(RtSimEntity(npc_id)),
@ -373,7 +376,10 @@ impl<'a> System<'a> for Sys {
.with_health(health) .with_health(health)
.with_poise(poise) .with_poise(poise)
.with_inventory(inventory) .with_inventory(inventory)
.with_agent(agent) .with_agent(agent.map(|agent| Agent {
rtsim_outbox: Some(Default::default()),
..agent
}))
.with_scale(scale) .with_scale(scale)
.with_loot(loot) .with_loot(loot)
.with_rtsim(RtSimEntity(npc_id)), .with_rtsim(RtSimEntity(npc_id)),
@ -417,6 +423,9 @@ impl<'a> System<'a> for Sys {
.rtsim_controller .rtsim_controller
.actions .actions
.extend(std::mem::take(&mut npc.controller.actions)); .extend(std::mem::take(&mut npc.controller.actions));
if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
npc.inbox.append(rtsim_outbox);
}
} }
}); });
} }

View File

@ -10,7 +10,7 @@ use common::{
UtteranceKind, UtteranceKind,
}, },
event::ServerEvent, event::ServerEvent,
rtsim::PersonalityTrait, rtsim::{Actor, NpcInput, PersonalityTrait},
trade::{TradeAction, TradePhase, TradeResult}, trade::{TradeAction, TradePhase, TradeResult},
}; };
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -87,8 +87,26 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
} }
if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() { if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() {
let by_entity = get_entity_by_id(by.id(), read_data);
if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
if let Subject::Regular
| Subject::Mood
| Subject::Work = subject
&& let Some(by_entity) = by_entity
&& let Some(actor) = read_data.presences
.get(by_entity)
.and_then(|p| p.kind.character_id().map(Actor::Character))
.or_else(|| Some(Actor::Npc(read_data.rtsim_entities
.get(by_entity)?.0)))
{
rtsim_outbox.push_back(NpcInput::Interaction(actor, subject));
return false;
}
}
if agent.allowed_to_speak() { if agent.allowed_to_speak() {
if let Some(target) = get_entity_by_id(by.id(), read_data) { if let Some(target) = by_entity {
let target_pos = read_data.positions.get(target).map(|pos| pos.0); let target_pos = read_data.positions.get(target).map(|pos| pos.0);
agent.target = Some(Target::new( agent.target = Some(Target::new(
@ -216,14 +234,17 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
if let Some(src_pos) = read_data.positions.get(target) { if let Some(src_pos) = read_data.positions.get(target) {
// TODO: Localise // TODO: Localise
let msg = if let Some(person_pos) = person.origin { let msg = if let Some(person_pos) = person.origin {
let distance = Distance::from_dir(person_pos.xy() - src_pos.0.xy()); let distance =
Distance::from_dir(person_pos.xy().as_() - src_pos.0.xy());
match distance { match distance {
Distance::NextTo | Distance::Near => { Distance::NextTo | Distance::Near => {
format!( format!(
"{} ? I think he's {} {} from here!", "{} ? I think he's {} {} from here!",
person.name(), person.name(),
distance.name(), distance.name(),
Direction::from_dir(person_pos.xy() - src_pos.0.xy(),) Direction::from_dir(
person_pos.xy().as_() - src_pos.0.xy()
)
.name() .name()
) )
}, },

View File

@ -15,6 +15,7 @@ use client::{self, Client};
use common::{ use common::{
comp, comp,
comp::{ comp::{
dialogue::Subject,
inventory::slot::{EquipSlot, Slot}, inventory::slot::{EquipSlot, Slot},
invite::InviteKind, invite::InviteKind,
item::{tool::ToolKind, ItemDesc}, item::{tool::ToolKind, ItemDesc},
@ -959,7 +960,7 @@ impl PlayState for SessionState {
// TODO: maybe start crafting instead? // TODO: maybe start crafting instead?
client.toggle_sit(); client.toggle_sit();
} else { } else {
client.npc_interact(*entity); client.npc_interact(*entity, Subject::Regular);
} }
}, },
} }