Cleaned up rtsim rules

This commit is contained in:
Joshua Barretto 2023-04-05 13:57:41 +01:00
parent 3e0f5295c0
commit 5614eaa7a5
9 changed files with 397 additions and 353 deletions

View File

@ -296,6 +296,11 @@ pub enum ServerChatCommand {
Respawn, Respawn,
RevokeBuild, RevokeBuild,
RevokeBuildAll, RevokeBuildAll,
RtsimChunk,
RtsimInfo,
RtsimNpc,
RtsimPurge,
RtsimTp,
Safezone, Safezone,
Say, Say,
Scale, Scale,
@ -310,11 +315,6 @@ pub enum ServerChatCommand {
Tell, Tell,
Time, Time,
Tp, Tp,
RtsimChunk,
RtsimInfo,
RtsimNpc,
RtsimPurge,
RtsimTp,
Unban, Unban,
Version, Version,
Waypoint, Waypoint,

View File

@ -60,8 +60,9 @@ impl RtState {
fn start_default_rules(&mut self) { fn start_default_rules(&mut self) {
info!("Starting default rtsim rules..."); info!("Starting default rtsim rules...");
self.start_rule::<rule::setup::Setup>(); self.start_rule::<rule::migrate::Migrate>();
self.start_rule::<rule::replenish_resources::ReplenishResources>(); self.start_rule::<rule::replenish_resources::ReplenishResources>();
self.start_rule::<rule::sync_npcs::SyncNpcs>();
self.start_rule::<rule::simulate_npcs::SimulateNpcs>(); self.start_rule::<rule::simulate_npcs::SimulateNpcs>();
self.start_rule::<rule::npc_ai::NpcAi>(); self.start_rule::<rule::npc_ai::NpcAi>();
} }

View File

@ -1,7 +1,8 @@
pub mod migrate;
pub mod npc_ai; pub mod npc_ai;
pub mod replenish_resources; pub mod replenish_resources;
pub mod setup;
pub mod simulate_npcs; pub mod simulate_npcs;
pub mod sync_npcs;
use super::RtState; use super::RtState;
use std::fmt; use std::fmt;

View File

@ -4,9 +4,9 @@ use tracing::warn;
/// This rule runs at rtsim startup and broadly acts to perform some primitive /// This rule runs at rtsim startup and broadly acts to perform some primitive
/// migration/sanitisation in order to ensure that the state of rtsim is mostly /// migration/sanitisation in order to ensure that the state of rtsim is mostly
/// sensible. /// sensible.
pub struct Setup; pub struct Migrate;
impl Rule for Setup { impl Rule for Migrate {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> { fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnSetup>(|ctx| { rtstate.bind::<Self, OnSetup>(|ctx| {
let data = &mut *ctx.state.data_mut(); let data = &mut *ctx.state.data_mut();

View File

@ -20,7 +20,7 @@ use common::{
vol::RectVolSize, vol::RectVolSize,
}; };
use fxhash::FxHasher64; use fxhash::FxHasher64;
use itertools::Itertools; use itertools::{Either, Itertools};
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaChaRng; use rand_chacha::ChaChaRng;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
@ -325,7 +325,7 @@ where
} }
if let Some(path) = path_site(wpos, site_exit, site, ctx.index) { if let Some(path) = path_site(wpos, site_exit, site, ctx.index) {
Some(itertools::Either::Left( Some(Either::Left(
seq(path.into_iter().map(|wpos| goto_2d(wpos, 1.0, 8.0))).then(goto_2d( seq(path.into_iter().map(|wpos| goto_2d(wpos, 1.0, 8.0))).then(goto_2d(
site_exit, site_exit,
speed_factor, speed_factor,
@ -333,14 +333,10 @@ where
)), )),
)) ))
} else { } else {
Some(itertools::Either::Right(goto_2d( Some(Either::Right(goto_2d(site_exit, speed_factor, 8.0)))
site_exit,
speed_factor,
8.0,
)))
} }
} else { } else {
Some(itertools::Either::Right(goto_2d(wpos, speed_factor, 8.0))) Some(Either::Right(goto_2d(wpos, speed_factor, 8.0)))
} }
}) })
} }
@ -410,9 +406,9 @@ fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action {
// let track_len = ctx.world.civs().tracks.get(track_id).path().len(); // let track_len = ctx.world.civs().tracks.get(track_id).path().len();
// // Tracks can be traversed backward (i.e: from end to beginning). Account for this. // // Tracks can be traversed backward (i.e: from end to beginning). Account for this.
// seq(if reversed { // seq(if reversed {
// itertools::Either::Left((0..track_len).rev()) // Either::Left((0..track_len).rev())
// } else { // } else {
// itertools::Either::Right(0..track_len) // Either::Right(0..track_len)
// } // }
// .enumerate() // .enumerate()
// .map(move |(i, node_idx)| now(move |ctx| { // .map(move |(i, node_idx)| now(move |ctx| {
@ -456,7 +452,7 @@ 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 // TODO: Bit odd, should wait for a while after greeting
if ctx.rng.gen_bool(0.0003) && let Some(other) = ctx if ctx.rng.gen_bool(0.004) && let Some(other) = ctx
.state .state
.data() .data()
.npcs .npcs
@ -680,20 +676,18 @@ fn villager(visiting_site: SiteId) -> impl Action {
}) })
{ {
// Walk to the plaza... // Walk to the plaza...
travel_to_point(plaza_wpos, 0.5) Either::Left(travel_to_point(plaza_wpos, 0.5)
.debug(|| "walk to plaza") .debug(|| "walk to plaza"))
// ...then wait for some time before moving on
.then({
let wait_time = ctx.rng.gen_range(30.0..90.0);
socialize().repeat().stop_if(timeout(wait_time))
.debug(|| "wait at plaza")
})
.map(|_| ())
.boxed()
} else { } else {
// No plazas? :( // No plazas? :(
finish().boxed() Either::Right(finish())
} }
// ...then socialize for some time before moving on
.then(socialize()
.repeat()
.stop_if(timeout(ctx.rng.gen_range(30.0..90.0)))
.debug(|| "wait at plaza"))
.map(|_| ())
})) }))
}) })
.debug(move || format!("villager at site {:?}", visiting_site)) .debug(move || format!("villager at site {:?}", visiting_site))

