diff --git a/CHANGELOG.md b/CHANGELOG.md index c24ad07e6b..58e1c16c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Use fluent for translations - First tab on Login screen triggers username focus +- Certain NPCs will now attack when alone with victim ### Removed diff --git a/assets/voxygen/i18n/en/npc.ftl b/assets/voxygen/i18n/en/npc.ftl index 708e649d7b..5d85c35b37 100644 --- a/assets/voxygen/i18n/en/npc.ftl +++ b/assets/voxygen/i18n/en/npc.ftl @@ -92,6 +92,9 @@ npc-speech-merchant_trade_declined = npc-speech-merchant_trade_cancelled_hostile = .a0 = Sorry to cut it short, we have a problem to solve here! .a1 = We'll trade later, I need to take care of this first! +npc-speech-ambush = + .a0 = It's unwise to travel alone! + .a1 = Like stealing candy from a baby! npc-speech-villager_cultist_alarm = .a0 = Lookout! There is a cultist on the loose! .a1 = To arms! The cultists are attacking! diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 6c87700846..3396f22016 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -130,6 +130,7 @@ pub enum UtteranceKind { Hurt, Greeting, Scream, + Ambush, /* Death, * TODO: Wait for more post-death features (i.e. animations) before implementing death * sounds */ diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 077d9c6628..25a25f9785 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -706,6 +706,8 @@ impl PersonalityBase { * don't want everyone to be completely weird. */ pub fn to_personality(&self) -> Personality { + let will_ambush = self.agreeableness < Personality::LOW_THRESHOLD + && self.conscientiousness < Personality::LOW_THRESHOLD; let mut chat_traits: EnumSet = EnumSet::new(); if self.openness > Personality::HIGH_THRESHOLD { chat_traits.insert(PersonalityTrait::Open); @@ -752,12 +754,14 @@ impl PersonalityBase { } Personality { personality_traits: chat_traits, + will_ambush, } } } pub struct Personality { pub personality_traits: EnumSet, + pub will_ambush: bool, } #[derive(EnumSetType)] diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 490a03b495..b172d040b6 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -48,6 +48,7 @@ use common::{ }; use common_base::prof_span; use common_ecs::{Job, Origin, ParMode, Phase, System}; +use itertools::Itertools; use rand::{thread_rng, Rng}; use rayon::iter::ParallelIterator; use specs::{Entity as EcsEntity, Join, ParJoin, Read, WriteExpect, WriteStorage}; @@ -744,15 +745,60 @@ impl<'a> AgentData<'a> { } } - fn choose_target(&self, agent: &mut Agent, controller: &mut Controller, read_data: &ReadData) { + fn choose_target( + &self, + agent: &mut Agent, + controller: &mut Controller, + read_data: &ReadData, + event_emitter: &mut Emitter, + ) { agent.action_state.timer = 0.0; let mut aggro_on = false; + // Search the area. + // TODO: choose target by more than just distance + let common::CachedSpatialGrid(grid) = self.cached_spatial_grid; + + let entities_nearby = grid + .in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist()) + .collect_vec(); + + let can_ambush = |entity: EcsEntity, read_data: &ReadData| { + let self_different_from_entity = || { + read_data + .uids + .get(entity) + .map_or(false, |eu| eu != self.uid) + }; + if self.will_ambush() + && self_different_from_entity() + && !self.passive_towards(entity, read_data) + { + let surrounding_humanoids = entities_nearby + .iter() + .filter(|e| read_data.bodies.get(**e).map_or(false, |b| b.is_humanoid())) + .collect_vec(); + surrounding_humanoids.len() == 2 + && surrounding_humanoids.iter().any(|e| **e == entity) + } else { + false + } + }; + let get_pos = |entity| read_data.positions.get(entity); let get_enemy = |(entity, attack_target): (EcsEntity, bool)| { if attack_target { if self.is_enemy(entity, read_data) { Some((entity, true)) + } else if can_ambush(entity, read_data) { + controller.clone().push_utterance(UtteranceKind::Ambush); + self.chat_npc_if_allowed_to_speak( + "npc-speech-ambush".to_string(), + agent, + event_emitter, + ); + aggro_on = true; + Some((entity, true)) } else if self.should_defend(entity, read_data) { if let Some(attacker) = get_attacker(entity, read_data) { if !self.passive_towards(attacker, read_data) { @@ -822,13 +868,9 @@ impl<'a> AgentData<'a> { || self.can_see_entity(agent, controller, entity, e_pos, read_data) }; - // Search the area. - // TODO: choose target by more than just distance - let common::CachedSpatialGrid(grid) = self.cached_spatial_grid; - - let target = grid - .in_circle_aabr(self.pos.0.xy(), agent.psyche.search_dist()) - .filter_map(is_valid_target) + let target = entities_nearby + .iter() + .filter_map(|e| is_valid_target(*e)) .filter_map(get_enemy) .filter_map(|(entity, attack_target)| { get_pos(entity).map(|pos| (entity, pos, attack_target)) @@ -1632,6 +1674,14 @@ impl<'a> AgentData<'a> { || (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data))) } + fn will_ambush(&self) -> bool { + self.health + .map_or(false, |h| h.current() / h.maximum() > 0.7) + && self + .rtsim_entity + .map_or(false, |re| re.brain.personality.will_ambush) + } + fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool { let entity_alignment = read_data.alignments.get(entity); diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index e4b6dc79ca..d6e42217b0 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -455,9 +455,12 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool { } if bdata.rng.gen::() < 0.1 { - bdata - .agent_data - .choose_target(bdata.agent, bdata.controller, bdata.read_data); + bdata.agent_data.choose_target( + bdata.agent, + bdata.controller, + bdata.read_data, + bdata.event_emitter, + ); } else { bdata.agent_data.handle_sounds_heard( bdata.agent, @@ -577,7 +580,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS; if !in_aggro_range && is_time_to_retarget { - agent_data.choose_target(agent, controller, read_data); + agent_data.choose_target(agent, controller, read_data, event_emitter); } if aggro_on { diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index f0216ab0b6..b49a4bdc31 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -94,7 +94,13 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { { let personality = &rtsim_entity.brain.personality; let standard_response_msg = || -> String { - if personality + if personality.will_ambush { + format!( + "I'm heading to {}! Want to come along? We'll make \ + great travel buddies, hehe.", + destination_name + ) + } else if personality .personality_traits .contains(PersonalityTrait::Extroverted) { @@ -121,7 +127,9 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { }, )); if rtsim_entity.brain.remembers_character(&tgt_stats.name) { - if personality + if personality.will_ambush { + "Just follow me a bit more, hehe.".to_string() + } else if personality .personality_traits .contains(PersonalityTrait::Extroverted) {