Weapons/GaussGun/GaussWeapon.cs
using Sandbox.Rendering;
using Sandbox.Utility;
public class GaussWeapon : BaseBulletWeapon
{
/// <summary>
/// Having this as its own event separate from weapon events until we have a reason to use it for another weapon
/// </summary>
public interface IGaussWeaponEvents : ISceneEvent<IGaussWeaponEvents>
{
/// <summary>
/// Called when we consume a bit of ammo
/// </summary>
void OnConsumedAmmo();
}
/// <summary>
/// Shot frequency delay
/// </summary>
[Property] public float TimeBetweenShots { get; set; } = 0.1f;
/// <summary>
/// How much damage
/// </summary>
[Property] public float Damage { get; set; } = 12.0f;
/// <summary>
/// How many units deep can geometry be for us to penetrate directly through it.
/// </summary>
[Property] public float PenetrationThickness { get; set; } = 32f;
[Property] public GameObject ImpactEffect { get; set; }
[Property] public GameObject LargeImpactEffect { get; set; }
/// <summary>
/// What looping sound should we play while charging the gun
/// </summary>
[Property, Feature( "Charge" )]
public SoundEvent ChargeSoundEvent { get; set; }
/// <summary>
/// A curve to get a damage value directly from ChargePower
/// </summary>
[Property, Feature( "Charge" )]
public Curve ChargeDamage { get; set; }
/// <summary>
/// How much force is applied when we shoot a charged shot
/// </summary>
[Property, Feature( "Charge" )]
public Curve ChargeForce { get; set; }
/// <summary>
/// How frequently does the ammo drain whilst charging the gauss gun
/// </summary>
[Property, Feature( "Charge" )]
public float ChargeAmmoDrainFrequency { get; set; } = 0.2f;
/// <summary>
/// How long do we charge for until the gun overloads
/// </summary>
[Property, Feature( "Overload" )]
public float OverloadTime { get; set; } = 5f;
/// <summary>
/// How much damage to inflict on the player if the gun overloads
/// </summary>
[Property, Feature( "Overload" )]
public float OverloadDamage { get; set; } = 20f;
[Property, Feature( "Overload" )]
public SoundEvent OverloadSound { get; set; }
/// <summary>
/// The charge loop sound handle, we handle its lifetime.
/// </summary>
SoundHandle chargeHandle;
/// <summary>
/// Normalized charge power between 0 and 1
/// </summary>
float chargePower = 0;
/// <summary>
/// Is this gun charging up to shoot?
/// </summary>
bool isCharging = false;
/// <summary>
/// How long has it been since we started charging?
/// </summary>
TimeSince timeSinceChargeStart = 0;
/// <summary>
/// How long since we drained ammo while charged?
/// </summary>
TimeSince timeSinceAmmoTick;
protected override void OnDisabled()
{
StopCharge();
base.OnDisabled();
}
[Rpc.Host]
private void HurtSelf()
{
if ( !Owner.IsValid() ) return;
Owner.OnDamage( new DamageInfo( OverloadDamage, Owner.GameObject, GameObject ) );
Sound.Play( OverloadSound, WorldPosition );
}
public override void OnControl( Player player )
{
base.OnControl( player );
if ( isCharging )
{
if ( chargeHandle is not null )
chargeHandle.Position = WorldPosition;
if ( timeSinceChargeStart > OverloadTime )
{
StopCharge();
HurtSelf();
if ( !player.Controller.ThirdPerson && player.IsLocalPlayer )
{
var target = new Vector3( Random.Shared.Float( -10, -15 ), Random.Shared.Float( -25, 0 ), 0 );
new Sandbox.CameraNoise.Punch( target, 1.0f, 3, 0.5f );
new Sandbox.CameraNoise.Shake( 0.3f, 1.2f );
}
return;
}
if ( chargePower < 1f && timeSinceAmmoTick > ChargeAmmoDrainFrequency )
{
timeSinceAmmoTick = 0;
if ( !TakeAmmo( 1 ) )
{
ShootCharged( player );
return;
}
IGaussWeaponEvents.PostToGameObject( GameObject.Root, x => x.OnConsumedAmmo() );
}
chargePower += 0.5f * Time.Delta;
chargePower = chargePower.Clamp( 0, 1 );
chargeHandle.Pitch = chargePower.Remap( 0, 1, 0.5f, 1.2f );
if ( !player.Controller.ThirdPerson && player.IsLocalPlayer )
{
new Sandbox.CameraNoise.Shake( 0.3f * chargePower.Remap( 0, 1, 0.5f, 3f ), Time.Delta );
}
}
if ( !isCharging && Input.Down( "attack1" ) )
{
Shoot( player );
}
if ( Input.Pressed( "Attack2" ) )
{
if ( !TakeAmmo( 1 ) )
{
TryAutoReload();
return;
}
else if ( CanShoot() )
{
timeSinceChargeStart = 0;
isCharging = true;
chargeHandle?.Stop();
chargeHandle = Sound.Play( ChargeSoundEvent );
StartAttack();
}
}
if ( Input.Released( "Attack2" ) && isCharging )
{
StopAttack();
ShootCharged( player );
}
}
public override bool IsInUse() => isCharging;
/// <summary>
/// Shoot a charged shot
/// </summary>
/// <param name="player"></param>
void ShootCharged( Player player )
{
ShootBullet( player, ChargeDamage.Evaluate( chargePower ), true, true );
// Fling the player
var controller = player.GetComponent<PlayerController>();
// Should expose all these to properties
controller.Jump( -player.EyeTransform.ForwardRay.Forward * ChargeForce.Evaluate( chargePower ) );
StopCharge();
}
/// <summary>
/// Stop charging the gun
/// </summary>
void StopCharge()
{
isCharging = false;
chargeHandle?.Stop();
chargePower = 0;
}
/// <summary>
/// Constructs a trace and returns it, doesn't run it yet!
/// </summary>
SceneTrace GetBulletTrace( Player player, Vector3 start, Vector3 end, float radius )
{
return Scene.Trace.Ray( start, end )
.IgnoreGameObjectHierarchy( player.GameObject )
.WithCollisionRules( "bullet" )
.UseHitboxes()
.Size( radius );
}
/// <summary>
/// Runs a trace with all the data we have supplied it, and returns the result
/// </summary>
IEnumerable<SceneTraceResult> GetShootTraceResults( Player player )
{
var hits = new List<SceneTraceResult>();
var start = player.EyeTransform.Position;
var rot = Rotation.LookAt( player.EyeTransform.Forward );
var forward = rot.Forward.WithAimCone( 2 );
var original = GetBulletTrace( player, start, player.EyeTransform.Position + forward * 4096f, 2f )
.RunAll();
if ( original.Count() < 1 )
{
hits.Add( GetBulletTrace( player, start, player.EyeTransform.Position + forward * 4096f, 2f ).Run() );
return hits;
}
// Run through and fix the start positions for the traces
// By using the last end position as the start
var startPos = original.ElementAt( 0 ).StartPosition;
List<SceneTraceResult> fixedPath = new();
for ( int i = 0; i < original.Count(); i++ )
{
var el = original.ElementAt( i );
fixedPath.Add( el with { StartPosition = startPos } );
startPos = el.EndPosition;
}
var entries = new List<(SceneTraceResult Trace, float Thickness)>();
// Then, trace backwards from the end so we can get exit points and thickness
for ( int i = fixedPath.Count - 1; i >= 0; i-- )
{
var el = fixedPath.ElementAt( i );
// Do a trace back, from the end position to the start, this'll give us the LAST entry's exit point.
var backTrace = GetBulletTrace( player, el.EndPosition, el.StartPosition, 2f )
.Run();
var impact = backTrace.EndPosition;
// From that, we can calculate the surface thickness
float thickness = (el.StartPosition - impact).Length;
// Return the element starting at the exit point, it's more useful that way.
el = el with { StartPosition = impact };
entries.Insert( 0, (el, thickness) );
}
// Thickness detection
{
var thickness = 0f;
foreach ( var el in entries )
{
thickness += el.Thickness;
if ( thickness >= PenetrationThickness )
break;
hits.Add( el.Trace );
}
}
return hits
.Where( x => x.Hit && x.Distance > 0f );
}
void Shoot( Player player )
{
if ( !CanShoot() || !TakeAmmo( 1 ) )
{
TryAutoReload();
return;
}
ShootBullet( player, Damage );
}
public void ShootBullet( Player player, float damage, bool isCharged = false, bool shouldRicochet = true )
{
AddShootDelay( TimeBetweenShots );
bool shouldPenetrate = isCharged;
IGaussWeaponEvents.PostToGameObject( GameObject.Root, x => x.OnConsumedAmmo() );
int count = 0;
var traces = GetShootTraceResults( player );
foreach ( var tr in traces )
{
ShootEffects( tr.EndPosition, false, tr.Normal, tr.GameObject, tr.Surface, count > 0 ? tr.StartPosition : null );
TraceAttack( TraceAttackInfo.From( tr, damage, damage > 70 ? [DamageTags.GibAlways] : null ) );
count++;
if ( player.IsLocalPlayer )
{
HitMarker.CreateFromTrace( tr );
}
if ( tr.Hit )
{
SpecialImpactEffects( tr.EndPosition, tr.Normal, tr.GameObject, tr.Surface );
}
if ( !shouldPenetrate ) break;
}
TimeSinceShoot = 0;
if ( shouldRicochet && traces.Any() )
{
// Grab the last sufficient trace, we only want to ricochet at the end
var tr = traces.Last();
var reflectDir = Vector3.Reflect( tr.Direction, tr.Normal ).Normal;
var angle = reflectDir.Angle( tr.Direction );
// Some acute angle
if ( angle < 45f )
{
tr = GetBulletTrace( player, tr.EndPosition, tr.EndPosition + (reflectDir * 4096), 2f ).Run();
ShootEffects( tr.EndPosition, false, tr.Normal, tr.GameObject, tr.Surface, count > 0 ? tr.StartPosition : null );
TraceAttack( TraceAttackInfo.From( tr, damage ) );
if ( player.IsLocalPlayer )
{
HitMarker.CreateFromTrace( tr );
}
if ( tr.Hit )
{
SpecialImpactEffects( tr.EndPosition, tr.Normal, tr.GameObject, tr.Surface );
}
}
}
player.Controller.EyeAngles += new Angles( Random.Shared.Float( -0.2f, -0.3f ), Random.Shared.Float( -0.1f, 0.1f ), 0 );
if ( !player.Controller.ThirdPerson && player.IsLocalPlayer )
{
var target = new Vector3( Random.Shared.Float( -10, -15 ), Random.Shared.Float( -10, 0 ), 0 );
target *= chargePower.Remap( 0, 1, 1, 10 );
new Sandbox.CameraNoise.Punch( target, 1.0f, 3, 0.5f );
new Sandbox.CameraNoise.Shake( 0.3f, 1.2f );
}
}
public override void DrawCrosshair( HudPainter hud, Vector2 center )
{
var tss = TimeSinceShoot.Relative.Remap( 0, 0.2f, 1, 0 );
var gap = 6 + Easing.EaseOut( tss ) * 32;
var len = 6;
var w = 2;
Color color = !CanShoot() ? UI.CrosshairInactive : UI.CrosshairActive;
hud.DrawLine( center + Vector2.Left * (len + gap) * 2, center + Vector2.Left * gap * 2, w, color );
hud.DrawLine( center - Vector2.Left * (len + gap) * 2, center - Vector2.Left * gap * 2, w, color );
hud.DrawLine( center + Vector2.Up * (len + gap), center + Vector2.Up * gap, w, color );
hud.DrawLine( center - Vector2.Up * (len + gap), center - Vector2.Up * gap, w, color );
}
[Rpc.Broadcast]
public void SpecialImpactEffects( Vector3 hitpoint, Vector3 normal, GameObject hitObject, Surface hitSurface )
{
if ( Application.IsDedicatedServer ) return;
if ( ImpactEffect is null )
return;
var impact = ImpactEffect.Clone();
impact.WorldPosition = hitpoint + normal;
impact.WorldRotation = Rotation.LookAt( normal ) * new Angles( 90, 0, 0 );
impact.SetParent( hitObject, true );
}
}