mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Cleaned up rtsim tick handling
This commit is contained in:
parent
80e4e8deae
commit
74610833d0
@ -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
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -42,7 +42,7 @@ impl Site {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Sites {
|
||||
pub sites: HopSlotMap<SiteId, Site>,
|
||||
|
||||
|
@ -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 {}
|
||||
|
@ -44,6 +44,7 @@ impl Data {
|
||||
factions: Default::default(),
|
||||
},
|
||||
|
||||
tick: 0,
|
||||
time_of_day: TimeOfDay(settings.start_time),
|
||||
should_purge: false,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -474,36 +474,31 @@ 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)
|
||||
{
|
||||
bdata.agent.target = Some(Target::new(
|
||||
target,
|
||||
false,
|
||||
bdata.read_data.time.0,
|
||||
false,
|
||||
bdata.read_data.positions.get(target).map(|p| p.0),
|
||||
));
|
||||
// We're always aware of someone we're talking to
|
||||
bdata.agent.awareness.set_maximally_aware();
|
||||
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,
|
||||
bdata.read_data.time.0,
|
||||
false,
|
||||
bdata.read_data.positions.get(target).map(|p| p.0),
|
||||
));
|
||||
// We're always aware of someone we're talking to
|
||||
bdata.agent.awareness.set_maximally_aware();
|
||||
// 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);
|
||||
}
|
||||
|
||||
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
|
||||
bdata
|
||||
.agent
|
||||
.timer
|
||||
.start(bdata.read_data.time.0, TimerAction::Interact);
|
||||
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
|
||||
}
|
||||
},
|
||||
NpcAction::Say(msg) => {
|
||||
bdata.controller.push_utterance(UtteranceKind::Greeting);
|
||||
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
|
||||
},
|
||||
}
|
||||
true
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user