View File

@ -1,16 +1,16 @@
use crate::{ use crate::{
data::{npc::SimulationMode, Npc}, data::{npc::SimulationMode, Npc},
event::{OnDeath, OnSetup, OnTick}, event::{EventCtx, OnDeath, OnSetup, OnTick},
RtState, Rule, RuleError, RtState, Rule, RuleError,
}; };
use common::{ use common::{
comp::{self, Body}, comp::{self, Body},
grid::Grid,
rtsim::{Actor, NpcAction, NpcActivity, Personality}, rtsim::{Actor, NpcAction, NpcActivity, Personality},
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
vol::RectVolSize, vol::RectVolSize,
}; };
use rand::{rngs::ThreadRng, seq::SliceRandom, Rng}; use rand::prelude::*;
use rand_chacha::ChaChaRng;
use tracing::warn; use tracing::warn;
use world::site::SiteKind; use world::site::SiteKind;
@ -18,10 +18,18 @@ pub struct SimulateNpcs;
impl Rule for SimulateNpcs { impl Rule for SimulateNpcs {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> { fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnSetup>(|ctx| { rtstate.bind::<Self, OnSetup>(on_setup);
let data = &mut *ctx.state.data_mut(); rtstate.bind::<Self, OnDeath>(on_death);
data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default()); rtstate.bind::<Self, OnTick>(on_tick);
Ok(Self)
}
}
fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
let data = &mut *ctx.state.data_mut();
// Add riders to vehicles
for (npc_id, npc) in data.npcs.npcs.iter() { for (npc_id, npc) in data.npcs.npcs.iter() {
if let Some(ride) = &npc.riding { if let Some(ride) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) { if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) {
@ -32,23 +40,18 @@ impl Rule for SimulateNpcs {
} }
} }
} }
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
home.population.insert(npc_id);
} }
} }
});
rtstate.bind::<Self, OnDeath>(|ctx| { fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
let data = &mut *ctx.state.data_mut(); let data = &mut *ctx.state.data_mut();
let npc_id = ctx.event.npc_id; let npc_id = ctx.event.npc_id;
let Some(npc) = data.npcs.get(npc_id) else { let Some(npc) = data.npcs.get(npc_id) else {
return; return;
}; };
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) { let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
home.population.remove(&npc_id);
} // Respawn dead NPCs
let mut rng = rand::thread_rng();
match npc.body { match npc.body {
Body::Humanoid(_) => { Body::Humanoid(_) => {
if let Some((site_id, site)) = data if let Some((site_id, site)) = data
@ -63,13 +66,13 @@ impl Rule for SimulateNpcs {
}) })
.min_by_key(|(_, site)| site.population.len()) .min_by_key(|(_, site)| site.population.len())
{ {
let rand_wpos = |rng: &mut ThreadRng| { let rand_wpos = |rng: &mut ChaChaRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d wpos2d
.map(|e| e as f32 + 0.5) .map(|e| e as f32 + 0.5)
.with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
}; };
let random_humanoid = |rng: &mut ThreadRng| { let random_humanoid = |rng: &mut ChaChaRng| {
let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap(); let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
}; };
@ -96,7 +99,7 @@ impl Rule for SimulateNpcs {
}) })
.min_by_key(|(_, site)| site.population.len()) .min_by_key(|(_, site)| site.population.len())
{ {
let rand_wpos = |rng: &mut ThreadRng| { let rand_wpos = |rng: &mut ChaChaRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d wpos2d
.map(|e| e as f32 + 0.5) .map(|e| e as f32 + 0.5)
@ -125,77 +128,29 @@ impl Rule for SimulateNpcs {
}, },
_ => unimplemented!(), _ => unimplemented!(),
} }
}); }
rtstate.bind::<Self, OnTick>(|ctx| { fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
let data = &mut *ctx.state.data_mut(); let data = &mut *ctx.state.data_mut();
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { for npc in data
let chunk_pos = .npcs
vehicle.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>(); .npcs
if vehicle.chunk_pos != Some(chunk_pos) { .values_mut()
if let Some(cell) = vehicle .filter(|npc| matches!(npc.mode, SimulationMode::Simulated))
.chunk_pos
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
{ {
if let Some(index) = cell.vehicles.iter().position(|id| *id == vehicle_id) {
cell.vehicles.swap_remove(index);
}
}
vehicle.chunk_pos = Some(chunk_pos);
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
cell.vehicles.push(vehicle_id);
}
}
}
for (npc_id, npc) in data.npcs.npcs.iter_mut() {
// Update the NPC's current site, if any
npc.current_site = ctx
.world
.sim()
.get(npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_())
.and_then(|chunk| {
chunk
.sites
.iter()
.find_map(|site| data.sites.world_site_map.get(site).copied())
});
let chunk_pos =
npc.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
if npc.chunk_pos != Some(chunk_pos) {
if let Some(cell) = npc
.chunk_pos
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
{
if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) {
cell.npcs.swap_remove(index);
}
}
npc.chunk_pos = Some(chunk_pos);
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
cell.npcs.push(npc_id);
}
}
// Simulate the NPC's movement and interactions
if matches!(npc.mode, SimulationMode::Simulated) {
// Simulate NPC movement when riding // Simulate NPC movement when riding
if let Some(riding) = &npc.riding { if let Some(riding) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
match npc.controller.activity { match npc.controller.activity {
// If steering, the NPC controls the vehicle's motion // If steering, the NPC controls the vehicle's motion
Some(NpcActivity::Goto(target, speed_factor)) Some(NpcActivity::Goto(target, speed_factor)) if riding.steering => {
if riding.steering =>
{
let diff = target.xy() - vehicle.wpos.xy(); let diff = target.xy() - vehicle.wpos.xy();
let dist2 = diff.magnitude_squared(); let dist2 = diff.magnitude_squared();
if dist2 > 0.5f32.powi(2) { if dist2 > 0.5f32.powi(2) {
let mut wpos = vehicle.wpos let mut wpos = vehicle.wpos
+ (diff + (diff
* (vehicle.get_speed() * (vehicle.get_speed() * speed_factor * ctx.event.dt
* speed_factor
* ctx.event.dt
/ dist2.sqrt()) / dist2.sqrt())
.min(1.0)) .min(1.0))
.with_z(0.0); .with_z(0.0);
@ -205,8 +160,11 @@ impl Rule for SimulateNpcs {
| common::comp::ship::Body::AirBalloon => true, | common::comp::ship::Body::AirBalloon => true,
common::comp::ship::Body::SailBoat common::comp::ship::Body::SailBoat
| common::comp::ship::Body::Galleon => { | common::comp::ship::Body::Galleon => {
let chunk_pos = wpos.xy().as_::<i32>() let chunk_pos =
/ TerrainChunkSize::RECT_SIZE.as_::<i32>(); wpos.xy().as_::<i32>().map2(
TerrainChunkSize::RECT_SIZE.as_::<i32>(),
|e, sz| e.div_euclid(sz),
);
ctx.world ctx.world
.sim() .sim()
.get(chunk_pos) .get(chunk_pos)
@ -269,19 +227,13 @@ impl Rule for SimulateNpcs {
if dist2 > 0.5f32.powi(2) { if dist2 > 0.5f32.powi(2) {
npc.wpos += (diff npc.wpos += (diff
* (npc.body.max_speed_approx() * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
* speed_factor
* ctx.event.dt
/ dist2.sqrt()) / dist2.sqrt())
.min(1.0)) .min(1.0))
.with_z(0.0); .with_z(0.0);
} }
}, },
Some( Some(NpcActivity::Gather(_) | NpcActivity::HuntAnimals | NpcActivity::Dance) => {
NpcActivity::Gather(_)
| NpcActivity::HuntAnimals
| NpcActivity::Dance,
) => {
// TODO: Maybe they should walk around randomly // TODO: Maybe they should walk around randomly
// when gathering resources? // when gathering resources?
}, },
@ -292,7 +244,8 @@ 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(_) | NpcAction::Say(_) => {}, // Currently, just swallow interactions NpcAction::Greet(_) | NpcAction::Say(_) => {}, /* Currently, just swallow
* interactions */
} }
} }
@ -305,8 +258,3 @@ impl Rule for SimulateNpcs {
+ npc.body.flying_height(); + npc.body.flying_height();
} }
} }
});
Ok(Self)
}
}

