Localised rtsim NPC speech

This commit is contained in:
Joshua Barretto 2023-04-11 17:00:08 +01:00
parent edcc2f1870
commit cf701fb604
7 changed files with 188 additions and 161 deletions

View File

@ -223,3 +223,35 @@ npc-speech-prisoner =
.a2 = That Cardinal can't be trusted. .a2 = That Cardinal can't be trusted.
.a3 = These Clerics are up to no good. .a3 = These Clerics are up to no good.
.a4 = I wish i still had my pick! .a4 = I wish i still had my pick!
npc-speech-moving_on =
.a0 = I've spent enough time here, onward to { $site }!
npc-speech-night_time =
.a0 = It's dark, time to head home.
.a1 = I'm tired.
.a2 = My bed beckons!
npc-speech-day_time =
.a0 = A new day begins!
.a1 = I never liked waking up...
npc-speech-start_hunting =
.a0 = Time to go hunting!
npc-speech-guard_thought =
.a0 = My brother's out fighting ogres. What do I get? Guard duty...
.a1 = Just one more patrol, then I can head home.
.a2 = No bandits are going to get past me.
npc-speech-merchant_sell_undirected =
.a0 = All my goods are of the highest quality!
.a1 = Does anybody want to buy my wares?
.a2 = I've got the best offers in town.
.a3 = Looking for supplies? I've got you covered.
npc-speech-merchant_sell_directed =
.a0 = You there! Are you in need of a new thingamabob?
.a1 = Are you hungry? I'm sure I've got some cheese you can buy.
.a2 = You look like you could do with some new armour!
npc-speech-witness_murder =
.a0 = Murderer!
.a1 = How could you do this?
.a2 = Aaargh!
npc-speech-witness_death =
.a0 = No!
.a1 = This is terrible!
.a2 = Oh my goodness!

View File

