fallingObjects/ArtilleryShell.cs

ArtilleryShell is a FallingObject that represents a spawned artillery projectile. It handles start/updating visuals and rotation, decides on impact behavior (drop loot or create an explosion using Manager RPCs), and spawns VFX and sound via a broadcast RPC.

NetworkingFile Access
using Sandbox;
using System;

public class ArtilleryShell : FallingObject
{
	[Property] public ModelRenderer ModelRenderer { get; set; }

	public const float EXPLOSION_RADIUS = 52f;
	public const float EXPLOSION_DAMAGE = 10f;

	private float _rotationSpeed;

	protected override void OnStart()
	{
		base.OnStart();

		TimeSinceSpawn = 0f;
		ModelRenderer.Tint = Color.White.WithAlpha( 0f );

		if ( IsProxy )
			return;

		_startingHeight = 1024f;
		Lifetime = Game.Random.Float( 1.75f, 2f );
		_rotationSpeed = Game.Random.Float( 100f, 600f );
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		ModelRenderer.Tint = Color.White.WithAlpha( Utils.Map( TimeSinceSpawn, 0f, 0.5f, 0f, 1f ) );

		if ( IsProxy )
			return;

		// todo: move with wind force

		WorldRotation = WorldRotation.RotateAroundAxis( Vector3.Forward, _rotationSpeed * Time.Delta );
	}

	public override void HitGround()
	{
		var itemChance = Shooter.IsValid() ? Shooter.Stats[PlayerStat.ArtillerySupplyChance] : 0f;

		if( Game.Random.Float( 0f, 1f ) < itemChance)
		{
			DropLoot( Shooter, (Vector2)WorldPosition );
		}
		else
		{
			float damage = EXPLOSION_DAMAGE * (Shooter.IsValid() ? Shooter.Stats[PlayerStat.ExplosionDamageMultiplier] : 1f);
			var radius = EXPLOSION_RADIUS * (Shooter.IsValid() ? Shooter.Stats[PlayerStat.ExplosionSizeMultiplier] * Shooter.Stats[PlayerStat.RadiusMultiplier] : 1f);

			Manager.Instance.CreateExplosionRpc( (Vector2)WorldPosition, radius, damage, repelRadius: radius * 1.15f, repelForce: damage * 20f, playerSource: Shooter, enemySource: null, enemyType: EnemyType.None, Color.Red );
		}

		GameObject.Destroy();
	}

	protected void DropLoot( Player player, Vector2 impactPos )
	{
		LootVfx( impactPos );

		float rand = Game.Random.Float( 0f, 1f );

		var pos = impactPos + Utils.GetRandomVector() * Game.Random.Float( 1f, 3f );
		var dir = (pos - impactPos).Normal;

		var numDeadPlayers = Manager.Instance.Players.Count - Manager.Instance.AlivePlayers.Count;

		if ( rand < 0.17f )
			Manager.Instance.SpawnItemRpc( "health_pack", pos, dir );
		else if ( rand < 0.20f )
			Manager.Instance.SpawnItemRpc( "magnet", pos, dir );
		else if ( rand < 0.27f )
			Manager.Instance.SpawnItemRpc( "bomb", pos, dir );
		else if ( rand < 0.4f )
			Manager.Instance.SpawnItemRpc( "reroll_item", pos, dir );
		else if ( rand < 0.60f )
			Manager.Instance.SpawnItemRpc( "banish_item", pos, dir );
		else if ( rand < 0.85f )
			Manager.Instance.SpawnItemRpc( "armor_item", pos, dir );
		else if ( rand < 0.88f && player.IsValid() )
			player.GiveRandomPerkItemRpc( pos, dir, Rarity.None );
		else if ( rand < 0.95f && numDeadPlayers > 0f )
			Manager.Instance.SpawnItemRpc( "revive_soul", pos, dir );
		else
			Manager.Instance.SpawnCoinRpc( pos, value: Game.Random.Int( 1, 2 ), dir );
	}

	[Rpc.Broadcast]
	public void LootVfx( Vector2 pos )
	{
		GameObject.Clone( "prefabs/effects/cloud.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 10f ) ) } );

		// todo: better sfx
		Manager.Instance.PlaySfxNearby( "enemy.explode", pos, pitch: Game.Random.Float( 1.8f, 2.2f ), volume: 0.7f, maxDist: 550f );
	}
}