112
rtsim/src/rule/sync_npcs.rs Normal file
View File

@ -0,0 +1,112 @@
use crate::{
event::{EventCtx, OnDeath, OnSetup, OnTick},
RtState, Rule, RuleError,
};
use common::{grid::Grid, terrain::TerrainChunkSize, vol::RectVolSize};
pub struct SyncNpcs;
impl Rule for SyncNpcs {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnSetup>(on_setup);
rtstate.bind::<Self, OnDeath>(on_death);
rtstate.bind::<Self, OnTick>(on_tick);
Ok(Self)
}
}
fn on_setup(ctx: EventCtx<SyncNpcs, OnSetup>) {
let data = &mut *ctx.state.data_mut();
// Create NPC grid
data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default());
// Add NPCs to home population (TODO: Do this on entity creation?)
for (npc_id, npc) in data.npcs.npcs.iter() {
if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) {
home.population.insert(npc_id);
}
}
}
fn on_death(ctx: EventCtx<SyncNpcs, OnDeath>) {
let data = &mut *ctx.state.data_mut();
println!("NPC DIED!");
// Remove NPC from home population
if let Some(home) = data
.npcs
.get(ctx.event.npc_id)
.and_then(|npc| npc.home)
.and_then(|home| data.sites.get_mut(home))
{
home.population.remove(&ctx.event.npc_id);
}
}
fn on_tick(ctx: EventCtx<SyncNpcs, OnTick>) {
let data = &mut *ctx.state.data_mut();
// Update vehicle grid cells
for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() {
let chunk_pos = vehicle.wpos.xy().as_::<i32>() / TerrainChunkSize::RECT_SIZE.as_::<i32>();
if vehicle.chunk_pos != Some(chunk_pos) {
if let Some(cell) = vehicle
.chunk_pos
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
{
if let Some(index) = cell.vehicles.iter().position(|id| *id == vehicle_id) {
cell.vehicles.swap_remove(index);
}
}
vehicle.chunk_pos = Some(chunk_pos);
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
cell.vehicles.push(vehicle_id);
}
}
}
for (npc_id, npc) in data.npcs.npcs.iter_mut() {
// Update the NPC's current site, if any
npc.current_site = ctx
.world
.sim()
.get(
npc.wpos
.xy()
.as_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
e.div_euclid(sz)
}),
)
.and_then(|chunk| {
chunk
.sites
.iter()
.find_map(|site| data.sites.world_site_map.get(site).copied())
});
// Update the NPC's grid cell
let chunk_pos = npc
.wpos
.xy()
.as_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
e.div_euclid(sz)
});
if npc.chunk_pos != Some(chunk_pos) {
if let Some(cell) = npc
.chunk_pos
.and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos))
{
if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) {
cell.npcs.swap_remove(index);
}
}
npc.chunk_pos = Some(chunk_pos);
if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) {
cell.npcs.push(npc_id);
}
}
}
}

