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/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..d336a01676 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}; @@ -748,11 +749,44 @@ impl<'a> AgentData<'a> { 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) { + 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 +856,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 +1662,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);