From 3ad3c653eec50a0f6810d942dc24b831617e5d4a Mon Sep 17 00:00:00 2001 From: Rain Date: Sun, 29 Mar 2026 17:49:24 +0300 Subject: [PATCH 1/5] Refactor --- src/game/shared/neo/weapons/weapon_knife.cpp | 88 +++++++++----------- src/game/shared/neo/weapons/weapon_knife.h | 3 +- 2 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_knife.cpp b/src/game/shared/neo/weapons/weapon_knife.cpp index 2952f45c3e..9e058a0623 100644 --- a/src/game/shared/neo/weapons/weapon_knife.cpp +++ b/src/game/shared/neo/weapons/weapon_knife.cpp @@ -255,64 +255,54 @@ Activity CWeaponKnife::ChooseIntersectionPointAndActivity(trace_t& hitTrace, con return KNIFE_VM_ATTACK_ACT; } -void CWeaponKnife::Hit(trace_t& traceHit, [[maybe_unused]] Activity nHitActivity) +inline void CWeaponKnife::ApplyDamageToHitTarget(trace_t& traceHit) { - Assert(nHitActivity == KNIFE_VM_ATTACK_ACT); - - CBasePlayer *pPlayer = ToBasePlayer(GetOwner()); - - // Do view kick - //AddViewKick(); + if (!traceHit.m_pEnt) + return; - CBaseEntity *pHitEntity = traceHit.m_pEnt; + auto *pPlayer = assert_cast(GetOwner()); + AssertMsg(pPlayer != traceHit.m_pEnt, "Shouldn't be able to hit self"); - //Apply damage to a hit target - if (pHitEntity != NULL) +#ifdef GAME_DLL + Vector forward; + AngleVectors(traceHit.m_pEnt->GetAbsAngles(), &forward); + Vector2D& forward2D = forward.AsVector2D(); + forward2D.NormalizeInPlace(); + + Vector attackerToTarget = traceHit.m_pEnt->GetAbsOrigin() - pPlayer->GetAbsOrigin(); + Vector2D& attackerToTarget2D = attackerToTarget.AsVector2D(); + attackerToTarget2D.NormalizeInPlace(); + + const float currentAngle = acos(forward2D.Dot(attackerToTarget2D)); + static constexpr float maxBackStabAngle = 0.6435011; // ~ asin(0.6); + + CTakeDamageInfo info(pPlayer, pPlayer, KNIFE_DAMAGE, DMG_SLASH); + Vector hitDirection; + pPlayer->EyeVectors(&hitDirection); + AssertFloatEquals(hitDirection.Length(), 1.f, 0.01f); + CalculateMeleeDamageForce(&info, hitDirection, traceHit.endpos, 0.05f); + + if (currentAngle <= maxBackStabAngle) { - // Shouldn't be able to hit self - Assert(pPlayer != pHitEntity); - - Vector hitDirection; - pPlayer->EyeVectors(&hitDirection, NULL, NULL); - VectorNormalize(hitDirection); - -#ifndef CLIENT_DLL - - Vector forward; - AngleVectors(pHitEntity->GetAbsAngles(), &forward); - - Vector2D forward2D = Vector2D(forward.x, forward.y); - forward2D.NormalizeInPlace(); - - Vector attackerToTarget = pHitEntity->GetAbsOrigin() - pPlayer->GetAbsOrigin(); - Vector2D attackerToTarget2D = Vector2D(attackerToTarget.x, attackerToTarget.y); - attackerToTarget2D.NormalizeInPlace(); - - const float currentAngle = acos(DotProduct2D(forward2D, attackerToTarget2D)); - - static constexpr float maxBackStabAngle = 0.6435011; // ~ asin(0.6); - static constexpr int damageToOneShotSupport = MAX_HEALTH_FOR_CLASS[NEO_CLASS_SUPPORT] + 1; + // increase damage if backstabbing only after melee damage force has been calculated, + // so objects cannot be "backstabbed" to launch them further + info.SetDamage(damageToOneShotSupport); + } - CTakeDamageInfo info(GetOwner(), GetOwner(), KNIFE_DAMAGE, DMG_SLASH); - - CalculateMeleeDamageForce(&info, hitDirection, traceHit.endpos, 0.05f); - - if (currentAngle <= maxBackStabAngle) - { // increase damage if backstabbing only after melee damage force has been calculated, so objects cannot be "backstabbed" to launch them further - info.SetDamage(damageToOneShotSupport); - } - - pHitEntity->DispatchTraceAttack(info, hitDirection, &traceHit); - ApplyMultiDamage(); + traceHit.m_pEnt->DispatchTraceAttack(info, hitDirection, &traceHit); + ApplyMultiDamage(); - // Now hit all triggers along the ray that... - TraceAttackToTriggers(info, traceHit.startpos, traceHit.endpos, hitDirection); + // Now hit all triggers along that ray... + TraceAttackToTriggers(info, traceHit.startpos, traceHit.endpos, hitDirection); #endif - WeaponSound(MELEE_HIT); - } +} - // Apply an impact effect +void CWeaponKnife::Hit(trace_t& traceHit, [[maybe_unused]] Activity nHitActivity) +{ + Assert(nHitActivity == KNIFE_VM_ATTACK_ACT); + ApplyDamageToHitTarget(traceHit); + WeaponSound(MELEE_HIT); ImpactEffect(traceHit); } diff --git a/src/game/shared/neo/weapons/weapon_knife.h b/src/game/shared/neo/weapons/weapon_knife.h index 13346eefc6..da6d7372d5 100644 --- a/src/game/shared/neo/weapons/weapon_knife.h +++ b/src/game/shared/neo/weapons/weapon_knife.h @@ -33,6 +33,7 @@ class CWeaponKnife : public CNEOBaseCombatWeapon #endif CWeaponKnife(); + CWeaponKnife(const CWeaponKnife& other) = delete; virtual void PrimaryAttack() final; virtual void Drop(const Vector &vecVelocity) final { /* knives shouldn't drop */ } @@ -63,7 +64,7 @@ class CWeaponKnife : public CNEOBaseCombatWeapon bool ImpactWater(const Vector &start, const Vector &end); void Hit(trace_t& traceHit, Activity nHitActivity); private: - CWeaponKnife(const CWeaponKnife &other); + inline void ApplyDamageToHitTarget(trace_t& traceHit); }; #endif // NEO_WEAPON_KNIFE_H From 4f56d4f114958cb3d5aae07f1a3ab1bade4ad008 Mon Sep 17 00:00:00 2001 From: Rain Date: Sun, 29 Mar 2026 19:28:07 +0300 Subject: [PATCH 2/5] Add bot_mimic_mirror Add a new bot debug command for mirroring the mimic XY axes --- src/game/server/NextBot/Player/NextBotPlayer.h | 7 +++++++ src/game/server/NextBot/Player/NextBotPlayerBody.cpp | 1 + 2 files changed, 8 insertions(+) diff --git a/src/game/server/NextBot/Player/NextBotPlayer.h b/src/game/server/NextBot/Player/NextBotPlayer.h index d12df0fe5e..5252f9b6f4 100644 --- a/src/game/server/NextBot/Player/NextBotPlayer.h +++ b/src/game/server/NextBot/Player/NextBotPlayer.h @@ -699,6 +699,7 @@ inline void _NextBot_BuildUserCommand( CUserCmd *cmd, const QAngle &viewangles, #ifdef NEO extern ConVar bot_mimic; extern ConVar bot_mimic_yaw_offset; +extern ConVar bot_mimic_mirror; #endif // NEO template < typename PlayerType > inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void ) @@ -743,6 +744,12 @@ inline void NextBotPlayer< PlayerType >::PhysicsSimulate( void ) cmd = *pPlayerMimicked->GetLastUserCommand(); cmd.viewangles[YAW] += bot_mimic_yaw_offset.GetFloat(); + if (bot_mimic_mirror.GetBool()) + { + cmd.viewangles[YAW] = -cmd.viewangles[YAW]; + cmd.viewangles[PITCH] = -cmd.viewangles[PITCH]; + } + // allocate a new command and add it to the player's list of command to process this->ProcessUsercmds(&cmd, 1, 1, 0, false); diff --git a/src/game/server/NextBot/Player/NextBotPlayerBody.cpp b/src/game/server/NextBot/Player/NextBotPlayerBody.cpp index 720aef93fd..f6c12f2c8a 100644 --- a/src/game/server/NextBot/Player/NextBotPlayerBody.cpp +++ b/src/game/server/NextBot/Player/NextBotPlayerBody.cpp @@ -121,6 +121,7 @@ void PlayerBody::Reset( void ) ConVar bot_mimic( "bot_mimic", "0", 0, "Bot uses usercmd of player by index." ); #ifdef NEO ConVar bot_mimic_yaw_offset("bot_mimic_yaw_offset", "0", 0, "Offsets the bot yaw."); +ConVar bot_mimic_mirror("bot_mimic_mirror", "0", 0, "Mirrors the mimic axes."); #endif // NEO //----------------------------------------------------------------------------------------------- From 3130eac5bfb5c15e87b8fc86f9fd9fa0a4080a97 Mon Sep 17 00:00:00 2001 From: Rain Date: Sun, 29 Mar 2026 19:11:36 +0300 Subject: [PATCH 3/5] Predict knife blood/hit effects Predict the knife hits client-side for a more responsive game feel at high latencty. Also changes the knife target angle from abs angles to eye angles, because the eye angles are networked to clients. Since the playermodel upper torso also twists according to the eye angle, it probably makes more sense to use that for the backstab logic. --- src/game/shared/gamemovement.cpp | 2 ++ src/game/shared/neo/weapons/weapon_knife.cpp | 28 +++++++++++++++----- src/game/shared/takedamageinfo.cpp | 4 +++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/game/shared/gamemovement.cpp b/src/game/shared/gamemovement.cpp index 6224668be0..0f9b051bda 100644 --- a/src/game/shared/gamemovement.cpp +++ b/src/game/shared/gamemovement.cpp @@ -14,9 +14,11 @@ #include "decals.h" #include "coordsize.h" #include "rumble_shared.h" +#ifndef NEO #ifdef CLIENT_DLL #include "prediction.h" #endif +#endif #if defined(HL2_DLL) || defined(HL2_CLIENT_DLL) #include "hl_movedata.h" diff --git a/src/game/shared/neo/weapons/weapon_knife.cpp b/src/game/shared/neo/weapons/weapon_knife.cpp index 9e058a0623..0b90671671 100644 --- a/src/game/shared/neo/weapons/weapon_knife.cpp +++ b/src/game/shared/neo/weapons/weapon_knife.cpp @@ -1,12 +1,16 @@ #include "cbase.h" #include "weapon_knife.h" -#ifndef CLIENT_DLL +#ifdef GAME_DLL #include "ilagcompensationmanager.h" #include "effect_dispatch_data.h" #include "te_effect_dispatch.h" +#else +#include "prediction.h" #endif +#include "takedamageinfo.h" + // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -257,15 +261,24 @@ Activity CWeaponKnife::ChooseIntersectionPointAndActivity(trace_t& hitTrace, con inline void CWeaponKnife::ApplyDamageToHitTarget(trace_t& traceHit) { +#ifdef CLIENT_DLL + if (prediction->InPrediction() && !prediction->IsFirstTimePredicted()) + return; +#endif + if (!traceHit.m_pEnt) return; auto *pPlayer = assert_cast(GetOwner()); AssertMsg(pPlayer != traceHit.m_pEnt, "Shouldn't be able to hit self"); - -#ifdef GAME_DLL + // NEO NOTE (Rain): client-side abs ang queries don't seem to work well, + // so I've changed the Vector forward to use the eye angles forward for + // prediction purposes. It probably feels more fair to the player as well, + // since as long as they keep their eyes on the attacker, i.e. the attacker + // is physically in front of them, they cannot be backstabbed (assuming the + // server agrees on those angles). Vector forward; - AngleVectors(traceHit.m_pEnt->GetAbsAngles(), &forward); + AngleVectors(traceHit.m_pEnt->EyeAngles(), &forward); Vector2D& forward2D = forward.AsVector2D(); forward2D.NormalizeInPlace(); @@ -273,9 +286,11 @@ inline void CWeaponKnife::ApplyDamageToHitTarget(trace_t& traceHit) Vector2D& attackerToTarget2D = attackerToTarget.AsVector2D(); attackerToTarget2D.NormalizeInPlace(); - const float currentAngle = acos(forward2D.Dot(attackerToTarget2D)); + // NEO NOTE (Rain): since this is 2D, a TF2 spy style stair stab is possible. + // If we want to make this harder, then could use the 3D vecs to account for pitch. + const float currentAngle = acos(DotProduct2D(forward2D, attackerToTarget2D)); static constexpr float maxBackStabAngle = 0.6435011; // ~ asin(0.6); - + CTakeDamageInfo info(pPlayer, pPlayer, KNIFE_DAMAGE, DMG_SLASH); Vector hitDirection; pPlayer->EyeVectors(&hitDirection); @@ -293,6 +308,7 @@ inline void CWeaponKnife::ApplyDamageToHitTarget(trace_t& traceHit) traceHit.m_pEnt->DispatchTraceAttack(info, hitDirection, &traceHit); ApplyMultiDamage(); +#ifdef GAME_DLL // Now hit all triggers along that ray... TraceAttackToTriggers(info, traceHit.startpos, traceHit.endpos, hitDirection); #endif diff --git a/src/game/shared/takedamageinfo.cpp b/src/game/shared/takedamageinfo.cpp index 6c60b56cf7..a707d5096e 100644 --- a/src/game/shared/takedamageinfo.cpp +++ b/src/game/shared/takedamageinfo.cpp @@ -215,12 +215,16 @@ void ApplyMultiDamage( void ) return; #ifndef CLIENT_DLL +#ifndef NEO const CBaseEntity *host = te->GetSuppressHost(); te->SetSuppressHost( NULL ); +#endif g_MultiDamage.GetTarget()->TakeDamage( g_MultiDamage ); +#ifndef NEO te->SetSuppressHost( (CBaseEntity*)host ); +#endif #endif // Damage is done, clear it out From 944a68924c847e22ea427a3bf0dfe1e6d0d69bce Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 30 Mar 2026 01:37:13 +0300 Subject: [PATCH 4/5] Refactor Add cvars for backstab angle (degrees instead of radians), and add a cvar for toggling whether to ignore backstab pitch component for adjusting the "stair stabbing" mechanic. --- src/game/shared/neo/weapons/weapon_knife.cpp | 46 ++++++++++++++------ src/game/shared/neo/weapons/weapon_knife.h | 2 +- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_knife.cpp b/src/game/shared/neo/weapons/weapon_knife.cpp index 0b90671671..9ffc4bc2d1 100644 --- a/src/game/shared/neo/weapons/weapon_knife.cpp +++ b/src/game/shared/neo/weapons/weapon_knife.cpp @@ -14,6 +14,14 @@ // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" +static ConVar sv_neo_backstab_ignorez("sv_neo_backstab_ignorez", "1", FCVAR_REPLICATED | FCVAR_NOTIFY, + "Whether knife backstabs angle calculations should ignore the pitch component.", true, false, true, true); + +// NEO NOTE (Rain): Changed to degrees to make it more intuitive for players. +// Was previously: 0.6435011 radians; // ~ asin(0.6); +static ConVar sv_neo_backstab_angle("sv_neo_backstab_angle", "37", FCVAR_REPLICATED | FCVAR_NOTIFY, + "Maximum angle away from perfectly behind the back that still counts as a backstab.", true, 0, true, 180); + #define KNIFE_VM_ATTACK_ACT ACT_VM_PRIMARYATTACK #define KNIFE_HULL_DIM 16.0f @@ -279,17 +287,25 @@ inline void CWeaponKnife::ApplyDamageToHitTarget(trace_t& traceHit) // server agrees on those angles). Vector forward; AngleVectors(traceHit.m_pEnt->EyeAngles(), &forward); - Vector2D& forward2D = forward.AsVector2D(); - forward2D.NormalizeInPlace(); Vector attackerToTarget = traceHit.m_pEnt->GetAbsOrigin() - pPlayer->GetAbsOrigin(); - Vector2D& attackerToTarget2D = attackerToTarget.AsVector2D(); - attackerToTarget2D.NormalizeInPlace(); - // NEO NOTE (Rain): since this is 2D, a TF2 spy style stair stab is possible. - // If we want to make this harder, then could use the 3D vecs to account for pitch. - const float currentAngle = acos(DotProduct2D(forward2D, attackerToTarget2D)); - static constexpr float maxBackStabAngle = 0.6435011; // ~ asin(0.6); + if (sv_neo_backstab_ignorez.GetBool()) + { + forward.AsVector2D().NormalizeInPlace(); + forward.z = 0; + + attackerToTarget.AsVector2D().NormalizeInPlace(); + attackerToTarget.z = 0; + } + else + { + attackerToTarget.NormalizeInPlace(); + } + AssertFloatEquals(forward.Length(), 1.f, 0.01f); + AssertFloatEquals(attackerToTarget.Length(), 1.f, 0.01f); + + const float currentAngle = RAD2DEG(acos(forward.Dot(attackerToTarget))); CTakeDamageInfo info(pPlayer, pPlayer, KNIFE_DAMAGE, DMG_SLASH); Vector hitDirection; @@ -297,12 +313,16 @@ inline void CWeaponKnife::ApplyDamageToHitTarget(trace_t& traceHit) AssertFloatEquals(hitDirection.Length(), 1.f, 0.01f); CalculateMeleeDamageForce(&info, hitDirection, traceHit.endpos, 0.05f); - if (currentAngle <= maxBackStabAngle) + const bool isBackstab = (currentAngle <= sv_neo_backstab_angle.GetFloat()); + // increase damage if backstabbing only after melee damage force has been calculated, + // so objects cannot be "backstabbed" to launch them further + if (isBackstab) { - static constexpr int damageToOneShotSupport = MAX_HEALTH_FOR_CLASS[NEO_CLASS_SUPPORT] + 1; - // increase damage if backstabbing only after melee damage force has been calculated, - // so objects cannot be "backstabbed" to launch them further - info.SetDamage(damageToOneShotSupport); + //DevMsg("[%s] Backstab\n", IsServer() ? "SRV" : "CLI"); + static constexpr float oneShotDamage = MAX_HEALTH_FOR_CLASS[NEO_CLASS_SUPPORT] + 1; + Assert(traceHit.m_pEnt->GetMaxHealth() < oneShotDamage); + info.SetMaxDamage(Max(info.GetMaxDamage(), oneShotDamage)); + info.SetDamage(oneShotDamage); } traceHit.m_pEnt->DispatchTraceAttack(info, hitDirection, &traceHit); diff --git a/src/game/shared/neo/weapons/weapon_knife.h b/src/game/shared/neo/weapons/weapon_knife.h index da6d7372d5..eeae459ea6 100644 --- a/src/game/shared/neo/weapons/weapon_knife.h +++ b/src/game/shared/neo/weapons/weapon_knife.h @@ -18,7 +18,7 @@ #define CWeaponKnife C_WeaponKnife #endif -#define NEO_WEP_KNIFE_RANGE 51.f; +#define NEO_WEP_KNIFE_RANGE 51.f class CWeaponKnife : public CNEOBaseCombatWeapon { From eeafa21b534851e71b8a6863edf67146fbe04d18 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 30 Mar 2026 02:12:01 +0300 Subject: [PATCH 5/5] Fix issues with backstab detection --- src/game/shared/neo/weapons/weapon_knife.cpp | 23 ++++++++++++++++---- src/game/shared/takedamageinfo.cpp | 4 ++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/game/shared/neo/weapons/weapon_knife.cpp b/src/game/shared/neo/weapons/weapon_knife.cpp index 9ffc4bc2d1..8e35cc96b9 100644 --- a/src/game/shared/neo/weapons/weapon_knife.cpp +++ b/src/game/shared/neo/weapons/weapon_knife.cpp @@ -318,11 +318,26 @@ inline void CWeaponKnife::ApplyDamageToHitTarget(trace_t& traceHit) // so objects cannot be "backstabbed" to launch them further if (isBackstab) { - //DevMsg("[%s] Backstab\n", IsServer() ? "SRV" : "CLI"); + // If this doesn't do any damage, step ApplyMultiDamage or the below: + // traceHit.m_pEnt->GetReceivedDamageScale(pPlayer); +#if 0 + const char* help = ""; +#ifdef GAME_DLL + if (!traceHit.m_pEnt->GetReceivedDamageScale(pPlayer)) + help = " (dmg scale 0, maybe in spawn protection?)"; +#endif + + DevMsg("[%s] \"%s\" backstabbed \"%s\"%s\n", + (IsServer() ? "SRV auth" : "CLI pred"), + pPlayer->GetNeoPlayerName(), + assert_cast(traceHit.m_pEnt)->GetNeoPlayerName(), + help + ); +#endif static constexpr float oneShotDamage = MAX_HEALTH_FOR_CLASS[NEO_CLASS_SUPPORT] + 1; - Assert(traceHit.m_pEnt->GetMaxHealth() < oneShotDamage); - info.SetMaxDamage(Max(info.GetMaxDamage(), oneShotDamage)); - info.SetDamage(oneShotDamage); + info.SetMaxDamage(info.GetMaxDamage() + oneShotDamage); + info.AddDamage(oneShotDamage); + Assert(traceHit.m_pEnt->GetMaxHealth() < info.GetDamage() + info.GetDamageBonus()); } traceHit.m_pEnt->DispatchTraceAttack(info, hitDirection, &traceHit); diff --git a/src/game/shared/takedamageinfo.cpp b/src/game/shared/takedamageinfo.cpp index a707d5096e..ee43a6db9e 100644 --- a/src/game/shared/takedamageinfo.cpp +++ b/src/game/shared/takedamageinfo.cpp @@ -250,7 +250,11 @@ void AddMultiDamage( const CTakeDamageInfo &info, CBaseEntity *pEntity ) g_MultiDamage.SetDamageForce( g_MultiDamage.GetDamageForce() + info.GetDamageForce() ); g_MultiDamage.SetDamagePosition( info.GetDamagePosition() ); g_MultiDamage.SetReportedPosition( info.GetReportedPosition() ); +#ifdef NEO + g_MultiDamage.SetMaxDamage( Max( g_MultiDamage.GetMaxDamage(), info.GetMaxDamage() ) ); +#else g_MultiDamage.SetMaxDamage( MAX( g_MultiDamage.GetMaxDamage(), info.GetDamage() ) ); +#endif g_MultiDamage.SetAmmoType( info.GetAmmoType() ); g_MultiDamage.SetCritType( info.GetCritType() );