diff --git a/CHANGELOG.md b/CHANGELOG.md index c5458fa2ed..59afead121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A new secondary charged melee attack for the hammer - Added Dutch translations - Buff system +- Sneaking lets you be closer to enemies without being detected ### Changed diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index ab0f92cd5d..85b819fe95 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -469,6 +469,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState { timer: Duration::default(), stage_section: StageSection::Buildup, was_wielded: false, // false by default. utils might set it to true + was_sneak: false, }), CharacterAbility::ComboMelee { stage_data, diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index f439fbcfdf..4b05a7df63 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -100,6 +100,10 @@ impl CharacterState { ) } + pub fn is_stealthy(&self) -> bool { + matches!(self, CharacterState::Sneak | CharacterState::Roll(_)) + } + pub fn is_attack(&self) -> bool { matches!(self, CharacterState::BasicMelee(_) diff --git a/common/src/states/roll.rs b/common/src/states/roll.rs index cb18a441f5..172d79254d 100644 --- a/common/src/states/roll.rs +++ b/common/src/states/roll.rs @@ -32,6 +32,8 @@ pub struct Data { pub stage_section: StageSection, /// Had weapon pub was_wielded: bool, + /// Was sneaking + pub was_sneak: bool, } impl CharacterBehavior for Data { @@ -102,6 +104,8 @@ impl CharacterBehavior for Data { // Done if self.was_wielded { update.character = CharacterState::Wielding; + } else if self.was_sneak { + update.character = CharacterState::Sneak; } else { update.character = CharacterState::Idle; } diff --git a/common/src/states/sneak.rs b/common/src/states/sneak.rs index 9efbdadb2b..d11e7a09c0 100644 --- a/common/src/states/sneak.rs +++ b/common/src/states/sneak.rs @@ -53,4 +53,10 @@ impl CharacterBehavior for Data { attempt_swap_loadout(data, &mut update); update } + + fn stand(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + update.character = CharacterState::Idle; + update + } } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index b2dd5ecb9e..df382ea93a 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -373,6 +373,11 @@ pub fn handle_dodge_input(data: &JoinData, update: &mut StateUpdate) { if let CharacterState::Roll(roll) = &mut update.character { roll.was_wielded = true; } + } else if data.character.is_stealthy() { + update.character = (ability, AbilityKey::Dodge).into(); + if let CharacterState::Roll(roll) = &mut update.character { + roll.was_sneak = true; + } } else { update.character = (ability, AbilityKey::Dodge).into(); } diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 06d39cb1c8..ba1bd17340 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -5,9 +5,9 @@ use crate::{ group, group::Invite, item::{tool::ToolKind, ItemKind}, - Agent, Alignment, Body, ControlAction, ControlEvent, Controller, Energy, GroupManip, - LightEmitter, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, UnresolvedChatMsg, - Vel, + Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy, + GroupManip, LightEmitter, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, + UnresolvedChatMsg, Vel, }, event::{EventBus, ServerEvent}, metrics::SysMetrics, @@ -60,6 +60,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Invite>, Read<'a, TimeOfDay>, ReadStorage<'a, LightEmitter>, + ReadStorage<'a, CharacterState>, ); #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 @@ -89,6 +90,7 @@ impl<'a> System<'a> for Sys { invites, time_of_day, light_emitter, + char_states, ): Self::SystemData, ) { let start_time = std::time::Instant::now(); @@ -191,6 +193,7 @@ impl<'a> System<'a> for Sys { const SIGHT_DIST: f32 = 80.0; const MIN_ATTACK_DIST: f32 = 2.0; const MAX_FLEE_DIST: f32 = 20.0; + const SNEAK_COEFFICIENT: f32 = 0.25; let scale = scales.get(entity).map(|s| s.0).unwrap_or(1.0); @@ -557,14 +560,21 @@ impl<'a> System<'a> for Sys { if choose_target { // Search for new targets (this looks expensive, but it's only run occasionally) // TODO: Replace this with a better system that doesn't consider *all* entities - let closest_entity = (&entities, &positions, &stats, alignments.maybe()) + let closest_entity = (&entities, &positions, &stats, alignments.maybe(), char_states.maybe()) .join() - .filter(|(e, e_pos, e_stats, e_alignment)| { - ((e_pos.0.distance_squared(pos.0) < SEARCH_DIST.powf(2.0) && + .filter(|(e, e_pos, e_stats, e_alignment, char_state)| { + let mut search_dist = SEARCH_DIST; + let mut listen_dist = LISTEN_DIST; + if char_state.map_or(false, |c_s| c_s.is_stealthy()) { + // TODO: make sneak more effective based on a stat like e_stats.fitness + search_dist *= SNEAK_COEFFICIENT; + listen_dist *= SNEAK_COEFFICIENT; + } + ((e_pos.0.distance_squared(pos.0) < search_dist.powf(2.0) && // Within our view (e_pos.0 - pos.0).try_normalized().map(|v| v.dot(*inputs.look_dir) > 0.15).unwrap_or(true)) // Within listen distance - || e_pos.0.distance_squared(pos.0) < LISTEN_DIST.powf(2.0)) + || e_pos.0.distance_squared(pos.0) < listen_dist.powf(2.0)) && *e != entity && !e_stats.is_dead && alignment @@ -572,13 +582,13 @@ impl<'a> System<'a> for Sys { .unwrap_or(false) }) // Can we even see them? - .filter(|(_, e_pos, _, _)| terrain + .filter(|(_, e_pos, _, _, _)| terrain .ray(pos.0 + Vec3::unit_z(), e_pos.0 + Vec3::unit_z()) .until(Block::is_opaque) .cast() .0 >= e_pos.0.distance(pos.0)) - .min_by_key(|(_, e_pos, _, _)| (e_pos.0.distance_squared(pos.0) * 100.0) as i32) - .map(|(e, _, _, _)| e); + .min_by_key(|(_, e_pos, _, _, _)| (e_pos.0.distance_squared(pos.0) * 100.0) as i32) + .map(|(e, _, _, _, _)| e); if let Some(target) = closest_entity { agent.activity = Activity::Attack { diff --git a/voxygen/src/audio/sfx/event_mapper/movement/mod.rs b/voxygen/src/audio/sfx/event_mapper/movement/mod.rs index 85c10f1a6d..8cdeb8c4da 100644 --- a/voxygen/src/audio/sfx/event_mapper/movement/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/movement/mod.rs @@ -158,8 +158,10 @@ impl MovementEventMapper { if physics_state.on_ground && vel.magnitude() > 0.1 || !previous_state.on_ground && physics_state.on_ground { - return if character_state.is_dodge() { + return if matches!(character_state, CharacterState::Roll(_)) { SfxEvent::Roll + } else if matches!(character_state, CharacterState::Sneak) { + SfxEvent::Sneak } else { SfxEvent::Run }; diff --git a/voxygen/src/audio/sfx/event_mapper/movement/tests.rs b/voxygen/src/audio/sfx/event_mapper/movement/tests.rs index 1ac43e80a3..8b557a74b1 100644 --- a/voxygen/src/audio/sfx/event_mapper/movement/tests.rs +++ b/voxygen/src/audio/sfx/event_mapper/movement/tests.rs @@ -161,6 +161,7 @@ fn maps_roll() { timer: Duration::default(), stage_section: states::utils::StageSection::Buildup, was_wielded: true, + was_sneak: false, }), &PhysicsState { on_ground: true, diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index e2ef526ffd..6f08f2b718 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -135,6 +135,7 @@ pub enum SfxEvent { Idle, Run, Roll, + Sneak, Climb, GliderOpen, Glide,