Added the ability for rtsim to tell NPCs to speak

This commit is contained in:
Joshua Barretto 2023-04-03 18:03:07 +01:00
parent 7dfbc2bdab
commit b72d8f3192
5 changed files with 32 additions and 12 deletions

View File

@ -7,7 +7,7 @@ use crate::character::CharacterId;
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::collections::VecDeque; use std::{borrow::Cow, collections::VecDeque};
use strum::{EnumIter, IntoEnumIterator}; use strum::{EnumIter, IntoEnumIterator};
use vek::*; use vek::*;
@ -219,9 +219,11 @@ pub enum NpcActivity {
Dance, Dance,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Debug)]
pub enum NpcAction { pub enum NpcAction {
Greet(Actor), Greet(Actor),
// TODO: Use some sort of structured, language-independent value that frontends can translate instead
Say(Cow<'static, str>),
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)]

View File

@ -15,6 +15,7 @@ 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},
}; };
@ -69,6 +70,10 @@ 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 greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } pub fn greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); }
pub fn say(&mut self, msg: impl Into<Cow<'static, str>>) {
self.actions.push(NpcAction::Say(msg.into()));
}
} }
pub struct Brain { pub struct Brain {

View File

@ -348,7 +348,7 @@ where
/// Try to travel to a site. Where practical, paths will be taken. /// Try to travel to a site. Where practical, paths will be taken.
fn travel_to_point(wpos: Vec2<f32>) -> impl Action { fn travel_to_point(wpos: Vec2<f32>) -> impl Action {
now(move |ctx| { now(move |ctx| {
const WAYPOINT: f32 = 24.0; const WAYPOINT: f32 = 48.0;
let start = ctx.npc.wpos.xy(); let start = ctx.npc.wpos.xy();
let diff = wpos - start; let diff = wpos - start;
let n = (diff.magnitude() / WAYPOINT).max(1.0); let n = (diff.magnitude() / WAYPOINT).max(1.0);
@ -506,9 +506,12 @@ fn adventure() -> impl Action {
} else { } else {
60.0 * 3.0 60.0 * 3.0
}; };
let site_name = ctx.state.data().sites[tgt_site].world_site
.map(|ws| ctx.index.sites.get(ws).name().to_string())
.unwrap_or_default();
// Travel to the site // Travel to the site
important( important(just(move |ctx| ctx.controller.say(format!("I've spent enough time here, onward to {}!", site_name)))
travel_to_site(tgt_site) .then(travel_to_site(tgt_site))
// 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)))
.map(|_| ()) .map(|_| ())
@ -597,10 +600,12 @@ 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_())
}) })
{ {
travel_to_point(house_wpos) just(|ctx| ctx.controller.say("It's dark, time to go home"))
.then(travel_to_point(house_wpos))
.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("A new day begins!")))
.map(|_| ()) .map(|_| ())
.boxed() .boxed()
} else { } else {
@ -610,9 +615,11 @@ fn villager(visiting_site: SiteId) -> impl Action {
.debug(|| "find somewhere to sleep"), .debug(|| "find somewhere to sleep"),
); );
// Villagers with roles should perform those roles // Villagers with roles should perform those roles
} else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) { } else if matches!(ctx.npc.profession, Some(Profession::Herbalist))
&& thread_rng().gen_bool(0.8)
{
if let Some(forest_wpos) = find_forest(ctx) { if let Some(forest_wpos) = find_forest(ctx) {
return important( return casual(
travel_to_point(forest_wpos) travel_to_point(forest_wpos)
.debug(|| "walk to forest") .debug(|| "walk to forest")
.then({ .then({
@ -622,10 +629,13 @@ fn villager(visiting_site: SiteId) -> impl Action {
.map(|_| ()), .map(|_| ()),
); );
} }
} else if matches!(ctx.npc.profession, Some(Profession::Hunter)) { } else if matches!(ctx.npc.profession, Some(Profession::Hunter))
&& thread_rng().gen_bool(0.8)
{
if let Some(forest_wpos) = find_forest(ctx) { if let Some(forest_wpos) = find_forest(ctx) {
return important( return casual(
travel_to_point(forest_wpos) just(|ctx| ctx.controller.say("Time to go hunting!"))
.then(travel_to_point(forest_wpos))
.debug(|| "walk to forest") .debug(|| "walk to forest")
.then({ .then({
let wait_time = thread_rng().gen_range(30.0..60.0); let wait_time = thread_rng().gen_range(30.0..60.0);

View File

@ -292,7 +292,7 @@ impl Rule for SimulateNpcs {
// Consume NPC actions // Consume NPC actions
for action in std::mem::take(&mut npc.controller.actions) { for action in std::mem::take(&mut npc.controller.actions) {
match action { match action {
NpcAction::Greet(_) => {}, // Currently, just swallow greeting actions NpcAction::Greet(_) | NpcAction::Say(_) => {}, // Currently, just swallow interactions
} }
} }

View File

@ -505,6 +505,9 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
} }
} }
}, },
NpcAction::Say(msg) => {
bdata.agent_data.chat_npc(msg, &mut bdata.event_emitter);
},
} }
} }
false false