From ce5ef481e14ed69b772dfacaf2363d22b013fe12 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Wed, 5 Apr 2023 21:45:10 +0100 Subject: [PATCH] Added interrupt_with combinator, guard patrol patterns --- rtsim/src/ai/mod.rs | 86 +++++++++++++++++++++++++++ rtsim/src/rule/npc_ai.rs | 47 +++++++++++---- server/src/sys/agent/behavior_tree.rs | 2 + 3 files changed, 124 insertions(+), 11 deletions(-) diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 2122442ef3..dbfc9b5874 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -134,6 +134,36 @@ pub trait Action: Any + Send + Sync { StopIf(self, f.clone(), f) } + /// Pause an action to possibly perform another action. + /// + /// # Example + /// + /// ```ignore + /// // Keep going on adventures until your 111th birthday + /// walk_to_the_shops() + /// .interrupt_with(|ctx| if ctx.npc.is_hungry() { + /// Some(eat_food()) + /// } else { + /// None + /// }) + /// ``` + #[must_use] + fn interrupt_with, R1, F: FnMut(&mut NpcCtx) -> Option + Clone>( + self, + f: F, + ) -> InterruptWith + where + Self: Sized, + { + InterruptWith { + a0: self, + f: f.clone(), + f2: f, + a1: None, + phantom: PhantomData, + } + } + /// Map the completion value of this action to something else. #[must_use] fn map R1, R1>(self, f: F) -> Map @@ -606,6 +636,62 @@ impl, A1: Action, R0: Send + Sync + 'static, R1: Send + Sync } } +// InterruptWith + +/// See [`Action::then`]. +#[derive(Copy, Clone)] +pub struct InterruptWith { + a0: A0, + f: F, + f2: F, + a1: Option, + phantom: PhantomData, +} + +impl< + A0: Action, + A1: Action, + F: FnMut(&mut NpcCtx) -> Option + Clone + Send + Sync + 'static, + R0: Send + Sync + 'static, + R1: Send + Sync + 'static, +> Action for InterruptWith +{ + fn is_same(&self, other: &Self) -> bool { self.a0.is_same(&other.a0) } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn backtrace(&self, bt: &mut Vec) { + if let Some(a1) = &self.a1 { + // TODO: Find a way to represent interrupts in backtraces + bt.push("".to_string()); + a1.backtrace(bt); + } else { + self.a0.backtrace(bt); + } + } + + fn reset(&mut self) { + self.a0.reset(); + self.f = self.f2.clone(); + self.a1 = None; + } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + if let Some(new_a1) = (self.f)(ctx) { + self.a1 = Some(new_a1); + } + + if let Some(a1) = &mut self.a1 { + match a1.tick(ctx) { + ControlFlow::Continue(()) => return ControlFlow::Continue(()), + ControlFlow::Break(_) => self.a1 = None, + } + } + + self.a0.tick(ctx) + } +} + // Repeat /// See [`Action::repeat`]. diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 885a97a663..44c3a82e6b 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -560,6 +560,18 @@ fn find_forest(ctx: &mut NpcCtx) -> Option> { .map(|chunk| TerrainChunkSize::center_wpos(chunk).as_()) } +fn choose_plaza(ctx: &mut NpcCtx, site: SiteId) -> Option> { + ctx.state + .data() + .sites + .get(site) + .and_then(|site| ctx.index.sites.get(site.world_site?).site2()) + .and_then(|site2| { + let plaza = &site2.plots[site2.plazas().choose(&mut ctx.rng)?]; + Some(site2.tile_center_wpos(plaza.root_tile()).as_()) + }) +} + fn villager(visiting_site: SiteId) -> impl Action { choose(move |ctx| { /* @@ -645,6 +657,29 @@ fn villager(visiting_site: SiteId) -> impl Action { .map(|_| ()), ); } + } else if matches!(ctx.npc.profession, Some(Profession::Guard)) && ctx.rng.gen_bool(0.5) { + if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) { + return important( + travel_to_point(plaza_wpos, 0.45) + .debug(|| "patrol") + .interrupt_with(|ctx| { + if ctx.rng.gen_bool(0.0003) { + let phrase = *[ + "My brother's out fighting ogres. What do I get? Guard duty...", + "Just one more patrol, then I can head home", + "No bandits are going to get past me", + ] + .iter() + .choose(&mut ctx.rng) + .unwrap(); // Can't fail + Some(just(move |ctx| ctx.controller.say(None, phrase))) + } else { + None + } + }) + .map(|_| ()), + ); + } } else if matches!(ctx.npc.profession, Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8) { return casual( @@ -688,17 +723,7 @@ fn villager(visiting_site: SiteId) -> impl Action { // 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 ctx.rng)?]; - Some(site2.tile_center_wpos(plaza.root_tile()).as_()) - }) - { + if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) { // Walk to the plaza... Either::Left(travel_to_point(plaza_wpos, 0.5) .debug(|| "walk to plaza")) diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index c069b530ad..9cccbbd75d 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -464,6 +464,8 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool { false, owner_pos, )); + // Always become aware of our owner no matter what + bdata.agent.awareness.set_maximally_aware(); } } }