PushAbility.cs

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.

NetworkingFile Access
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();
        }
    }
}