Added rtsim_npc, made herbalists gather ingredients

This commit is contained in:
Joshua Barretto 2023-04-02 23:02:06 +01:00
parent 1e70ccfb8d
commit b402e450cf
10 changed files with 438 additions and 309 deletions

View File

@ -310,10 +310,11 @@ pub enum ServerChatCommand {
Tell, Tell,
Time, Time,
Tp, Tp,
RtsimTp,
RtsimInfo,
RtsimPurge,
RtsimChunk, RtsimChunk,
RtsimInfo,
RtsimNpc,
RtsimPurge,
RtsimTp,
Unban, Unban,
Version, Version,
Waypoint, Waypoint,
@ -693,6 +694,11 @@ impl ServerChatCommand {
"Display information about an rtsim NPC", "Display information about an rtsim NPC",
Some(Moderator), Some(Moderator),
), ),
ServerChatCommand::RtsimNpc => cmd(
vec![Any("query", Required)],
"List rtsim NPCs that fit a given query (e.g: simulated,merchant)",
Some(Moderator),
),
ServerChatCommand::RtsimPurge => cmd( ServerChatCommand::RtsimPurge => cmd(
vec![Boolean( vec![Boolean(
"whether purging of rtsim data should occur on next startup", "whether purging of rtsim data should occur on next startup",
@ -836,6 +842,7 @@ impl ServerChatCommand {
ServerChatCommand::Tp => "tp", ServerChatCommand::Tp => "tp",
ServerChatCommand::RtsimTp => "rtsim_tp", ServerChatCommand::RtsimTp => "rtsim_tp",
ServerChatCommand::RtsimInfo => "rtsim_info", ServerChatCommand::RtsimInfo => "rtsim_info",
ServerChatCommand::RtsimNpc => "rtsim_npc",
ServerChatCommand::RtsimPurge => "rtsim_purge", ServerChatCommand::RtsimPurge => "rtsim_purge",
ServerChatCommand::RtsimChunk => "rtsim_chunk", ServerChatCommand::RtsimChunk => "rtsim_chunk",
ServerChatCommand::Unban => "unban", ServerChatCommand::Unban => "unban",

View File

@ -192,43 +192,30 @@ impl Default for Personality {
/// into the game as a physical entity or not). Agent code should attempt to act /// into the game as a physical entity or not). Agent code should attempt to act
/// upon its instructions where reasonable although deviations for various /// upon its instructions where reasonable although deviations for various
/// reasons (obstacle avoidance, counter-attacking, etc.) are expected. /// reasons (obstacle avoidance, counter-attacking, etc.) are expected.
#[derive(Clone, Debug)] #[derive(Clone, Debug, Default)]
pub struct RtSimController { pub struct RtSimController {
/// When this field is `Some(..)`, the agent should attempt to make progress pub activity: Option<NpcActivity>,
/// toward the given location, accounting for obstacles and other pub actions: VecDeque<NpcAction>,
/// high-priority situations like being attacked.
pub travel_to: Option<Vec3<f32>>,
pub personality: Personality, pub personality: Personality,
pub heading_to: Option<String>, pub heading_to: Option<String>,
/// Proportion of full speed to move
pub speed_factor: f32,
pub actions: VecDeque<NpcAction>,
}
impl Default for RtSimController {
fn default() -> Self {
Self {
travel_to: None,
personality: Personality::default(),
heading_to: None,
speed_factor: 1.0,
actions: VecDeque::new(),
}
}
} }
impl RtSimController { impl RtSimController {
pub fn with_destination(pos: Vec3<f32>) -> Self { pub fn with_destination(pos: Vec3<f32>) -> Self {
Self { Self {
travel_to: Some(pos), activity: Some(NpcActivity::Goto(pos, 0.5)),
personality: Personality::default(), ..Default::default()
heading_to: None,
speed_factor: 0.5,
actions: VecDeque::new(),
} }
} }
} }
#[derive(Clone, Copy, Debug)]
pub enum NpcActivity {
/// (travel_to, speed_factor)
Goto(Vec3<f32>, f32),
Gather(&'static [ChunkResource]),
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum NpcAction { pub enum NpcAction {
Greet(Actor), Greet(Actor),

View File

@ -86,6 +86,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// // Walk toward an enemy NPC and, once done, attack the enemy NPC /// // Walk toward an enemy NPC and, once done, attack the enemy NPC
/// goto(enemy_npc).then(attack(enemy_npc)) /// 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<R1>, R1>(self, other: A1) -> Then<Self, A1, R>
where where
Self: Sized, Self: Sized,
@ -106,6 +107,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// // Endlessly collect flax from the environment /// // Endlessly collect flax from the environment
/// find_and_collect(ChunkResource::Flax).repeat() /// find_and_collect(ChunkResource::Flax).repeat()
/// ``` /// ```
#[must_use]
fn repeat<R1>(self) -> Repeat<Self, R1> fn repeat<R1>(self) -> Repeat<Self, R1>
where where
Self: Sized, Self: Sized,
@ -121,6 +123,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// // Keep going on adventures until your 111th birthday /// // Keep going on adventures until your 111th birthday
/// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0) /// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0)
/// ``` /// ```
#[must_use]
fn stop_if<F: FnMut(&mut NpcCtx) -> bool>(self, f: F) -> StopIf<Self, F> fn stop_if<F: FnMut(&mut NpcCtx) -> bool>(self, f: F) -> StopIf<Self, F>
where where
Self: Sized, Self: Sized,
@ -129,6 +132,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
} }
/// Map the completion value of this action to something else. /// 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: FnMut(R) -> R1, R1>(self, f: F) -> Map<Self, F, R>
where where
Self: Sized, Self: Sized,
@ -157,6 +161,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// go_on_an_adventure().boxed() /// go_on_an_adventure().boxed()
/// } /// }
/// ``` /// ```
#[must_use]
fn boxed(self) -> Box<dyn Action<R>> fn boxed(self) -> Box<dyn Action<R>>
where where
Self: Sized, Self: Sized,
@ -172,6 +177,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// ```ignore /// ```ignore
/// goto(npc.home).debug(|| "Going home") /// goto(npc.home).debug(|| "Going home")
/// ``` /// ```
#[must_use]
fn debug<F, T>(self, mk_info: F) -> Debug<Self, F, T> fn debug<F, T>(self, mk_info: F) -> Debug<Self, F, T>
where where
Self: Sized, Self: Sized,
@ -412,6 +418,7 @@ impl Action<()> for Finish {
/// } /// }
/// }) /// })
/// ``` /// ```
#[must_use]
pub fn finish() -> Finish { Finish } pub fn finish() -> Finish { Finish }
// Tree // Tree
@ -425,12 +432,15 @@ pub const CASUAL: Priority = 2;
pub struct Node<R>(Box<dyn Action<R>>, Priority); pub struct Node<R>(Box<dyn Action<R>>, Priority);
/// Perform an action with [`URGENT`] priority (see [`choose`]). /// 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<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), URGENT) }
/// Perform an action with [`IMPORTANT`] priority (see [`choose`]). /// 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<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), IMPORTANT) }
/// Perform an action with [`CASUAL`] priority (see [`choose`]). /// 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<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), CASUAL) }
/// See [`choose`] and [`watch`]. /// See [`choose`] and [`watch`].
@ -501,6 +511,7 @@ 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<R: 'static, F>(f: F) -> impl Action<R>
where where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
@ -535,6 +546,7 @@ where
/// } /// }
/// }) /// })
/// ``` /// ```
#[must_use]
pub fn watch<R: 'static, F>(f: F) -> impl Action<R> pub fn watch<R: 'static, F>(f: F) -> impl Action<R>
where where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
@ -679,6 +691,7 @@ impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'st
/// .into_iter() /// .into_iter()
/// .map(|enemy| attack(enemy))) /// .map(|enemy| attack(enemy)))
/// ``` /// ```
#[must_use]
pub fn seq<I, A, R>(iter: I) -> Sequence<I, A, R> pub fn seq<I, A, R>(iter: I) -> Sequence<I, A, R>
where where
I: Iterator<Item = A> + Clone, I: Iterator<Item = A> + Clone,

View File

@ -3,7 +3,9 @@ pub use common::rtsim::{NpcId, Profession};
use common::{ use common::{
comp, comp,
grid::Grid, grid::Grid,
rtsim::{Actor, FactionId, NpcAction, Personality, SiteId, VehicleId}, rtsim::{
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId,
},
store::Id, store::Id,
vol::RectVolSize, vol::RectVolSize,
}; };
@ -46,15 +48,18 @@ pub struct PathingMemory {
#[derive(Default)] #[derive(Default)]
pub struct Controller { pub struct Controller {
pub actions: Vec<NpcAction>, pub actions: Vec<NpcAction>,
/// (wpos, speed_factor) pub activity: Option<NpcActivity>,
pub goto: Option<(Vec3<f32>, f32)>,
} }
impl Controller { impl Controller {
pub fn do_idle(&mut self) { self.goto = None; } pub fn do_idle(&mut self) { self.activity = None; }
pub fn do_goto(&mut self, wpos: Vec3<f32>, speed_factor: f32) { pub fn do_goto(&mut self, wpos: Vec3<f32>, speed_factor: f32) {
self.goto = Some((wpos, speed_factor)); self.activity = Some(NpcActivity::Goto(wpos, speed_factor));
}
pub fn do_gather(&mut self, resources: &'static [ChunkResource]) {
self.activity = Some(NpcActivity::Gather(resources));
} }
pub fn do_greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } pub fn do_greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); }

View File

@ -12,7 +12,8 @@ use crate::{
use common::{ use common::{
astar::{Astar, PathResult}, astar::{Astar, PathResult},
path::Path, path::Path,
rtsim::{Profession, SiteId}, rtsim::{ChunkResource, Profession, SiteId},
spiral::Spiral2d,
store::Id, store::Id,
terrain::{SiteKindMeta, TerrainChunkSize}, terrain::{SiteKindMeta, TerrainChunkSize},
time::DayPeriod, time::DayPeriod,
@ -514,8 +515,22 @@ fn adventure() -> impl Action {
.debug(move || "adventure") .debug(move || "adventure")
} }
fn gather_ingredients() -> impl Action {
just(|ctx| {
ctx.controller.do_gather(
&[
ChunkResource::Fruit,
ChunkResource::Mushroom,
ChunkResource::Plant,
][..],
)
})
.debug(|| "gather ingredients")
}
fn villager(visiting_site: SiteId) -> impl Action { fn villager(visiting_site: SiteId) -> impl Action {
choose(move |ctx| { choose(move |ctx| {
/*
if ctx if ctx
.state .state
.data() .data()
@ -523,23 +538,24 @@ fn villager(visiting_site: SiteId) -> impl Action {
.get(visiting_site) .get(visiting_site)
.map_or(true, |s| s.world_site.is_none()) .map_or(true, |s| s.world_site.is_none())
{ {
casual( return casual(idle()
idle().debug(|| "idling (visiting site does not exist, perhaps it's stale data?)"), .debug(|| "idling (visiting site does not exist, perhaps it's stale data?)"));
)
} else if ctx.npc.current_site != Some(visiting_site) { } else if ctx.npc.current_site != Some(visiting_site) {
let npc_home = ctx.npc.home; let npc_home = ctx.npc.home;
// Travel to the site we're supposed to be in // Travel to the site we're supposed to be in
urgent(travel_to_site(visiting_site).debug(move || { return urgent(travel_to_site(visiting_site).debug(move || {
if npc_home == Some(visiting_site) { if npc_home == Some(visiting_site) {
"travel home".to_string() "travel home".to_string()
} else { } else {
"travel to visiting site".to_string() "travel to visiting site".to_string()
} }
})) }));
} else if DayPeriod::from(ctx.time_of_day.0).is_dark() } else
*/
if DayPeriod::from(ctx.time_of_day.0).is_dark()
&& !matches!(ctx.npc.profession, Some(Profession::Guard)) && !matches!(ctx.npc.profession, Some(Profession::Guard))
{ {
important( return important(
now(move |ctx| { now(move |ctx| {
if let Some(house_wpos) = ctx if let Some(house_wpos) = ctx
.state .state
@ -567,38 +583,63 @@ fn villager(visiting_site: SiteId) -> impl Action {
} }
}) })
.debug(|| "find somewhere to sleep"), .debug(|| "find somewhere to sleep"),
) );
} else { // Villagers with roles should perform those roles
casual(now(move |ctx| { } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) {
// Choose a plaza in the site we're visiting to walk to let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_();
if let Some(plaza_wpos) = ctx if let Some(tree_chunk) = Spiral2d::new()
.state .skip(thread_rng().gen_range(1..=8))
.data() .take(49)
.sites .map(|rpos| chunk_pos + rpos)
.get(visiting_site) .find(|cpos| {
.and_then(|site| ctx.index.sites.get(site.world_site?).site2()) ctx.world
.and_then(|site2| { .sim()
let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; .get(*cpos)
Some(site2.tile_center_wpos(plaza.root_tile()).as_()) .map_or(false, |c| c.tree_density > 0.75)
}) })
{ {
// Walk to the plaza... return important(
travel_to_point(plaza_wpos) travel_to_point(TerrainChunkSize::center_wpos(tree_chunk).as_())
.debug(|| "walk to plaza") .debug(|| "walk to forest")
// ...then wait for some time before moving on
.then({ .then({
let wait_time = thread_rng().gen_range(10.0..30.0); let wait_time = thread_rng().gen_range(10.0..30.0);
socialize().repeat().stop_if(timeout(wait_time)) gather_ingredients().repeat().stop_if(timeout(wait_time))
.debug(|| "wait at plaza")
}) })
.map(|_| ()) .map(|_| ()),
.boxed() );
} else { }
// No plazas? :(
finish().boxed()
}
}))
} }
// If nothing else needs doing, walk between plazas and socialize
casual(now(move |ctx| {
// Choose a plaza in the site we're visiting to walk to
if let Some(plaza_wpos) = ctx
.state
.data()
.sites
.get(visiting_site)
.and_then(|site| ctx.index.sites.get(site.world_site?).site2())
.and_then(|site2| {
let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?];
Some(site2.tile_center_wpos(plaza.root_tile()).as_())
})
{
// Walk to the plaza...
travel_to_point(plaza_wpos)
.debug(|| "walk to plaza")
// ...then wait for some time before moving on
.then({
let wait_time = thread_rng().gen_range(30.0..90.0);
socialize().repeat().stop_if(timeout(wait_time))
.debug(|| "wait at plaza")
})
.map(|_| ())
.boxed()
} else {
// No plazas? :(
finish().boxed()
}
}))
}) })
.debug(move || format!("villager at site {:?}", visiting_site)) .debug(move || format!("villager at site {:?}", visiting_site))
} }

View File

@ -6,7 +6,7 @@ use crate::{
use common::{ use common::{
comp::{self, Body}, comp::{self, Body},
grid::Grid, grid::Grid,
rtsim::{Actor, NpcAction, Personality}, rtsim::{Actor, NpcAction, NpcActivity, Personality},
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
vol::RectVolSize, vol::RectVolSize,
}; };
@ -179,13 +179,14 @@ impl Rule for SimulateNpcs {
// Simulate the NPC's movement and interactions // Simulate the NPC's movement and interactions
if matches!(npc.mode, SimulationMode::Simulated) { if matches!(npc.mode, SimulationMode::Simulated) {
// Move NPCs if they have a target destination // Simulate NPC movement when riding
if let Some((target, speed_factor)) = npc.controller.goto { if let Some(riding) = &npc.riding {
// Simulate NPC movement when riding if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
if let Some(riding) = &npc.riding { match npc.controller.activity {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
// If steering, the NPC controls the vehicle's motion // If steering, the NPC controls the vehicle's motion
if riding.steering { Some(NpcActivity::Goto(target, speed_factor))
if riding.steering =>
{
let diff = target.xy() - vehicle.wpos.xy(); let diff = target.xy() - vehicle.wpos.xy();
let dist2 = diff.magnitude_squared(); let dist2 = diff.magnitude_squared();
@ -243,24 +244,39 @@ impl Rule for SimulateNpcs {
vehicle.wpos = wpos; vehicle.wpos = wpos;
} }
} }
} },
npc.wpos = vehicle.wpos; // When riding, other actions are disabled
} else { Some(NpcActivity::Goto(_, _) | NpcActivity::Gather(_)) => {},
// Vehicle doens't exist anymore None => {},
npc.riding = None;
} }
// If not riding, we assume they're just walking npc.wpos = vehicle.wpos;
} else { } else {
let diff = target.xy() - npc.wpos.xy(); // Vehicle doens't exist anymore
let dist2 = diff.magnitude_squared(); npc.riding = None;
}
// If not riding, we assume they're just walking
} else {
match npc.controller.activity {
// Move NPCs if they have a target destination
Some(NpcActivity::Goto(target, speed_factor)) => {
let diff = target.xy() - npc.wpos.xy();
let dist2 = diff.magnitude_squared();
if dist2 > 0.5f32.powi(2) { if dist2 > 0.5f32.powi(2) {
npc.wpos += (diff npc.wpos += (diff
* (npc.body.max_speed_approx() * speed_factor * ctx.event.dt * (npc.body.max_speed_approx()
/ dist2.sqrt()) * speed_factor
.min(1.0)) * ctx.event.dt
.with_z(0.0); / dist2.sqrt())
} .min(1.0))
.with_z(0.0);
}
},
Some(NpcActivity::Gather(_)) => {
// TODO: Maybe they should walk around randomly
// when gathering resources?
},
None => {},
} }
} }

View File

@ -28,6 +28,7 @@ use common::{
effect::{BuffEffect, Effect}, effect::{BuffEffect, Effect},
event::{Emitter, ServerEvent}, event::{Emitter, ServerEvent},
path::TraversalConfig, path::TraversalConfig,
rtsim::NpcActivity,
states::basic_beam, states::basic_beam,
terrain::{Block, TerrainGrid}, terrain::{Block, TerrainGrid},
time::DayPeriod, time::DayPeriod,
@ -212,118 +213,230 @@ impl<'a> AgentData<'a> {
} }
agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0; agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
if let Some(travel_to) = &agent.rtsim_controller.travel_to { match agent.rtsim_controller.activity {
// If it has an rtsim destination and can fly, then it should. Some(NpcActivity::Goto(travel_to, speed_factor)) => {
// If it is flying and bumps something above it, then it should move down. // If it has an rtsim destination and can fly, then it should.
if self.traversal_config.can_fly // If it is flying and bumps something above it, then it should move down.
&& !read_data if self.traversal_config.can_fly
&& !read_data
.terrain
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0))
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some())
{
controller.push_basic_input(InputKind::Fly);
} else {
controller.push_cancel_input(InputKind::Fly)
}
let chase_tgt = read_data
.terrain .terrain
.ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0)) .try_find_space(travel_to.as_())
.until(Block::is_solid) .map(|pos| pos.as_())
.cast() .unwrap_or(travel_to);
.1
.map_or(true, |b| b.is_some())
{
controller.push_basic_input(InputKind::Fly);
} else {
controller.push_cancel_input(InputKind::Fly)
}
let chase_tgt = read_data if let Some((bearing, speed)) = agent.chaser.chase(
.terrain &*read_data.terrain,
.try_find_space(travel_to.as_()) self.pos.0,
.map(|pos| pos.as_()) self.vel.0,
.unwrap_or(*travel_to); chase_tgt,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config
},
) {
controller.inputs.move_dir =
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero)
* speed.min(speed_factor);
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller);
controller.inputs.climb = Some(comp::Climb::Up);
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some());
if let Some((bearing, speed)) = agent.chaser.chase( let height_offset = bearing.z
&*read_data.terrain, + if self.traversal_config.can_fly {
self.pos.0, // NOTE: costs 4 us (imbris)
self.vel.0, let obstacle_ahead = read_data
chase_tgt, .terrain
TraversalConfig { .ray(
min_tgt_dist: 1.25, self.pos.0 + Vec3::unit_z(),
..self.traversal_config self.pos.0
}, + bearing.try_normalized().unwrap_or_else(Vec3::unit_y)
) { * 80.0
controller.inputs.move_dir = + Vec3::unit_z(),
bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) )
* speed.min(agent.rtsim_controller.speed_factor); .until(Block::is_solid)
self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller); .cast()
controller.inputs.climb = Some(comp::Climb::Up); .1
//.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some()); .map_or(true, |b| b.is_some());
let height_offset = bearing.z let mut ground_too_close = self
+ if self.traversal_config.can_fly { .body
// NOTE: costs 4 us (imbris) .map(|body| {
let obstacle_ahead = read_data #[cfg(feature = "worldgen")]
let height_approx = self.pos.0.z
- read_data
.world
.sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.z;
height_approx < body.flying_height()
})
.unwrap_or(false);
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to detect a
// wedge of obstacles we might fly into (inclusive so that both
// vectors are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
if obstacle_ahead || ground_too_close {
5.0 //fly up when approaching obstacles
} else {
-2.0
} //flying things should slowly come down from the stratosphere
} else {
0.05 //normal land traveller offset
};
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.sp = self.pos.0.z + height_offset * Vec3::unit_z();
controller.inputs.move_z = pid.calc_err();
} else {
controller.inputs.move_z = height_offset;
}
// Put away weapon
if rng.gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding(_))
)
{
controller.push_action(ControlAction::Unwield);
}
}
},
Some(NpcActivity::Gather(resources)) => {
// TODO: Implement
controller.push_action(ControlAction::Dance);
},
None => {
// Bats should fly
// Use a proportional controller as the bouncing effect mimics bat flight
if self.traversal_config.can_fly
&& self
.inventory
.equipped(EquipSlot::ActiveMainhand)
.as_ref()
.map_or(false, |item| {
item.ability_spec().map_or(false, |a_s| match &*a_s {
AbilitySpec::Custom(spec) => {
matches!(
spec.as_str(),
"Simple Flying Melee"
| "Flame Wyvern"
| "Frost Wyvern"
| "Cloud Wyvern"
| "Sea Wyvern"
| "Weald Wyvern"
)
},
_ => false,
})
})
{
// Bats don't like the ground, so make sure they are always flying
controller.push_basic_input(InputKind::Fly);
// Use a proportional controller with a coefficient of 1.0 to
// maintain altitude
let alt = read_data
.terrain
.ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
.until(Block::is_solid)
.cast()
.0;
let set_point = 5.0;
let error = set_point - alt;
controller.inputs.move_z = error;
// If on the ground, jump
if self.physics_state.on_ground.is_some() {
controller.push_basic_input(InputKind::Jump);
}
}
agent.bearing += Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5) * 0.1
- agent.bearing * 0.003
- agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| {
(self.pos.0 - patrol_origin).xy() * 0.0002
});
// Stop if we're too close to a wall
// or about to walk off a cliff
// NOTE: costs 1 us (imbris) <- before cliff raycast added
agent.bearing *= 0.1
+ if read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
* 5.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_none())
&& read_data
.terrain .terrain
.ray( .ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0 self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0 + Vec3::from(agent.bearing)
+ Vec3::unit_z(), .try_normalized()
.unwrap_or_else(Vec3::unit_y),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
- Vec3::unit_z() * 4.0,
) )
.until(Block::is_solid) .until(Block::is_solid)
.cast() .cast()
.1 .0
.map_or(true, |b| b.is_some()); < 3.0
{
let mut ground_too_close = self 0.9
.body
.map(|body| {
#[cfg(feature = "worldgen")]
let height_approx = self.pos.0.z
- read_data
.world
.sim()
.get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32))
.unwrap_or(0.0);
#[cfg(not(feature = "worldgen"))]
let height_approx = self.pos.0.z;
height_approx < body.flying_height()
})
.unwrap_or(false);
const NUM_RAYS: usize = 5;
// NOTE: costs 15-20 us (imbris)
for i in 0..=NUM_RAYS {
let magnitude = self.body.map_or(20.0, |b| b.flying_height());
// Lerp between a line straight ahead and straight down to detect a
// wedge of obstacles we might fly into (inclusive so that both vectors
// are sampled)
if let Some(dir) = Lerp::lerp(
-Vec3::unit_z(),
Vec3::new(bearing.x, bearing.y, 0.0),
i as f32 / NUM_RAYS as f32,
)
.try_normalized()
{
ground_too_close |= read_data
.terrain
.ray(self.pos.0, self.pos.0 + magnitude * dir)
.until(|b: &Block| b.is_solid() || b.is_liquid())
.cast()
.1
.map_or(false, |b| b.is_some())
}
}
if obstacle_ahead || ground_too_close {
5.0 //fly up when approaching obstacles
} else {
-2.0
} //flying things should slowly come down from the stratosphere
} else { } else {
0.05 //normal land traveller offset 0.0
}; };
if let Some(pid) = agent.position_pid_controller.as_mut() {
pid.sp = self.pos.0.z + height_offset * Vec3::unit_z(); if agent.bearing.magnitude_squared() > 0.5f32.powi(2) {
controller.inputs.move_z = pid.calc_err(); controller.inputs.move_dir = agent.bearing * 0.65;
} else {
controller.inputs.move_z = height_offset;
} }
// Put away weapon // Put away weapon
if rng.gen_bool(0.1) if rng.gen_bool(0.1)
&& matches!( && matches!(
@ -333,120 +446,16 @@ impl<'a> AgentData<'a> {
{ {
controller.push_action(ControlAction::Unwield); controller.push_action(ControlAction::Unwield);
} }
}
} else { if rng.gen::<f32>() < 0.0015 {
// Bats should fly controller.push_utterance(UtteranceKind::Calm);
// Use a proportional controller as the bouncing effect mimics bat flight
if self.traversal_config.can_fly
&& self
.inventory
.equipped(EquipSlot::ActiveMainhand)
.as_ref()
.map_or(false, |item| {
item.ability_spec().map_or(false, |a_s| match &*a_s {
AbilitySpec::Custom(spec) => {
matches!(
spec.as_str(),
"Simple Flying Melee"
| "Flame Wyvern"
| "Frost Wyvern"
| "Cloud Wyvern"
| "Sea Wyvern"
| "Weald Wyvern"
)
},
_ => false,
})
})
{
// Bats don't like the ground, so make sure they are always flying
controller.push_basic_input(InputKind::Fly);
// Use a proportional controller with a coefficient of 1.0 to
// maintain altitude
let alt = read_data
.terrain
.ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
.until(Block::is_solid)
.cast()
.0;
let set_point = 5.0;
let error = set_point - alt;
controller.inputs.move_z = error;
// If on the ground, jump
if self.physics_state.on_ground.is_some() {
controller.push_basic_input(InputKind::Jump);
} }
}
agent.bearing += Vec2::new(rng.gen::<f32>() - 0.5, rng.gen::<f32>() - 0.5) * 0.1
- agent.bearing * 0.003
- agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| {
(self.pos.0 - patrol_origin).xy() * 0.0002
});
// Stop if we're too close to a wall // Sit
// or about to walk off a cliff if rng.gen::<f32>() < 0.0035 {
// NOTE: costs 1 us (imbris) <- before cliff raycast added controller.push_action(ControlAction::Sit);
agent.bearing *= 0.1 }
+ if read_data },
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
* 5.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_none())
&& read_data
.terrain
.ray(
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y),
self.pos.0
+ Vec3::from(agent.bearing)
.try_normalized()
.unwrap_or_else(Vec3::unit_y)
- Vec3::unit_z() * 4.0,
)
.until(Block::is_solid)
.cast()
.0
< 3.0
{
0.9
} else {
0.0
};
if agent.bearing.magnitude_squared() > 0.5f32.powi(2) {
controller.inputs.move_dir = agent.bearing * 0.65;
}
// Put away weapon
if rng.gen_bool(0.1)
&& matches!(
read_data.char_states.get(*self.entity),
Some(CharacterState::Wielding(_))
)
{
controller.push_action(ControlAction::Unwield);
}
if rng.gen::<f32>() < 0.0015 {
controller.push_utterance(UtteranceKind::Calm);
}
// Sit
if rng.gen::<f32>() < 0.0035 {
controller.push_action(ControlAction::Sit);
}
} }
} }