@ -200,10 +200,12 @@ pub enum Content {
}, },
} }
// TODO: Remove impl and make use of `Plain` explicit (to discourage it)
impl From<String> for Content { impl From<String> for Content {
fn from(text: String) -> Self { Self::Plain(text) } fn from(text: String) -> Self { Self::Plain(text) }
} }
// TODO: Remove impl and make use of `Plain` explicit (to discourage it)
impl<'a> From<&'a str> for Content { impl<'a> From<&'a str> for Content {
fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) } fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) }
} }
@ -212,7 +214,7 @@ impl Content {
pub fn localized(key: impl ToString) -> Self { pub fn localized(key: impl ToString) -> Self {
Self::Localized { Self::Localized {
key: key.to_string(), key: key.to_string(),
r: rand::random(), seed: rand::random(),
args: HashMap::default(), args: HashMap::default(),
} }
} }
@ -223,7 +225,7 @@ impl Content {
) -> Self { ) -> Self {
Self::Localized { Self::Localized {
key: key.to_string(), key: key.to_string(),
r: rand::random(), seed: rand::random(),
args: args args: args
.into_iter() .into_iter()
.map(|(k, v)| (k.to_string(), v.to_string())) .map(|(k, v)| (k.to_string(), v.to_string()))

View File

@ -3,11 +3,11 @@
// `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; use crate::{character::CharacterId, comp::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;
use std::{borrow::Cow, collections::VecDeque}; use std::collections::VecDeque;
use strum::{EnumIter, IntoEnumIterator}; use strum::{EnumIter, IntoEnumIterator};
use vek::*; use vek::*;
@ -169,6 +169,33 @@ impl Personality {
pub fn will_ambush(&self) -> bool { pub fn will_ambush(&self) -> bool {
self.agreeableness < Self::LOW_THRESHOLD && self.conscientiousness < Self::LOW_THRESHOLD self.agreeableness < Self::LOW_THRESHOLD && self.conscientiousness < Self::LOW_THRESHOLD
} }
pub fn get_generic_comment(&self, rng: &mut impl Rng) -> Content {
let i18n_key = if let Some(extreme_trait) = self.chat_trait(rng) {
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",
}
} else {
"npc-speech-villager"
};
Content::localized(i18n_key)
}
} }
impl Default for Personality { impl Default for Personality {
@ -226,7 +253,7 @@ pub enum NpcAction {
/// Speak the given message, with an optional target for that speech. /// Speak the given message, with an optional target for that speech.
// TODO: Use some sort of structured, language-independent value that frontends can translate // TODO: Use some sort of structured, language-independent value that frontends can translate
// instead // instead
Say(Option<Actor>, Cow<'static, str>), Say(Option<Actor>, Content),
/// Attack the given target /// Attack the given target
Attack(Actor), Attack(Actor),
} }

View File

@ -20,7 +20,6 @@ use rand::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use slotmap::HopSlotMap; use slotmap::HopSlotMap;
use std::{ use std::{
borrow::Cow,
collections::VecDeque, collections::VecDeque,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -74,8 +73,8 @@ impl Controller {
pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); } pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); }
pub fn say(&mut self, target: impl Into<Option<Actor>>, msg: impl Into<Cow<'static, str>>) { pub fn say(&mut self, target: impl Into<Option<Actor>>, content: comp::Content) {
self.actions.push(NpcAction::Say(target.into(), msg.into())); self.actions.push(NpcAction::Say(target.into(), content));
} }
pub fn attack(&mut self, target: impl Into<Actor>) { pub fn attack(&mut self, target: impl Into<Actor>) {

View File

@ -11,6 +11,7 @@ use crate::{
}; };
use common::{ use common::{
astar::{Astar, PathResult}, astar::{Astar, PathResult},
comp::Content,
path::Path, path::Path,
rtsim::{ChunkResource, Profession, SiteId}, rtsim::{ChunkResource, Profession, SiteId},
spiral::Spiral2d, spiral::Spiral2d,
@ -470,15 +471,17 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
fn socialize() -> impl Action { fn socialize() -> impl Action {
now(|ctx| { now(|ctx| {
// TODO: Bit odd, should wait for a while after greeting // Skip most socialising actions if we're not loaded
if ctx.rng.gen_bool(0.002) { if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) {
if ctx.rng.gen_bool(0.15) { if ctx.rng.gen_bool(0.15) {
return just(|ctx| ctx.controller.do_dance()) return Either::Left(
just(|ctx| ctx.controller.do_dance())
.repeat() .repeat()
.stop_if(timeout(6.0)) .stop_if(timeout(6.0))
.debug(|| "dancing") .debug(|| "dancing")
.map(|_| ()) .map(|_| ())
.boxed(); .boxed(),
);
} else if let Some(other) = ctx } else if let Some(other) = ctx
.state .state
.data() .data()
@ -486,12 +489,17 @@ fn socialize() -> impl Action {
.nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0)
.choose(&mut ctx.rng) .choose(&mut ctx.rng)
{ {
return just(move |ctx| ctx.controller.say(other, "npc-speech-villager_open")) return Either::Left(
.boxed(); just(move |ctx| ctx.controller.say(other, ctx.npc.personality.get_generic_comment(&mut ctx.rng)))
// After greeting the actor, wait for a while
.then(idle().repeat().stop_if(timeout(4.0)))
.map(|_| ())
.boxed(),
);
} }
} }
idle().boxed() Either::Right(idle())
}) })
} }
@ -528,7 +536,7 @@ fn adventure() -> impl Action {
.map(|ws| ctx.index.sites.get(ws).name().to_string()) .map(|ws| ctx.index.sites.get(ws).name().to_string())
.unwrap_or_default(); .unwrap_or_default();
// Travel to the site // Travel to the site
important(just(move |ctx| ctx.controller.say(None, format!("I've spent enough time here, onward to {}!", site_name))) important(just(move |ctx| ctx.controller.say(None, Content::localized_with_args("npc-speech-moving_on", [("site", site_name.clone())])))
.then(travel_to_site(tgt_site, 0.6)) .then(travel_to_site(tgt_site, 0.6))
// Stop for a few minutes // Stop for a few minutes
.then(villager(tgt_site).repeat().stop_if(timeout(wait_time))) .then(villager(tgt_site).repeat().stop_if(timeout(wait_time)))
@ -634,12 +642,18 @@ fn villager(visiting_site: SiteId) -> impl Action {
Some(site2.tile_center_wpos(house.root_tile()).as_()) Some(site2.tile_center_wpos(house.root_tile()).as_())
}) })
{ {
just(|ctx| ctx.controller.say(None, "It's dark, time to go home")) just(|ctx| {
ctx.controller
.say(None, Content::localized("npc-speech-night_time"))
})
.then(travel_to_point(house_wpos, 0.65)) .then(travel_to_point(house_wpos, 0.65))
.debug(|| "walk to house") .debug(|| "walk to house")
.then(socialize().repeat().debug(|| "wait in house")) .then(socialize().repeat().debug(|| "wait in house"))
.stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light())
.then(just(|ctx| ctx.controller.say(None, "A new day begins!"))) .then(just(|ctx| {
ctx.controller
.say(None, Content::localized("npc-speech-day_time"))
}))
.map(|_| ()) .map(|_| ())
.boxed() .boxed()
} else { } else {
@ -665,7 +679,10 @@ fn villager(visiting_site: SiteId) -> impl Action {
} else if matches!(ctx.npc.profession, Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) { } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) {
if let Some(forest_wpos) = find_forest(ctx) { if let Some(forest_wpos) = find_forest(ctx) {
return casual( return casual(
just(|ctx| ctx.controller.say(None, "Time to go hunting!")) just(|ctx| {
ctx.controller
.say(None, Content::localized("npc-speech-start_hunting"))
})
.then(travel_to_point(forest_wpos, 0.75)) .then(travel_to_point(forest_wpos, 0.75))
.debug(|| "walk to forest") .debug(|| "walk to forest")
.then({ .then({
@ -682,15 +699,10 @@ fn villager(visiting_site: SiteId) -> impl Action {
.debug(|| "patrol") .debug(|| "patrol")
.interrupt_with(|ctx| { .interrupt_with(|ctx| {
if ctx.rng.gen_bool(0.0003) { if ctx.rng.gen_bool(0.0003) {
let phrase = *[ Some(just(move |ctx| {
"My brother's out fighting ogres. What do I get? Guard duty...", ctx.controller
"Just one more patrol, then I can head home", .say(None, Content::localized("npc-speech-guard_thought"))
"No bandits are going to get past me", }))
]
.iter()
.choose(&mut ctx.rng)
.unwrap(); // Can't fail
Some(just(move |ctx| ctx.controller.say(None, phrase)))
} else { } else {
None None
} }
@ -703,32 +715,20 @@ fn villager(visiting_site: SiteId) -> impl Action {
return casual( return casual(
just(|ctx| { just(|ctx| {
// Try to direct our speech at nearby actors, if there are any // Try to direct our speech at nearby actors, if there are any
let (target, phrases) = if ctx.rng.gen_bool(0.3) && let Some(other) = ctx let (target, phrase) = if ctx.rng.gen_bool(0.3) && let Some(other) = ctx
.state .state
.data() .data()
.npcs .npcs
.nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0)
.choose(&mut ctx.rng) .choose(&mut ctx.rng)
{ {
(Some(other), &[ (Some(other), "npc-speech-merchant_sell_directed")
"You there! Are you in need of a new thingamabob?",
"Are you hungry? I'm sure I've got some cheese you can buy",
"You look like you could do with some new armour!",
][..])
// Otherwise, resort to generic expressions
} else { } else {
(None, &[ // Otherwise, resort to generic expressions
"All my goods are of the highest quality!", (None, "npc-speech-merchant_sell_undirected")
"Does anybody want to buy my wares?",
"I've got the best offers in town",
"Looking for supplies? I've got you covered",
][..])
}; };
ctx.controller.say( ctx.controller.say(target, Content::localized(phrase));
target,
*phrases.iter().choose(&mut ctx.rng).unwrap(), // Can't fail
);
}) })
.then(idle().repeat().stop_if(timeout(8.0))) .then(idle().repeat().stop_if(timeout(8.0)))
.repeat() .repeat()
@ -898,16 +898,17 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
Some(ReportKind::Death { killer, .. }) => { Some(ReportKind::Death { killer, .. }) => {
// TODO: Sentiment should be positive if we didn't like actor that died // TODO: Sentiment should be positive if we didn't like actor that died
// TODO: Don't report self // TODO: Don't report self
let phrases = if let Some(killer) = killer { let phrase = if let Some(killer) = killer {
// TODO: Don't hard-code sentiment change // TODO: Don't hard-code sentiment change
ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN); ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN);
&["Murderer!", "How could you do this?", "Aaargh!"][..] "npc-speech-witness_murder"
} else { } else {
&["No!", "This is terrible!", "Oh my goodness!"][..] "npc-speech-witness_death"
}; };
let phrase = *phrases.iter().choose(&mut ctx.rng).unwrap(); // Can't fail
ctx.known_reports.insert(report_id); ctx.known_reports.insert(report_id);
break Some(just(move |ctx| ctx.controller.say(killer, phrase))); break Some(just(move |ctx| {
ctx.controller.say(killer, Content::localized(phrase))
}));
}, },
None => {}, // Stale report, ignore None => {}, // Stale report, ignore
} }

View File

@ -731,7 +731,7 @@ impl<'a> AgentData<'a> {
} else if can_ambush(entity, read_data) { } else if can_ambush(entity, read_data) {
controller.clone().push_utterance(UtteranceKind::Ambush); controller.clone().push_utterance(UtteranceKind::Ambush);
self.chat_npc_if_allowed_to_speak( self.chat_npc_if_allowed_to_speak(
"npc-speech-ambush".to_string(), Content::localized("npc-speech-ambush"),
agent, agent,
event_emitter, event_emitter,
); );
@ -1517,7 +1517,7 @@ impl<'a> AgentData<'a> {
pub fn chat_npc_if_allowed_to_speak( pub fn chat_npc_if_allowed_to_speak(
&self, &self,
msg: impl ToString, msg: Content,
agent: &Agent, agent: &Agent,
event_emitter: &mut Emitter<'_, ServerEvent>, event_emitter: &mut Emitter<'_, ServerEvent>,
) -> bool { ) -> bool {
@ -1529,10 +1529,9 @@ impl<'a> AgentData<'a> {
} }
} }
pub fn chat_npc(&self, key: impl ToString, event_emitter: &mut Emitter<'_, ServerEvent>) { pub fn chat_npc(&self, content: Content, event_emitter: &mut Emitter<'_, ServerEvent>) {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc(
*self.uid, *self.uid, content,
Content::localized(key),
))); )));
} }
@ -1561,13 +1560,13 @@ impl<'a> AgentData<'a> {
// FIXME: If going to use "cultist + low health + fleeing" string, make sure // FIXME: If going to use "cultist + low health + fleeing" string, make sure
// they are each true. // they are each true.
self.chat_npc_if_allowed_to_speak( self.chat_npc_if_allowed_to_speak(
"npc-speech-cultist_low_health_fleeing", Content::localized("npc-speech-cultist_low_health_fleeing"),
agent, agent,
event_emitter, event_emitter,
); );
} else if is_villager(self.alignment) { } else if is_villager(self.alignment) {
self.chat_npc_if_allowed_to_speak( self.chat_npc_if_allowed_to_speak(
"npc-speech-villager_under_attack", Content::localized("npc-speech-villager_under_attack"),
agent, agent,
event_emitter, event_emitter,
); );
@ -1582,7 +1581,7 @@ impl<'a> AgentData<'a> {
) { ) {
if is_villager(self.alignment) { if is_villager(self.alignment) {
self.chat_npc_if_allowed_to_speak( self.chat_npc_if_allowed_to_speak(
"npc-speech-villager_enemy_killed", Content::localized("npc-speech-villager_enemy_killed"),
agent, agent,
event_emitter, event_emitter,
); );
@ -1756,16 +1755,22 @@ impl<'a> AgentData<'a> {
let move_dir = controller.inputs.move_dir; let move_dir = controller.inputs.move_dir;
let move_dir_mag = move_dir.magnitude(); let move_dir_mag = move_dir.magnitude();
let small_chance = rng.gen::<f32>() < read_data.dt.0 * 0.25; let small_chance = rng.gen::<f32>() < read_data.dt.0 * 0.25;
let mut chat = |msg: &str| { let mut chat = |content: Content| {
self.chat_npc_if_allowed_to_speak(msg.to_string(), agent, event_emitter); self.chat_npc_if_allowed_to_speak(content, agent, event_emitter);
}; };
let mut chat_villager_remembers_fighting = || { let mut chat_villager_remembers_fighting = || {
let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone()); let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone());
// TODO: Localise
if let Some(tgt_name) = tgt_name { if let Some(tgt_name) = tgt_name {
chat(format!("{}! How dare you cross me again!", &tgt_name).as_str()); chat(Content::Plain(format!(
"{}! How dare you cross me again!",
&tgt_name
)));
} else { } else {
chat("You! How dare you cross me again!"); chat(Content::Plain(
"You! How dare you cross me again!".to_string(),
));
} }
}; };
@ -1782,12 +1787,12 @@ impl<'a> AgentData<'a> {
if remembers_fight_with_target { if remembers_fight_with_target {
chat_villager_remembers_fighting(); chat_villager_remembers_fighting();
} else if is_dressed_as_cultist(target, read_data) { } else if is_dressed_as_cultist(target, read_data) {
chat("npc-speech-villager_cultist_alarm"); chat(Content::localized("npc-speech-villager_cultist_alarm"));
} else { } else {
chat("npc-speech-menacing"); chat(Content::localized("npc-speech-menacing"));
} }
} else { } else {
chat("npc-speech-menacing"); chat(Content::localized("npc-speech-menacing"));
} }
} }
} }

View File

@ -155,56 +155,17 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
} else { } else {
standard_response_msg() standard_response_msg()
}; };
agent_data.chat_npc(msg, event_emitter); // TODO: Localise
agent_data.chat_npc(Content::Plain(msg), event_emitter);
} else { } else {
let mut rng = thread_rng(); let mut rng = thread_rng();
if let Some(extreme_trait) = agent_data.chat_npc(
agent.rtsim_controller.personality.chat_trait(&mut rng) agent
{ .rtsim_controller
let msg = match extreme_trait { .personality
PersonalityTrait::Open => "npc-speech-villager_open", .get_generic_comment(&mut rng),
PersonalityTrait::Adventurous => { event_emitter,
"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);
}
} }
} }
}, },
@ -213,13 +174,13 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
if !agent.behavior.is(BehaviorState::TRADING) { if !agent.behavior.is(BehaviorState::TRADING) {
controller.push_initiate_invite(by, InviteKind::Trade); controller.push_initiate_invite(by, InviteKind::Trade);
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_advertisement", Content::localized("npc-speech-merchant_advertisement"),
agent, agent,
event_emitter, event_emitter,
); );
} else { } else {
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_busy", Content::localized("npc-speech-merchant_busy"),
agent, agent,
event_emitter, event_emitter,
); );
@ -228,7 +189,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
// TODO: maybe make some travellers willing to trade with // TODO: maybe make some travellers willing to trade with
// simpler goods like potions // simpler goods like potions
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-villager_decline_trade", Content::localized("npc-speech-villager_decline_trade"),
agent, agent,
event_emitter, event_emitter,
); );
@ -243,15 +204,17 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
let dist = Distance::from_dir(raw_dir).name(); let dist = Distance::from_dir(raw_dir).name();
let dir = Direction::from_dir(raw_dir).name(); let dir = Direction::from_dir(raw_dir).name();
// TODO: Localise
let msg = format!( let msg = format!(
"{} ? I think it's {} {} from here!", "{} ? I think it's {} {} from here!",
location.name, dist, dir location.name, dist, dir
); );
agent_data.chat_npc(msg, event_emitter); agent_data.chat_npc(Content::Plain(msg), event_emitter);
} }
}, },
Subject::Person(person) => { Subject::Person(person) => {
if let Some(src_pos) = read_data.positions.get(target) { if let Some(src_pos) = read_data.positions.get(target) {
// 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() - src_pos.0.xy());
match distance { match distance {
@ -278,7 +241,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
person.name() person.name()
) )
}; };
agent_data.chat_npc(msg, event_emitter); agent_data.chat_npc(Content::Plain(msg), event_emitter);
} }
}, },
Subject::Work => {}, Subject::Work => {},
@ -330,7 +293,7 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool {
} else { } else {
controller.push_invite_response(InviteResponse::Decline); controller.push_invite_response(InviteResponse::Decline);
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_busy", Content::localized("npc-speech-merchant_busy"),
agent, agent,
event_emitter, event_emitter,
); );
@ -339,7 +302,7 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool {
// TODO: Provide a hint where to find the closest merchant? // TODO: Provide a hint where to find the closest merchant?
controller.push_invite_response(InviteResponse::Decline); controller.push_invite_response(InviteResponse::Decline);
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-villager_decline_trade", Content::localized("npc-speech-villager_decline_trade"),
agent, agent,
event_emitter, event_emitter,
); );
@ -396,14 +359,14 @@ pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool {
match result { match result {
TradeResult::Completed => { TradeResult::Completed => {
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_trade_successful", Content::localized("npc-speech-merchant_trade_successful"),
agent, agent,
event_emitter, event_emitter,
); );
}, },
_ => { _ => {
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_trade_declined", Content::localized("npc-speech-merchant_trade_declined"),
agent, agent,
event_emitter, event_emitter,
); );
@ -435,7 +398,7 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
let (tradeid, pending, prices, inventories) = *boxval; let (tradeid, pending, prices, inventories) = *boxval;
if agent.behavior.is(BehaviorState::TRADING) { if agent.behavior.is(BehaviorState::TRADING) {
let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER)); let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER));
let mut message = |text| { let mut message = |content: Content| {
if let Some(with) = agent if let Some(with) = agent
.target .target
.as_ref() .as_ref()
@ -444,14 +407,12 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell(
*agent_data.uid, *agent_data.uid,
*with, *with,
// TODO: localise this content,
Content::Plain(text),
))); )));
} else { } else {
event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say(
*agent_data.uid, *agent_data.uid,
// TODO: localise this content,
Content::Plain(text),
))); )));
} }
}; };
@ -460,14 +421,14 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
let balance0 = prices.balance(&pending.offers, &inventories, 1 - who, true); let balance0 = prices.balance(&pending.offers, &inventories, 1 - who, true);
let balance1 = prices.balance(&pending.offers, &inventories, who, false); let balance1 = prices.balance(&pending.offers, &inventories, who, false);
match (balance0, balance1) { match (balance0, balance1) {
(_, None) => { // TODO: Localise
let msg = "I'm not willing to sell that item".to_string(); (_, None) => message(Content::Plain(
message(msg); "I'm not willing to sell that item".to_string(),
}, )),
(None, _) => { // TODO: Localise
let msg = "I'm not willing to buy that item".to_string(); (None, _) => message(Content::Plain(
message(msg); "I'm not willing to buy that item".to_string(),
}, )),
(Some(balance0), Some(balance1)) => { (Some(balance0), Some(balance1)) => {
if balance0 >= balance1 { if balance0 >= balance1 {
// If the trade is favourable to us, only send an accept message if // If the trade is favourable to us, only send an accept message if
@ -492,11 +453,11 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool {
} }
} else { } else {
if balance1 > 0.0 { if balance1 > 0.0 {
let msg = format!( // TODO: Localise
message(Content::Plain(format!(
"That only covers {:.0}% of my costs!", "That only covers {:.0}% of my costs!",
(balance0 / balance1 * 100.0).floor() (balance0 / balance1 * 100.0).floor()
); )));
message(msg);
} }
if pending.phase != TradePhase::Mutate { if pending.phase != TradePhase::Mutate {
// we got into the review phase but without balanced goods, // we got into the review phase but without balanced goods,
@ -584,7 +545,7 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool {
// in combat, speak to players that aren't the current target // in combat, speak to players that aren't the current target
if !target.hostile || target.target != speaker { if !target.hostile || target.target != speaker {
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-villager_busy", Content::localized("npc-speech-villager_busy"),
agent, agent,
event_emitter, event_emitter,
); );
@ -602,13 +563,13 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool {
if !target.hostile || target.target != speaker { if !target.hostile || target.target != speaker {
if agent.behavior.can_trade(agent_data.alignment.copied(), *by) { if agent.behavior.can_trade(agent_data.alignment.copied(), *by) {
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_busy", Content::localized("npc-speech-merchant_busy"),
agent, agent,
event_emitter, event_emitter,
); );
} else { } else {
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-villager_busy", Content::localized("npc-speech-villager_busy"),
agent, agent,
event_emitter, event_emitter,
); );
@ -624,14 +585,14 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool {
match result { match result {
TradeResult::Completed => { TradeResult::Completed => {
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_trade_successful", Content::localized("npc-speech-merchant_trade_successful"),
agent, agent,
event_emitter, event_emitter,
); );
}, },
_ => { _ => {
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_trade_declined", Content::localized("npc-speech-merchant_trade_declined"),
agent, agent,
event_emitter, event_emitter,
); );
@ -653,7 +614,7 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool {
TradeAction::Decline, TradeAction::Decline,
)); ));
agent_data.chat_npc_if_allowed_to_speak( agent_data.chat_npc_if_allowed_to_speak(
"npc-speech-merchant_trade_cancelled_hostile", Content::localized("npc-speech-merchant_trade_cancelled_hostile"),
agent, agent,
event_emitter, event_emitter,
); );