From cf701fb604e6dae77f3a3070feaf203aac1e226b Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Tue, 11 Apr 2023 17:00:08 +0100 Subject: [PATCH] Localised rtsim NPC speech --- assets/voxygen/i18n/en/npc.ftl | 32 +++++ common/src/comp/chat.rs | 6 +- common/src/rtsim.rs | 33 ++++- rtsim/src/data/npc.rs | 5 +- rtsim/src/rule/npc_ai.rs | 119 +++++++++--------- server/agent/src/action_nodes.rs | 35 +++--- .../sys/agent/behavior_tree/interaction.rs | 119 ++++++------------ 7 files changed, 188 insertions(+), 161 deletions(-) diff --git a/assets/voxygen/i18n/en/npc.ftl b/assets/voxygen/i18n/en/npc.ftl index 097f9c4f18..5e53621f74 100644 --- a/assets/voxygen/i18n/en/npc.ftl +++ b/assets/voxygen/i18n/en/npc.ftl @@ -223,3 +223,35 @@ npc-speech-prisoner = .a2 = That Cardinal can't be trusted. .a3 = These Clerics are up to no good. .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! diff --git a/common/src/comp/chat.rs b/common/src/comp/chat.rs index 0322b51472..56cd6664c4 100644 --- a/common/src/comp/chat.rs +++ b/common/src/comp/chat.rs @@ -200,10 +200,12 @@ pub enum Content { }, } +// TODO: Remove impl and make use of `Plain` explicit (to discourage it) impl From for Content { 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 { fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) } } @@ -212,7 +214,7 @@ impl Content { pub fn localized(key: impl ToString) -> Self { Self::Localized { key: key.to_string(), - r: rand::random(), + seed: rand::random(), args: HashMap::default(), } } @@ -223,7 +225,7 @@ impl Content { ) -> Self { Self::Localized { key: key.to_string(), - r: rand::random(), + seed: rand::random(), args: args .into_iter() .map(|(k, v)| (k.to_string(), v.to_string())) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index aee3d932a0..9dc0dfa728 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -3,11 +3,11 @@ // `Agent`). When possible, this should be moved to the `rtsim` // module in `server`. -use crate::character::CharacterId; +use crate::{character::CharacterId, comp::Content}; use rand::{seq::IteratorRandom, Rng}; use serde::{Deserialize, Serialize}; use specs::Component; -use std::{borrow::Cow, collections::VecDeque}; +use std::collections::VecDeque; use strum::{EnumIter, IntoEnumIterator}; use vek::*; @@ -169,6 +169,33 @@ impl Personality { pub fn will_ambush(&self) -> bool { 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 { @@ -226,7 +253,7 @@ pub enum NpcAction { /// Speak the given message, with an optional target for that speech. // TODO: Use some sort of structured, language-independent value that frontends can translate // instead - Say(Option, Cow<'static, str>), + Say(Option, Content), /// Attack the given target Attack(Actor), } diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index f892537e1f..f9b436ca9e 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -20,7 +20,6 @@ use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ - borrow::Cow, collections::VecDeque, ops::{Deref, DerefMut}, }; @@ -74,8 +73,8 @@ impl Controller { pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); } - pub fn say(&mut self, target: impl Into>, msg: impl Into>) { - self.actions.push(NpcAction::Say(target.into(), msg.into())); + pub fn say(&mut self, target: impl Into>, content: comp::Content) { + self.actions.push(NpcAction::Say(target.into(), content)); } pub fn attack(&mut self, target: impl Into) { diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index c964ca0869..1cab7e7210 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -11,6 +11,7 @@ use crate::{ }; use common::{ astar::{Astar, PathResult}, + comp::Content, path::Path, rtsim::{ChunkResource, Profession, SiteId}, spiral::Spiral2d, @@ -470,15 +471,17 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { fn socialize() -> impl Action { now(|ctx| { - // TODO: Bit odd, should wait for a while after greeting - if ctx.rng.gen_bool(0.002) { + // Skip most socialising actions if we're not loaded + if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) { if ctx.rng.gen_bool(0.15) { - return just(|ctx| ctx.controller.do_dance()) - .repeat() - .stop_if(timeout(6.0)) - .debug(|| "dancing") - .map(|_| ()) - .boxed(); + return Either::Left( + just(|ctx| ctx.controller.do_dance()) + .repeat() + .stop_if(timeout(6.0)) + .debug(|| "dancing") + .map(|_| ()) + .boxed(), + ); } else if let Some(other) = ctx .state .data() @@ -486,12 +489,17 @@ fn socialize() -> impl Action { .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) .choose(&mut ctx.rng) { - return just(move |ctx| ctx.controller.say(other, "npc-speech-villager_open")) - .boxed(); + return Either::Left( + 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()) .unwrap_or_default(); // 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)) // Stop for a few minutes .then(villager(tgt_site).repeat().stop_if(timeout(wait_time))) @@ -634,14 +642,20 @@ fn villager(visiting_site: SiteId) -> impl Action { Some(site2.tile_center_wpos(house.root_tile()).as_()) }) { - just(|ctx| ctx.controller.say(None, "It's dark, time to go home")) - .then(travel_to_point(house_wpos, 0.65)) - .debug(|| "walk to house") - .then(socialize().repeat().debug(|| "wait in house")) - .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) - .then(just(|ctx| ctx.controller.say(None, "A new day begins!"))) - .map(|_| ()) - .boxed() + just(|ctx| { + ctx.controller + .say(None, Content::localized("npc-speech-night_time")) + }) + .then(travel_to_point(house_wpos, 0.65)) + .debug(|| "walk to house") + .then(socialize().repeat().debug(|| "wait in house")) + .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) + .then(just(|ctx| { + ctx.controller + .say(None, Content::localized("npc-speech-day_time")) + })) + .map(|_| ()) + .boxed() } else { finish().boxed() } @@ -665,14 +679,17 @@ fn villager(visiting_site: SiteId) -> impl Action { } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) { if let Some(forest_wpos) = find_forest(ctx) { return casual( - just(|ctx| ctx.controller.say(None, "Time to go hunting!")) - .then(travel_to_point(forest_wpos, 0.75)) - .debug(|| "walk to forest") - .then({ - let wait_time = ctx.rng.gen_range(30.0..60.0); - hunt_animals().repeat().stop_if(timeout(wait_time)) - }) - .map(|_| ()), + just(|ctx| { + ctx.controller + .say(None, Content::localized("npc-speech-start_hunting")) + }) + .then(travel_to_point(forest_wpos, 0.75)) + .debug(|| "walk to forest") + .then({ + let wait_time = ctx.rng.gen_range(30.0..60.0); + hunt_animals().repeat().stop_if(timeout(wait_time)) + }) + .map(|_| ()), ); } } else if matches!(ctx.npc.profession, Some(Profession::Guard)) && ctx.rng.gen_bool(0.5) { @@ -682,15 +699,10 @@ fn villager(visiting_site: SiteId) -> impl Action { .debug(|| "patrol") .interrupt_with(|ctx| { if ctx.rng.gen_bool(0.0003) { - let phrase = *[ - "My brother's out fighting ogres. What do I get? Guard duty...", - "Just one more patrol, then I can head home", - "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))) + Some(just(move |ctx| { + ctx.controller + .say(None, Content::localized("npc-speech-guard_thought")) + })) } else { None } @@ -703,32 +715,20 @@ fn villager(visiting_site: SiteId) -> impl Action { return casual( just(|ctx| { // 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 .data() .npcs .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0) .choose(&mut ctx.rng) { - (Some(other), &[ - "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 + (Some(other), "npc-speech-merchant_sell_directed") } else { - (None, &[ - "All my goods are of the highest quality!", - "Does anybody want to buy my wares?", - "I've got the best offers in town", - "Looking for supplies? I've got you covered", - ][..]) + // Otherwise, resort to generic expressions + (None, "npc-speech-merchant_sell_undirected") }; - ctx.controller.say( - target, - *phrases.iter().choose(&mut ctx.rng).unwrap(), // Can't fail - ); + ctx.controller.say(target, Content::localized(phrase)); }) .then(idle().repeat().stop_if(timeout(8.0))) .repeat() @@ -898,16 +898,17 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option { Some(ReportKind::Death { killer, .. }) => { // TODO: Sentiment should be positive if we didn't like actor that died // 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 ctx.sentiments.change_by(killer, -0.7, Sentiment::VILLAIN); - &["Murderer!", "How could you do this?", "Aaargh!"][..] + "npc-speech-witness_murder" } 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); - 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 } diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index bad47a3f8b..a41be73d53 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -731,7 +731,7 @@ impl<'a> AgentData<'a> { } else if can_ambush(entity, read_data) { controller.clone().push_utterance(UtteranceKind::Ambush); self.chat_npc_if_allowed_to_speak( - "npc-speech-ambush".to_string(), + Content::localized("npc-speech-ambush"), agent, event_emitter, ); @@ -1517,7 +1517,7 @@ impl<'a> AgentData<'a> { pub fn chat_npc_if_allowed_to_speak( &self, - msg: impl ToString, + msg: Content, agent: &Agent, event_emitter: &mut Emitter<'_, ServerEvent>, ) -> 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( - *self.uid, - Content::localized(key), + *self.uid, content, ))); } @@ -1561,13 +1560,13 @@ impl<'a> AgentData<'a> { // FIXME: If going to use "cultist + low health + fleeing" string, make sure // they are each true. self.chat_npc_if_allowed_to_speak( - "npc-speech-cultist_low_health_fleeing", + Content::localized("npc-speech-cultist_low_health_fleeing"), agent, event_emitter, ); } else if is_villager(self.alignment) { self.chat_npc_if_allowed_to_speak( - "npc-speech-villager_under_attack", + Content::localized("npc-speech-villager_under_attack"), agent, event_emitter, ); @@ -1582,7 +1581,7 @@ impl<'a> AgentData<'a> { ) { if is_villager(self.alignment) { self.chat_npc_if_allowed_to_speak( - "npc-speech-villager_enemy_killed", + Content::localized("npc-speech-villager_enemy_killed"), agent, event_emitter, ); @@ -1756,16 +1755,22 @@ impl<'a> AgentData<'a> { let move_dir = controller.inputs.move_dir; let move_dir_mag = move_dir.magnitude(); let small_chance = rng.gen::() < read_data.dt.0 * 0.25; - let mut chat = |msg: &str| { - self.chat_npc_if_allowed_to_speak(msg.to_string(), agent, event_emitter); + let mut chat = |content: Content| { + self.chat_npc_if_allowed_to_speak(content, agent, event_emitter); }; let mut chat_villager_remembers_fighting = || { let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone()); + // TODO: Localise 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 { - 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 { chat_villager_remembers_fighting(); } else if is_dressed_as_cultist(target, read_data) { - chat("npc-speech-villager_cultist_alarm"); + chat(Content::localized("npc-speech-villager_cultist_alarm")); } else { - chat("npc-speech-menacing"); + chat(Content::localized("npc-speech-menacing")); } } else { - chat("npc-speech-menacing"); + chat(Content::localized("npc-speech-menacing")); } } } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index e097e1eae4..d9f3e23bad 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -155,56 +155,17 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { } else { standard_response_msg() }; - agent_data.chat_npc(msg, event_emitter); + // TODO: Localise + agent_data.chat_npc(Content::Plain(msg), event_emitter); } else { let mut rng = thread_rng(); - if let Some(extreme_trait) = - agent.rtsim_controller.personality.chat_trait(&mut rng) - { - let msg = match extreme_trait { - PersonalityTrait::Open => "npc-speech-villager_open", - PersonalityTrait::Adventurous => { - "npc-speech-villager_adventurous" - }, - PersonalityTrait::Closed => "npc-speech-villager_closed", - PersonalityTrait::Conscientious => { - "npc-speech-villager_conscientious" - }, - PersonalityTrait::Busybody => { - "npc-speech-villager_busybody" - }, - PersonalityTrait::Unconscientious => { - "npc-speech-villager_unconscientious" - }, - PersonalityTrait::Extroverted => { - "npc-speech-villager_extroverted" - }, - PersonalityTrait::Introverted => { - "npc-speech-villager_introverted" - }, - PersonalityTrait::Agreeable => { - "npc-speech-villager_agreeable" - }, - PersonalityTrait::Sociable => { - "npc-speech-villager_sociable" - }, - PersonalityTrait::Disagreeable => { - "npc-speech-villager_disagreeable" - }, - PersonalityTrait::Neurotic => { - "npc-speech-villager_neurotic" - }, - PersonalityTrait::Seeker => "npc-speech-villager_seeker", - PersonalityTrait::SadLoner => { - "npc-speech-villager_sad_loner" - }, - PersonalityTrait::Worried => "npc-speech-villager_worried", - PersonalityTrait::Stable => "npc-speech-villager_stable", - }; - agent_data.chat_npc(msg, event_emitter); - } else { - agent_data.chat_npc("npc-speech-villager", event_emitter); - } + agent_data.chat_npc( + agent + .rtsim_controller + .personality + .get_generic_comment(&mut rng), + event_emitter, + ); } } }, @@ -213,13 +174,13 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { if !agent.behavior.is(BehaviorState::TRADING) { controller.push_initiate_invite(by, InviteKind::Trade); agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_advertisement", + Content::localized("npc-speech-merchant_advertisement"), agent, event_emitter, ); } else { agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_busy", + Content::localized("npc-speech-merchant_busy"), agent, event_emitter, ); @@ -228,7 +189,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { // TODO: maybe make some travellers willing to trade with // simpler goods like potions agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-villager_decline_trade", + Content::localized("npc-speech-villager_decline_trade"), agent, event_emitter, ); @@ -243,15 +204,17 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { let dist = Distance::from_dir(raw_dir).name(); let dir = Direction::from_dir(raw_dir).name(); + // TODO: Localise let msg = format!( "{} ? I think it's {} {} from here!", location.name, dist, dir ); - agent_data.chat_npc(msg, event_emitter); + agent_data.chat_npc(Content::Plain(msg), event_emitter); } }, Subject::Person(person) => { if let Some(src_pos) = read_data.positions.get(target) { + // TODO: Localise let msg = if let Some(person_pos) = person.origin { let distance = Distance::from_dir(person_pos.xy() - src_pos.0.xy()); match distance { @@ -278,7 +241,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { person.name() ) }; - agent_data.chat_npc(msg, event_emitter); + agent_data.chat_npc(Content::Plain(msg), event_emitter); } }, Subject::Work => {}, @@ -330,7 +293,7 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { } else { controller.push_invite_response(InviteResponse::Decline); agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_busy", + Content::localized("npc-speech-merchant_busy"), agent, 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? controller.push_invite_response(InviteResponse::Decline); agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-villager_decline_trade", + Content::localized("npc-speech-villager_decline_trade"), agent, event_emitter, ); @@ -396,14 +359,14 @@ pub fn handle_inbox_finished_trade(bdata: &mut BehaviorData) -> bool { match result { TradeResult::Completed => { agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_trade_successful", + Content::localized("npc-speech-merchant_trade_successful"), agent, event_emitter, ); }, _ => { agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_trade_declined", + Content::localized("npc-speech-merchant_trade_declined"), agent, event_emitter, ); @@ -435,7 +398,7 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { let (tradeid, pending, prices, inventories) = *boxval; if agent.behavior.is(BehaviorState::TRADING) { let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER)); - let mut message = |text| { + let mut message = |content: Content| { if let Some(with) = agent .target .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( *agent_data.uid, *with, - // TODO: localise this - Content::Plain(text), + content, ))); } else { event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( *agent_data.uid, - // TODO: localise this - Content::Plain(text), + content, ))); } }; @@ -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 balance1 = prices.balance(&pending.offers, &inventories, who, false); match (balance0, balance1) { - (_, None) => { - let msg = "I'm not willing to sell that item".to_string(); - message(msg); - }, - (None, _) => { - let msg = "I'm not willing to buy that item".to_string(); - message(msg); - }, + // TODO: Localise + (_, None) => message(Content::Plain( + "I'm not willing to sell that item".to_string(), + )), + // TODO: Localise + (None, _) => message(Content::Plain( + "I'm not willing to buy that item".to_string(), + )), (Some(balance0), Some(balance1)) => { if balance0 >= balance1 { // 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 { if balance1 > 0.0 { - let msg = format!( + // TODO: Localise + message(Content::Plain(format!( "That only covers {:.0}% of my costs!", (balance0 / balance1 * 100.0).floor() - ); - message(msg); + ))); } if pending.phase != TradePhase::Mutate { // 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 if !target.hostile || target.target != speaker { agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-villager_busy", + Content::localized("npc-speech-villager_busy"), agent, event_emitter, ); @@ -602,13 +563,13 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { if !target.hostile || target.target != speaker { if agent.behavior.can_trade(agent_data.alignment.copied(), *by) { agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_busy", + Content::localized("npc-speech-merchant_busy"), agent, event_emitter, ); } else { agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-villager_busy", + Content::localized("npc-speech-villager_busy"), agent, event_emitter, ); @@ -624,14 +585,14 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { match result { TradeResult::Completed => { agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_trade_successful", + Content::localized("npc-speech-merchant_trade_successful"), agent, event_emitter, ); }, _ => { agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_trade_declined", + Content::localized("npc-speech-merchant_trade_declined"), agent, event_emitter, ); @@ -653,7 +614,7 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { TradeAction::Decline, )); agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_trade_cancelled_hostile", + Content::localized("npc-speech-merchant_trade_cancelled_hostile"), agent, event_emitter, );