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,
|
Dance,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents event-like actions that rtsim NPCs can perform to interact with
|
||||||
|
/// the world
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum NpcAction {
|
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
|
// TODO: Use some sort of structured, language-independent value that frontends can translate
|
||||||
// instead
|
// instead
|
||||||
Say(Cow<'static, str>),
|
Say(Option<Actor>, Cow<'static, str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: the `serde(name = "...")` is to minimise the length of field
|
// 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
|
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 struct Factions {
|
||||||
pub factions: HopSlotMap<FactionId, Faction>,
|
pub factions: HopSlotMap<FactionId, Faction>,
|
||||||
}
|
}
|
||||||
|
@ -23,12 +23,20 @@ use std::{
|
|||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Data {
|
pub struct Data {
|
||||||
pub nature: Nature,
|
pub nature: Nature,
|
||||||
|
#[serde(default)]
|
||||||
pub npcs: Npcs,
|
pub npcs: Npcs,
|
||||||
|
#[serde(default)]
|
||||||
pub sites: Sites,
|
pub sites: Sites,
|
||||||
|
#[serde(default)]
|
||||||
pub factions: Factions,
|
pub factions: Factions,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub tick: u64,
|
||||||
|
#[serde(default)]
|
||||||
pub time_of_day: TimeOfDay,
|
pub time_of_day: TimeOfDay,
|
||||||
|
|
||||||
// If true, rtsim data will be ignored (and, hence, overwritten on next save) on load.
|
// If true, rtsim data will be ignored (and, hence, overwritten on next save) on load.
|
||||||
|
#[serde(default)]
|
||||||
pub should_purge: bool,
|
pub should_purge: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ use common::{
|
|||||||
comp,
|
comp,
|
||||||
grid::Grid,
|
grid::Grid,
|
||||||
rtsim::{
|
rtsim::{
|
||||||
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId,
|
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, NpcMsg, Personality, SiteId,
|
||||||
|
VehicleId,
|
||||||
},
|
},
|
||||||
store::Id,
|
store::Id,
|
||||||
terrain::TerrainChunkSize,
|
terrain::TerrainChunkSize,
|
||||||
@ -69,10 +70,8 @@ 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 say(&mut self, target: impl Into<Option<Actor>>, msg: impl Into<Cow<'static, str>>) {
|
||||||
|
self.actions.push(NpcAction::Say(target.into(), msg.into()));
|
||||||
pub fn say(&mut self, msg: impl Into<Cow<'static, str>>) {
|
|
||||||
self.actions.push(NpcAction::Say(msg.into()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,6 +205,7 @@ pub enum VehicleKind {
|
|||||||
Boat,
|
Boat,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Merge into `Npc`?
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Vehicle {
|
pub struct Vehicle {
|
||||||
pub wpos: Vec3<f32>,
|
pub wpos: Vec3<f32>,
|
||||||
@ -273,6 +273,17 @@ pub struct Npcs {
|
|||||||
pub character_map: HashMap<Vec2<i32>, Vec<(common::character::CharacterId, Vec3<f32>)>>,
|
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()) }
|
fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) }
|
||||||
|
|
||||||
impl Npcs {
|
impl Npcs {
|
||||||
|
@ -42,7 +42,7 @@ impl Site {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct Sites {
|
pub struct Sites {
|
||||||
pub sites: HopSlotMap<SiteId, Site>,
|
pub sites: HopSlotMap<SiteId, Site>,
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ impl Event for OnSetup {}
|
|||||||
pub struct OnTick {
|
pub struct OnTick {
|
||||||
pub time_of_day: TimeOfDay,
|
pub time_of_day: TimeOfDay,
|
||||||
pub time: Time,
|
pub time: Time,
|
||||||
|
pub tick: u64,
|
||||||
pub dt: f32,
|
pub dt: f32,
|
||||||
}
|
}
|
||||||
impl Event for OnTick {}
|
impl Event for OnTick {}
|
||||||
|
@ -44,6 +44,7 @@ impl Data {
|
|||||||
factions: Default::default(),
|
factions: Default::default(),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
tick: 0,
|
||||||
time_of_day: TimeOfDay(settings.start_time),
|
time_of_day: TimeOfDay(settings.start_time),
|
||||||
should_purge: false,
|
should_purge: false,
|
||||||
};
|
};
|
||||||
|
@ -150,9 +150,15 @@ impl RtState {
|
|||||||
time: Time,
|
time: Time,
|
||||||
dt: f32,
|
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 {
|
let event = OnTick {
|
||||||
time_of_day,
|
time_of_day,
|
||||||
|
tick,
|
||||||
time,
|
time,
|
||||||
dt,
|
dt,
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ use std::hash::BuildHasherDefault;
|
|||||||
use crate::{
|
use crate::{
|
||||||
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx},
|
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx},
|
||||||
data::{
|
data::{
|
||||||
npc::{Brain, PathData},
|
npc::{Brain, PathData, SimulationMode},
|
||||||
Sites,
|
Sites,
|
||||||
},
|
},
|
||||||
event::OnTick,
|
event::OnTick,
|
||||||
@ -33,6 +33,12 @@ use world::{
|
|||||||
IndexRef, 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;
|
pub struct NpcAi;
|
||||||
|
|
||||||
const CARDINALS: &[Vec2<i32>] = &[
|
const CARDINALS: &[Vec2<i32>] = &[
|
||||||
@ -209,6 +215,8 @@ impl Rule for NpcAi {
|
|||||||
let mut data = ctx.state.data_mut();
|
let mut data = ctx.state.data_mut();
|
||||||
data.npcs
|
data.npcs
|
||||||
.iter_mut()
|
.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)| {
|
.map(|(npc_id, npc)| {
|
||||||
let controller = std::mem::take(&mut npc.controller);
|
let controller = std::mem::take(&mut npc.controller);
|
||||||
let brain = npc.brain.take().unwrap_or_else(|| Brain {
|
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)
|
.nearby(Some(ctx.npc_id), ctx.npc.wpos.xy(), 8.0)
|
||||||
.choose(&mut ctx.rng)
|
.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) {
|
} else if ctx.rng.gen_bool(0.0003) {
|
||||||
just(|ctx| ctx.controller.do_dance())
|
just(|ctx| ctx.controller.do_dance())
|
||||||
.repeat()
|
.repeat()
|
||||||
@ -506,7 +514,7 @@ fn adventure() -> impl Action {
|
|||||||
.map(|ws| ctx.index.sites.get(ws).name().to_string())
|
.map(|ws| ctx.index.sites.get(ws).name().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
// Travel to the site
|
// 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))
|
.then(travel_to_site(tgt_site, 0.6))
|
||||||
// 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)))
|
||||||
@ -596,12 +604,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_())
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
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))
|
.then(travel_to_point(house_wpos, 0.65))
|
||||||
.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!")))
|
.then(just(|ctx| ctx.controller.say(None, "A new day begins!")))
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.boxed()
|
.boxed()
|
||||||
} else {
|
} 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) {
|
} else if matches!(ctx.npc.profession, Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) {
|
||||||
if let Some(forest_wpos) = find_forest(ctx) {
|
if let Some(forest_wpos) = find_forest(ctx) {
|
||||||
return casual(
|
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))
|
.then(travel_to_point(forest_wpos, 0.75))
|
||||||
.debug(|| "walk to forest")
|
.debug(|| "walk to forest")
|
||||||
.then({
|
.then({
|
||||||
@ -641,17 +649,33 @@ fn villager(visiting_site: SiteId) -> impl Action {
|
|||||||
{
|
{
|
||||||
return casual(
|
return casual(
|
||||||
just(|ctx| {
|
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!",
|
"All my goods are of the highest quality!",
|
||||||
"Does anybody want to buy my wares?",
|
"Does anybody want to buy my wares?",
|
||||||
"I've got the best offers in town.",
|
"I've got the best offers in town",
|
||||||
"Looking for supplies? I've got you covered.",
|
"Looking for supplies? I've got you covered",
|
||||||
]
|
][..])
|
||||||
.iter()
|
};
|
||||||
.choose(&mut ctx.rng)
|
|
||||||
.unwrap(),
|
ctx.controller.say(
|
||||||
) // Can't fail
|
target,
|
||||||
|
*phrases.iter().choose(&mut ctx.rng).unwrap(), // Can't fail
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.then(idle().repeat().stop_if(timeout(8.0)))
|
.then(idle().repeat().stop_if(timeout(8.0)))
|
||||||
.repeat()
|
.repeat()
|
||||||
|
@ -244,8 +244,7 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
|
|||||||
// 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
|
NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
|
||||||
* interactions */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,36 +474,31 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool {
|
|||||||
fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
|
fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
|
||||||
if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() {
|
if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() {
|
||||||
match action {
|
match action {
|
||||||
NpcAction::Greet(actor) => {
|
NpcAction::Say(target, msg) => {
|
||||||
if bdata.agent.allowed_to_speak()
|
if bdata.agent.allowed_to_speak() {
|
||||||
&& let Some(target) = bdata.read_data.lookup_actor(actor)
|
// 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(
|
bdata.agent.target = Some(Target::new(
|
||||||
target,
|
target,
|
||||||
false,
|
false,
|
||||||
bdata.read_data.time.0,
|
bdata.read_data.time.0,
|
||||||
false,
|
false,
|
||||||
bdata.read_data.positions.get(target).map(|p| p.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();
|
||||||
|
// 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.controller.push_utterance(UtteranceKind::Greeting);
|
||||||
bdata
|
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
NpcAction::Say(msg) => {
|
|
||||||
bdata.controller.push_utterance(UtteranceKind::Greeting);
|
|
||||||
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
Reference in New Issue
Block a user