diff --git a/src/game/client/neo/c_neo_player.cpp b/src/game/client/neo/c_neo_player.cpp index 2010f66563..6f81470c63 100644 --- a/src/game/client/neo/c_neo_player.cpp +++ b/src/game/client/neo/c_neo_player.cpp @@ -123,6 +123,20 @@ BEGIN_PREDICTION_DATA(C_NEO_Player) DEFINE_PRED_FIELD_TOL(m_flVisionLastTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), DEFINE_PRED_FIELD_TOL(m_flJumpLastTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), DEFINE_PRED_FIELD_TOL(m_flNextPingTime, FIELD_FLOAT, FTYPEDESC_INSENDTABLE, TD_MSECTOLERANCE), + DEFINE_FIELD(m_iHeldStep, FIELD_INTEGER), + DEFINE_FIELD(m_nHeldStepKeysDown, FIELD_INTEGER), + DEFINE_FIELD(m_nHeldStepArmedKeys, FIELD_INTEGER), + DEFINE_FIELD(m_flHeldStepQuietTime, FIELD_FLOAT), + DEFINE_FIELD(m_flHeldStepDropTime, FIELD_FLOAT), + DEFINE_FIELD(m_flHeldStepWindow, FIELD_FLOAT), + DEFINE_FIELD(m_flFootstepAirTime, FIELD_FLOAT), + DEFINE_FIELD(m_bFootstepLanding, FIELD_BOOLEAN), + DEFINE_FIELD(m_flFootstepTurnRate, FIELD_FLOAT), + DEFINE_FIELD(m_flFootstepPrevYaw, FIELD_FLOAT), + DEFINE_FIELD(m_flFootstepTurnTime, FIELD_FLOAT), + DEFINE_FIELD(m_bFootstepForgivenessReady, FIELD_BOOLEAN), + DEFINE_FIELD(m_flSilentModeOnGroundTime, FIELD_FLOAT), + DEFINE_FIELD(m_flFootstepQuietTime, FIELD_FLOAT), END_PREDICTION_DATA() static void __MsgFunc_IdleRespawnShowMenu(bf_read &) diff --git a/src/game/client/neo/c_neo_player.h b/src/game/client/neo/c_neo_player.h index ce5374de23..9090ff475a 100644 --- a/src/game/client/neo/c_neo_player.h +++ b/src/game/client/neo/c_neo_player.h @@ -94,6 +94,18 @@ class C_NEO_Player : public C_HL2MP_Player virtual void StartWalking(void); virtual void StopWalking(void); bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; + bool IsInSilentMoveMode() const; + int GetSilentMoveKeys(bool bCrouchFromPress) const; + bool IsOnFootstepGround() const; + bool IsLoudFootstep() const; + float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; + void UpdateFootstepTurnRate(); + void UpdateFootstepLanding(); + void OnFootstepDecided(); + void UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin); + void UpdateFootstepForgiveness(bool bLoud); + bool ForgiveStep(); + bool HoldStep(surfacedata_t *psurface, const Vector &vecOrigin); void Lean(void); @@ -278,6 +290,30 @@ class C_NEO_Player : public C_HL2MP_Player private: C_NEO_Player(const C_NEO_Player &); + + // Held step: the first audible step after pressing crouch, walk or aim, see UpdateHeldStep + bool IsHeldStepPending() const; + void PlayHeldStep(surfacedata_t *psurface, const Vector &vecOrigin); + int m_iHeldStep = NEO_FS_HELD_NONE; + int m_nHeldStepKeysDown = 0; + int m_nHeldStepArmedKeys = 0; + float m_flHeldStepQuietTime = -1.f; + float m_flHeldStepDropTime = 0.f; + float m_flHeldStepWindow = 0.f; + + // Landing, see UpdateFootstepLanding + float m_flFootstepAirTime = -1.f; + bool m_bFootstepLanding = false; + + // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate + float m_flFootstepTurnRate = 0.f; + float m_flFootstepPrevYaw = 0.f; + float m_flFootstepTurnTime = 0.f; + + // First step forgiveness, see ForgiveStep + bool m_bFootstepForgivenessReady = true; + float m_flSilentModeOnGroundTime = -1.f; + float m_flFootstepQuietTime = -1.f; }; inline C_NEO_Player *ToNEOPlayer(C_BaseEntity *pEntity) diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 3475e8b0e8..ea92489d87 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -201,6 +201,18 @@ class CNEO_Player : public CHL2MP_Player virtual void StartWalking(void) OVERRIDE; virtual void StopWalking(void) OVERRIDE; bool IsMakingFootstepSounds(float* pVelRun={}, float* pSpeed={}, int* fLadder={}) const; + bool IsInSilentMoveMode() const; + int GetSilentMoveKeys(bool bCrouchFromPress) const; + bool IsOnFootstepGround() const; + bool IsLoudFootstep() const; + float GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const; + void UpdateFootstepTurnRate(); + void UpdateFootstepLanding(); + void OnFootstepDecided(); + void UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin); + void UpdateFootstepForgiveness(bool bLoud); + bool ForgiveStep(); + bool HoldStep(surfacedata_t *psurface, const Vector &vecOrigin); // Cloak Power Interface float CloakPower_Get(void) const ; @@ -377,6 +389,29 @@ class CNEO_Player : public CHL2MP_Player CNetworkHandle(CNEO_Player, m_hSpectatorTakeoverPlayerTarget); CNetworkHandle(CNEO_Player, m_hSpectatorTakeoverPlayerImpersonatingMe); + // Held step: the first audible step after pressing crouch, walk or aim, see UpdateHeldStep + bool IsHeldStepPending() const; + void PlayHeldStep(surfacedata_t *psurface, const Vector &vecOrigin); + int m_iHeldStep = NEO_FS_HELD_NONE; + int m_nHeldStepKeysDown = 0; + int m_nHeldStepArmedKeys = 0; + float m_flHeldStepQuietTime = -1.f; + float m_flHeldStepDropTime = 0.f; + float m_flHeldStepWindow = 0.f; + + // Landing, see UpdateFootstepLanding + float m_flFootstepAirTime = -1.f; + bool m_bFootstepLanding = false; + + // View turn rate in degrees per second, held at its peak and decaying, see UpdateFootstepTurnRate + float m_flFootstepTurnRate = 0.f; + float m_flFootstepPrevYaw = 0.f; + float m_flFootstepTurnTime = 0.f; + + // First step forgiveness, see ForgiveStep + bool m_bFootstepForgivenessReady = true; + float m_flSilentModeOnGroundTime = -1.f; + float m_flFootstepQuietTime = -1.f; }; inline CNEO_Player *ToNEOPlayer(CBaseEntity *pEntity) diff --git a/src/game/shared/baseplayer_shared.cpp b/src/game/shared/baseplayer_shared.cpp index 3d0b0927dc..5d1dbdd7d9 100644 --- a/src/game/shared/baseplayer_shared.cpp +++ b/src/game/shared/baseplayer_shared.cpp @@ -551,7 +551,12 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri int fLadder; #ifdef NEO - const bool bSilent = !assert_cast(this)->IsMakingFootstepSounds(&velrun, &speed, &fLadder); + CNEO_Player *pNeoPlayer = assert_cast(this); + pNeoPlayer->UpdateFootstepTurnRate(); + pNeoPlayer->UpdateFootstepLanding(); + const bool bSilent = !pNeoPlayer->IsMakingFootstepSounds(&velrun, &speed, &fLadder); + pNeoPlayer->UpdateHeldStep(!bSilent, psurface, vecOrigin); + pNeoPlayer->UpdateFootstepForgiveness(!bSilent); #endif @@ -566,6 +571,12 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri if ( m_flStepSoundTime > 0 ) return; +#ifdef NEO + pNeoPlayer->OnFootstepDecided(); + // Off the ground the step timer waits at zero, as in the SDK, so the step lands on the tick the player does + if ( !fLadder && !( GetFlags() & FL_ONGROUND ) && GetWaterLevel() == WL_NotInWater ) + return; +#endif #ifndef NEO if ( GetFlags() & (FL_FROZEN|FL_ATCONTROLS)) @@ -606,6 +617,11 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri #endif bWalking = speed < velrun; +#ifdef NEO + const bool bLoudStep = pNeoPlayer->IsLoudFootstep(); +#else + const bool bLoudStep = !bWalking; +#endif #ifndef NEO VectorCopy( vecOrigin, knee ); @@ -651,7 +667,7 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri else if ( GetWaterLevel() == WL_Feet ) { psurface = physprops->GetSurfaceData( physprops->GetSurfaceIndex( "water" ) ); - fvol = bWalking ? 0.2 : 0.5; + fvol = bLoudStep ? 0.5 : 0.2; SetStepSoundTime( STEPSOUNDTIME_WATER_FOOT, bWalking ); } @@ -662,6 +678,9 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri SetStepSoundTime( STEPSOUNDTIME_NORMAL, bWalking ); +#ifdef NEO + fvol = pNeoPlayer->GetSurfaceStepVolume(psurface, bLoudStep); +#else switch ( psurface->game.material ) { default: @@ -693,6 +712,7 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri fvol = bWalking ? 0.2 : 0.5; break; } +#endif } #ifdef NEO @@ -704,8 +724,18 @@ void CBasePlayer::UpdateStepSound( surfacedata_t *psurface, const Vector &vecOri // 65% volume if ducking if ( GetFlags() & FL_DUCKING ) { +#ifdef NEO + fvol *= NEO_FS_CROUCH_VOLUME_SCALE; +#else fvol *= 0.65; +#endif } +#ifdef NEO + if (pNeoPlayer->HoldStep(psurface, feet)) + return; + if (pNeoPlayer->ForgiveStep()) + return; +#endif PlayStepSound( feet, psurface, fvol, false ); } diff --git a/src/game/shared/gamemovement.cpp b/src/game/shared/gamemovement.cpp index e2f751263d..70299022f9 100644 --- a/src/game/shared/gamemovement.cpp +++ b/src/game/shared/gamemovement.cpp @@ -4717,6 +4717,18 @@ void CGameMovement::Duck( void ) mv->m_nOldButtons &= ~IN_DUCK; } +#ifdef NEO + // Networked to every client, so other players' footsteps can tell a crouch that is starting + if ( mv->m_nButtons & IN_DUCK ) + { + player->AddFlag( FL_ANIMDUCKING ); + } + else + { + player->RemoveFlag( FL_ANIMDUCKING ); + } +#endif + // Handle death. if ( IsDead() ) return; diff --git a/src/game/shared/neo/neo_player_shared.cpp b/src/game/shared/neo/neo_player_shared.cpp index 50754f8c07..1cd2928ef5 100644 --- a/src/game/shared/neo/neo_player_shared.cpp +++ b/src/game/shared/neo/neo_player_shared.cpp @@ -2,6 +2,7 @@ #include "neo_player_shared.h" #include "in_buttons.h" +#include "decals.h" #include "neo_gamerules.h" #include "tier0/valve_minmax_off.h" @@ -357,6 +358,27 @@ bool GetClNeoDisplayName(wchar_t (&pWszDisplayName)[NEO_MAX_DISPLAYNAME], return GetClNeoDisplayName(pWszDisplayName, wszNeoName, wszNeoClantag, flags); } +namespace +{ + // Footstep tuning. Fractions are of the player's run speed with the weapon's weight speed modifier applied + + // Crouching, walking and aiming are silent until moving faster than this. Anything above it is a sprint, a slide or a push + constexpr float FS_SILENT_HEADROOM = 0.10f; + constexpr float FS_SILENT_SPEED_FRACTION = float(NEO_CROUCH_WALK_MODIFIER) * (1.f + FS_SILENT_HEADROOM); // 0.825 + static_assert(FS_SILENT_SPEED_FRACTION < 1.f, "Running must never be silent"); + static_assert(FS_SILENT_SPEED_FRACTION > NEO_AIM_MODIFIER, "Aiming must be silent"); + + // Turning while moving raises ground speed by up to about this much of crouching speed at a fast turn, so the + // headroom grows with the view turn rate until it covers that. Full headroom arrives at FS_TURN_RATE_FOR_FULL_HEADROOM + constexpr float FS_TURN_HEADROOM = float(NEO_CROUCH_WALK_MODIFIER) * 0.45f; // 0.3375 + constexpr float FS_TURN_RATE_FOR_FULL_HEADROOM = 180.f; + + // Forgiveness only applies to a small overshoot. A direction change adds at most as much speed as a fast turn, + // so the cap is the silent limit plus the full turn headroom. Anything faster is a sprint or a slide + constexpr float FS_FORGIVENESS_MAX_SPEED_FRACTION = FS_SILENT_SPEED_FRACTION + FS_TURN_HEADROOM; // 1.1625 + static_assert(FS_FORGIVENESS_MAX_SPEED_FRACTION < NEO_MIN_SPRINT_MODIFIER, "A sprint at full speed must never be forgiven"); +} + bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLadder) const { if (GetFlags() & (FL_FROZEN|FL_ATCONTROLS)) @@ -385,32 +407,334 @@ bool CNEO_Player::IsMakingFootstepSounds(float* pVelRun, float* pSpeed, int* pLa bool onground = (GetFlags() & FL_ONGROUND); bool movingalongground = (groundspeed > 0.0001f); - // Support crouched with a supa7 aimed in has a speed of 55.f - const bool moving_fast_enough = speed >= 50.f; + // Below this nothing plays, on a ladder or off. It catches drift that is not movement at all, such as being + // nudged, and sits just under the 55 u/s floor on max speed so no movement mode is ever filtered here. + // Whether crouching, walking or aiming is silent is decided below + constexpr float FS_MIN_STEP_SPEED = 50.f; + const bool moving_fast_enough = speed >= FS_MIN_STEP_SPEED; // To hear step sounds you must be either on a ladder or moving along the ground AND // You must be moving fast enough if (!moving_fast_enough || !(isOnLadder || (onground && movingalongground))) return false; - // Changing movement direction, looking around, wall-running accelerate the player. Threshold should be lower than regular speed, but higher than walk/aim speed - constexpr float SILENT_THRESHOLD_GRACE = 0.7f; + // Landing from a jump or a drop makes a footstep whatever the posture, see UpdateFootstepLanding + if (m_bFootstepLanding) + return true; - if (GetFlags() & FL_DUCKING) + // A ladder has no ground entity, so crouching takes effect at once, but ground speed is near zero while climbing + if (IsInSilentMoveMode() && !isOnLadder) { - if ((IsInAim() || IsWalking()) && speed <= (GetCrouchSpeed() * SILENT_THRESHOLD_GRACE)) + float silentThreshold = FS_SILENT_SPEED_FRACTION; + // Sprinting stays audible while turning + if (!m_fIsSprinting) + { + silentThreshold += FS_TURN_HEADROOM * Min(m_flFootstepTurnRate / FS_TURN_RATE_FOR_FULL_HEADROOM, 1.f); + } + // Ground speed is the horizontal part of velocity. Full speed would differ by client: other clients rebuild a + // remote player's velocity from origin changes, and stairs move the origin up a step at a time, so they see a + // vertical spike the local player never does. Ground speed is the same everywhere, so every client agrees + if (groundspeed <= (GetNormSpeed_WithActiveWepEncumberment() * silentThreshold)) { return false; } } - else if ((IsInAim() || IsWalking()) && speed <= (GetNormSpeed() * SILENT_THRESHOLD_GRACE)) + + return true; +} + +// Crouching speed and walking speed are the same, so crouching, walking and aiming share one silent limit. +// Crouch counts once the crouch animation ends +bool CNEO_Player::IsInSilentMoveMode() const +{ + return GetSilentMoveKeys(false) != 0; +} + +// The same modes as a key mask. Crouch counts from the key press (FL_ANIMDUCKING, see CGameMovement::Duck) or once the +// crouch animation ends (FL_DUCKING); walking and aiming take effect at once either way +int CNEO_Player::GetSilentMoveKeys(bool bCrouchFromPress) const +{ + return ((GetFlags() & (bCrouchFromPress ? FL_ANIMDUCKING : FL_DUCKING)) ? NEO_FS_SILENT_MOVE_KEY_CROUCH : 0) | + (IsWalking() ? NEO_FS_SILENT_MOVE_KEY_WALK : 0) | + (IsInAim() ? NEO_FS_SILENT_MOVE_KEY_AIM : 0); +} + +// Starts a timestamp the first time a condition holds and clears it when it stops +static void TrackSince(float &flSince, bool bActive, float flNow) +{ + if (!bActive) + { + flSince = -1.f; + } + else if (flSince < 0.f) + { + flSince = flNow; + } +} + +// The SDK picks the loud step volume by speed, with limits tuned for its slow crouch that every NEO crouch exceeds. +// Pick it by intent instead: only sprinting is loud. The SDK's crouched volume reduction still applies on top +bool CNEO_Player::IsLoudFootstep() const +{ + return m_fIsSprinting; +} + +// The SDK's soft or loud step volume for a surface, before crouched steps are scaled by NEO_FS_CROUCH_VOLUME_SCALE +float CNEO_Player::GetSurfaceStepVolume(const surfacedata_t *psurface, bool bLoud) const +{ + switch (psurface->game.material) + { + case CHAR_TEX_DIRT: + return bLoud ? 0.55f : 0.25f; + case CHAR_TEX_VENT: + return bLoud ? 0.7f : 0.4f; + default: // Concrete, metal, grate, tile and slosh + return bLoud ? 0.5f : 0.2f; + } +} + +// Tracks what ForgiveStep needs: how long the player has been settled into a silent mode on the ground, and +// when a used forgiveness is earned back +void CNEO_Player::UpdateFootstepForgiveness(bool bLoud) +{ + // Time under the silent limit that earns the forgiveness back + constexpr float FS_FORGIVENESS_RECHARGE_TIME = 1.f; + + const float flNow = gpGlobals->curtime; + TrackSince(m_flSilentModeOnGroundTime, IsInSilentMoveMode() && (GetFlags() & FL_ONGROUND), flNow); + TrackSince(m_flFootstepQuietTime, !bLoud, flNow); + if (!bLoud && !m_bFootstepForgivenessReady && flNow - m_flFootstepQuietTime >= FS_FORGIVENESS_RECHARGE_TIME) + { + m_bFootstepForgivenessReady = true; + } +} + +// Forgiveness: one step that is only slightly over the silent limit is skipped instead of played, for a player +// settled into crouching, walking or aiming. It covers a direction change, which raises ground speed the same +// way turning does but without moving the view, so the turn headroom cannot see it. The forgiveness is spent +// on use and earned back after a short stretch under the limit, so one correction stays silent and +// wiggle-walking does not +bool CNEO_Player::ForgiveStep() +{ + // Settled into the crouch, walk or aim first, so slides from a run or a landing stay audible + constexpr float FS_FORGIVENESS_SETTLE_TIME = 0.5f; + + if (!m_bFootstepForgivenessReady || m_fIsSprinting || m_flSilentModeOnGroundTime < 0.f || + gpGlobals->curtime - m_flSilentModeOnGroundTime < FS_FORGIVENESS_SETTLE_TIME) + { + return false; + } + + if (GetAbsVelocity().Length2D() > GetNormSpeed_WithActiveWepEncumberment() * FS_FORGIVENESS_MAX_SPEED_FRACTION) { return false; } + m_bFootstepForgivenessReady = false; return true; } +// Marks a landing after a jump or a drop. The mark lasts until the next step decision: after a jump the step timer +// has run out in the air, so that is the landing tick; after a short drop it is the first step due +void CNEO_Player::UpdateFootstepLanding() +{ + // Shorter than this is a stair edge or a bump, not a drop + constexpr float FS_LANDING_MIN_AIR_TIME = 0.1f; + + const float flNow = gpGlobals->curtime; + const bool bInAir = !IsOnFootstepGround(); + if (!bInAir && m_flFootstepAirTime >= 0.f && flNow - m_flFootstepAirTime >= FS_LANDING_MIN_AIR_TIME) + { + m_bFootstepLanding = true; + } + TrackSince(m_flFootstepAirTime, bInAir, flNow); +} + +void CNEO_Player::OnFootstepDecided() +{ + m_bFootstepLanding = false; +} + +// Acceleration builds toward a new view direction faster than friction removes the old one, +// so turning while moving raises ground speed until friction catches up +void CNEO_Player::UpdateFootstepTurnRate() +{ + // Friction's time constant, 1 / sv_friction, so the extra headroom fades as fast as the speed bump it covers + constexpr float FS_TURN_RATE_DECAY_TIME = 0.25f; + + const float flNow = gpGlobals->curtime; + const float flDt = flNow - m_flFootstepTurnTime; + if (flDt <= 0.f) + { + return; + } + + // A larger change in one sample is a snap, such as a respawn, a spectator takeover or a remote player leaving dormancy + constexpr float FS_TURN_SNAP_DEGREES = 90.f; + + const float flYaw = EyeAngles()[YAW]; + const float flYawDelta = fabsf(AngleDiff(flYaw, m_flFootstepPrevYaw)); + const float flRate = (flDt < 0.5f && flYawDelta <= FS_TURN_SNAP_DEGREES) ? flYawDelta / flDt : 0.f; + m_flFootstepTurnRate = Max(flRate, m_flFootstepTurnRate * expf(-flDt / FS_TURN_RATE_DECAY_TIME)); + m_flFootstepPrevYaw = flYaw; + m_flFootstepTurnTime = flNow; +} + +// On the ground and not climbing. A ladder has no ground entity, and its steps are always audible +bool CNEO_Player::IsOnFootstepGround() const +{ + return (GetFlags() & FL_ONGROUND) && GetMoveType() != MOVETYPE_LADDER; +} + +// A step is armed or already held back +bool CNEO_Player::IsHeldStepPending() const +{ + return m_iHeldStep == NEO_FS_HELD_ARMED || m_iHeldStep == NEO_FS_HELD_HELD; +} + +// Crouch only counts once the crouch animation ends, so without this one running step plays after pressing crouch. +// Releasing early gives the step back, so tapping crouch, walk or aim can delay a step but never skip one. +void CNEO_Player::UpdateHeldStep(bool bLoud, surfacedata_t *psurface, const Vector &vecOrigin) +{ + // Time to slow down under the silent limit after crouching, walking or aiming takes effect + constexpr float FS_HELD_STEP_SETTLE_TIME = 0.1f; + + // Each key arms from its own press, as pressing crouch while still aiming keeps run speed through the crouch animation + const int nKeysDown = GetSilentMoveKeys(true); + const int nPressed = nKeysDown & ~m_nHeldStepKeysDown; + m_nHeldStepKeysDown = nKeysDown; + + // The held step only settles or plays on the ground. A jump lands with its own sound and restarts the step cadence, + // and a ladder has every step audible, so a step held across either would swallow the steps that follow + if (!IsAlive() || !IsOnFootstepGround()) + { + m_iHeldStep = NEO_FS_HELD_NONE; + m_nHeldStepArmedKeys = 0; + return; + } + + const float flNow = gpGlobals->curtime; + // Letting go of a key that armed the step is the release. A step that is still held plays now: holding it back + // delayed it, releasing early means the player never settled, so it was never earned. A step that was already + // dropped plays only if the release came soon enough and no other silent mode is still held, so going from + // crouch to walk is not a tap + if (m_iHeldStep != NEO_FS_HELD_NONE && (m_nHeldStepArmedKeys & ~nKeysDown)) + { + const bool bStillQuiet = (GetSilentMoveKeys(false) & nKeysDown) != 0; + if (m_iHeldStep == NEO_FS_HELD_HELD || + (m_iHeldStep == NEO_FS_HELD_DROPPED && !bStillQuiet && flNow - m_flHeldStepDropTime <= m_flHeldStepWindow)) + { + PlayHeldStep(psurface, vecOrigin); + } + m_iHeldStep = NEO_FS_HELD_NONE; + m_nHeldStepArmedKeys = 0; + } + + // Already crouched: walking or aiming does not raise the threshold, so there is no step to hold + if (nPressed && !(GetFlags() & FL_DUCKING)) + { + if (IsHeldStepPending()) + { + m_nHeldStepArmedKeys |= nPressed; + } + else + { + m_iHeldStep = NEO_FS_HELD_ARMED; + m_nHeldStepArmedKeys = nPressed; + m_flHeldStepQuietTime = -1.f; + } + } + + if (IsHeldStepPending()) + { + // Has a mode the player pressed taken effect yet. It must be one that armed the step, not any silent mode the + // player was already in: pressing crouch while aiming, the aim is already silent but the crouch only counts + // once its animation ends. Until then the player is still at run speed, and the step that would play is the + // accidental running step this hold exists to stop + const bool bArmedModeActive = (m_nHeldStepArmedKeys & GetSilentMoveKeys(false)) != 0; + TrackSince(m_flHeldStepQuietTime, bArmedModeActive, flNow); + + if (bArmedModeActive) + { + if (!bLoud) + { + // Settled into a silent crouch, walk or aim + if (m_iHeldStep == NEO_FS_HELD_HELD) + { + m_iHeldStep = NEO_FS_HELD_DROPPED; + m_flHeldStepDropTime = flNow; + } + else + { + m_iHeldStep = NEO_FS_HELD_NONE; + } + } + else if (flNow - m_flHeldStepQuietTime >= FS_HELD_STEP_SETTLE_TIME) + { + // Still loud after crouching, walking or aiming took effect, such as a slide, a push or a crouch sprint + if (m_iHeldStep == NEO_FS_HELD_HELD) + { + PlayHeldStep(psurface, vecOrigin); + } + m_iHeldStep = NEO_FS_HELD_NONE; + } + } + } + else if (m_iHeldStep == NEO_FS_HELD_DROPPED && flNow - m_flHeldStepDropTime > m_flHeldStepWindow) + { + m_iHeldStep = NEO_FS_HELD_NONE; + } +} + +// Returns true if this audible step is held back instead of played +bool CNEO_Player::HoldStep(surfacedata_t *psurface, const Vector &vecOrigin) +{ + if (!IsHeldStepPending()) + { + return false; + } + + if (m_iHeldStep == NEO_FS_HELD_HELD) + { + // Crouching or walking took effect and speed is still dropping, so keep waiting on the same step + if (m_flHeldStepQuietTime >= 0.f) + { + return true; + } + + // Another step before crouching takes effect: the earlier one plays and this one waits, so no step is skipped + PlayHeldStep(psurface, vecOrigin); + } + + m_iHeldStep = NEO_FS_HELD_HELD; + // Releasing within one step interval of dropping the step still plays it + m_flHeldStepWindow = m_flStepSoundTime * 0.001f; + return true; +} + +void CNEO_Player::PlayHeldStep(surfacedata_t *psurface, const Vector &vecOrigin) +{ + // Only reached on the ground, where the surface was just traced; without one there is no step to play + if (!psurface) + { + return; + } + // The volume for how the player moves now, not when the step was held: crouching, sprinting or releasing aim since + // then changes it, and the listener hears the step with the player as they are + float flVolume = GetSurfaceStepVolume(psurface, IsLoudFootstep()); + if (GetFlags() & FL_DUCKING) + { + flVolume *= NEO_FS_CROUCH_VOLUME_SCALE; + } + Vector vecFeet = vecOrigin; + PlayStepSound(vecFeet, psurface, flVolume, false); + + // Keep the next step off the top of this one without restarting the step cadence, + // as a full interval here lets tapping crouch push every step back and skip half of them + constexpr float FS_HELD_STEP_MIN_GAP_MS = 100.f; + m_flStepSoundTime = Max(m_flStepSoundTime, FS_HELD_STEP_MIN_GAP_MS); +} + int GetRank(const int xp) { int iRank = NEO_RANK_RANKLESS_DOG; diff --git a/src/game/shared/neo/neo_player_shared.h b/src/game/shared/neo/neo_player_shared.h index 181f3b85bb..35d5a29f27 100644 --- a/src/game/shared/neo/neo_player_shared.h +++ b/src/game/shared/neo/neo_player_shared.h @@ -39,11 +39,39 @@ extern ConVar sv_neo_spec_replace_player_min_exp; #define NEO_ASSAULT_SPRINT_MODIFIER 1.6 #define NEO_SUPPORT_SPRINT_MODIFIER 1.35 // Redundant, but for future usecases +constexpr float NEO_Min(float a, float b) { return a < b ? a : b; } + +// The slowest any class sprints. Support cannot sprint today, but its modifier is defined for future use, so it +// is counted. Anything that must stay under every sprint, such as a footstep rule, compares against this +static constexpr float NEO_MIN_SPRINT_MODIFIER = + NEO_Min(NEO_Min(NEO_RECON_SPRINT_MODIFIER, NEO_ASSAULT_SPRINT_MODIFIER), NEO_SUPPORT_SPRINT_MODIFIER); +static_assert(NEO_MIN_SPRINT_MODIFIER > 1.f, "Sprinting must be faster than running"); + // Aim Modifier #define NEO_AIM_MODIFIER 0.6 // Crouch/Walk Modifier #define NEO_CROUCH_WALK_MODIFIER 0.75 +// Footsteps: the SDK plays crouched steps at 65% volume +#define NEO_FS_CROUCH_VOLUME_SCALE 0.65f + +// Held step: the first audible step after pressing crouch, walk or aim waits until the player settles or lets go +enum NeoFootstepHeldState +{ + NEO_FS_HELD_NONE = 0, + NEO_FS_HELD_ARMED, // Crouch, walk or aim pressed, the next audible step is held + NEO_FS_HELD_HELD, // A step is held back + NEO_FS_HELD_DROPPED, // The held step was dropped, but plays if the key is released soon after +}; + +// Keys that start a silent movement mode, see CNEO_Player::GetSilentMoveKeys +enum NeoFootstepSilentMoveKey +{ + NEO_FS_SILENT_MOVE_KEY_CROUCH = (1 << 0), + NEO_FS_SILENT_MOVE_KEY_WALK = (1 << 1), + NEO_FS_SILENT_MOVE_KEY_AIM = (1 << 2), +}; + // Movement Calculations // Recon