mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Added rtsim_npc, made herbalists gather ingredients
This commit is contained in:
parent
1e70ccfb8d
commit
b402e450cf
@ -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",
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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)); }
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user