Component that implements a charged radial push ability for a game object. Handles input charging, cooldowns, spawning effect and sound, tracing nearby objects and applying impulses, damage, interactions with shields, parry/reflect, barrels, anvils, slicers, rigidbodies and character controllers.
using Sandbox;
using System;
using System.Threading.Tasks;
public sealed class PushAbility : Component
{
[Property] public float MinPushForce { get; set; } = 15000f;
[Property] public float MaxPushForce { get; set; } = 45000f;
[Property] public float MinPushRadius { get; set; } = 250f;
[Property] public float MaxPushRadius { get; set; } = 500f;
[Property] public float MaxChargeTime { get; set; } = 1.5f;
[Property] public float Cooldown { get; set; } = 0.5f;
[Property] public GameObject PushParticle { get; set; }
[Property] public SoundEvent PushSound { get; set; }
private TimeSince _lastPush;
private float _currentCharge = 0f;
public bool IsCharging { get; private set; } = false;
public float ChargeProgress => MathF.Min( _currentCharge / MaxChargeTime, 1f );
public float CooldownProgress => MathF.Min( _lastPush / Cooldown, 1f );
public bool IsReady => _lastPush >= Cooldown;
protected override void OnUpdate()
{
if ( IsProxy ) return;
if ( _lastPush < Cooldown ) return;
if ( Input.Down( "attack1" ) )
{
IsCharging = true;
_currentCharge += Time.Delta;
if ( _currentCharge >= MaxChargeTime )
{
ExecutePush( 1f );
}
}
else if ( Input.Released( "attack1" ) && IsCharging )
{
float chargeRatio = MathF.Min( _currentCharge / MaxChargeTime, 1f );
ExecutePush( chargeRatio );
}
}
public void TryExecuteBotPush()
{
if ( IsReady )
{
ExecutePush( 0.8f );
}
}
private void ExecutePush( float chargeRatio )
{
IsCharging = false;
_lastPush = 0;
_currentCharge = 0f;
float finalForce = MathX.Lerp( MinPushForce, MaxPushForce, chargeRatio );
float finalRadius = MathX.Lerp( MinPushRadius, MaxPushRadius, chargeRatio );
float shopMult = ShopManager.Instance?.GetCurrentForceMultiplier() ?? 1.0f;
float suddenDeathMult = GameModeManager.Instance?.GetForceMultiplier() ?? 1.0f;
finalForce *= (shopMult * suddenDeathMult);
if ( PushParticle.IsValid() )
{
var p = PushParticle.Clone( WorldPosition );
_ = DestroyAfterDelay( p, 2000 );
}
if ( PushSound != null )
{
Sound.Play( PushSound, WorldPosition );
}
var startPos = WorldPosition;
var endPos = WorldPosition + Vector3.Up * 1f;
var hits = Scene.Trace.Sphere( finalRadius, startPos, endPos )
.IgnoreGameObject( GameObject )
.RunAll();
bool hitAnyTarget = false;
foreach ( var hit in hits )
{
if ( !hit.GameObject.IsValid() ) continue;
var targetObj = hit.GameObject.Root;
var targetShield = targetObj.Components.Get<ShieldAbility>() ?? hit.GameObject.Components.Get<ShieldAbility>();
if ( targetShield.IsValid() && targetShield.IsShieldActive )
{
continue;
}
var targetManeuver = targetObj.Components.Get<ManeuverAbility>() ?? hit.GameObject.Components.Get<ManeuverAbility>();
if ( targetManeuver.IsValid() && targetManeuver.IsParrying )
{
targetManeuver.OnSuccessfulReflect( WorldPosition );
var myCc = Components.Get<CharacterController>();
var reflectDir = (WorldPosition - targetObj.WorldPosition).Normal;
reflectDir.z = 0.5f;
if ( myCc.IsValid() )
{
myCc.Velocity += reflectDir * 1200f;
}
var myRb = Components.Get<Rigidbody>();
if ( myRb.IsValid() )
{
myRb.ApplyImpulse( reflectDir * finalForce * 1.5f );
}
CameraJuice.Instance?.Shake( 25f );
CameraJuice.Instance?.TriggerHitstop( 0.06f );
continue;
}
var barrel = targetObj.Components.Get<ExplosiveBarrel>() ?? hit.GameObject.Components.Get<ExplosiveBarrel>();
if ( barrel.IsValid() )
{
barrel.Explode();
hitAnyTarget = true;
continue;
}
float currentForce = finalForce;
var anvil = targetObj.Components.Get<HeavyAnvil>() ?? hit.GameObject.Components.Get<HeavyAnvil>();
if ( anvil.IsValid() )
{
currentForce *= anvil.PushResistanceMultiplier;
}
var pushDirection = (hit.GameObject.WorldPosition - WorldPosition).Normal;
pushDirection.z = 0.4f;
var stress = targetObj.Components.Get<StructuralStress>() ?? hit.GameObject.Components.Get<StructuralStress>();
if ( stress.IsValid() )
{
stress.ApplyImpulseDamage( pushDirection * currentForce );
hitAnyTarget = true;
}
var slicer = targetObj.Components.Get<MeshSlicer>() ?? hit.GameObject.Components.Get<MeshSlicer>();
if ( slicer.IsValid() && currentForce > 20000f )
{
slicer.SliceOrBreak( hit.GameObject.WorldPosition, pushDirection );
hitAnyTarget = true;
}
var health = targetObj.Components.Get<PlayerHealth>() ?? hit.GameObject.Components.Get<PlayerHealth>();
if ( health.IsValid() )
{
health.TakeDamage( currentForce * 0.001f, pushDirection );
hitAnyTarget = true;
}
var rb = hit.GameObject.Components.Get<Rigidbody>();
if ( rb.IsValid() )
{
rb.ApplyImpulse( pushDirection * currentForce );
hitAnyTarget = true;
}
var cc = targetObj.Components.Get<CharacterController>() ?? hit.GameObject.Components.Get<CharacterController>();
if ( cc.IsValid() )
{
cc.Velocity += pushDirection * (currentForce * 0.05f);
hitAnyTarget = true;
}
}
float baseShake = chargeRatio * 15f;
if ( hitAnyTarget )
{
baseShake += 10f;
GameServices.RecordPush();
if ( chargeRatio > 0.6f )
{
CameraJuice.Instance?.TriggerHitstop( 0.04f );
}
}
CameraJuice.Instance?.Shake( baseShake );
}
private async Task DestroyAfterDelay( GameObject obj, int delayMs )
{
await Task.Delay( delayMs );
if ( obj.IsValid() )
{
obj.Destroy();
}
}
}