Cleaned up rtsim tick handling

This commit is contained in:
Joshua Barretto 2023-04-05 16:32:54 +01:00
parent 80e4e8deae
commit 74610833d0
11 changed files with 100 additions and 53 deletions

View File

@ -219,12 +219,14 @@ pub enum NpcActivity {
Dance,
}
/// Represents event-like actions that rtsim NPCs can perform to interact with
/// the world
#[derive(Clone, Debug)]
pub enum NpcAction {
Greet(Actor),
/// 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(Cow<'static, str>),
Say(Option<Actor>, Cow<'static, str>),
}
// Note: the `serde(name = "...")` is to minimise the length of field

View File

@ -10,7 +10,7 @@ pub struct Faction {
pub good_or_evil: bool, // TODO: Very stupid, get rid of this
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Factions {
pub factions: HopSlotMap<FactionId, Faction>,
}

View File

@ -23,12 +23,20 @@ use std::{
#[derive(Clone, Serialize, Deserialize)]
pub struct Data {
pub nature: Nature,
#[serde(default)]
pub npcs: Npcs,
#[serde(default)]
pub sites: Sites,
#[serde(default)]
pub factions: Factions,
#[serde(default)]
pub tick: u64,
#[serde(default)]
pub time_of_day: TimeOfDay,
// If true, rtsim data will be ignored (and, hence, overwritten on next save) on load.
#[serde(default)]
pub should_purge: bool,
}

View File

@ -4,7 +4,8 @@ use common::{
comp,
grid::Grid,
rtsim::{
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId,
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, NpcMsg, Personality, SiteId,
VehicleId,
},
store::Id,
terrain::TerrainChunkSize,
@ -69,10 +70,8 @@ impl Controller {
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 say(&mut self, msg: impl Into<Cow<'static, str>>) {
self.actions.push(NpcAction::Say(msg.into()));
pub fn say(&mut self, target: impl Into<Option<Actor>>, msg: impl Into<Cow<'static, str>>) {
self.actions.push(NpcAction::Say(target.into(), msg.into()));
}
}
@ -206,6 +205,7 @@ pub enum VehicleKind {
Boat,
}
// TODO: Merge into `Npc`?
#[derive(Clone, Serialize, Deserialize)]
pub struct Vehicle {
pub wpos: Vec3<f32>,
@ -273,6 +273,17 @@ pub struct Npcs {
pub character_map: HashMap<Vec2<i32>, Vec<(common::character::CharacterId, Vec3<f32>)>>,
}
impl Default for Npcs {
fn default() -> Self {
Self {
npcs: Default::default(),
vehicles: Default::default(),
npc_grid: construct_npc_grid(),
character_map: Default::default(),
}
}
}
fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) }
impl Npcs {

View File

@ -42,7 +42,7 @@ impl Site {
}
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Sites {
pub sites: HopSlotMap<SiteId, Site>,

View File

@ -20,6 +20,7 @@ impl Event for OnSetup {}
pub struct OnTick {
pub time_of_day: TimeOfDay,
pub time: Time,
pub tick: u64,
pub dt: f32,
}
impl Event for OnTick {}

View File

@ -44,6 +44,7 @@ impl Data {
factions: Default::default(),
},
tick: 0,
time_of_day: TimeOfDay(settings.start_time),
should_purge: false,
};

View File

@ -150,9 +150,15 @@ impl RtState {
time: Time,
dt: f32,
) {
self.data_mut().time_of_day = time_of_day;
let tick = {
let mut data = self.data_mut();
data.time_of_day = time_of_day;
data.tick += 1;
data.tick
};
let event = OnTick {
time_of_day,
tick,
time,
dt,
};

View File

@ -3,7 +3,7 @@ use std::hash::BuildHasherDefault;
use crate::{
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx},
data::{
npc::{Brain, PathData},
npc::{Brain, PathData, SimulationMode},
Sites,
},
event::OnTick,
@ -33,6 +33,12 @@ use world::{
IndexRef, World,
};
/// How many ticks should pass between running NPC AI.
/// Note that this only applies to simulated NPCs: loaded NPCs have their AI
/// code run every tick. This means that AI code should be broadly
/// DT-independent.
const SIMULATED_TICK_SKIP: u64 = 10;
pub struct NpcAi;
const CARDINALS: &[Vec2<i32>] = &[
@ -209,6 +215,8 @@ impl Rule for NpcAi {
let mut data = ctx.state.data_mut();
data.npcs
.iter_mut()
// Don't run AI for simulated NPCs every tick
.filter(|(_, npc)| matches!(npc.mode, SimulationMode::Loaded) || (npc.seed as u64 + ctx.event.tick) % SIMULATED_TICK_SKIP == 0)
.map(|(npc_id, npc)| {
let controller = std::mem::take(&mut npc.controller);
let brain = npc.brain.take().unwrap_or_else(|| Brain {
@ -459,7 +467,7 @@ fn socialize() -> impl Action {
.nearby(Some(ctx.npc_id), ctx.npc.wpos.xy(), 8.0)
.choose(&mut ctx.rng)
{
just(move |ctx| ctx.controller.greet(other)).boxed()
just(move |ctx| ctx.controller.say(other, "npc-speech-villager_open")).boxed()
} else if ctx.rng.gen_bool(0.0003) {
just(|ctx| ctx.controller.do_dance())
.repeat()
@ -506,7 +514,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(format!("I've spent enough time here, onward to {}!", site_name)))
important(just(move |ctx| ctx.controller.say(None, format!("I've spent enough time here, onward to {}!", site_name)))
.then(travel_to_site(tgt_site, 0.6))
// Stop for a few minutes
.then(villager(tgt_site).repeat().stop_if(timeout(wait_time)))
@ -596,12 +604,12 @@ fn villager(visiting_site: SiteId) -> impl Action {
Some(site2.tile_center_wpos(house.root_tile()).as_())
})
{
just(|ctx| ctx.controller.say("It's dark, time to go home"))
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("A new day begins!")))
.then(just(|ctx| ctx.controller.say(None, "A new day begins!")))
.map(|_| ())
.boxed()
} else {
@ -627,7 +635,7 @@ 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("Time to go hunting!"))
just(|ctx| ctx.controller.say(None, "Time to go hunting!"))
.then(travel_to_point(forest_wpos, 0.75))
.debug(|| "walk to forest")
.then({
@ -641,17 +649,33 @@ fn villager(visiting_site: SiteId) -> impl Action {
{
return casual(
just(|ctx| {
ctx.controller.say(
*[
// 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
.state
.data()
.npcs
.nearby(Some(ctx.npc_id), ctx.npc.wpos.xy(), 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
} 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.",
]
.iter()
.choose(&mut ctx.rng)
.unwrap(),
) // Can't fail
"I've got the best offers in town",
"Looking for supplies? I've got you covered",
][..])
};
ctx.controller.say(
target,
*phrases.iter().choose(&mut ctx.rng).unwrap(), // Can't fail
);
})
.then(idle().repeat().stop_if(timeout(8.0)))
.repeat()

View File

@ -244,8 +244,7 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
// Consume NPC actions
for action in std::mem::take(&mut npc.controller.actions) {
match action {
NpcAction::Greet(_) | NpcAction::Say(_) => {}, /* Currently, just swallow
* interactions */
NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
}
}

View File

@ -474,10 +474,10 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool {
fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() {
match action {
NpcAction::Greet(actor) => {
if bdata.agent.allowed_to_speak()
&& let Some(target) = bdata.read_data.lookup_actor(actor)
{
NpcAction::Say(target, msg) => {
if bdata.agent.allowed_to_speak() {
// Aim the speech toward a target
if let Some(target) = target.and_then(|tgt| bdata.read_data.lookup_actor(tgt)) {
bdata.agent.target = Some(Target::new(
target,
false,
@ -487,22 +487,17 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
));
// We're always aware of someone we're talking to
bdata.agent.awareness.set_maximally_aware();
bdata.controller.push_action(ControlAction::Stand);
bdata.controller.push_utterance(UtteranceKind::Greeting);
bdata
.agent_data
.chat_npc("npc-speech-villager_open", bdata.event_emitter);
// Start a timer so that they eventually stop interacting
// Start a timer so that we eventually stop interacting
bdata
.agent
.timer
.start(bdata.read_data.time.0, TimerAction::Interact);
bdata.controller.push_action(ControlAction::Stand);
}
},
NpcAction::Say(msg) => {
bdata.controller.push_utterance(UtteranceKind::Greeting);
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
}
},
}
true