swb_base/Weapon.AimAssist.cs
using SWB.Shared;
using System;
using System.Collections.Generic;
namespace SWB.Base;
public partial class Weapon
{
IPlayerBase aimAssistTarget;
TimeSince timeSinceAimAssist;
float assistSensitivityMod = 1;
float assistSensitivityTarget = 1;
float assistAreaWidth = 0;
bool assistWaitingForLookInput;
bool aimAssistDebug = false;
public virtual void OnAimStart() { }
public virtual void OnAimStop()
{
if ( !Settings.AimAssist || !Input.UsingController ) return;
assistSensitivityTarget = 1;
}
public virtual void OnAimAssistUpdate()
{
if ( !Settings.AimAssist || !Input.UsingController ) return;
// Slow down sens
var speed = IsAiming ? 2f : 4f;
assistSensitivityMod = MathUtil.FILerp( assistSensitivityMod, assistSensitivityTarget, speed );
Owner.InputSensitivity /= assistSensitivityMod;
// Debug
if ( aimAssistDebug )
GetAimAssistTarget();
}
public virtual void OnAimUpdate()
{
if ( !Settings.AimAssist || !Input.UsingController ) return;
// Stop assist when target dies (prevent snapping to other players)
if ( aimAssistTarget is not null && !aimAssistTarget.IsAlive )
{
aimAssistTarget = null;
assistWaitingForLookInput = true;
Input.Suppressed = true;
}
// Only work when actively looking
if ( assistWaitingForLookInput )
{
var lookMagnitude = MathF.Abs( Input.AnalogLook.pitch ) + MathF.Abs( Input.AnalogLook.yaw );
if ( lookMagnitude > 0.05f )
assistWaitingForLookInput = false;
else
{
assistSensitivityTarget = 1;
return;
}
}
if ( timeSinceAimAssist > 0.1f )
{
timeSinceAimAssist = 0;
aimAssistTarget = GetAimAssistTarget();
}
if ( aimAssistTarget is null || !aimAssistTarget.IsValid() )
{
assistSensitivityTarget = 1;
return;
}
// Sens lerp
var lowestSens = IsScoping ? 1 : 0.1f;
assistSensitivityTarget = Math.Clamp( 200f / assistAreaWidth, lowestSens, 6f );
// Aim assist dead zone
var sp = Owner.Camera.PointToScreenPixels( aimAssistTarget.EyePos );
var center = new Vector2( Screen.Width / 2, Screen.Height / 2 );
var delta2 = sp - center;
var dist = delta2.Length;
if ( dist < 10f ) return;
// Update angles
var from = Owner.Camera.WorldPosition;
var to = aimAssistTarget.EyePos + Vector3.Down * 20;
var dir = (to - from).Normal;
var desiredAngles = dir.EulerAngles;
var delta = (desiredAngles - Owner.Camera.WorldRotation.Angles()).Normal;
// Scale by time and strength (frame-rate independent)
var strength = 6f;
var assistOffset = delta * (Time.Delta * strength);
Owner.ApplyEyeAnglesOffset( assistOffset );
}
public virtual bool CanAimAssistOnTarget( IPlayerBase target )
{
return true;
}
public bool HasLOS( IPlayerBase target )
{
var from = Owner.Camera.WorldPosition;
var toPoints = new List<Vector3>()
{
target.EyePos,
// For now we only care if the eyepos is visible
// target.EyePos + Vector3.Down * 20 + Vector3.Left * 7,
// target.EyePos + Vector3.Down * 20 + Vector3.Right * 7,
// target.GameObject.WorldPosition + Vector3.Up * 10,
};
foreach ( var to in toPoints )
{
var tr = TraceBullet( Owner.GameObject, from, to, 1f );
if ( tr.GameObject == target.GameObject || tr.GameObject?.Parent == target.GameObject )
return true;
// DebugOverlay.Line( from, to, Color.Red, 1 );
}
return false;
}
public float ScaleRadiusByFov( float baseRadius, float baseFov, float currentFov )
{
var scaled = baseRadius * (baseFov / currentFov);
// Log.Info( $"radius: {baseRadius}, fov: {baseFov}, curr fov: {currentFov}, scaled: {scaled}" );
return scaled;
}
public IPlayerBase GetAimAssistTarget()
{
var players = Game.ActiveScene.GetAllComponents<IPlayerBase>();
var targetScore = 999999f;
var center = new Vector2( Screen.Width / 2, Screen.Height / 2 );
IPlayerBase target = null;
foreach ( var ply in players )
{
if ( ply == Owner || !ply.IsAlive || !CanAimAssistOnTarget( ply ) ) continue;
var pos = ply.GameObject.WorldPosition + Vector3.Up * 32;
var posToScreen = Owner.Camera.PointToScreenPixels( pos, out bool isBehind );
if ( isBehind ) continue;
var size = !IsScoping ? 110f : 110f;
var fov = Preferences.FieldOfView;
if ( IsScoping && ScopeInfo.FOV != -1 )
fov = ScopeInfo.FOV;
size = ScaleRadiusByFov( 110f, Preferences.FieldOfView, fov );
var threshold = 30000;
var dist = ply.GameObject.WorldPosition.DistanceSquared( Owner.GameObject.WorldPosition );
var maxDist = Math.Clamp( dist, 1, threshold );
var width = Math.Max( 20, size * (35000 / (maxDist + (dist * 0.05f))) );
var height = width * 1.5f;
var zone = new Vector4( center.x - width, center.x + width, center.y - height, center.y + height );
var inZone = false;
var score = 0f;
if ( posToScreen.x > zone.x && posToScreen.x < zone.y && posToScreen.y > zone.z && posToScreen.y < zone.w )
{
var aimDist = (posToScreen - center).Length;
score = aimDist + (dist / 1000);
if ( score < targetScore && HasLOS( ply ) )
{
inZone = true;
target = ply;
targetScore = score;
// Share for sens calculation
assistAreaWidth = width;
}
}
// Debug
if ( aimAssistDebug )
{
var color = inZone ? Color.Green : Color.Red;
var painter = Owner.IsFirstPerson ? Owner.ViewModelCamera.Hud : Owner.Camera.Hud;
var debugRect = new Rect()
{
Position = new( posToScreen.x - width, posToScreen.y - height ),
Size = new( width * 2, height * 2 )
};
var debugTextPos = debugRect.Position.WithX( debugRect.Position.x + 4 ).WithY( debugRect.Position.y + 4 );
var debugText =
$"Width: {Math.Round( width )}\n" +
$"Height: {Math.Round( height )}\n" +
$"Dist: {Math.Round( dist )}\n" +
$"Score: {Math.Round( score )}";
painter.DrawRect( debugRect, color.WithAlpha( 0.35f ) );
painter.DrawText( debugText, 12f, Color.Blue, debugTextPos );
}
}
return target;
}
}