diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 270b60377a..ef152d7334 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -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", diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 6b04ff8e14..0d58760f19 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -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>, + pub activity: Option, + pub actions: VecDeque, pub personality: Personality, pub heading_to: Option, - /// Proportion of full speed to move - pub speed_factor: f32, - pub actions: VecDeque, -} - -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) -> 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), + Gather(&'static [ChunkResource]), +} + #[derive(Clone, Copy, Debug)] pub enum NpcAction { Greet(Actor), diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 2d27002298..3de71dc85d 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -86,6 +86,7 @@ pub trait Action: 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, R1>(self, other: A1) -> Then where Self: Sized, @@ -106,6 +107,7 @@ pub trait Action: Any + Send + Sync { /// // Endlessly collect flax from the environment /// find_and_collect(ChunkResource::Flax).repeat() /// ``` + #[must_use] fn repeat(self) -> Repeat where Self: Sized, @@ -121,6 +123,7 @@ pub trait Action: 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 bool>(self, f: F) -> StopIf where Self: Sized, @@ -129,6 +132,7 @@ pub trait Action: Any + Send + Sync { } /// Map the completion value of this action to something else. + #[must_use] fn map R1, R1>(self, f: F) -> Map where Self: Sized, @@ -157,6 +161,7 @@ pub trait Action: Any + Send + Sync { /// go_on_an_adventure().boxed() /// } /// ``` + #[must_use] fn boxed(self) -> Box> where Self: Sized, @@ -172,6 +177,7 @@ pub trait Action: Any + Send + Sync { /// ```ignore /// goto(npc.home).debug(|| "Going home") /// ``` + #[must_use] fn debug(self, mk_info: F) -> Debug 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(Box>, Priority); /// Perform an action with [`URGENT`] priority (see [`choose`]). +#[must_use] pub fn urgent, R>(a: A) -> Node { Node(Box::new(a), URGENT) } /// Perform an action with [`IMPORTANT`] priority (see [`choose`]). +#[must_use] pub fn important, R>(a: A) -> Node { Node(Box::new(a), IMPORTANT) } /// Perform an action with [`CASUAL`] priority (see [`choose`]). +#[must_use] pub fn casual, R>(a: A) -> Node { Node(Box::new(a), CASUAL) } /// See [`choose`] and [`watch`]. @@ -501,6 +511,7 @@ impl Node + Send + Sync + 'static, R: 'static> Actio /// } /// }) /// ``` +#[must_use] pub fn choose(f: F) -> impl Action where F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, @@ -535,6 +546,7 @@ where /// } /// }) /// ``` +#[must_use] pub fn watch(f: F) -> impl Action where F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, @@ -679,6 +691,7 @@ impl + Clone + Send + Sync + 'st /// .into_iter() /// .map(|enemy| attack(enemy))) /// ``` +#[must_use] pub fn seq(iter: I) -> Sequence where I: Iterator + Clone, diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 99f4c5de96..cbfc8fe258 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -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, - /// (wpos, speed_factor) - pub goto: Option<(Vec3, f32)>, + pub activity: Option, } 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, 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)); } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 718c04535a..7ca787d5f8 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -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)) } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 42c0e709af..68c15c7915 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -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 => {}, } } diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index d1ab7689ed..8c1169f27d 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -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::() - 0.5, rng.gen::() - 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::() < 0.0015 { + controller.push_utterance(UtteranceKind::Calm); } - } - agent.bearing += Vec2::new(rng.gen::() - 0.5, rng.gen::() - 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::() < 0.0015 { - controller.push_utterance(UtteranceKind::Calm); - } - - // Sit - if rng.gen::() < 0.0035 { - controller.push_action(ControlAction::Sit); - } + // Sit + if rng.gen::() < 0.0035 { + controller.push_action(ControlAction::Sit); + } + }, } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 4459993b97..246dd56875 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -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, + 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::>(); + + let rtsim = server.state.ecs().read_resource::(); + 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::>(); + + 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, diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index ca0544da03..2cb8fef1f6 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -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 diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index f261ecd937..5845ea8d0e 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -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