View File

@ -185,6 +185,7 @@ fn do_command(
ServerChatCommand::Tp => handle_tp, ServerChatCommand::Tp => handle_tp,
ServerChatCommand::RtsimTp => handle_rtsim_tp, ServerChatCommand::RtsimTp => handle_rtsim_tp,
ServerChatCommand::RtsimInfo => handle_rtsim_info, ServerChatCommand::RtsimInfo => handle_rtsim_info,
ServerChatCommand::RtsimNpc => handle_rtsim_npc,
ServerChatCommand::RtsimPurge => handle_rtsim_purge, ServerChatCommand::RtsimPurge => handle_rtsim_purge,
ServerChatCommand::RtsimChunk => handle_rtsim_chunk, ServerChatCommand::RtsimChunk => handle_rtsim_chunk,
ServerChatCommand::Unban => handle_unban, ServerChatCommand::Unban => handle_unban,
@ -1263,6 +1264,61 @@ fn handle_rtsim_info(
} }
} }
fn handle_rtsim_npc(
server: &mut Server,
client: EcsEntity,
_target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
use crate::rtsim::RtSim;
if let Some(query) = parse_cmd_args!(args, String) {
let terms = query
.split(',')
.map(|s| s.trim().to_lowercase())
.collect::<Vec<_>>();
let rtsim = server.state.ecs().read_resource::<RtSim>();
let data = rtsim.state().data();
let npcs = data
.npcs
.values()
.enumerate()
.filter(|(idx, npc)| {
let tags = [
npc.profession
.as_ref()
.map(|p| format!("{:?}", p))
.unwrap_or_default(),
format!("{:?}", npc.mode),
format!("{}", idx),
];
terms
.iter()
.all(|term| tags.iter().any(|tag| term.eq_ignore_ascii_case(tag.trim())))
})
.collect::<Vec<_>>();
let mut info = String::new();
let _ = writeln!(&mut info, "-- NPCs matching [{}] --", terms.join(", "));
for (idx, _) in &npcs {
let _ = write!(&mut info, "{}, ", idx);
}
let _ = writeln!(&mut info, "");
let _ = writeln!(&mut info, "Matched {} NPCs.", npcs.len());
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, info),
);
Ok(())
} else {
Err(action.help_string())
}
}
fn handle_rtsim_purge( fn handle_rtsim_purge(
server: &mut Server, server: &mut Server,
client: EcsEntity, client: EcsEntity,

View File

@ -366,13 +366,7 @@ impl<'a> System<'a> for Sys {
// Update entity state // Update entity state
if let Some(agent) = agent { if let Some(agent) = agent {
agent.rtsim_controller.personality = npc.personality; agent.rtsim_controller.personality = npc.personality;
if let Some((wpos, speed_factor)) = npc.controller.goto { agent.rtsim_controller.activity = npc.controller.activity;
agent.rtsim_controller.travel_to = Some(wpos);
agent.rtsim_controller.speed_factor = speed_factor;
} else {
agent.rtsim_controller.travel_to = None;
agent.rtsim_controller.speed_factor = 1.0;
}
agent agent
.rtsim_controller .rtsim_controller
.actions .actions

View File

@ -495,6 +495,7 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
bdata bdata
.agent_data .agent_data
.chat_npc("npc-speech-villager", &mut bdata.event_emitter); .chat_npc("npc-speech-villager", &mut bdata.event_emitter);
// Start a timer so that they eventually stop interacting
bdata bdata
.agent .agent
.timer .timer