diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc034fc32..71f618726d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sword - Doors now animate opening when entities are near them. - Musical instruments can now be crafted, looted and played +- NPCs now move to their target's last known position. ### Changed diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 2768bae257..e45c9e468d 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -420,15 +420,23 @@ pub struct Target { pub selected_at: f64, /// Whether the target has come close enough to trigger aggro. pub aggro_on: bool, + pub last_known_pos: Option>, } impl Target { - pub fn new(target: EcsEntity, hostile: bool, selected_at: f64, aggro_on: bool) -> Self { + pub fn new( + target: EcsEntity, + hostile: bool, + selected_at: f64, + aggro_on: bool, + last_known_pos: Option>, + ) -> Self { Self { target, hostile, selected_at, aggro_on, + last_known_pos, } } } diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index eeec85efd0..79ffcc259f 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -701,9 +701,8 @@ impl<'a> AgentData<'a> { }, }; - let is_detected = |entity: EcsEntity, e_pos: &Pos| { - self.can_sense_directly_near(e_pos) - || self.can_see_entity(agent, controller, entity, e_pos, read_data) + let is_detected = |entity: &EcsEntity, e_pos: &Pos| { + self.detects_other(agent, controller, entity, e_pos, read_data) }; let target = entities_nearby @@ -713,7 +712,7 @@ impl<'a> AgentData<'a> { .filter_map(|(entity, attack_target)| { get_pos(entity).map(|pos| (entity, pos, attack_target)) }) - .filter(|(entity, e_pos, _)| is_detected(*entity, e_pos)) + .filter(|(entity, e_pos, _)| is_detected(entity, e_pos)) .min_by_key(|(_, e_pos, attack_target)| { ( *attack_target, @@ -735,6 +734,7 @@ impl<'a> AgentData<'a> { hostile: attack_target, selected_at: read_data.time.0, aggro_on, + last_known_pos: get_pos(entity).map(|pos| pos.0), }) } @@ -1341,14 +1341,24 @@ impl<'a> AgentData<'a> { controller.push_utterance(UtteranceKind::Angry); } - agent.target = Some(Target::new(attacker, true, read_data.time.0, true)); + let attacker_pos = read_data.positions.get(attacker).map(|pos| pos.0); + agent.target = Some(Target::new( + attacker, + true, + read_data.time.0, + true, + attacker_pos, + )); if let Some(tgt_pos) = read_data.positions.get(attacker) { if is_dead_or_invulnerable(attacker, read_data) { - // FIXME?: Shouldn't target be set to `None`? - // If is dead, then probably. If invulnerable, maybe not. - agent.target = - Some(Target::new(target, false, read_data.time.0, false)); + agent.target = Some(Target::new( + target, + false, + read_data.time.0, + false, + Some(tgt_pos.0), + )); self.idle(agent, controller, read_data, rng); } else { @@ -1551,6 +1561,18 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data) } + pub fn detects_other( + &self, + agent: &Agent, + controller: &Controller, + other: &EcsEntity, + other_pos: &Pos, + read_data: &ReadData, + ) -> bool { + self.can_sense_directly_near(other_pos) + || self.can_see_entity(agent, controller, *other, other_pos, read_data) + } + pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool { let chance = thread_rng().gen_bool(0.3); e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) && chance diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 18a34b5372..f487c6030b 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -100,8 +100,10 @@ impl BehaviorTree { pub fn target() -> Self { Self { tree: vec![ + update_last_known_pos, untarget_if_dead, update_target_awareness, + search_last_known_pos_if_not_alert, do_hostile_tree_if_hostile_and_aware, do_pet_tree_if_owned, do_pickup_loot, @@ -266,6 +268,11 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool { hostile: true, selected_at: bdata.read_data.time.0, aggro_on: true, + last_known_pos: bdata + .read_data + .positions + .get(attacker) + .map(|pos| pos.0), }); } @@ -440,7 +447,15 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool { if bdata.agent.target.is_none() && small_chance { if let Some(Alignment::Owned(owner)) = bdata.agent_data.alignment { if let Some(owner) = get_entity_by_id(owner.id(), bdata.read_data) { - bdata.agent.target = Some(Target::new(owner, false, bdata.read_data.time.0, false)); + let owner_pos = bdata.read_data.positions.get(owner).map(|pos| pos.0); + + bdata.agent.target = Some(Target::new( + owner, + false, + bdata.read_data.time.0, + false, + owner_pos, + )); } } } @@ -496,6 +511,43 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool { false } +fn update_last_known_pos(bdata: &mut BehaviorData) -> bool { + let BehaviorData { + agent, + agent_data, + read_data, + controller, + .. + } = bdata; + + if let Some(target_info) = agent.target { + let target = target_info.target; + + if let Some(target_pos) = read_data.positions.get(target) { + if agent_data.detects_other(agent, controller, &target, target_pos, read_data) { + let updated_pos = Some(target_pos.0); + + let Target { + hostile, + selected_at, + aggro_on, + .. + } = target_info; + + agent.target = Some(Target::new( + target, + hostile, + selected_at, + aggro_on, + updated_pos, + )); + } + } + } + + false +} + /// Try to heal self if our damage went below a certain threshold fn heal_self_if_hurt(bdata: &mut BehaviorData) -> bool { if bdata.agent_data.damage < HEALING_ITEM_THRESHOLD @@ -556,6 +608,31 @@ fn update_target_awareness(bdata: &mut BehaviorData) -> bool { false } +fn search_last_known_pos_if_not_alert(bdata: &mut BehaviorData) -> bool { + let awareness = &bdata.agent.awareness; + if awareness.reached() || awareness.state() < AwarenessState::Low { + return false; + } + + let BehaviorData { + agent, + agent_data, + controller, + read_data, + .. + } = bdata; + + if let Some(target) = agent.target { + if let Some(last_known_pos) = target.last_known_pos { + agent_data.follow(agent, controller, &read_data.terrain, &Pos(last_known_pos)); + + return true; + } + } + + false +} + fn do_combat(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 5d13bb524d..9d1088287f 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -91,7 +91,15 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { if let Some(AgentEvent::Talk(by, subject)) = agent.inbox.pop_front() { if agent.allowed_to_speak() { if let Some(target) = get_entity_by_id(by.id(), read_data) { - agent.target = Some(Target::new(target, false, read_data.time.0, false)); + let target_pos = read_data.positions.get(target).map(|pos| pos.0); + + agent.target = Some(Target::new( + target, + false, + read_data.time.0, + false, + target_pos, + )); if agent_data.look_toward(controller, read_data, target) { controller.push_action(ControlAction::Stand); @@ -415,7 +423,15 @@ pub fn handle_inbox_trade_invite(bdata: &mut BehaviorData) -> bool { controller.push_action(ControlAction::Stand); controller.push_action(ControlAction::Talk); if let Some(target) = get_entity_by_id(with.id(), read_data) { - agent.target = Some(Target::new(target, false, read_data.time.0, false)); + let target_pos = read_data.positions.get(target).map(|pos| pos.0); + + agent.target = Some(Target::new( + target, + false, + read_data.time.0, + false, + target_pos, + )); } controller.push_invite_response(InviteResponse::Accept); agent.behavior.unset(BehaviorState::TRADING_ISSUER); @@ -454,7 +470,15 @@ pub fn handle_inbox_trade_accepted(bdata: &mut BehaviorData) -> bool { if let Some(AgentEvent::TradeAccepted(with)) = agent.inbox.pop_front() { if !agent.behavior.is(BehaviorState::TRADING) { if let Some(target) = get_entity_by_id(with.id(), read_data) { - agent.target = Some(Target::new(target, false, read_data.time.0, false)); + let target_pos = read_data.positions.get(target).map(|pos| pos.0); + + agent.target = Some(Target::new( + target, + false, + read_data.time.0, + false, + target_pos, + )); } agent.behavior.set(BehaviorState::TRADING); agent.behavior.set(BehaviorState::TRADING_ISSUER);