View File

@ -224,20 +224,12 @@ impl<'a> System<'a> for Sys {
data.time_of_day = *time_of_day; data.time_of_day = *time_of_day;
// Update character map (i.e: so that rtsim knows where players are) // Update character map (i.e: so that rtsim knows where players are)
// TODO: Other entities too? Or do we now care about that? // TODO: Other entities too like animals? Or do we now care about that?
data.npcs.character_map.clear(); data.npcs.character_map.clear();
for (character, wpos) in for (presence, wpos) in (&presences, &positions).join() {
(&presences, &positions)
.join()
.filter_map(|(presence, pos)| {
if let PresenceKind::Character(character) = &presence.kind { if let PresenceKind::Character(character) = &presence.kind {
Some((character, pos.0))
} else {
None
}
})
{
let chunk_pos = wpos let chunk_pos = wpos
.0
.xy() .xy()
.as_::<i32>() .as_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| { .map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |e, sz| {
@ -247,7 +239,8 @@ impl<'a> System<'a> for Sys {
.character_map .character_map
.entry(chunk_pos) .entry(chunk_pos)
.or_default() .or_default()
.push((*character, wpos)); .push((*character, wpos.0));
}
} }
} }

View File

@ -477,18 +477,18 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
NpcAction::Greet(actor) => { NpcAction::Greet(actor) => {
if bdata.agent.allowed_to_speak() if bdata.agent.allowed_to_speak()
&& let Some(target) = bdata.read_data.lookup_actor(actor) && let Some(target) = bdata.read_data.lookup_actor(actor)
&& let Some(target_pos) = bdata.read_data.positions.get(target)
{ {
bdata.agent.target = Some(Target::new( bdata.agent.target = Some(Target::new(
target, target,
false, false,
bdata.read_data.time.0, bdata.read_data.time.0,
false, false,
Some(target_pos.0), bdata.read_data.positions.get(target).map(|p| p.0),
)); ));
// We're always aware of someone we're talking to // We're always aware of someone we're talking to
bdata.agent.awareness.set_maximally_aware(); bdata.agent.awareness.set_maximally_aware();
bdata.controller.push_action(ControlAction::Stand);
bdata.controller.push_action(ControlAction::Talk); bdata.controller.push_action(ControlAction::Talk);
bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.controller.push_utterance(UtteranceKind::Greeting);
bdata bdata
@ -499,20 +499,15 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
.agent .agent
.timer .timer
.start(bdata.read_data.time.0, TimerAction::Interact); .start(bdata.read_data.time.0, TimerAction::Interact);
true
} else {
false
} }
}, },
NpcAction::Say(msg) => { NpcAction::Say(msg) => {
bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.controller.push_utterance(UtteranceKind::Greeting);
bdata.agent_data.chat_npc(msg, bdata.event_emitter); bdata.agent_data.chat_npc(msg, bdata.event_emitter);
false
}, },
} }
} else {
false
} }
false
} }
/// Handle timed events, like looking at the player we are talking to /// Handle timed events, like looking at the player we are talking to