Various rtsim related stuff

This commit is contained in:
Isse 2023-06-03 22:14:18 +00:00
parent 272d57b4fa
commit a884e0e058
25 changed files with 824 additions and 292 deletions

View File

@ -87,6 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Underwater fall damage
- The scale component now behaves properly
- Multiple model support for dropped items (orichalcum armor)
- Made rtsim monsters not go into too deep water, and certainly not outside the map.
- Fixed bug where npcs would be dismounted from vehicles if loaded/unloaded in a certain order.
## [0.14.0] - 2023-01-07

View File

@ -269,6 +269,10 @@ npc-speech-witness_death =
.a0 = No!
.a1 = This is terrible!
.a2 = Oh my goodness!
npc-speech-welcome-aboard =
.a0 = Welcome aboard!
.a1 = Can I see your ticket... just kidding it's free!
.a2 = Have a nice ride!
npc-speech-dir_north = north
npc-speech-dir_north_east = north-east
npc-speech-dir_east = east

View File

@ -2233,7 +2233,7 @@ impl Client {
return Err(Error::Other("Failed to find entity from uid.".into()));
}
},
ServerGeneral::TimeOfDay(time_of_day, calendar, new_time) => {
ServerGeneral::TimeOfDay(time_of_day, calendar, new_time, time_scale) => {
self.target_time_of_day = Some(time_of_day);
*self.state.ecs_mut().write_resource() = calendar;
let mut time = self.state.ecs_mut().write_resource::<Time>();
@ -2249,7 +2249,7 @@ impl Client {
} else {
0.99
};
self.dt_adjustment = dt_adjustment;
self.dt_adjustment = dt_adjustment * time_scale.0;
},
ServerGeneral::EntitySync(entity_sync_package) => {
self.state

View File

@ -11,7 +11,7 @@ use common::{
lod,
outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{Time, TimeOfDay},
resources::{Time, TimeOfDay, TimeScale},
shared_server_config::ServerConstants,
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
trade::{PendingTrade, SitePrices, TradeId, TradeResult},
@ -195,7 +195,7 @@ pub enum ServerGeneral {
ChatMsg(comp::ChatMsg),
ChatMode(comp::ChatMode),
SetPlayerEntity(Uid),
TimeOfDay(TimeOfDay, Calendar, Time),
TimeOfDay(TimeOfDay, Calendar, Time, TimeScale),
EntitySync(sync::EntitySyncPackage),
CompSync(sync::CompSyncPackage<EcsCompPacket>, u64),
CreateEntity(sync::EntityPackage<EcsCompPacket>),
@ -342,7 +342,7 @@ impl ServerMsg {
| ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_, _, _)
| ServerGeneral::TimeOfDay(_, _, _, _)
| ServerGeneral::EntitySync(_)
| ServerGeneral::CompSync(_, _)
| ServerGeneral::CreateEntity(_)

View File

@ -328,6 +328,7 @@ pub enum ServerChatCommand {
Sudo,
Tell,
Time,
TimeScale,
Tp,
Unban,
Version,
@ -722,6 +723,11 @@ impl ServerChatCommand {
"Set the time of day",
Some(Admin),
),
ServerChatCommand::TimeScale => cmd(
vec![Float("time scale", 1.0, Optional)],
"Set scaling of delta time",
Some(Admin),
),
ServerChatCommand::Tp => cmd(
vec![
PlayerName(Optional),
@ -894,6 +900,7 @@ impl ServerChatCommand {
ServerChatCommand::Sudo => "sudo",
ServerChatCommand::Tell => "tell",
ServerChatCommand::Time => "time",
ServerChatCommand::TimeScale => "time_scale",
ServerChatCommand::Tp => "tp",
ServerChatCommand::RtsimTp => "rtsim_tp",
ServerChatCommand::RtsimInfo => "rtsim_info",

View File

@ -103,6 +103,32 @@ pub struct AllBodies<BodyMeta, SpeciesMeta> {
pub arthropod: BodyData<BodyMeta, arthropod::AllSpecies<SpeciesMeta>>,
}
impl<BodyMeta, SpeciesMeta> AllBodies<BodyMeta, SpeciesMeta> {
/// Get species meta associated with the body.
///
/// Returns `None` if the body doesn't have any associated meta, i.e ships,
/// objects, itemdrops.
pub fn get_species_meta<'a>(&'a self, body: &Body) -> Option<&'a SpeciesMeta> {
Some(match body {
Body::Humanoid(b) => &self.humanoid.species[&b.species],
Body::QuadrupedSmall(b) => &self.quadruped_small.species[&b.species],
Body::QuadrupedMedium(b) => &self.quadruped_medium.species[&b.species],
Body::BirdMedium(b) => &self.bird_medium.species[&b.species],
Body::BirdLarge(b) => &self.bird_large.species[&b.species],
Body::FishMedium(b) => &self.fish_medium.species[&b.species],
Body::Dragon(b) => &self.dragon.species[&b.species],
Body::FishSmall(b) => &self.fish_small.species[&b.species],
Body::BipedLarge(b) => &self.biped_large.species[&b.species],
Body::BipedSmall(b) => &self.biped_small.species[&b.species],
Body::Golem(b) => &self.golem.species[&b.species],
Body::Theropod(b) => &self.theropod.species[&b.species],
Body::QuadrupedLow(b) => &self.quadruped_low.species[&b.species],
Body::Arthropod(b) => &self.arthropod.species[&b.species],
_ => return None,
})
}
}
/// Can only retrieve body metadata by direct index.
impl<BodyMeta, SpeciesMeta> core::ops::Index<NpcKind> for AllBodies<BodyMeta, SpeciesMeta> {
type Output = BodyMeta;

View File

@ -99,6 +99,7 @@ impl ModularBase {
hand_restriction.unwrap_or(Hands::One)
}
#[inline(never)]
pub(super) fn kind(
&self,
components: &[Item],

View File

@ -173,18 +173,18 @@ impl Role for VolumeRider {
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Volume {
pub enum Volume<E> {
Terrain,
Entity(Uid),
Entity(E),
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct VolumePos {
pub kind: Volume,
pub struct VolumePos<E = Uid> {
pub kind: Volume<E>,
pub pos: Vec3<i32>,
}
impl VolumePos {
impl<E> VolumePos<E> {
pub fn terrain(block_pos: Vec3<i32>) -> Self {
Self {
kind: Volume::Terrain,
@ -192,12 +192,22 @@ impl VolumePos {
}
}
pub fn entity(block_pos: Vec3<i32>, uid: Uid) -> Self {
pub fn entity(block_pos: Vec3<i32>, uid: E) -> Self {
Self {
kind: Volume::Entity(uid),
pos: block_pos,
}
}
pub fn try_map_entity<U>(self, f: impl FnOnce(E) -> Option<U>) -> Option<VolumePos<U>> {
Some(VolumePos {
pos: self.pos,
kind: match self.kind {
Volume::Terrain => Volume::Terrain,
Volume::Entity(e) => Volume::Entity(f(e)?),
},
})
}
}
impl VolumePos {

View File

@ -13,6 +13,13 @@ pub struct TimeOfDay(pub f64);
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Time(pub f64);
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub struct TimeScale(pub f64);
impl Default for TimeScale {
fn default() -> Self { Self(1.0) }
}
/// A resource that stores the time since the previous tick.
#[derive(Default)]
pub struct DeltaTime(pub f32);

View File

@ -15,7 +15,7 @@ use common::{
region::RegionMap,
resources::{
DeltaTime, EntitiesDiedLastTick, GameMode, PlayerEntity, PlayerPhysicsSettings, Time,
TimeOfDay,
TimeOfDay, TimeScale,
},
shared_server_config::ServerConstants,
slowjob::SlowJobPool,
@ -269,6 +269,7 @@ impl State {
ecs.insert(Calendar::default());
ecs.insert(WeatherGrid::new(Vec2::zero()));
ecs.insert(Time(0.0));
ecs.insert(TimeScale(1.0));
// Register unsynced resources used by the ECS.
ecs.insert(DeltaTime(0.0));
@ -644,14 +645,16 @@ impl State {
}
// Change the time accordingly.
let time_scale = self.ecs.read_resource::<TimeScale>().0;
self.ecs.write_resource::<TimeOfDay>().0 +=
dt.as_secs_f64() * server_constants.day_cycle_coefficient;
self.ecs.write_resource::<Time>().0 += dt.as_secs_f64();
dt.as_secs_f64() * server_constants.day_cycle_coefficient * time_scale;
self.ecs.write_resource::<Time>().0 += dt.as_secs_f64() * time_scale;
// Update delta time.
// Beyond a delta time of MAX_DELTA_TIME, start lagging to avoid skipping
// important physics events.
self.ecs.write_resource::<DeltaTime>().0 = dt.as_secs_f32().min(MAX_DELTA_TIME);
self.ecs.write_resource::<DeltaTime>().0 =
(dt.as_secs_f32() * time_scale as f32).min(MAX_DELTA_TIME);
if update_terrain_and_regions {
self.update_region_map();

View File

@ -1,3 +1,7 @@
pub mod predicate;
use predicate::Predicate;
use crate::{
data::{
npc::{Controller, Npc, NpcId},
@ -15,6 +19,39 @@ use rand_chacha::ChaChaRng;
use std::{any::Any, collections::VecDeque, marker::PhantomData, ops::ControlFlow};
use world::{IndexRef, World};
pub trait State: Clone + Send + Sync + 'static {}
impl<T: Clone + Send + Sync + 'static> State for T {}
#[derive(Clone, Copy)]
struct Resettable<T> {
original: T,
current: T,
}
impl<T: Clone> From<T> for Resettable<T> {
fn from(value: T) -> Self {
Self {
original: value.clone(),
current: value,
}
}
}
impl<T: Clone> Resettable<T> {
fn reset(&mut self) { self.current = self.original.clone(); }
}
impl<T> std::ops::Deref for Resettable<T> {
type Target = T;
fn deref(&self) -> &Self::Target { &self.current }
}
impl<T> std::ops::DerefMut for Resettable<T> {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.current }
}
/// The context provided to an [`Action`] while it is being performed. It should
/// be possible to access any and all important information about the game world
/// through this struct.
@ -33,6 +70,8 @@ pub struct NpcCtx<'a> {
pub sentiments: &'a mut Sentiments,
pub known_reports: &'a mut HashSet<ReportId>,
/// The delta time since this npcs ai was last ran.
pub dt: f32,
pub rng: ChaChaRng,
}
@ -55,7 +94,7 @@ pub struct NpcCtx<'a> {
/// You should not need to implement this trait yourself when writing AI code.
/// If you find yourself wanting to implement it, please discuss with the core
/// dev team first.
pub trait Action<R = ()>: Any + Send + Sync {
pub trait Action<S = (), R = ()>: Any + Send + Sync {
/// Returns `true` if the action should be considered the 'same' (i.e:
/// achieving the same objective) as another. In general, the AI system
/// will try to avoid switching (and therefore restarting) tasks when the
@ -68,7 +107,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
Self: Sized;
/// Like [`Action::is_same`], but allows for dynamic dispatch.
fn dyn_is_same_sized(&self, other: &dyn Action<R>) -> bool
fn dyn_is_same_sized(&self, other: &dyn Action<S, R>) -> bool
where
Self: Sized,
{
@ -79,7 +118,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
}
/// Like [`Action::is_same`], but allows for dynamic dispatch.
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool;
fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool;
/// Generate a backtrace for the action. The action should recursively push
/// all of the tasks it is currently performing.
@ -89,7 +128,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
fn reset(&mut self);
/// Perform the action for the current tick.
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R>;
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R>;
/// Create an action that chains together two sub-actions, one after the
/// other.
@ -101,7 +140,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// goto(enemy_npc).then(attack(enemy_npc))
/// ```
#[must_use]
fn then<A1: Action<R1>, R1>(self, other: A1) -> Then<Self, A1, R>
fn then<A1: Action<S, R1>, R1>(self, other: A1) -> Then<Self, A1, R>
where
Self: Sized,
{
@ -122,7 +161,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// find_and_collect(ChunkResource::Flax).repeat()
/// ```
#[must_use]
fn repeat<R1>(self) -> Repeat<Self, R1>
fn repeat(self) -> Repeat<Self, R>
where
Self: Sized,
{
@ -138,11 +177,11 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0)
/// ```
#[must_use]
fn stop_if<F: FnMut(&mut NpcCtx) -> bool + Clone>(self, f: F) -> StopIf<Self, F>
fn stop_if<P: Predicate + Clone>(self, p: P) -> StopIf<Self, P>
where
Self: Sized,
{
StopIf(self, f.clone(), f)
StopIf(self, p.into())
}
/// Pause an action to possibly perform another action.
@ -159,7 +198,11 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// })
/// ```
#[must_use]
fn interrupt_with<A1: Action<R1>, R1, F: FnMut(&mut NpcCtx) -> Option<A1> + Clone>(
fn interrupt_with<
A1: Action<S, R1>,
R1,
F: Fn(&mut NpcCtx, &mut S) -> Option<A1> + Send + Sync + 'static,
>(
self,
f: F,
) -> InterruptWith<Self, F, A1, R1>
@ -168,8 +211,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
{
InterruptWith {
a0: self,
f: f.clone(),
f2: f,
f,
a1: None,
phantom: PhantomData,
}
@ -177,7 +219,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// Map the completion value of this action to something else.
#[must_use]
fn map<F: FnMut(R) -> R1, R1>(self, f: F) -> Map<Self, F, R>
fn map<F: Fn(R, &mut S) -> R1, R1>(self, f: F) -> Map<Self, F, R>
where
Self: Sized,
{
@ -209,13 +251,54 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// }
/// ```
#[must_use]
fn boxed(self) -> Box<dyn Action<R>>
fn boxed(self) -> Box<dyn Action<S, R>>
where
Self: Sized,
{
Box::new(self)
}
/// Set the state for child actions.
///
/// Note that state is reset when repeated.
///
/// # Example
///
/// ```ignore
/// just(|_, state: &mut i32| *state += 2)
/// // Outputs 5
/// .then(just(|_, state: &mut i32| println!("{state}")))
/// .with_state(3)
/// ```
#[must_use]
fn with_state<S0>(self, s: S) -> WithState<Self, S, S0>
where
Self: Sized,
S: Clone,
{
WithState(self, s.into(), PhantomData)
}
/// Map the current state for child actions, this map expects the return
/// value to have the same lifetime as the input state.
///
/// # Example
///
/// ```ignore
/// // Goes forward 5 steps
/// just(|_, state: &mut i32| go_forward(*state))
/// .map_state(|state: &mut (i32, i32)| &mut state.1)
/// .with_state((14, 5))
/// ```
#[must_use]
fn map_state<S0, F>(self, f: F) -> MapState<Self, F, S, S0>
where
F: Fn(&mut S0) -> &mut S,
Self: Sized,
{
MapState(self, f, PhantomData)
}
/// Add debugging information to the action that will be visible when using
/// the `/npc_info` command.
///
@ -249,10 +332,10 @@ pub trait Action<R = ()>: Any + Send + Sync {
}
}
impl<R: 'static> Action<R> for Box<dyn Action<R>> {
impl<S: State, R: 'static> Action<S, R> for Box<dyn Action<S, R>> {
fn is_same(&self, other: &Self) -> bool { (**self).dyn_is_same(other) }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool {
fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool {
match (other as &dyn Any).downcast_ref::<Self>() {
Some(other) => self.is_same(other),
None => false,
@ -263,10 +346,12 @@ impl<R: 'static> Action<R> for Box<dyn Action<R>> {
fn reset(&mut self) { (**self).reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { (**self).tick(ctx) }
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
(**self).tick(ctx, state)
}
}
impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> {
impl<S: State, R: 'static, A: Action<S, R>, B: Action<S, R>> Action<S, R> for Either<A, B> {
fn is_same(&self, other: &Self) -> bool {
match (self, other) {
(Either::Left(x), Either::Left(y)) => x.is_same(y),
@ -275,7 +360,7 @@ impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> {
}
}
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
match self {
@ -291,10 +376,10 @@ impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> {
}
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
match self {
Either::Left(x) => x.tick(ctx),
Either::Right(x) => x.tick(ctx),
Either::Left(x) => x.tick(ctx, state),
Either::Right(x) => x.tick(ctx, state),
}
}
}
@ -305,13 +390,17 @@ impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> {
#[derive(Copy, Clone)]
pub struct Now<F, A>(F, Option<A>);
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'static, A: Action<R>>
Action<R> for Now<F, A>
impl<
S: State,
R: Send + Sync + 'static,
F: Fn(&mut NpcCtx, &mut S) -> A + Send + Sync + 'static,
A: Action<S, R>,
> Action<S, R> for Now<F, A>
{
// TODO: This doesn't compare?!
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.1 {
@ -321,12 +410,11 @@ impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'stati
}
}
// TODO: Reset closure?
fn reset(&mut self) { self.1 = None; }
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
(self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx)
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
(self.1.get_or_insert_with(|| (self.0)(ctx, state))).tick(ctx, state)
}
}
@ -342,9 +430,9 @@ impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'stati
/// // An action that makes an NPC immediately travel to its *current* home
/// now(|ctx| goto(ctx.npc.home))
/// ```
pub fn now<F, A>(f: F) -> Now<F, A>
pub fn now<S, R, F, A: Action<S, R>>(f: F) -> Now<F, A>
where
F: FnMut(&mut NpcCtx) -> A,
F: Fn(&mut NpcCtx, &mut S) -> A + Send + Sync + 'static,
{
Now(f, None)
}
@ -356,15 +444,16 @@ where
pub struct Until<F, A, R>(F, Option<A>, PhantomData<R>);
impl<
S: State,
R: Send + Sync + 'static,
F: FnMut(&mut NpcCtx) -> Option<A> + Send + Sync + 'static,
A: Action<R>,
> Action<()> for Until<F, A, R>
F: Fn(&mut NpcCtx, &mut S) -> Option<A> + Send + Sync + 'static,
A: Action<S, R>,
> Action<S, ()> for Until<F, A, R>
{
// TODO: This doesn't compare?!
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, ()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.1 {
@ -374,20 +463,18 @@ impl<
}
}
// TODO: Reset closure?
fn reset(&mut self) { self.1 = None; }
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> {
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<()> {
match &mut self.1 {
Some(x) => match x.tick(ctx) {
Some(x) => match x.tick(ctx, state) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(_) => {
self.1 = None;
ControlFlow::Continue(())
},
},
None => match (self.0)(ctx) {
None => match (self.0)(ctx, state) {
Some(x) => {
self.1 = Some(x);
ControlFlow::Continue(())
@ -398,9 +485,9 @@ impl<
}
}
pub fn until<F, A, R>(f: F) -> Until<F, A, R>
pub fn until<S, F, A: Action<S, R>, R>(f: F) -> Until<F, A, R>
where
F: FnMut(&mut NpcCtx) -> Option<A>,
F: Fn(&mut NpcCtx, &mut S) -> Option<A>,
{
Until(f, None, PhantomData)
}
@ -411,20 +498,20 @@ where
#[derive(Copy, Clone)]
pub struct Just<F, R = ()>(F, PhantomData<R>);
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static> Action<R>
for Just<F, R>
impl<S: State, R: Send + Sync + 'static, F: Fn(&mut NpcCtx, &mut S) -> R + Send + Sync + 'static>
Action<S, R> for Just<F, R>
{
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, _bt: &mut Vec<String>) {}
// TODO: Reset closure?
fn reset(&mut self) {}
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { ControlFlow::Break((self.0)(ctx)) }
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
ControlFlow::Break((self.0)(ctx, state))
}
}
/// An action that executes some code just once when performed.
@ -438,9 +525,9 @@ impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'stati
/// // Make the current NPC say 'Hello, world!' exactly once
/// just(|ctx| ctx.controller.say("Hello, world!"))
/// ```
pub fn just<F, R: Send + Sync + 'static>(f: F) -> Just<F, R>
pub fn just<S: State, F, R: Send + Sync + 'static>(f: F) -> Just<F, R>
where
F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static,
F: Fn(&mut NpcCtx, &mut S) -> R + Send + Sync + 'static,
{
Just(f, PhantomData)
}
@ -451,16 +538,18 @@ where
#[derive(Copy, Clone)]
pub struct Finish;
impl Action<()> for Finish {
impl<S: State> Action<S, ()> for Finish {
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, ()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, _bt: &mut Vec<String>) {}
fn reset(&mut self) {}
fn tick(&mut self, _ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) }
fn tick(&mut self, _ctx: &mut NpcCtx, _state: &mut S) -> ControlFlow<()> {
ControlFlow::Break(())
}
}
/// An action that immediately finishes without doing anything.
@ -492,33 +581,33 @@ pub const URGENT: Priority = 0;
pub const IMPORTANT: Priority = 1;
pub const CASUAL: Priority = 2;
pub struct Node<R>(Box<dyn Action<R>>, Priority);
pub struct Node<S, R>(Box<dyn Action<S, R>>, Priority);
/// Perform an action with [`URGENT`] priority (see [`choose`]).
#[must_use]
pub fn urgent<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), URGENT) }
pub fn urgent<S, A: Action<S, R>, R>(a: A) -> Node<S, R> { Node(Box::new(a), URGENT) }
/// Perform an action with [`IMPORTANT`] priority (see [`choose`]).
#[must_use]
pub fn important<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), IMPORTANT) }
pub fn important<S, A: Action<S, R>, R>(a: A) -> Node<S, R> { Node(Box::new(a), IMPORTANT) }
/// Perform an action with [`CASUAL`] priority (see [`choose`]).
#[must_use]
pub fn casual<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), CASUAL) }
pub fn casual<S, A: Action<S, R>, R>(a: A) -> Node<S, R> { Node(Box::new(a), CASUAL) }
/// See [`choose`] and [`watch`].
pub struct Tree<F, R> {
pub struct Tree<S, F, R> {
next: F,
prev: Option<Node<R>>,
prev: Option<Node<S, R>>,
interrupt: bool,
}
impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Action<R>
for Tree<F, R>
impl<S: State, F: Fn(&mut NpcCtx, &mut S) -> Node<S, R> + Send + Sync + 'static, R: 'static>
Action<S, R> for Tree<S, F, R>
{
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(prev) = &self.prev {
@ -530,9 +619,8 @@ impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Actio
fn reset(&mut self) { self.prev = None; }
// TODO: Reset `next` too?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
let new = (self.next)(ctx);
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
let new = (self.next)(ctx, state);
let prev = match &mut self.prev {
Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => {
@ -541,7 +629,7 @@ impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Actio
_ => self.prev.insert(new),
};
match prev.0.tick(ctx) {
match prev.0.tick(ctx, state) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(r) => {
self.prev = None;
@ -575,9 +663,9 @@ impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Actio
/// })
/// ```
#[must_use]
pub fn choose<R: 'static, F>(f: F) -> impl Action<R>
pub fn choose<S: State, R: 'static, F>(f: F) -> impl Action<S, R>
where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
F: Fn(&mut NpcCtx, &mut S) -> Node<S, R> + Send + Sync + 'static,
{
Tree {
next: f,
@ -610,9 +698,9 @@ where
/// })
/// ```
#[must_use]
pub fn watch<R: 'static, F>(f: F) -> impl Action<R>
pub fn watch<S: State, R: 'static, F>(f: F) -> impl Action<S, R>
where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
F: Fn(&mut NpcCtx, &mut S) -> Node<S, R> + Send + Sync + 'static,
{
Tree {
next: f,
@ -632,14 +720,19 @@ pub struct Then<A0, A1, R0> {
phantom: PhantomData<R0>,
}
impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync + 'static>
Action<R1> for Then<A0, A1, R0>
impl<
S: State,
A0: Action<S, R0>,
A1: Action<S, R1>,
R0: Send + Sync + 'static,
R1: Send + Sync + 'static,
> Action<S, R1> for Then<A0, A1, R0>
{
fn is_same(&self, other: &Self) -> bool {
self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1)
}
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, R1>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if self.a0_finished {
@ -655,14 +748,14 @@ impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync
self.a1.reset();
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R1> {
if !self.a0_finished {
match self.a0.tick(ctx) {
match self.a0.tick(ctx, state) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(_) => self.a0_finished = true,
}
}
self.a1.tick(ctx)
self.a1.tick(ctx, state)
}
}
@ -673,22 +766,22 @@ impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync
pub struct InterruptWith<A0, F, A1, R1> {
a0: A0,
f: F,
f2: F,
a1: Option<A1>,
phantom: PhantomData<R1>,
}
impl<
A0: Action<R0>,
A1: Action<R1>,
F: FnMut(&mut NpcCtx) -> Option<A1> + Clone + Send + Sync + 'static,
S: State,
A0: Action<S, R0>,
A1: Action<S, R1>,
F: Fn(&mut NpcCtx, &mut S) -> Option<A1> + Send + Sync + 'static,
R0: Send + Sync + 'static,
R1: Send + Sync + 'static,
> Action<R0> for InterruptWith<A0, F, A1, R1>
> Action<S, R0> for InterruptWith<A0, F, A1, R1>
{
fn is_same(&self, other: &Self) -> bool { self.a0.is_same(&other.a0) }
fn dyn_is_same(&self, other: &dyn Action<R0>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, R0>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(a1) = &self.a1 {
@ -702,23 +795,22 @@ impl<
fn reset(&mut self) {
self.a0.reset();
self.f = self.f2.clone();
self.a1 = None;
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R0> {
if let Some(new_a1) = (self.f)(ctx) {
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R0> {
if let Some(new_a1) = (self.f)(ctx, state) {
self.a1 = Some(new_a1);
}
if let Some(a1) = &mut self.a1 {
match a1.tick(ctx) {
match a1.tick(ctx, state) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(_) => self.a1 = None,
}
}
self.a0.tick(ctx)
self.a0.tick(ctx, state)
}
}
@ -728,17 +820,17 @@ impl<
#[derive(Copy, Clone)]
pub struct Repeat<A, R = ()>(A, PhantomData<R>);
impl<R: Send + Sync + 'static, A: Action<R>> Action<!> for Repeat<A, R> {
impl<S: State, R: Send + Sync + 'static, A: Action<S, R>> Action<S, !> for Repeat<A, R> {
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<!>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, !>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<!> {
match self.0.tick(ctx) {
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<!> {
match self.0.tick(ctx, state) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(_) => {
self.0.reset();
@ -752,17 +844,21 @@ impl<R: Send + Sync + 'static, A: Action<R>> Action<!> for Repeat<A, R> {
/// See [`seq`].
#[derive(Copy, Clone)]
pub struct Sequence<I, A, R = ()>(I, I, Option<A>, PhantomData<R>);
pub struct Sequence<I, A, R = ()>(Resettable<I>, Option<A>, PhantomData<R>);
impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'static, A: Action<R>>
Action<()> for Sequence<I, A, R>
impl<
S: State,
R: Send + Sync + 'static,
I: Iterator<Item = A> + Clone + Send + Sync + 'static,
A: Action<S, R>,
> Action<S, ()> for Sequence<I, A, R>
{
fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, ()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.2 {
if let Some(action) = &self.1 {
action.backtrace(bt);
} else {
bt.push("<thinking>".to_string());
@ -770,22 +866,22 @@ impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'st
}
fn reset(&mut self) {
self.0 = self.1.clone();
self.2 = None;
self.0.reset();
self.1 = None;
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> {
let item = if let Some(prev) = &mut self.2 {
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<()> {
let item = if let Some(prev) = &mut self.1 {
prev
} else {
match self.0.next() {
Some(next) => self.2.insert(next),
Some(next) => self.1.insert(next),
None => return ControlFlow::Break(()),
}
};
if let ControlFlow::Break(_) = item.tick(ctx) {
self.2 = None;
if let ControlFlow::Break(_) = item.tick(ctx, state) {
self.1 = None;
}
ControlFlow::Continue(())
@ -811,39 +907,41 @@ impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'st
/// .map(|enemy| attack(enemy)))
/// ```
#[must_use]
pub fn seq<I, A, R>(iter: I) -> Sequence<I, A, R>
pub fn seq<S, I, A, R>(iter: I) -> Sequence<I, A, R>
where
I: Iterator<Item = A> + Clone,
A: Action<R>,
A: Action<S, R>,
{
Sequence(iter.clone(), iter, None, PhantomData)
Sequence(iter.into(), None, PhantomData)
}
// StopIf
/// See [`Action::stop_if`].
#[derive(Copy, Clone)]
pub struct StopIf<A, F>(A, F, F);
pub struct StopIf<A, P>(A, Resettable<P>);
impl<A: Action<R>, F: FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync + 'static, R>
Action<Option<R>> for StopIf<A, F>
impl<S: State, A: Action<S, R>, P: Predicate + Clone + Send + Sync + 'static, R>
Action<S, Option<R>> for StopIf<A, P>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<Option<R>>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, Option<R>>) -> bool {
self.dyn_is_same_sized(other)
}
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) {
self.0.reset();
self.1 = self.2.clone();
self.1.reset();
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<Option<R>> {
if (self.1)(ctx) {
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<Option<R>> {
if self.1.should(ctx) {
ControlFlow::Break(None)
} else {
self.0.tick(ctx).map_break(Some)
self.0.tick(ctx, state).map_break(Some)
}
}
}
@ -854,19 +952,24 @@ impl<A: Action<R>, F: FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync + 'static
#[derive(Copy, Clone)]
pub struct Map<A, F, R>(A, F, PhantomData<R>);
impl<A: Action<R>, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1>
Action<R1> for Map<A, F, R>
impl<
S: State,
A: Action<S, R>,
F: Fn(R, &mut S) -> R1 + Send + Sync + 'static,
R: Send + Sync + 'static,
R1,
> Action<S, R1> for Map<A, F, R>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, R1>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
self.0.tick(ctx).map_break(&mut self.1)
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R1> {
self.0.tick(ctx, state).map_break(|t| (self.1)(t, state))
}
}
@ -877,15 +980,16 @@ impl<A: Action<R>, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + '
pub struct Debug<A, F, T>(A, F, PhantomData<T>);
impl<
A: Action<R>,
S: 'static,
A: Action<S, R>,
F: Fn() -> T + Send + Sync + 'static,
R: Send + Sync + 'static,
T: Send + Sync + std::fmt::Display + 'static,
> Action<R> for Debug<A, F, T>
> Action<S, R> for Debug<A, F, T>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
bt.push((self.1)().to_string());
@ -894,5 +998,56 @@ impl<
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { self.0.tick(ctx) }
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
self.0.tick(ctx, state)
}
}
#[derive(Copy, Clone)]
pub struct WithState<A, S, S0>(A, Resettable<S>, PhantomData<S0>);
impl<S0: State, S: State, R, A: Action<S, R>> Action<S0, R> for WithState<A, S, S0> {
fn is_same(&self, other: &Self) -> bool
where
Self: Sized,
{
self.0.is_same(&other.0)
}
fn dyn_is_same(&self, other: &dyn Action<S0, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt) }
fn reset(&mut self) {
self.0.reset();
self.1.reset();
}
fn tick(&mut self, ctx: &mut NpcCtx, _state: &mut S0) -> ControlFlow<R> {
self.0.tick(ctx, &mut self.1.current)
}
}
#[derive(Copy, Clone)]
pub struct MapState<A, F, S, S0>(A, F, PhantomData<(S, S0)>);
impl<S0: State, S: State, R, A: Action<S, R>, F: Fn(&mut S0) -> &mut S + Send + Sync + 'static>
Action<S0, R> for MapState<A, F, S, S0>
{
fn is_same(&self, other: &Self) -> bool
where
Self: Sized,
{
self.0.is_same(&other.0)
}
fn dyn_is_same(&self, other: &dyn Action<S0, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt) }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S0) -> ControlFlow<R> {
self.0.tick(ctx, (self.1)(state))
}
}

93
rtsim/src/ai/predicate.rs Normal file
View File

@ -0,0 +1,93 @@
use rand::Rng;
use super::NpcCtx;
pub trait Predicate: Sized + Clone {
fn should(&mut self, ctx: &mut NpcCtx) -> bool;
fn chance(self, chance: f32) -> Chance<Self> {
Chance {
predicate: self,
chance,
}
}
/// Hint for when this will be true.
fn time_hint(&self) -> Option<f32> { None }
}
#[derive(Clone, Copy)]
pub struct Yes;
impl Predicate for Yes {
fn should(&mut self, _ctx: &mut NpcCtx) -> bool { true }
fn time_hint(&self) -> Option<f32> { Some(0.0) }
}
#[derive(Clone)]
pub struct EveryRange {
next: Option<f32>,
sample: std::ops::Range<f32>,
}
pub fn every_range(r: std::ops::Range<f32>) -> EveryRange {
EveryRange {
next: None,
sample: r,
}
}
impl Predicate for EveryRange {
fn should(&mut self, ctx: &mut NpcCtx) -> bool {
if let Some(ref mut next) = self.next {
*next -= ctx.dt;
if *next <= 0.0 {
*next += ctx.rng.gen_range(self.sample.clone());
true
} else {
false
}
} else {
self.next = Some(ctx.rng.gen_range(self.sample.clone()));
false
}
}
fn time_hint(&self) -> Option<f32> { self.next }
}
#[derive(Clone, Copy)]
pub struct Chance<P> {
predicate: P,
chance: f32,
}
impl<P: Predicate> Predicate for Chance<P> {
fn should(&mut self, ctx: &mut NpcCtx) -> bool {
self.predicate.should(ctx) && ctx.rng.gen_bool(self.chance as f64)
}
fn time_hint(&self) -> Option<f32> { self.predicate.time_hint() }
}
impl<F: Fn(&mut NpcCtx) -> bool + Clone> Predicate for F {
fn should(&mut self, ctx: &mut NpcCtx) -> bool { self(ctx) }
}
// Seconds
pub fn timeout(time: f64) -> Timeout { Timeout { seconds_left: time } }
#[derive(Clone, Copy)]
pub struct Timeout {
seconds_left: f64,
}
impl Predicate for Timeout {
fn should(&mut self, ctx: &mut NpcCtx) -> bool {
self.seconds_left -= ctx.dt as f64;
self.seconds_left <= 0.0
}
fn time_hint(&self) -> Option<f32> { Some(self.seconds_left.max(0.0) as f32) }
}

View File

@ -29,7 +29,7 @@ use std::{
/// Note that this number does *not* need incrementing on every change: most
/// field removals/additions are fine. This number should only be incremented
/// when we wish to perform a *hard purge* of rtsim data.
pub const CURRENT_VERSION: u32 = 2;
pub const CURRENT_VERSION: u32 = 3;
#[derive(Clone, Serialize, Deserialize)]
pub struct Data {

View File

@ -86,7 +86,7 @@ impl Controller {
}
pub struct Brain {
pub action: Box<dyn Action<!>>,
pub action: Box<dyn Action<(), !>>,
}
#[derive(Serialize, Deserialize)]

View File

@ -1,7 +1,8 @@
use crate::{RtState, Rule};
use common::{
mounting::VolumePos,
resources::{Time, TimeOfDay},
rtsim::Actor,
rtsim::{Actor, VehicleId},
};
use vek::*;
use world::{IndexRef, World};
@ -36,3 +37,10 @@ pub struct OnDeath {
pub killer: Option<Actor>,
}
impl Event for OnDeath {}
#[derive(Clone)]
pub struct OnMountVolume {
pub actor: Actor,
pub pos: VolumePos<VehicleId>,
}
impl Event for OnMountVolume {}

View File

@ -7,7 +7,10 @@
trait_upcasting,
control_flow_enum,
let_chains,
binary_heap_drain_sorted
binary_heap_drain_sorted,
fn_traits,
unboxed_closures,
tuple_trait
)]
pub mod ai;

View File

@ -1,7 +1,11 @@
use std::hash::BuildHasherDefault;
use std::{collections::VecDeque, hash::BuildHasherDefault};
use crate::{
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx},
ai::{
casual, choose, finish, important, just, now,
predicate::{every_range, timeout, Chance, EveryRange, Predicate},
seq, until, Action, NpcCtx, State,
},
data::{
npc::{Brain, PathData, SimulationMode},
ReportKind, Sentiment, Sites,
@ -52,6 +56,12 @@ const CARDINALS: &[Vec2<i32>] = &[
Vec2::new(0, -1),
];
#[derive(Clone)]
struct DefaultState {
socialize_timer: EveryRange,
move_home_timer: Chance<EveryRange>,
}
fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathResult<Vec2<i32>> {
let heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default());
@ -222,7 +232,17 @@ fn path_towns(
impl Rule for NpcAi {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnTick>(|ctx| {
// Keep track of the last `SIMULATED_TICK_SKIP` ticks, to know the deltatime
// since the last tick we ran the npc.
let mut last_ticks: VecDeque<_> = [1.0 / 30.0; SIMULATED_TICK_SKIP as usize]
.into_iter()
.collect();
rtstate.bind::<Self, OnTick>(move |ctx| {
last_ticks.push_front(ctx.event.dt);
if last_ticks.len() >= SIMULATED_TICK_SKIP as usize {
last_ticks.pop_back();
}
// Temporarily take the brains of NPCs out of their heads to appease the borrow
// checker
let mut npc_data = {
@ -239,13 +259,20 @@ impl Rule for NpcAi {
let sentiments = std::mem::take(&mut npc.sentiments);
let known_reports = std::mem::take(&mut npc.known_reports);
let brain = npc.brain.take().unwrap_or_else(|| Brain {
action: Box::new(think().repeat()),
action: Box::new(think().repeat().with_state(DefaultState {
socialize_timer: every_range(15.0..30.0),
move_home_timer: every_range(400.0..2000.0).chance(0.5),
})),
});
(npc_id, controller, inbox, sentiments, known_reports, brain)
})
.collect::<Vec<_>>()
};
// The sum of the last `SIMULATED_TICK_SKIP` tick deltatimes is the deltatime since
// simulated npcs ran this tick had their ai ran.
let simulated_dt = last_ticks.iter().sum::<f32>();
// Do a little thinking
{
let data = &*ctx.state.data();
@ -267,8 +294,13 @@ impl Rule for NpcAi {
inbox,
known_reports,
sentiments,
dt: if matches!(npc.mode, SimulationMode::Loaded) {
ctx.event.dt
} else {
simulated_dt
},
rng: ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()),
});
}, &mut ());
});
}
@ -287,20 +319,19 @@ impl Rule for NpcAi {
}
}
fn idle() -> impl Action { just(|ctx| ctx.controller.do_idle()).debug(|| "idle") }
fn idle<S: State>() -> impl Action<S> { just(|ctx, _| ctx.controller.do_idle()).debug(|| "idle") }
/// Try to walk toward a 3D position without caring for obstacles.
fn goto(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
fn goto<S: State>(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action<S> {
const STEP_DIST: f32 = 24.0;
const WAYPOINT_DIST: f32 = 12.0;
let mut waypoint = None;
just(move |ctx| {
just(move |ctx, waypoint: &mut Option<Vec3<f32>>| {
// If we're close to the next waypoint, complete it
if waypoint.map_or(false, |waypoint: Vec3<f32>| {
ctx.npc.wpos.xy().distance_squared(waypoint.xy()) < WAYPOINT_DIST.powi(2)
}) {
waypoint = None;
*waypoint = None;
}
// Get the next waypoint on the route toward the goal
@ -309,40 +340,36 @@ fn goto(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
let len = rpos.magnitude();
let wpos = ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST);
wpos.with_z(
ctx.world
.sim()
.get_surface_alt_approx(wpos.xy().as_())
.unwrap_or(wpos.z),
)
wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()))
});
ctx.controller.do_goto(*waypoint, speed_factor);
})
.repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2))
.stop_if(move |ctx: &mut NpcCtx| {
ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)
})
.with_state(None)
.debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z))
.map(|_| {})
.map(|_, _| {})
}
/// Try to walk fly a 3D position following the terrain altitude at an offset
/// without caring for obstacles.
fn goto_flying(
fn goto_flying<S: State>(
wpos: Vec3<f32>,
speed_factor: f32,
goal_dist: f32,
step_dist: f32,
waypoint_dist: f32,
height_offset: f32,
) -> impl Action {
let mut waypoint = None;
just(move |ctx| {
) -> impl Action<S> {
just(move |ctx, waypoint: &mut Option<Vec3<f32>>| {
// If we're close to the next waypoint, complete it
if waypoint.map_or(false, |waypoint: Vec3<f32>| {
ctx.npc.wpos.distance_squared(waypoint) < waypoint_dist.powi(2)
}) {
waypoint = None;
*waypoint = None;
}
// Get the next waypoint on the route toward the goal
@ -351,43 +378,41 @@ fn goto_flying(
let len = rpos.magnitude();
let wpos = ctx.npc.wpos + (rpos / len) * len.min(step_dist);
wpos.with_z(
ctx.world
.sim()
.get_surface_alt_approx(wpos.xy().as_())
.map(|alt| alt + height_offset)
.unwrap_or(wpos.z),
)
wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()) + height_offset)
});
ctx.controller.do_goto(*waypoint, speed_factor);
})
.repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2))
.boxed()
.with_state(None)
.stop_if(move |ctx: &mut NpcCtx| {
ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)
})
.debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z))
.map(|_| {})
.map(|_, _| {})
}
/// Try to walk toward a 2D position on the terrain without caring for
/// Try to walk toward a 2D position on the surface without caring for
/// obstacles.
fn goto_2d(wpos2d: Vec2<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
now(move |ctx| {
let wpos = wpos2d.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0));
fn goto_2d<S: State>(wpos2d: Vec2<f32>, speed_factor: f32, goal_dist: f32) -> impl Action<S> {
now(move |ctx, _| {
let wpos = wpos2d.with_z(ctx.world.sim().get_surface_alt_approx(wpos2d.as_()));
goto(wpos, speed_factor, goal_dist)
})
}
/// Try to fly toward a 2D position following the terrain altitude at an offset
/// without caring for obstacles.
fn goto_2d_flying(
fn goto_2d_flying<S: State>(
wpos2d: Vec2<f32>,
speed_factor: f32,
goal_dist: f32,
step_dist: f32,
waypoint_dist: f32,
height_offset: f32,
) -> impl Action {
now(move |ctx| {
) -> impl Action<S> {
now(move |ctx, _| {
let wpos = wpos2d
.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0) + height_offset);
goto_flying(
@ -401,11 +426,11 @@ fn goto_2d_flying(
})
}
fn traverse_points<F>(mut next_point: F, speed_factor: f32) -> impl Action
fn traverse_points<S: State, F>(next_point: F, speed_factor: f32) -> impl Action<S>
where
F: FnMut(&mut NpcCtx) -> Option<Vec2<f32>> + Send + Sync + 'static,
F: FnMut(&mut NpcCtx) -> Option<Vec2<f32>> + Clone + Send + Sync + 'static,
{
until(move |ctx| {
until(move |ctx, next_point: &mut F| {
let wpos = next_point(ctx)?;
let wpos_site = |wpos: Vec2<f32>| {
@ -437,11 +462,12 @@ where
Some(Either::Right(goto_2d(wpos, speed_factor, 8.0)))
}
})
.with_state(next_point)
}
/// Try to travel to a site. Where practical, paths will be taken.
fn travel_to_point(wpos: Vec2<f32>, speed_factor: f32) -> impl Action {
now(move |ctx| {
fn travel_to_point<S: State>(wpos: Vec2<f32>, speed_factor: f32) -> impl Action<S> {
now(move |ctx, _| {
const WAYPOINT: f32 = 48.0;
let start = ctx.npc.wpos.xy();
let diff = wpos - start;
@ -453,8 +479,8 @@ fn travel_to_point(wpos: Vec2<f32>, speed_factor: f32) -> impl Action {
}
/// Try to travel to a site. Where practical, paths will be taken.
fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action {
now(move |ctx| {
fn travel_to_site<S: State>(tgt_site: SiteId, speed_factor: f32) -> impl Action<S> {
now(move |ctx, _| {
let sites = &ctx.state.data().sites;
let site_wpos = sites.get(tgt_site).map(|site| site.wpos.as_());
@ -540,20 +566,14 @@ fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action {
finish().boxed()
}
// Stop the NPC early if we're near the site to prevent huddling around the centre
.stop_if(move |ctx| site_wpos.map_or(false, |site_wpos| ctx.npc.wpos.xy().distance_squared(site_wpos) < 16f32.powi(2)))
.stop_if(move |ctx: &mut NpcCtx| site_wpos.map_or(false, |site_wpos| ctx.npc.wpos.xy().distance_squared(site_wpos) < 16f32.powi(2)))
})
.debug(move || format!("travel_to_site {:?}", tgt_site))
.map(|_| ())
.map(|_, _| ())
}
// Seconds
fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
let mut timeout = None;
move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time)
}
fn talk_to(tgt: Actor, _subject: Option<Subject>) -> impl Action {
now(move |ctx| {
fn talk_to<S: State>(tgt: Actor, _subject: Option<Subject>) -> impl Action<S> {
now(move |ctx, _| {
if matches!(tgt, Actor::Npc(_)) && ctx.rng.gen_bool(0.2) {
// Cut off the conversation sometimes to avoid infinite conversations (but only
// if the target is an NPC!) TODO: Don't special case this, have
@ -598,23 +618,23 @@ fn talk_to(tgt: Actor, _subject: Option<Subject>) -> impl Action {
idle()
.repeat()
.stop_if(timeout(wait))
.then(just(move |ctx| ctx.controller.say(tgt, comment.clone())))
.then(just(move |ctx, _| ctx.controller.say(tgt, comment.clone())))
.r()
}
})
}
fn socialize() -> impl Action {
now(|ctx| {
fn socialize() -> impl Action<EveryRange> {
now(move |ctx, socialize: &mut EveryRange| {
// Skip most socialising actions if we're not loaded
if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) {
if matches!(ctx.npc.mode, SimulationMode::Loaded) && socialize.should(ctx) {
// Sometimes dance
if ctx.rng.gen_bool(0.15) {
return just(|ctx| ctx.controller.do_dance())
return just(|ctx, _| ctx.controller.do_dance())
.repeat()
.stop_if(timeout(6.0))
.debug(|| "dancing")
.map(|_| ())
.map(|_, _| ())
.l()
.l();
// Talk to nearby NPCs
@ -628,17 +648,16 @@ fn socialize() -> impl Action {
return talk_to(other, None)
// After talking, wait for a while
.then(idle().repeat().stop_if(timeout(4.0)))
.map(|_| ())
.map(|_, _| ())
.r().l();
}
}
idle().r()
})
}
fn adventure() -> impl Action {
choose(|ctx| {
fn adventure() -> impl Action<DefaultState> {
choose(|ctx, _| {
// Choose a random site that's fairly close by
if let Some(tgt_site) = ctx
.state
@ -670,11 +689,11 @@ 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(None, Content::localized_with_args("npc-speech-moving_on", [("site", site_name.clone())])))
important(just(move |ctx, _| ctx.controller.say(None, Content::localized_with_args("npc-speech-moving_on", [("site", site_name.clone())])))
.then(travel_to_site(tgt_site, 0.6))
// Stop for a few minutes
.then(villager(tgt_site).repeat().stop_if(timeout(wait_time)))
.map(|_| ())
.map(|_, _| ())
.boxed(),
)
} else {
@ -684,8 +703,8 @@ fn adventure() -> impl Action {
.debug(move || "adventure")
}
fn gather_ingredients() -> impl Action {
just(|ctx| {
fn gather_ingredients<S: State>() -> impl Action<S> {
just(|ctx, _| {
ctx.controller.do_gather(
&[
ChunkResource::Fruit,
@ -697,8 +716,8 @@ fn gather_ingredients() -> impl Action {
.debug(|| "gather ingredients")
}
fn hunt_animals() -> impl Action {
just(|ctx| ctx.controller.do_hunt_animals()).debug(|| "hunt_animals")
fn hunt_animals<S: State>() -> impl Action<S> {
just(|ctx, _| ctx.controller.do_hunt_animals()).debug(|| "hunt_animals")
}
fn find_forest(ctx: &mut NpcCtx) -> Option<Vec2<f32>> {
@ -732,10 +751,10 @@ fn choose_plaza(ctx: &mut NpcCtx, site: SiteId) -> Option<Vec2<f32>> {
})
}
fn villager(visiting_site: SiteId) -> impl Action {
choose(move |ctx| {
fn villager(visiting_site: SiteId) -> impl Action<DefaultState> {
choose(move |ctx, state: &mut DefaultState| {
// Consider moving home if the home site gets too full
if ctx.rng.gen_bool(0.0001)
if state.move_home_timer.should(ctx)
&& let Some(home) = ctx.npc.home
&& Some(home) == ctx.npc.current_site
&& let Some(home_pop_ratio) = ctx.state.data().sites.get(home)
@ -769,20 +788,20 @@ fn villager(visiting_site: SiteId) -> impl Action {
{
let site_name = ctx.state.data().sites[new_home].world_site
.map(|ws| ctx.index.sites.get(ws).name().to_string());
return important(just(move |ctx| {
return important(just(move |ctx, _| {
if let Some(site_name) = &site_name {
ctx.controller.say(None, Content::localized_with_args("npc-speech-migrating", [("site", site_name.clone())]))
}
})
.then(travel_to_site(new_home, 0.5))
.then(just(move |ctx| ctx.controller.set_new_home(new_home))));
.then(just(move |ctx, _| ctx.controller.set_new_home(new_home))));
}
if DayPeriod::from(ctx.time_of_day.0).is_dark()
&& !matches!(ctx.npc.profession(), Some(Profession::Guard))
{
return important(
now(move |ctx| {
now(move |ctx, _| {
if let Some(house_wpos) = ctx
.state
.data()
@ -798,19 +817,19 @@ fn villager(visiting_site: SiteId) -> impl Action {
Some(site2.tile_center_wpos(house.root_tile()).as_())
})
{
just(|ctx| {
just(|ctx, _| {
ctx.controller
.say(None, Content::localized("npc-speech-night_time"))
})
.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| {
.then(socialize().repeat().map_state(|state: &mut DefaultState| &mut state.socialize_timer).debug(|| "wait in house"))
.stop_if(|ctx: &mut NpcCtx| DayPeriod::from(ctx.time_of_day.0).is_light())
.then(just(|ctx, _| {
ctx.controller
.say(None, Content::localized("npc-speech-day_time"))
}))
.map(|_| ())
.map(|_, _| ())
.boxed()
} else {
finish().boxed()
@ -829,13 +848,13 @@ fn villager(visiting_site: SiteId) -> impl Action {
let wait_time = ctx.rng.gen_range(10.0..30.0);
gather_ingredients().repeat().stop_if(timeout(wait_time))
})
.map(|_| ()),
.map(|_, _| ()),
);
}
} 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| {
just(|ctx, _| {
ctx.controller
.say(None, Content::localized("npc-speech-start_hunting"))
})
@ -845,7 +864,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
let wait_time = ctx.rng.gen_range(30.0..60.0);
hunt_animals().repeat().stop_if(timeout(wait_time))
})
.map(|_| ()),
.map(|_, _| ()),
);
}
} else if matches!(ctx.npc.profession(), Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) {
@ -853,9 +872,9 @@ fn villager(visiting_site: SiteId) -> impl Action {
return casual(
travel_to_point(plaza_wpos, 0.4)
.debug(|| "patrol")
.interrupt_with(|ctx| {
.interrupt_with(move |ctx, _| {
if ctx.rng.gen_bool(0.0003) {
Some(just(move |ctx| {
Some(just(move |ctx, _| {
ctx.controller
.say(None, Content::localized("npc-speech-guard_thought"))
}))
@ -863,13 +882,13 @@ fn villager(visiting_site: SiteId) -> impl Action {
None
}
})
.map(|_| ()),
.map(|_, _| ()),
);
}
} else if matches!(ctx.npc.profession(), Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8)
{
return casual(
just(|ctx| {
just(|ctx, _| {
// Try to direct our speech at nearby actors, if there are any
let (target, phrase) = if ctx.rng.gen_bool(0.3) && let Some(other) = ctx
.state
@ -890,12 +909,12 @@ fn villager(visiting_site: SiteId) -> impl Action {
.repeat()
.stop_if(timeout(60.0))
.debug(|| "sell wares")
.map(|_| ()),
.map(|_, _| ()),
);
}
// If nothing else needs doing, walk between plazas and socialize
casual(now(move |ctx| {
casual(now(move |ctx, _| {
// Choose a plaza in the site we're visiting to walk to
if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) {
// Walk to the plaza...
@ -908,9 +927,10 @@ fn villager(visiting_site: SiteId) -> impl Action {
// ...then socialize for some time before moving on
.then(socialize()
.repeat()
.map_state(|state: &mut DefaultState| &mut state.socialize_timer)
.stop_if(timeout(ctx.rng.gen_range(30.0..90.0)))
.debug(|| "wait at plaza"))
.map(|_| ())
.map(|_, _| ())
}))
})
.debug(move || format!("villager at site {:?}", visiting_site))
@ -937,9 +957,9 @@ fn follow(npc: NpcId, distance: f32) -> impl Action {
}
*/
fn pilot(ship: common::comp::ship::Body) -> impl Action {
fn pilot<S: State>(ship: common::comp::ship::Body) -> impl Action<S> {
// Travel between different towns in a straight line
now(move |ctx| {
now(move |ctx, _| {
let data = &*ctx.state.data();
let site = data
.sites
@ -968,12 +988,12 @@ fn pilot(ship: common::comp::ship::Body) -> impl Action {
}
})
.repeat()
.map(|_| ())
.map(|_, _| ())
}
fn captain() -> impl Action {
fn captain<S: State>() -> impl Action<S> {
// For now just randomly travel the sea
now(|ctx| {
now(|ctx, _| {
let chunk = ctx.npc.wpos.xy().as_().wpos_to_cpos();
if let Some(chunk) = NEIGHBORS
.into_iter()
@ -999,10 +1019,10 @@ fn captain() -> impl Action {
}
})
.repeat()
.map(|_| ())
.map(|_, _| ())
}
fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
fn check_inbox<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S>> {
loop {
match ctx.inbox.pop_front() {
Some(NpcInput::Report(report_id)) if !ctx.known_reports.contains(&report_id) => {
@ -1047,7 +1067,9 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
};
ctx.known_reports.insert(report_id);
break Some(
just(move |ctx| ctx.controller.say(killer, Content::localized(phrase)))
just(move |ctx, _| {
ctx.controller.say(killer, Content::localized(phrase))
})
.l(),
);
},
@ -1062,7 +1084,7 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
}
}
fn check_for_enemies(ctx: &mut NpcCtx) -> Option<impl Action> {
fn check_for_enemies<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S>> {
// TODO: Instead of checking all nearby actors every tick, it would be more
// effective to have the actor grid generate a per-tick diff so that we only
// need to check new actors in the local area. Be careful though:
@ -1074,17 +1096,17 @@ fn check_for_enemies(ctx: &mut NpcCtx) -> Option<impl Action> {
.npcs
.nearby(Some(ctx.npc_id), ctx.npc.wpos, 24.0)
.find(|actor| ctx.sentiments.toward(*actor).is(Sentiment::ENEMY))
.map(|enemy| just(move |ctx| ctx.controller.attack(enemy)))
.map(|enemy| just(move |ctx, _| ctx.controller.attack(enemy)))
}
fn react_to_events(ctx: &mut NpcCtx) -> Option<impl Action> {
check_inbox(ctx)
fn react_to_events<S: State>(ctx: &mut NpcCtx, _: &mut S) -> Option<impl Action<S>> {
check_inbox::<S>(ctx)
.map(|action| action.boxed())
.or_else(|| check_for_enemies(ctx).map(|action| action.boxed()))
}
fn humanoid() -> impl Action {
choose(|ctx| {
fn humanoid() -> impl Action<DefaultState> {
choose(|ctx, _| {
if let Some(riding) = &ctx.npc.riding {
if riding.steering {
if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) {
@ -1100,7 +1122,9 @@ fn humanoid() -> impl Action {
casual(finish())
}
} else {
important(socialize())
important(
socialize().map_state(|state: &mut DefaultState| &mut state.socialize_timer),
)
}
} else {
let action = if matches!(
@ -1119,8 +1143,8 @@ fn humanoid() -> impl Action {
})
}
fn bird_large() -> impl Action {
choose(|ctx| {
fn bird_large() -> impl Action<DefaultState> {
choose(|ctx, _| {
let data = ctx.state.data();
if let Some(home) = ctx.npc.home {
let is_home = ctx.npc.current_site.map_or(false, |site| home == site);
@ -1161,26 +1185,58 @@ fn bird_large() -> impl Action {
})
}
fn monster() -> impl Action {
let mut bearing = Vec2::zero();
now(move |ctx| {
bearing = bearing
fn monster() -> impl Action<DefaultState> {
now(|ctx, bearing: &mut Vec2<f32>| {
*bearing = bearing
.map(|e| e + ctx.rng.gen_range(-0.1..0.1))
.try_normalized()
.unwrap_or_default();
goto_2d(ctx.npc.wpos.xy() + bearing * 24.0, 0.7, 8.0)
.debug(move || format!("Moving with a bearing of {:?}", bearing))
let bearing_dist = 24.0;
let mut pos = ctx.npc.wpos.xy() + *bearing * bearing_dist;
let is_deep_water = ctx
.world
.sim()
.get_interpolated(pos.as_(), |c| c.alt - c.water_alt)
.unwrap_or(f32::NEG_INFINITY)
< -10.0
&& ctx
.world
.sim()
.get(pos.as_().wpos_to_cpos())
.map_or(false, |c| c.river.is_ocean() || c.river.is_lake());
if !is_deep_water {
goto_2d(pos, 0.7, 8.0)
} else {
*bearing *= -1.0;
pos = ctx.npc.wpos.xy() + *bearing * 24.0;
goto_2d(pos, 0.7, 8.0)
}
// If we are too far away from our goal position we can stop since we aren't going to a specific place.
.stop_if(move |ctx: &mut NpcCtx| {
ctx.npc.wpos.xy().distance_squared(pos) > (bearing_dist + 5.0).powi(2)
})
.debug({
let bearing = *bearing;
move || format!("Moving with a bearing of {:?}", bearing)
})
})
.repeat()
.map(|_| ())
.with_state(Vec2::<f32>::zero())
.map(|_, _| ())
}
fn think() -> impl Action {
now(|ctx| match ctx.npc.body {
fn think() -> impl Action<DefaultState> {
now(|ctx, _| match ctx.npc.body {
common::comp::Body::Humanoid(_) => humanoid().l().l().l(),
common::comp::Body::BirdLarge(_) => bird_large().r().l().l(),
_ => match &ctx.npc.role {
Role::Civilised(_) => socialize().l().r().l(),
Role::Civilised(_) => socialize()
.map_state(|state: &mut DefaultState| &mut state.socialize_timer)
.l()
.r()
.l(),
Role::Monster => monster().r().r().l(),
Role::Wild => idle().r(),
},

View File

@ -1,26 +1,29 @@
use crate::{
data::{npc::SimulationMode, Npc},
event::{EventCtx, OnDeath, OnSetup, OnTick},
event::{EventCtx, OnDeath, OnMountVolume, OnSetup, OnTick},
RtState, Rule, RuleError,
};
use common::{
comp::{self, Body},
mounting::{Volume, VolumePos},
rtsim::{Actor, NpcAction, NpcActivity, Personality},
terrain::CoordinateConversions,
terrain::{CoordinateConversions, TerrainChunkSize},
vol::RectVolSize,
};
use rand::prelude::*;
use rand_chacha::ChaChaRng;
use tracing::{error, warn};
use vek::Vec2;
use vek::{Clamp, Vec2};
use world::site::SiteKind;
pub struct SimulateNpcs;
impl Rule for SimulateNpcs {
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);
rtstate.bind(on_setup);
rtstate.bind(on_death);
rtstate.bind(on_tick);
rtstate.bind(on_mount_volume);
Ok(Self)
}
@ -43,6 +46,17 @@ fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
}
}
fn on_mount_volume(ctx: EventCtx<SimulateNpcs, OnMountVolume>) {
let data = &mut *ctx.state.data_mut();
if let VolumePos { kind: Volume::Entity(vehicle), .. } = ctx.event.pos
&& let Some(vehicle) = data.npcs.vehicles.get(vehicle)
&& let Some(Actor::Npc(driver)) = vehicle.driver
&& let Some(driver) = data.npcs.get_mut(driver) {
driver.controller.actions.push(NpcAction::Say(Some(ctx.event.actor), comp::Content::localized("npc-speech-welcome-aboard")))
}
}
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
let data = &mut *ctx.state.data_mut();
@ -238,6 +252,20 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
},
None => {},
}
// Make sure NPCs remain on the surface and within the map
npc.wpos = npc
.wpos
.xy()
.clamped(
Vec2::zero(),
(ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
)
.with_z(
ctx.world
.sim()
.get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
+ npc.body.flying_height(),
);
}
// Consume NPC actions
@ -247,13 +275,6 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
}
}
// Make sure NPCs remain on the surface
npc.wpos.z = ctx
.world
.sim()
.get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
.unwrap_or(0.0)
+ npc.body.flying_height();
}
// Move home if required
@ -271,4 +292,24 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
npc.home = Some(new_home);
}
}
for (_, vehicle) in data.npcs.vehicles.iter_mut() {
// Try to keep ships above ground and within the map
if matches!(vehicle.mode, SimulationMode::Simulated) {
vehicle.wpos = vehicle
.wpos
.xy()
.clamped(
Vec2::zero(),
(ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
)
.with_z(
vehicle.wpos.z.max(
ctx.world
.sim()
.get_surface_alt_approx(vehicle.wpos.xy().as_()),
),
);
}
}
}

View File

@ -203,7 +203,7 @@ impl Client {
| ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_, _, _)
| ServerGeneral::TimeOfDay(_, _, _, _)
| ServerGeneral::EntitySync(_)
| ServerGeneral::CompSync(_, _)
| ServerGeneral::CreateEntity(_)

View File

@ -44,7 +44,7 @@ use common::{
npc::{self, get_npc_name},
outcome::Outcome,
parse_cmd_args,
resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay},
resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay, TimeScale},
rtsim::{Actor, Role},
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
uid::{Uid, UidAllocator},
@ -186,6 +186,7 @@ fn do_command(
ServerChatCommand::Sudo => handle_sudo,
ServerChatCommand::Tell => handle_tell,
ServerChatCommand::Time => handle_time,
ServerChatCommand::TimeScale => handle_time_scale,
ServerChatCommand::Tp => handle_tp,
ServerChatCommand::RtsimTp => handle_rtsim_tp,
ServerChatCommand::RtsimInfo => handle_rtsim_info,
@ -1118,12 +1119,14 @@ fn handle_time(
let mut tod_lazymsg = None;
let clients = server.state.ecs().read_storage::<Client>();
let calendar = server.state.ecs().read_resource::<Calendar>();
let time_scale = server.state.ecs().read_resource::<TimeScale>();
for client in (&clients).join() {
let msg = tod_lazymsg.unwrap_or_else(|| {
client.prepare(ServerGeneral::TimeOfDay(
TimeOfDay(new_time),
(*calendar).clone(),
*time,
*time_scale,
))
});
let _ = client.send_prepared(&msg);
@ -1144,6 +1147,64 @@ fn handle_time(
Ok(())
}
fn handle_time_scale(
server: &mut Server,
client: EcsEntity,
_target: EcsEntity,
args: Vec<String>,
_action: &ServerChatCommand,
) -> CmdResult<()> {
let time_scale = server
.state
.ecs_mut()
.get_mut::<TimeScale>()
.expect("Expected time scale to be added.");
if args.is_empty() {
let time_scale = time_scale.0;
server.notify_client(
client,
ServerGeneral::server_msg(
ChatType::CommandInfo,
format!("The current time scale is {time_scale}."),
),
);
} else if let Some(scale) = parse_cmd_args!(args, f64) {
time_scale.0 = scale.clamp(0.0001, 1000.0);
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, format!("Set time scale to {scale}.")),
);
// Update all clients with the new TimeOfDay (without this they would have to
// wait for the next 100th tick to receive the update).
let mut tod_lazymsg = None;
let clients = server.state.ecs().read_storage::<Client>();
let time = server.state.ecs().read_resource::<Time>();
let time_of_day = server.state.ecs().read_resource::<TimeOfDay>();
let calendar = server.state.ecs().read_resource::<Calendar>();
for client in (&clients).join() {
let msg = tod_lazymsg.unwrap_or_else(|| {
client.prepare(ServerGeneral::TimeOfDay(
*time_of_day,
(*calendar).clone(),
*time,
TimeScale(scale),
))
});
let _ = client.send_prepared(&msg);
tod_lazymsg = Some(msg);
}
} else {
server.notify_client(
client,
ServerGeneral::server_msg(
ChatType::CommandError,
"Wrong parameter, expected f32.".to_string(),
),
);
}
Ok(())
}
fn handle_health(
server: &mut Server,
_client: EcsEntity,
@ -1302,6 +1363,7 @@ fn handle_rtsim_info(
let _ = writeln!(&mut info, "-- General Information --");
let _ = writeln!(&mut info, "Seed: {}", npc.seed);
let _ = writeln!(&mut info, "Pos: {:?}", npc.wpos);
let _ = writeln!(&mut info, "Role: {:?}", npc.role);
let _ = writeln!(&mut info, "Home: {:?}", npc.home);
let _ = writeln!(&mut info, "Faction: {:?}", npc.faction);
@ -1309,6 +1371,11 @@ fn handle_rtsim_info(
let _ = writeln!(&mut info, "-- Status --");
let _ = writeln!(&mut info, "Current site: {:?}", npc.current_site);
let _ = writeln!(&mut info, "Current mode: {:?}", npc.mode);
let _ = writeln!(
&mut info,
"Riding: {:?}",
npc.riding.as_ref().map(|riding| riding.vehicle)
);
let _ = writeln!(&mut info, "-- Action State --");
if let Some(brain) = &npc.brain {
let mut bt = Vec::new();
@ -1345,7 +1412,7 @@ fn handle_rtsim_npc(
.filter(|s| !s.is_empty())
.map(|s| s.trim().to_lowercase())
.collect::<Vec<_>>();
let npc_names = &*common::npc::NPC_NAMES.read();
let rtsim = server.state.ecs().read_resource::<RtSim>();
let data = rtsim.state().data();
let mut npcs = data
@ -1353,7 +1420,7 @@ fn handle_rtsim_npc(
.values()
.enumerate()
.filter(|(idx, npc)| {
let tags = [
let mut tags = vec![
npc.profession()
.map(|p| format!("{:?}", p))
.unwrap_or_default(),
@ -1364,7 +1431,11 @@ fn handle_rtsim_npc(
},
format!("{:?}", npc.mode),
format!("{}", idx),
npc_names[&npc.body].keyword.clone(),
];
if let Some(species_meta) = npc_names.get_species_meta(&npc.body) {
tags.push(species_meta.keyword.clone());
}
terms
.iter()
.all(|term| tags.iter().any(|tag| term.eq_ignore_ascii_case(tag.trim())))

View File

@ -1,4 +1,4 @@
use specs::{world::WorldExt, Entity as EcsEntity, Join};
use specs::{saveload::MarkerAllocator, world::WorldExt, Entity as EcsEntity, Join};
use vek::*;
use common::{
@ -19,19 +19,20 @@ use common::{
link::Is,
mounting::{Mounting, Rider, VolumeMounting, VolumePos, VolumeRider},
outcome::Outcome,
rtsim::RtSimVehicle,
terrain::{Block, SpriteKind},
uid::Uid,
uid::{Uid, UidAllocator},
vol::ReadVol,
};
use common_net::sync::WorldSyncExt;
use crate::{state_ext::StateExt, Server, Time};
use crate::{rtsim::RtSim, state_ext::StateExt, Server, Time};
use crate::pet::tame_pet;
use hashbrown::{HashMap, HashSet};
use lazy_static::lazy_static;
use serde::Deserialize;
use std::iter::FromIterator;
use std::{iter::FromIterator, sync::Arc};
pub fn handle_lantern(server: &mut Server, entity: EcsEntity, enable: bool) {
let ecs = server.state_mut().ecs();
@ -169,11 +170,28 @@ pub fn handle_mount_volume(server: &mut Server, rider: EcsEntity, volume_pos: Vo
let maybe_uid = state.ecs().read_storage::<Uid>().get(rider).copied();
if let Some(rider) = maybe_uid && within_range {
let _ = state.link(VolumeMounting {
let _link_successful = state.link(VolumeMounting {
pos: volume_pos,
block,
rider,
});
}).is_ok();
#[cfg(feature = "worldgen")]
if _link_successful {
let uid_allocator = state.ecs().read_resource::<UidAllocator>();
if let Some(rider_entity) = uid_allocator.retrieve_entity_internal(rider.0)
&& let Some(rider_actor) = state.entity_as_actor(rider_entity)
&& let Some(volume_pos) = volume_pos.try_map_entity(|uid| {
let entity = uid_allocator.retrieve_entity_internal(uid.0)?;
state.read_storage::<RtSimVehicle>().get(entity).map(|v| v.0)
}) {
state.ecs().write_resource::<RtSim>().hook_character_mount_volume(
&state.ecs().read_resource::<Arc<world::World>>(),
state.ecs().read_resource::<world::IndexOwned>().as_index_ref(),
volume_pos,
rider_actor,
);
}
}
}
}
}

View File

@ -5,7 +5,8 @@ pub mod tick;
use atomicwrites::{AtomicFile, OverwriteBehavior};
use common::{
grid::Grid,
rtsim::{Actor, ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings},
mounting::VolumePos,
rtsim::{Actor, ChunkResource, RtSimEntity, RtSimVehicle, VehicleId, WorldSettings},
};
use common_ecs::dispatch;
use common_state::BlockDiff;
@ -13,7 +14,7 @@ use crossbeam_channel::{unbounded, Receiver, Sender};
use enum_map::EnumMap;
use rtsim::{
data::{npc::SimulationMode, Data, ReadError},
event::{OnDeath, OnSetup},
event::{OnDeath, OnMountVolume, OnSetup},
RtState,
};
use specs::DispatcherBuilder;
@ -147,6 +148,16 @@ impl RtSim {
path
}
pub fn hook_character_mount_volume(
&mut self,
world: &World,
index: IndexRef,
pos: VolumePos<VehicleId>,
actor: Actor,
) {
self.state.emit(OnMountVolume { actor, pos }, world, index)
}
pub fn hook_load_chunk(&mut self, key: Vec2<i32>, max_res: EnumMap<ChunkResource, usize>) {
if let Some(chunk_state) = self.state.get_resource_mut::<ChunkStates>().0.get_mut(key) {
*chunk_state = Some(LoadedChunkState { max_res });
@ -167,14 +178,26 @@ impl RtSim {
}
pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) {
if let Some(npc) = self.state.get_data_mut().npcs.get_mut(entity.0) {
if let Some(npc) = self
.state
.get_data_mut()
.npcs
.get_mut(entity.0)
.filter(|npc| npc.riding.is_none())
{
npc.mode = SimulationMode::Simulated;
}
}
pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) {
if let Some(vehicle) = self.state.get_data_mut().npcs.vehicles.get_mut(entity.0) {
let data = self.state.get_data_mut();
if let Some(vehicle) = data.npcs.vehicles.get_mut(entity.0) {
vehicle.mode = SimulationMode::Simulated;
if let Some(Actor::Npc(npc)) = vehicle.driver {
if let Some(npc) = data.npcs.get_mut(npc) {
npc.mode = SimulationMode::Simulated;
}
}
}
}

View File

@ -6,7 +6,7 @@ use common::{
event::EventBus,
outcome::Outcome,
region::{Event as RegionEvent, RegionMap},
resources::{PlayerPhysicsSettings, Time, TimeOfDay},
resources::{PlayerPhysicsSettings, Time, TimeOfDay, TimeScale},
terrain::TerrainChunkSize,
uid::Uid,
vol::RectVolSize,
@ -31,6 +31,7 @@ impl<'a> System<'a> for Sys {
ReadExpect<'a, TimeOfDay>,
ReadExpect<'a, Time>,
ReadExpect<'a, Calendar>,
ReadExpect<'a, TimeScale>,
ReadExpect<'a, RegionMap>,
ReadExpect<'a, UpdateTrackers>,
ReadStorage<'a, Pos>,
@ -63,6 +64,7 @@ impl<'a> System<'a> for Sys {
time_of_day,
time,
calendar,
time_scale,
region_map,
trackers,
positions,
@ -418,6 +420,7 @@ impl<'a> System<'a> for Sys {
*time_of_day,
(*calendar).clone(),
*time,
*time_scale,
))
});
// We don't care much about stream errors here since they could just represent

View File

@ -1931,10 +1931,11 @@ impl WorldSim {
}
/// Get the altitude of the surface, could be water or ground.
pub fn get_surface_alt_approx(&self, wpos: Vec2<i32>) -> Option<f32> {
pub fn get_surface_alt_approx(&self, wpos: Vec2<i32>) -> f32 {
self.get_interpolated(wpos, |chunk| chunk.alt)
.zip(self.get_interpolated(wpos, |chunk| chunk.water_alt))
.map(|(alt, water_alt)| alt.max(water_alt))
.unwrap_or(CONFIG.sea_level)
}
pub fn get_alt_approx(&self, wpos: Vec2<i32>) -> Option<f32> {

View File

@ -258,7 +258,7 @@ fn render_flat(bridge: &Bridge, painter: &Painter) {
let len = bridge.dir.select(aabr.size());
let true_offset = vault_width + vault_offset;
let n = len / true_offset;
let n = (len / true_offset).max(1);
let p = len / n;
let holes = painter