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,
Time,
Tp,
RtsimTp,
RtsimInfo,
RtsimPurge,
RtsimChunk,
RtsimInfo,
RtsimNpc,
RtsimPurge,
RtsimTp,
Unban,
Version,
Waypoint,
@ -693,6 +694,11 @@ impl ServerChatCommand {
"Display information about an rtsim NPC",
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(
vec![Boolean(
"whether purging of rtsim data should occur on next startup",
@ -836,6 +842,7 @@ impl ServerChatCommand {
ServerChatCommand::Tp => "tp",
ServerChatCommand::RtsimTp => "rtsim_tp",
ServerChatCommand::RtsimInfo => "rtsim_info",
ServerChatCommand::RtsimNpc => "rtsim_npc",
ServerChatCommand::RtsimPurge => "rtsim_purge",
ServerChatCommand::RtsimChunk => "rtsim_chunk",
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
/// upon its instructions where reasonable although deviations for various
/// reasons (obstacle avoidance, counter-attacking, etc.) are expected.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct RtSimController {
/// When this field is `Some(..)`, the agent should attempt to make progress
/// toward the given location, accounting for obstacles and other
/// high-priority situations like being attacked.
pub travel_to: Option<Vec3<f32>>,
pub activity: Option<NpcActivity>,
pub actions: VecDeque<NpcAction>,
pub personality: Personality,
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 {
pub fn with_destination(pos: Vec3<f32>) -> Self {
Self {
travel_to: Some(pos),
personality: Personality::default(),
heading_to: None,
speed_factor: 0.5,
actions: VecDeque::new(),
activity: Some(NpcActivity::Goto(pos, 0.5)),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum NpcActivity {
/// (travel_to, speed_factor)
Goto(Vec3<f32>, f32),
Gather(&'static [ChunkResource]),
}
#[derive(Clone, Copy, Debug)]
pub enum NpcAction {
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
/// goto(enemy_npc).then(attack(enemy_npc))
/// ```
#[must_use]
fn then<A1: Action<R1>, R1>(self, other: A1) -> Then<Self, A1, R>
where
Self: Sized,
@ -106,6 +107,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// // Endlessly collect flax from the environment
/// find_and_collect(ChunkResource::Flax).repeat()
/// ```
#[must_use]
fn repeat<R1>(self) -> Repeat<Self, R1>
where
Self: Sized,
@ -121,6 +123,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// // Keep going on adventures until your 111th birthday
/// 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>
where
Self: Sized,
@ -129,6 +132,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>
where
Self: Sized,
@ -157,6 +161,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// go_on_an_adventure().boxed()
/// }
/// ```
#[must_use]
fn boxed(self) -> Box<dyn Action<R>>
where
Self: Sized,
@ -172,6 +177,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// ```ignore
/// goto(npc.home).debug(|| "Going home")
/// ```
#[must_use]
fn debug<F, T>(self, mk_info: F) -> Debug<Self, F, T>
where
Self: Sized,
@ -412,6 +418,7 @@ impl Action<()> for Finish {
/// }
/// })
/// ```
#[must_use]
pub fn finish() -> Finish { Finish }
// Tree
@ -425,12 +432,15 @@ pub const CASUAL: Priority = 2;
pub struct Node<R>(Box<dyn Action<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) }
/// 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) }
/// 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) }
/// 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>
where
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>
where
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()
/// .map(|enemy| attack(enemy)))
/// ```
#[must_use]
pub fn seq<I, A, R>(iter: I) -> Sequence<I, A, R>
where
I: Iterator<Item = A> + Clone,

View File

@ -3,7 +3,9 @@ pub use common::rtsim::{NpcId, Profession};
use common::{
comp,
grid::Grid,
rtsim::{Actor, FactionId, NpcAction, Personality, SiteId, VehicleId},
rtsim::{
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId,
},
store::Id,
vol::RectVolSize,
};
@ -46,15 +48,18 @@ pub struct PathingMemory {
#[derive(Default)]
pub struct Controller {
pub actions: Vec<NpcAction>,
/// (wpos, speed_factor)
pub goto: Option<(Vec3<f32>, f32)>,
pub activity: Option<NpcActivity>,
}
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) {
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)); }

View File

@ -12,7 +12,8 @@ use crate::{
use common::{
astar::{Astar, PathResult},
path::Path,
rtsim::{Profession, SiteId},
rtsim::{ChunkResource, Profession, SiteId},
spiral::Spiral2d,
store::Id,
terrain::{SiteKindMeta, TerrainChunkSize},
time::DayPeriod,
@ -514,8 +515,22 @@ fn adventure() -> impl Action {
.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 {
choose(move |ctx| {
/*
if ctx
.state
.data()
@ -523,23 +538,24 @@ fn villager(visiting_site: SiteId) -> impl Action {
.get(visiting_site)
.map_or(true, |s| s.world_site.is_none())
{
casual(
idle().debug(|| "idling (visiting site does not exist, perhaps it's stale data?)"),
)
return casual(idle()
.debug(|| "idling (visiting site does not exist, perhaps it's stale data?)"));
} else if ctx.npc.current_site != Some(visiting_site) {
let npc_home = ctx.npc.home;
// 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) {
"travel home".to_string()
} else {
"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))
{
important(
return important(
now(move |ctx| {
if let Some(house_wpos) = ctx
.state
@ -567,38 +583,63 @@ fn villager(visiting_site: SiteId) -> impl Action {
}
})
.debug(|| "find somewhere to sleep"),
)
} else {
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
);
// Villagers with roles should perform those roles
} else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) {
let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_();
if let Some(tree_chunk) = Spiral2d::new()
.skip(thread_rng().gen_range(1..=8))
.take(49)
.map(|rpos| chunk_pos + rpos)
.find(|cpos| {
ctx.world
.sim()
.get(*cpos)
.map_or(false, |c| c.tree_density > 0.75)
})
{
return important(
travel_to_point(TerrainChunkSize::center_wpos(tree_chunk).as_())
.debug(|| "walk to forest")
.then({
let wait_time = thread_rng().gen_range(10.0..30.0);
socialize().repeat().stop_if(timeout(wait_time))
.debug(|| "wait at plaza")
gather_ingredients().repeat().stop_if(timeout(wait_time))
})
.map(|_| ())
.boxed()
} else {
// No plazas? :(
finish().boxed()
}
}))
.map(|_| ()),
);
}
}
// 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))
}

View File

@ -6,7 +6,7 @@ use crate::{
use common::{
comp::{self, Body},
grid::Grid,
rtsim::{Actor, NpcAction, Personality},
rtsim::{Actor, NpcAction, NpcActivity, Personality},
terrain::TerrainChunkSize,
vol::RectVolSize,
};
@ -179,13 +179,14 @@ impl Rule for SimulateNpcs {
// Simulate the NPC's movement and interactions
if matches!(npc.mode, SimulationMode::Simulated) {
// Move NPCs if they have a target destination
if let Some((target, speed_factor)) = npc.controller.goto {
// Simulate NPC movement when riding
if let Some(riding) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
// Simulate NPC movement when riding
if let Some(riding) = &npc.riding {
if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) {
match npc.controller.activity {
// 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 dist2 = diff.magnitude_squared();
@ -243,24 +244,39 @@ impl Rule for SimulateNpcs {
vehicle.wpos = wpos;
}
}
}
npc.wpos = vehicle.wpos;
} else {
// Vehicle doens't exist anymore
npc.riding = None;
},
// When riding, other actions are disabled
Some(NpcActivity::Goto(_, _) | NpcActivity::Gather(_)) => {},
None => {},
}
// If not riding, we assume they're just walking
npc.wpos = vehicle.wpos;
} else {
let diff = target.xy() - npc.wpos.xy();
let dist2 = diff.magnitude_squared();
// Vehicle doens't exist anymore
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) {
npc.wpos += (diff
* (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
/ dist2.sqrt())
.min(1.0))
.with_z(0.0);
}
if dist2 > 0.5f32.powi(2) {
npc.wpos += (diff
* (npc.body.max_speed_approx()
* speed_factor
* ctx.event.dt
/ 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},
event::{Emitter, ServerEvent},
path::TraversalConfig,
rtsim::NpcActivity,
states::basic_beam,
terrain::{Block, TerrainGrid},
time::DayPeriod,
@ -212,118 +213,230 @@ impl<'a> AgentData<'a> {
}
agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0;
if let Some(travel_to) = &agent.rtsim_controller.travel_to {
// If it has an rtsim destination and can fly, then it should.
// If it is flying and bumps something above it, then it should move down.
if self.traversal_config.can_fly
&& !read_data
match agent.rtsim_controller.activity {
Some(NpcActivity::Goto(travel_to, speed_factor)) => {
// If it has an rtsim destination and can fly, then it should.
// If it is flying and bumps something above it, then it should move down.
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
.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)
}
.try_find_space(travel_to.as_())
.map(|pos| pos.as_())
.unwrap_or(travel_to);
let chase_tgt = read_data
.terrain
.try_find_space(travel_to.as_())
.map(|pos| pos.as_())
.unwrap_or(*travel_to);
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
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(
&*read_data.terrain,
self.pos.0,
self.vel.0,
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(agent.rtsim_controller.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());
let height_offset = bearing.z
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
.terrain
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y)
* 80.0
+ Vec3::unit_z(),
)
.until(Block::is_solid)
.cast()
.1
.map_or(true, |b| b.is_some());
let height_offset = bearing.z
+ if self.traversal_config.can_fly {
// NOTE: costs 4 us (imbris)
let obstacle_ahead = read_data
let mut ground_too_close = self
.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 {
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
.ray(
self.pos.0 + Vec3::unit_z(),
self.pos.0
+ bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0
+ Vec3::unit_z(),
+ 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()
.1
.map_or(true, |b| b.is_some());
let mut ground_too_close = self
.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
.0
< 3.0
{
0.9
} 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();
controller.inputs.move_z = pid.calc_err();
} else {
controller.inputs.move_z = height_offset;
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!(
@ -333,120 +446,16 @@ impl<'a> AgentData<'a> {
{
controller.push_action(ControlAction::Unwield);
}
}
} else {
// 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);
if rng.gen::<f32>() < 0.0015 {
controller.push_utterance(UtteranceKind::Calm);
}
}
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
.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);
}
// 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::RtsimTp => handle_rtsim_tp,
ServerChatCommand::RtsimInfo => handle_rtsim_info,
ServerChatCommand::RtsimNpc => handle_rtsim_npc,
ServerChatCommand::RtsimPurge => handle_rtsim_purge,
ServerChatCommand::RtsimChunk => handle_rtsim_chunk,
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(
server: &mut Server,
client: EcsEntity,

View File

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

View File

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