From a1f6a37189ef6fb71624dde1a1523914171c1df6 Mon Sep 17 00:00:00 2001 From: jota Date: Mon, 8 Jun 2026 17:21:44 -0300 Subject: [PATCH] cl_dll: add gamepad aim assist (soft-lock) with debug overlay Hold-to-aim soft-lock for gamepad/touch: while a bindable button (+aimassist) is held, acquire the nearest visible enemy within a wide cone and steer the crosshair toward it (magnetism) with sticky slowdown. All behaviour is governed by aim_assist* cvars and is off by default. - input_xash3d.cpp: target acquisition (team/alive/range/cone/LOS), magnetism + sticky in IN_Move, +/-aimassist commands and cvars. Eye uses the real camera origin (v_origin) to avoid close-range parallax; cone is distance-adjusted; LOS traces world-only. - aimassist HUD element: debug overlay (state, target, angles), view cone ring, aim-direction and nearest-enemy markers, head marker. - entity.cpp: glow shell over the target (HUD_AddEntity). - kb_act.lst: expose "Aim Assist" in the Controls key-binding menu. - aim_assist_debug acts as a master switch for all debug visuals. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cs16client-extras/gfx/shell/kb_act.lst | 75 +++++++ cl_dll/entity.cpp | 12 + cl_dll/hud.cpp | 1 + cl_dll/hud.h | 2 + cl_dll/hud/aimassist.cpp | 147 +++++++++++++ cl_dll/include/hud/aimassist.h | 37 ++++ cl_dll/input/input_xash3d.cpp | 206 ++++++++++++++++++ 7 files changed, 480 insertions(+) create mode 100644 3rdparty/cs16client-extras/gfx/shell/kb_act.lst create mode 100644 cl_dll/hud/aimassist.cpp create mode 100644 cl_dll/include/hud/aimassist.h diff --git a/3rdparty/cs16client-extras/gfx/shell/kb_act.lst b/3rdparty/cs16client-extras/gfx/shell/kb_act.lst new file mode 100644 index 000000000..e77af795d --- /dev/null +++ b/3rdparty/cs16client-extras/gfx/shell/kb_act.lst @@ -0,0 +1,75 @@ +"blank" "==========================" +"blank" "#Valve_Movement_Title" +"blank" "==========================" +"+forward" "#Valve_Move_Forward" +"+back" "#Valve_Move_Back" +"+left" "#Valve_Turn_Left" +"+right" "#Valve_Turn_Right" +"+moveleft" "#Valve_Move_Left" +"+moveright" "#Valve_Move_Right" +"+speed" "#Valve_Walk" +"+jump" "#Valve_Jump" +"+duck" "#Valve_Duck" +"+moveup" "#Valve_Swim_Up" +"+movedown" "#Valve_Swim_Down" +"+lookup" "#Valve_Look_Up" +"+lookdown" "#Valve_Look_Down" +"force_centerview" "#Cstrike_Reset_View" +"+strafe" "#Valve_Strafe_Modifier" +"+mlook" "#Cstrike_Mouse_Look" +"+klook" "#Cstrike_Keyboard_Look" +"blank" "==========================" +"blank" "#Valve_Communication_Title" +"blank" "==========================" +"+voicerecord" "#Valve_Use_Voice_Communication" +"radio1" "#Cstrike_Standard_Radio" +"radio2" "#Cstrike_Group_Radio" +"radio3" "#Cstrike_Report_Radio" +"+commandmenu" "#Valve_Activate_In_Game_GUI" +"messagemode" "#Valve_Chat_Message" +"messagemode2" "#Valve_Team_Message" +"blank" "==========================" +"blank" "#Cstrike_Menu_Title" +"blank" "==========================" +"buy" "#Cstrike_Buy_Menu" +"buyammo1" "#Cstrike_Buy_Primary_Ammo" +"buyammo2" "#Cstrike_Buy_Secondary_Ammo" +"buyequip" "#Cstrike_Buy_Equip_Menu" +"autobuy" "#Cstrike_Auto_Buy" +"rebuy" "#Cstrike_ReBuy" +"chooseteam" "#Cstrike_Select_Team" +"slot10" "#Cstrike_Menu_Item_0" +"slot1" "#Cstrike_Menu_Item_1" +"slot2" "#Cstrike_Menu_Item_2" +"slot3" "#Cstrike_Menu_Item_3" +"slot4" "#Cstrike_Menu_Item_4" +"slot5" "#Cstrike_Menu_Item_5" +"slot6" "#Cstrike_Menu_Item_6" +"slot7" "#Cstrike_Menu_Item_7" +"slot8" "#Cstrike_Menu_Item_8" +"slot9" "#Cstrike_Menu_Item_9" +"+showscores" "#Valve_Display_Scores" +"showbriefing" "#Cstrike_Mission_Briefing" +"blank" "==========================" +"blank" "#Cstrike_Combat_Title" +"blank" "==========================" +"+attack" "#Cstrike_Fire" +"+attack2" "#Cstrike_Weapon_Special_Func" +"+reload" "#Valve_Reload_Weapon" +"invprev" "#Valve_Previous_Weapon" +"invnext" "#Valve_Next_Weapon" +"lastinv" "#Valve_Last_Weapon_Used" +"nightvision" "#Cstrike_Nightvision" +"drop" "#Cstrike_Drop_Weapon" +"blank" "==========================" +"blank" "#Valve_Miscellaneous_Title" +"blank" "==========================" +"+use" "#Valve_Use_Items" +"impulse 100" "#Valve_Flashlight" +"impulse 201" "#Valve_Spray_Logo" +"snapshot" "#Valve_Take_Screen_Shot" +"quit" "#Valve_Quit_Game" +"blank" "==========================" +"blank" "CS16Client" +"blank" "==========================" +"+aimassist" "Aim Assist" diff --git a/cl_dll/entity.cpp b/cl_dll/entity.cpp index 582ef5303..0396ecad3 100644 --- a/cl_dll/entity.cpp +++ b/cl_dll/entity.cpp @@ -48,6 +48,18 @@ int DLLEXPORT HUD_AddEntity( int type, struct cl_entity_s *ent, const char *mode VectorCopy(ent->curstate.origin, ent->origin); VectorCopy(ent->curstate.angles, ent->angles); } + // aim assist: glow shell over the highlighted target. + // Enabled by either the dedicated highlight cvar or the master debug cvar. + if( ent->player && g_iAimAssistTarget && ent->index == g_iAimAssistTarget + && ( ( aim_assist_highlight && aim_assist_highlight->value ) + || ( aim_assist_debug && aim_assist_debug->value ) ) ) + { + ent->curstate.renderfx = kRenderFxGlowShell; + ent->curstate.rendercolor.r = 0; + ent->curstate.rendercolor.g = 255; + ent->curstate.rendercolor.b = 0; + ent->curstate.renderamt = 75; // shell thickness + } break; case ET_BEAM: case ET_TEMPENTITY: diff --git a/cl_dll/hud.cpp b/cl_dll/hud.cpp index a8f0f077a..426a80b66 100644 --- a/cl_dll/hud.cpp +++ b/cl_dll/hud.cpp @@ -403,6 +403,7 @@ void CHud :: Init( void ) m_StatusIcons.Init(); m_Radar.Init(); m_Scenario.Init(); + m_AimAssist.Init(); // chat, death notice, status bars and other m_SayText.Init(); diff --git a/cl_dll/hud.h b/cl_dll/hud.h index f23bb7ce7..4b296863c 100644 --- a/cl_dll/hud.h +++ b/cl_dll/hud.h @@ -290,6 +290,7 @@ class CHudAmmoSecondary: public CHudBase #include "health.h" #include "radar.h" +#include "aimassist.h" #define FADE_TIME 100 @@ -1089,6 +1090,7 @@ class CHud CHudRadar m_Radar; CHudSpectatorGui m_SpectatorGui; CHudScenario m_Scenario; + CHudAimAssist m_AimAssist; // user messages CHudMsgFunc(Damage); diff --git a/cl_dll/hud/aimassist.cpp b/cl_dll/hud/aimassist.cpp new file mode 100644 index 000000000..9e2b9567a --- /dev/null +++ b/cl_dll/hud/aimassist.cpp @@ -0,0 +1,147 @@ +/* +aimassist.cpp -- debug overlay + target highlight for the gamepad aim assist +*/ + +#define _USE_MATH_DEFINES // for M_PI (used by DEG2RAD on MSVC) +#include "stdio.h" +#include "math.h" +#include "hud.h" +#include "cl_util.h" +#include "draw_util.h" +#include "triangleapi.h" +#include "cl_entity.h" +#include "aimassist.h" + +// Project a world point to screen pixels. Returns false if behind the viewer. +static bool AA_Project( float *world, int &sx, int &sy ) +{ + vec3_t screen; + if( gEngfuncs.pTriAPI->WorldToScreen( world, screen ) ) + return false; + sx = (int)XPROJECT( screen[0] ); + sy = (int)YPROJECT( screen[1] ); + return true; +} + +int CHudAimAssist::Init( void ) +{ + gHUD.AddHudElem( this ); + m_iFlags = HUD_DRAW; + return 1; +} + +int CHudAimAssist::VidInit( void ) +{ + return 1; +} + +int CHudAimAssist::Draw( float flTime ) +{ + if( !aim_assist_debug || !aim_assist_debug->value ) + return 1; + + int lh = gHUD.m_iFontHeight + 1; + int x = XRES( 8 ); + int y = YRES( 70 ); + char line[128]; + + // background box + FillRGBA( x - 4, y - 4, XRES( 240 ), lh * 5 + 8, 0, 0, 0, 150 ); + + DrawUtils::DrawHudString( x, y, ScreenWidth, "AIM ASSIST DEBUG", 255, 200, 0 ); + y += lh; + + snprintf( line, sizeof( line ), "enabled: %s key: %s", + ( aim_assist && aim_assist->value ) ? "ON" : "OFF", + g_bAimAssistKey ? "HELD" : "released" ); + DrawUtils::DrawHudString( x, y, ScreenWidth, line, 255, 255, 255 ); + y += lh; + + if( g_iAimAssistTarget ) + { + const char *name = g_PlayerInfoList[g_iAimAssistTarget].name; + snprintf( line, sizeof( line ), "target: %s (#%d) d=%.0f a=%.1f", + name ? name : "?", g_iAimAssistTarget, g_flAimAssistDist, g_flAimAssistAngle ); + DrawUtils::DrawHudString( x, y, ScreenWidth, line, 0, 255, 0 ); + } + else + { + DrawUtils::DrawHudString( x, y, ScreenWidth, "target: none", 255, 100, 100 ); + } + y += lh; + + if( g_iAimAssistNearestIdx ) + snprintf( line, sizeof( line ), "nearest enemy: %.1f deg (lock_fov %.0f)", + g_flAimAssistNearestAngle, aim_assist_lock_fov ? aim_assist_lock_fov->value : 0.0f ); + else + snprintf( line, sizeof( line ), "nearest enemy: none (team/alive/range)" ); + DrawUtils::DrawHudString( x, y, ScreenWidth, line, 200, 200, 200 ); + y += lh; + + snprintf( line, sizeof( line ), "applying: %s", g_bAimAssistApplying ? "YES" : "no" ); + DrawUtils::DrawHudString( x, y, ScreenWidth, line, 255, 255, 255 ); + + // --- view cone + computed aim direction (to diagnose eye/forward) --- + // magenta dot = where the assist thinks you aim (eye + fwd). It MUST sit on + // your crosshair (screen center); if it is off-center, eye/forward is wrong. + { + vec3_t p; + int px, py; + for( int j = 0; j < 3; j++ ) + p[j] = g_vecAimEye[j] + g_vecAimFwd[j] * 300.0f; + if( AA_Project( p, px, py ) ) + FillRGBA( px - 3, py - 3, 6, 6, 255, 0, 255, 255 ); + + // cone edge ring at the acquisition fov, projected into the world + float fov = aim_assist_lock_fov ? aim_assist_lock_fov->value : 45.0f; + float cf = cos( DEG2RAD( fov ) ), sf = sin( DEG2RAD( fov ) ); + for( int k = 0; k < 24; k++ ) + { + float a = DEG2RAD( k * 15.0f ); + float ca = cos( a ), sa = sin( a ); + vec3_t dir, q; + int qx, qy; + for( int j = 0; j < 3; j++ ) + { + dir[j] = g_vecAimFwd[j] * cf + ( g_vecAimRight[j] * ca + g_vecAimUp[j] * sa ) * sf; + q[j] = g_vecAimEye[j] + dir[j] * 300.0f; + } + if( AA_Project( q, qx, qy ) ) + FillRGBA( qx - 1, qy - 1, 3, 3, 0, 200, 255, 220 ); + } + } + + // nearest enemy (yellow) even if outside the cone -- shows where it projects + if( g_iAimAssistNearestIdx ) + { + cl_entity_t *n = gEngfuncs.GetEntityByIndex( g_iAimAssistNearestIdx ); + int nx, ny; + if( n && AA_Project( n->curstate.origin, nx, ny ) ) + FillRGBA( nx - 4, ny - 4, 8, 8, 255, 255, 0, 220 ); + } + + // floating marker over the target's head + if( g_iAimAssistTarget ) + { + cl_entity_t *t = gEngfuncs.GetEntityByIndex( g_iAimAssistTarget ); + if( t ) + { + vec3_t head, screen; + VectorCopy( t->origin, head ); + head[2] += 40.0f; // approx. above the head + + // WorldToScreen returns non-zero when the point is behind the viewer + if( gEngfuncs.pTriAPI->WorldToScreen( head, screen ) == 0 ) + { + int sx = XPROJECT( screen[0] ); + int sy = YPROJECT( screen[1] ); + int s = XRES( 5 ); + int r = g_bAimAssistApplying ? 0 : 255; + int g = g_bAimAssistApplying ? 255 : 200; + FillRGBA( sx - s, sy - s, s * 2, s * 2, r, g, 0, 220 ); + } + } + } + + return 1; +} diff --git a/cl_dll/include/hud/aimassist.h b/cl_dll/include/hud/aimassist.h new file mode 100644 index 000000000..18d73b404 --- /dev/null +++ b/cl_dll/include/hud/aimassist.h @@ -0,0 +1,37 @@ +/* +aimassist.h - debug overlay + target highlight for the gamepad aim assist +*/ +#pragma once +#ifndef AIMASSIST_H +#define AIMASSIST_H + +// Shared state produced by the aim assist in input_xash3d.cpp and consumed by +// the HUD overlay (this element) and the model highlight (entity.cpp). +extern bool g_bAimAssistKey; // dedicated aim button is held +extern int g_iAimAssistTarget; // entity index of the chosen target (0 = none) +extern bool g_bAimAssistApplying; // assist actively steering (key held + target) +extern float g_flAimAssistDist; // distance to the chosen target +extern float g_flAimAssistAngle; // angular separation (deg) to the chosen target +extern int g_iAimAssistNearestIdx; // nearest visible enemy by angle, ignoring the cone +extern float g_flAimAssistNearestAngle; // its angle (to diagnose a too-tight cone) + +extern vec3_t g_vecAimEye; // camera origin used by the assist (for the debug cone) +extern vec3_t g_vecAimFwd; // view forward +extern vec3_t g_vecAimRight; // view right +extern vec3_t g_vecAimUp; // view up + +extern cvar_t *aim_assist; // master on/off (shown in the overlay) +extern cvar_t *aim_assist_debug; // text overlay + floating head marker +extern cvar_t *aim_assist_highlight; // glow shell over the target model +extern cvar_t *aim_assist_fov; // legacy tight cone +extern cvar_t *aim_assist_lock_fov; // acquisition cone (shown in the overlay / drawn as the ring) + +class CHudAimAssist : public CHudBase +{ +public: + int Init( void ); + int VidInit( void ); + int Draw( float flTime ); +}; + +#endif // AIMASSIST_H diff --git a/cl_dll/input/input_xash3d.cpp b/cl_dll/input/input_xash3d.cpp index 036da3f9c..9f739e360 100644 --- a/cl_dll/input/input_xash3d.cpp +++ b/cl_dll/input/input_xash3d.cpp @@ -1,9 +1,17 @@ +#define _USE_MATH_DEFINES // for M_PI (used by DEG2RAD/RAD2DEG on MSVC) #include "hud.h" #include "usercmd.h" #include "cvardef.h" #include "kbutton.h" #include "keydefs.h" #include "input.h" +#include "cl_util.h" +#include "cl_entity.h" +#include "const.h" +#include "pmtrace.h" +#include "event_api.h" +#include "pm_defs.h" +#include #define PITCH 0 #define YAW 1 @@ -14,6 +22,32 @@ cvar_t *sensitivity; cvar_t *in_joystick; cvar_t *evdev_grab; +// Aim assist (gamepad soft-lock, Max Payne 3 style) +bool g_bAimAssistKey = false; // hold state of the dedicated aim button (shared) +cvar_t *aim_assist; // master on/off +cvar_t *aim_assist_fov; // (legacy) tight cone, kept for compatibility +cvar_t *aim_assist_lock_fov; // acquisition cone half-angle while the button is held (degrees) +cvar_t *aim_assist_pull; // magnetism strength 0..1 +cvar_t *aim_assist_slow; // sticky slowdown factor applied to stick input +cvar_t *aim_assist_range; // max target distance (units) +cvar_t *aim_assist_wallcheck; // require line of sight to the target +cvar_t *aim_assist_debug; // debug overlay (text + head marker) +cvar_t *aim_assist_highlight; // glow shell over the target model + +// Shared state for the debug overlay (cl_dll/hud/aimassist.cpp) and highlight (entity.cpp) +int g_iAimAssistTarget = 0; // entity index of the chosen target (0 = none) +bool g_bAimAssistApplying = false; // assist actively steering (key held + target) +float g_flAimAssistDist = 0.0f; // distance to the chosen target +float g_flAimAssistAngle = 0.0f; // angular separation (deg) to the chosen target +int g_iAimAssistNearestIdx = 0; // nearest visible enemy by angle, ignoring the cone +float g_flAimAssistNearestAngle = 0.0f; // its angle (diagnose a too-tight cone) + +// view basis used by the assist, stored each frame for the debug cone visualization +vec3_t g_vecAimEye = { 0, 0, 0 }; +vec3_t g_vecAimFwd = { 0, 0, 0 }; +vec3_t g_vecAimRight = { 0, 0, 0 }; +vec3_t g_vecAimUp = { 0, 0, 0 }; + float ac_forwardmove; float ac_sidemove; @@ -24,6 +58,7 @@ bool bMouseInUse = false; extern Vector dead_viewangles; extern bool evdev_open; +extern vec3_t v_origin; // actual camera/eye origin, set each frame in view.cpp #define F 1U<<0 // Forward #define B 1U<<1 // Back @@ -132,6 +167,99 @@ void IN_ClientLookEvent( float relyaw, float relpitch ) rel_pitch += relpitch; } +// Console commands bound through the Controls menu (+aimassist / -aimassist) +void IN_AimAssistDown( void ) { g_bAimAssistKey = true; } +void IN_AimAssistUp( void ) { g_bAimAssistKey = false; } + +// Returns true if no world geometry blocks the line from start to end. +// We trace against the world ONLY (PM_WORLD_ONLY): tracing against solid players +// would stop the ray at the very enemy we are checking and report "not visible". +static bool AimAssist_Visible( float *start, float *end ) +{ + pmtrace_t tr; + + gEngfuncs.pEventAPI->EV_SetUpPlayerPrediction( false, true ); + gEngfuncs.pEventAPI->EV_PushPMStates(); + gEngfuncs.pEventAPI->EV_SetSolidPlayers( -1 ); + gEngfuncs.pEventAPI->EV_SetTraceHull( 2 ); + gEngfuncs.pEventAPI->EV_PlayerTrace( start, end, PM_STUDIO_BOX | PM_WORLD_ONLY, -1, &tr ); + gEngfuncs.pEventAPI->EV_PopPMStates(); + + return tr.fraction >= 0.95f; // ~1.0 means nothing solid in the world blocks the view +} + +// Picks the enemy closest to the crosshair within the assist cone, alive and visible. +static cl_entity_t *AimAssist_FindTarget( float *eye, float *fwd ) +{ + cl_entity_t *local = gEngfuncs.GetLocalPlayer(); + int maxc = gEngfuncs.GetMaxClients(); + int me = local ? local->index : 0; + float fov = aim_assist_lock_fov->value; // wide acquisition cone (grab nearest target in front) + float maxRange = aim_assist_range->value; + float bestAngle = 9999.0f; + float nearestAngle = 9999.0f; + cl_entity_t *best = NULL; + + g_iAimAssistNearestIdx = 0; + g_flAimAssistNearestAngle = 0.0f; + + for( int i = 1; i <= maxc; i++ ) + { + if( i == me ) + continue; + + cl_entity_t *e = gEngfuncs.GetEntityByIndex( i ); + if( !e || !e->player ) + continue; + if( e->curstate.solid == SOLID_NOT || g_PlayerExtraInfo[i].dead ) + continue; // dead / non-solid + if( g_iTeamNumber != 0 && g_PlayerExtraInfo[i].teamnumber == g_iTeamNumber ) + continue; // teammate (skip filter in FFA where team is 0) + + vec3_t dir; + VectorSubtract( e->curstate.origin, eye, dir ); + float dist = sqrt( dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2] ); + if( dist < 1.0f || dist > maxRange ) + continue; + + float inv = 1.0f / dist; + dir[0] *= inv; dir[1] *= inv; dir[2] *= inv; + + float d = DotProduct( dir, fwd ); + if( d > 1.0f ) d = 1.0f; + float angle = RAD2DEG( acos( d ) ); + + // diagnostics: nearest enemy by angle, ignoring the cone and LOS + if( angle < nearestAngle ) + { + nearestAngle = angle; + g_iAimAssistNearestIdx = i; + g_flAimAssistNearestAngle = angle; + } + + // distance-adjusted cone: a closer enemy subtends a larger angle, so the + // assist window grows when you're near them (fixes "target none" up close). + float effFov = fov + RAD2DEG( atan2( 32.0f, dist ) ); + if( angle > effFov ) + continue; // crosshair not on the enemy + if( angle >= bestAngle ) + continue; // keep the one closest to the crosshair + + if( aim_assist_wallcheck->value ) + { + vec3_t tgt; + VectorCopy( e->curstate.origin, tgt ); + if( !AimAssist_Visible( eye, tgt ) ) + continue; + } + + bestAngle = angle; + best = e; + } + + return best; +} + // Rotate camera and add move values to usercmd void IN_Move( float frametime, usercmd_t *cmd ) { @@ -176,6 +304,56 @@ void IN_Move( float frametime, usercmd_t *cmd ) rel_yaw *= sensitivity->value; rel_pitch *= sensitivity->value; } + + // --- Aim assist: find target (for steering and/or the debug overlay) --- + cl_entity_t *aaTarget = NULL; + vec3_t aaDesired = { 0, 0, 0 }; + g_iAimAssistTarget = 0; + g_bAimAssistApplying = false; + g_flAimAssistDist = g_flAimAssistAngle = 0.0f; + + // store the view basis every frame so the debug cone can be drawn (even while dead) + VectorCopy( v_origin, g_vecAimEye ); + AngleVectors( viewangles, g_vecAimFwd, g_vecAimRight, g_vecAimUp ); + + // scan when steering (key held) OR when a debug/highlight view wants the target + bool aaScan = aim_assist->value && !CL_IsDead() + && ( g_bAimAssistKey || aim_assist_debug->value || aim_assist_highlight->value ) + && !( gHUD.m_MOTD.cl_hide_motd->value == 0.0f && gHUD.m_MOTD.m_bShow ); + if( aaScan ) + { + cl_entity_t *local = gEngfuncs.GetLocalPlayer(); + if( local ) + { + aaTarget = AimAssist_FindTarget( g_vecAimEye, g_vecAimFwd ); + if( aaTarget ) + { + vec3_t dir; + VectorSubtract( aaTarget->curstate.origin, g_vecAimEye, dir ); + float dist = sqrt( dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2] ); + float hyp = sqrt( dir[0] * dir[0] + dir[1] * dir[1] ); + aaDesired[YAW] = RAD2DEG( atan2( dir[1], dir[0] ) ); + aaDesired[PITCH] = -RAD2DEG( atan2( dir[2], hyp ) ); // GoldSrc: positive pitch looks down + + g_iAimAssistTarget = aaTarget->index; + g_flAimAssistDist = dist; + if( dist > 0.0f ) + { + float dot = DotProduct( dir, g_vecAimFwd ) / dist; + g_flAimAssistAngle = RAD2DEG( acos( dot > 1.0f ? 1.0f : dot ) ); + } + + // sticky: dampen manual rotation, but only while actually steering (key held) + if( g_bAimAssistKey ) + { + g_bAimAssistApplying = true; + rel_yaw *= aim_assist_slow->value; + rel_pitch *= aim_assist_slow->value; + } + } + } + } + if(gHUD.m_MOTD.cl_hide_motd->value == 0.0f && gHUD.m_MOTD.m_bShow) { gHUD.m_MOTD.scroll += rel_pitch; @@ -191,6 +369,21 @@ void IN_Move( float frametime, usercmd_t *cmd ) ac_sidemove = 0; } } + + // --- Aim assist: magnetism pull toward the target (only while steering) --- + if( aaTarget && g_bAimAssistKey ) + { + float dyaw = aaDesired[YAW] - viewangles[YAW]; + float dpitch = aaDesired[PITCH] - viewangles[PITCH]; + while( dyaw > 180.0f ) dyaw -= 360.0f; // shortest way around + while( dyaw < -180.0f ) dyaw += 360.0f; + float t = aim_assist_pull->value * frametime * 60.0f; + if( t > 1.0f ) t = 1.0f; + if( t < 0.0f ) t = 0.0f; + viewangles[YAW] += dyaw * t; + viewangles[PITCH] += dpitch * t; + } + if (viewangles[PITCH] > cl_pitchdown->value) viewangles[PITCH] = cl_pitchdown->value; if (viewangles[PITCH] < -cl_pitchup->value) @@ -279,5 +472,18 @@ void IN_Init( void ) cl_laddermode = gEngfuncs.pfnRegisterVariable ( "cl_laddermode", "2", FCVAR_ARCHIVE ); evdev_grab = gEngfuncs.pfnGetCvarPointer("evdev_grab"); + // Aim assist (bindable via Controls menu as "+aimassist") + gEngfuncs.pfnAddCommand( "+aimassist", IN_AimAssistDown ); + gEngfuncs.pfnAddCommand( "-aimassist", IN_AimAssistUp ); + aim_assist = gEngfuncs.pfnRegisterVariable( "aim_assist", "0", FCVAR_ARCHIVE ); + aim_assist_fov = gEngfuncs.pfnRegisterVariable( "aim_assist_fov", "10", FCVAR_ARCHIVE ); + aim_assist_lock_fov = gEngfuncs.pfnRegisterVariable( "aim_assist_lock_fov", "45", FCVAR_ARCHIVE ); + aim_assist_pull = gEngfuncs.pfnRegisterVariable( "aim_assist_pull", "0.25", FCVAR_ARCHIVE ); + aim_assist_slow = gEngfuncs.pfnRegisterVariable( "aim_assist_slow", "0.4", FCVAR_ARCHIVE ); + aim_assist_range = gEngfuncs.pfnRegisterVariable( "aim_assist_range", "1500", FCVAR_ARCHIVE ); + aim_assist_wallcheck = gEngfuncs.pfnRegisterVariable( "aim_assist_wallcheck", "1", FCVAR_ARCHIVE ); + aim_assist_debug = gEngfuncs.pfnRegisterVariable( "aim_assist_debug", "0", FCVAR_ARCHIVE ); + aim_assist_highlight = gEngfuncs.pfnRegisterVariable( "aim_assist_highlight", "0", FCVAR_ARCHIVE ); + ac_forwardmove = ac_sidemove = rel_yaw = rel_pitch = 0; }