things/enemies/Barrel.cs

Enemy subclass representing a destructible barrel object. It sets barrel-specific stats and behavior, updates position with wind/friction, handles flinch visuals, spawns loot and gibs on death, and plays hurt/death sound effects.

File AccessNetworking
using Sandbox;
using Sandbox.UI;
using System;
using System.Threading;

public class Barrel : Enemy
{
	public override EnemyType EnemyType => EnemyType.Barrel;
	public override float GetMaxHealth()
	{
		return 45f;
	}

	public override bool CanHaveTarget => false;
	public override bool CanAttack => false;
	public override bool CanTurn => false;
	public override bool CanBeBackstabbed => false;
	public override bool CountsAsKill => false;
	public override bool CanMove => false;
	public override bool IsInanimate => true;
	public override bool CanBeTargeted => false;

	protected string _debrisName;

	public override string GibFolder => "wood";
	public override float OverrideGibChance => 1f;

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

		CoinValueMin = 0;
		CoinValueMax = 0;
		CoinChance = 0f;

		PushStrength = 10000f;
		Weight = 2f;

		_debrisName = "barrel_debris";

		if ( IsProxy )
			return;

		Deceleration = 2.5f;
	}

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

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"{Health}", new global::Transform( WorldPosition ) );

		if ( IsProxy || IsDying || IsSpawning )
			return;

		if ( Manager.Instance.IsWindActive )
			Velocity += (Manager.Instance.GlobalWindForce / (Weight * 3f)) * Time.Delta;

		Velocity *= Math.Max( 1f - Time.Delta * Deceleration * Manager.Instance.GlobalFrictionModifier, 0f );

		WorldPosition += (Vector3)Velocity * Time.Delta;
	}

	public override void Flinch( float time, Vector2 dir )
	{
		base.Flinch( time, dir );

		ModelRenderer.LocalRotation = new Angles( Game.Random.Float( -10f, 10f ), 0f, Game.Random.Float( -10f, 10f ) );
		//Transform.ClearInterpolation();
	}

	public override void StopFlinching()
	{
		base.StopFlinching();

		ModelRenderer.LocalRotation = new Angles( 0f, 0f, 0f );
	}

	protected override void HandleAnimation()
	{

	}

	public override void SetAnim( string name, bool forceRestart = false )
	{
		
	}

	protected override void DropLoot( Player player )
	{
		var dropDir = player.IsValid() ? (player.Position2D - Position2D).Normal : Utils.GetRandomVector();
		float lootChanceBonus = player.IsValid() ? player.GetSyncStat( PlayerStat.BarrelLootChance ) : 0f;

		// COINS
		int numCoins = Game.Random.Int( 2, 3 );
		for ( int i = 0; i < numCoins; i++ )
		{
			var pos = Position2D + Utils.GetRandomVectorInCone( dropDir, coneDegrees: 180f ) * Game.Random.Float( 1f, 3f );
			var dir = (pos - Position2D).Normal;

			var magnetizeChance = player.IsValid() ? player.GetSyncStat( PlayerStat.KillMagnetizeCoinChance ) : 0f;
			Player magnetizePlayer = Game.Random.Float( 0f, 1f ) < magnetizeChance ? player : null;

			Manager.Instance.SpawnCoin( pos, value: Game.Random.Int( 1, 2 ), dir, magnetizePlayer );
		}

		// HEALTH PACK
		float lowestHpPercent = 1f;
		foreach ( var p in Manager.Instance.AlivePlayers )
			lowestHpPercent = MathF.Min( lowestHpPercent, p.Health / p.GetSyncStat( PlayerStat.MaxHp ) );

		var healthPackChance = Utils.Map( lowestHpPercent, 1f, 0f, 0.2f, 0.75f );
		healthPackChance *= Utils.Select( Manager.Instance.Difficulty, 1.25f, 1f, 1f );
		//Log.Info( $"Barrel DropLoot - health_pack: {healthPackChance * (1f + lootChanceBonus) * 100f:F1}%" );
		if ( Game.Random.Float( 0f, 1f ) < healthPackChance * (1f + lootChanceBonus) )
			Manager.Instance.SpawnItemRpc( "health_pack", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );

		// MAGNET
		if ( Manager.Instance.TimeSinceMagnet > 50f )
		{
			var magnetChance = 0.05f * Utils.Map( Manager.Instance.TimeSinceMagnet, 50f, 480f, 1f, 6.5f, EasingType.Linear );
			//Log.Info( $"Barrel DropLoot - magnet: {magnetChance * (1f + lootChanceBonus) * 100f:F1}%" );
			if ( Game.Random.Float( 0f, 1f ) < magnetChance * (1f + lootChanceBonus) )
			{
				var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
				Manager.Instance.SpawnItem( "magnet", pos, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
			}

		}

		// ARMOR
		var armorChance = Utils.Select( Manager.Instance.Difficulty, 0.33f, 0.15f, 0.1f );
		//Log.Info( $"Barrel DropLoot - armor: {armorChance * (1f + lootChanceBonus) * 100f:F1}%" );
		if ( Game.Random.Float( 0f, 1f ) < armorChance * (1f + lootChanceBonus) )
		{
			var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
			Manager.Instance.SpawnItem( "armor_item", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
		}

		var bombChance = 0.15f;
		//Log.Info( $"Barrel DropLoot - bomb: {bombChance * (1f + lootChanceBonus) * 100f:F1}%" );
		if ( Game.Random.Float( 0f, 1f ) < bombChance * (1f + lootChanceBonus) )
		{
			var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
			Manager.Instance.SpawnItem( "bomb", Position2D, Utils.GetRandomVector() ); // todo: player that destroyed barrel should be Player that created bomb?
		}

		var rerollChance = Utils.Select( Manager.Instance.Difficulty, 0.25f, 0.2f, 0.2f );
		//Log.Info( $"Barrel DropLoot - reroll (x2): {rerollChance * (1f + lootChanceBonus) * 100f:F1}%" );
		for ( int i = 0; i < 2; i++ )
		{
			if ( Game.Random.Float( 0f, 1f ) < rerollChance * (1f + lootChanceBonus) )
			{
				var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
				Manager.Instance.SpawnItem( "reroll_item", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
			}
		}

		var banishChance = 0.04f;
		//Log.Info( $"Barrel DropLoot - banish: {banishChance * (1f + lootChanceBonus) * 100f:F1}%" );
		if ( Game.Random.Float( 0f, 1f ) < banishChance * (1f + lootChanceBonus) )
		{
			var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
			Manager.Instance.SpawnItem( "banish_item", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
		}

		var numDeadPlayers = Manager.Instance.Players.Count - Manager.Instance.AlivePlayers.Count;
		if ( numDeadPlayers > 0 )
		{
			var reviveChance = Utils.Map( numDeadPlayers, 1, Manager.MAX_PLAYERS - 1, 0.6f, 1f );
			//Log.Info( $"Barrel DropLoot - revive: {reviveChance * (1f + lootChanceBonus) * 100f:F1}%" );
			if ( Game.Random.Float( 0f, 1f ) < reviveChance * (1f + lootChanceBonus) )
			{
				var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
				Manager.Instance.SpawnItem( "revive_soul", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 180f ) );
			}
		}

		CheckHazard();
	}

	protected void CheckHazard()
	{
		bool isHazard = false;
		Player targetPlayer = null;

		foreach ( var player in Manager.Instance.Players )
		{
			if ( !player.IsValid() )
				continue;

			var hazardChange = player.GetSyncStat( PlayerStat.BarrelHazardChance );
			if( Game.Random.Float( 0f, 1f ) < hazardChange )
			{
				isHazard = true;
				targetPlayer = player;
				break;	
			}
		}

		if( isHazard && targetPlayer.IsValid() )
			ChestEvil.SpawnHazard( targetPlayer.Position2D, chestPos: Position2D, enemySource: this, enemyType: this.EnemyType );
	}

	protected override void SpawnGibs( Vector2 dir, float force, DamageType damageType )
	{
		//GameObject.Clone( $"prefabs/effects/{_debrisName}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( WorldPosition.WithZ( Game.Random.Float( 40f, 60f ) ), Rotation.Identity ) } );

		GameObject.Clone( $"prefabs/effects/dark_cloud_explosion.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( WorldPosition.WithZ( Game.Random.Float( 40f, 60f ) ), Rotation.Identity ) } );

		SpawnGibs( "fragment", Game.Random.Int( 4, 6 ), force, damageType );
		SpawnGibs( "fragment_2", Game.Random.Int( 3, 6 ), force, damageType );
		SpawnGibs( "fragment_3", Game.Random.Int( 3, 6 ), force, damageType );
	}

	void SpawnGibs( string name, int count, float force, DamageType damageType )
	{
		bool isExplodingBarrel = this is BarrelExploding;

		for ( int i = 0; i < count; i++ )
		{
			var color = Color.Lerp( TintFullHp, TintZeroHp, Game.Random.Float( 0.25f, 1f ) );
			if ( isExplodingBarrel )
				color = Color.Lerp( color, new Color( 0.4f, 0f, 0f ), 0.8f );

			SpawnGoreGib(
				$"{GibFolder}/{name}",
				localPos: new Vector3( 0f, 0, Game.Random.Float( 20f, 45f ) ) + Vector3.Random * Game.Random.Float( 0f, 20f ),
				localRot: Rotation.Random,
				scaleMultiplier: Game.Random.Float( 1f, 1.5f ),
				dir: Vector3.Random,
				force: force * Game.Random.Float( 0.05f, 0.8f ),
				color,
				damageType
			);
		}
	}

	public override void PlayHurtSfx( float damage, DamageType damageType, Vector3 hitPos, Player player, DamageResultFlags damageFlags )
	{
		// sfx
		if ( _realTimeSinceHurtSfx > 0.0175f )
		{
			if ( damage == 0f )
			{
				Manager.Instance.PlaySfxNearby( "barrel.hit", hitPos, pitch: Game.Random.Float( 1.5f, 1.6f ), volume: 1.3f, maxDist: 300f );
			}
			else if ( damageType == DamageType.Punch ) { Manager.Instance.PlaySfxNearby( "barrel.hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 1.2f, 1.4f, EasingType.SineIn ), volume: 1.3f, maxDist: 400f ); }
			//else if ( damageType == DamageType.DashSlash ) { Manager.Instance.PlaySfxNearby( "player.dash.slash.hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 0.9f, 1.1f, EasingType.SineIn ) * Game.Random.Float( 0.95f, 1.05f ), volume: 0.8f, maxDist: 350f ); }
			else if ( damageType == DamageType.Fire ) { Manager.Instance.PlaySfxNearby( "burn_2", hitPos, pitch: Game.Random.Float( 1.15f, 1.35f ), volume: 0.7f, maxDist: 300f ); }
			else if ( damageType == DamageType.Poison ) { Manager.Instance.PlaySfxNearby( "poisoned", hitPos, pitch: Game.Random.Float( 1.55f, 1.65f ), volume: 0.35f, maxDist: 300f ); }
			else if ( damageType == DamageType.SpikerHead ) { Manager.Instance.PlaySfxNearby( "spike.stab", hitPos, pitch: Game.Random.Float( 0.95f, 1f ), volume: 0.8f, maxDist: 300f ); }
			else if ( damageType == DamageType.SpitterProjectile || damageType == DamageType.SpitterProjectileHoming ) { Manager.Instance.PlaySfxNearby( "splash", hitPos, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f ); }
			else if ( damageType == DamageType.Aoe || damageType == DamageType.BulletSplash ) { /* no sfx */ }
			else if ( damageType == DamageType.Radiation ) { /* no sfx */ }
			else if ( damageType == DamageType.Shock ) { /* no sfx */ }
			else if ( damageType == DamageType.Explosion ) { /* no sfx */ }
			else if ( damageType == DamageType.JumpFinish ) { Manager.Instance.PlaySfxNearby( "slam", hitPos, pitch: Game.Random.Float( 0.85f, 0.95f ), volume: 0.8f, maxDist: 150f ); }
			else { Manager.Instance.PlaySfxNearby( "barrel.hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 1f, 1.25f, EasingType.SineIn ) * Game.Random.Float( 0.95f, 1.05f ), volume: 1.3f, maxDist: 400f ); }
			// todo: OrbitingBlade hit sfx

			_realTimeSinceHurtSfx = 0f;
		}
	}

	protected override void PlayDeathSfx( Vector2 pos )
	{
		Manager.Instance.PlaySfxNearby( "barrel.die", pos, pitch: Game.Random.Float( 0.9f, 1.3f ), volume: 1.8f, maxDist: 500f );
	}
}