things/enemies/Crate.cs

An Enemy subclass representing a crate pickup and optional mystery box. It configures visual and physics properties, handles collisions by applying push forces, drops various loot (coins, health packs, magnets, grenades, reroll pickups) on death, and can spawn a random enemy when a mystery box.

Reflection
using Sandbox;

public class Crate : Enemy
{
	public override bool CanBleed => false;

	public bool IsMysteryBox { get; set; }

	[Property] public Sprite MysteryBoxSprite { get; set; }

	protected override void OnAwake()
	{
		//OffsetY = -0.4f;
		ShadowScale = 1.3f;
		ShadowFullOpacity = 0.8f;
		ShadowOpacity = 0f;

		Scale = 0.95f;

		base.OnAwake();

		//AnimSpeed = 2f;
		//BasePivotY = 0.05f;

		//Sprite.Texture = Texture.Load("textures/sprites/crate.vtex");

		//ScaleFactor = 0.95f;
		//Sprite.Size = new Vector2( 1f, 1f ) * ScaleFactor;

		PushStrength = 8f;
		Deceleration = 15f;

		Radius = 0.25f;
		Health = 45f;

		if ( Manager.Instance.Difficulty < 0 )
			Health = 35f;

		MaxHealth = Health;

		CanTurn = false;
		CanAttack = false;
		FlipX = false;

		Sprite.PlayAnimation( AnimSpawnPath );

		AnimIdlePath = "idle";

		if ( IsProxy )
			return;

		CollideWith.Add( typeof( Enemy ) );
		CollideWith.Add( typeof( Player ) );
	}

	public void BecomeMysteryBox()
	{
		Sprite.Sprite = MysteryBoxSprite;
		IsMysteryBox = true;

		TintFullHp = new Color( 1f, 0.8f, 1f );
		TintZeroHp = new Color( 0.5f, 0.2f, 0.5f );
		TintDeath = new Color( 0.6f, 0.3f, 0.6f );
		Sprite.Tint = TintFullHp;

		FlipX = false;
	}

	protected override void UpdatePosition( float dt )
	{
		base.UpdatePosition( dt );

		WorldPosition += (Vector3)Velocity * dt;
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( other is Enemy enemy && !enemy.IsDying )
		{
			var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
			Velocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;
		}
		else if ( other is Player player )
		{
			if ( !player.IsDead )
			{
				Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;
			}
		}
	}

	public override void DropLoot( Player player )
	{
		float RAND_POS = 0.2f;

		int num_coins = Game.Random.Int( 2, (IsMysteryBox ? 7 : 3) );
		for ( int i = 0; i < num_coins; i++ )
		{
			var pos = Position2D + new Vector2( Game.Random.Float( -RAND_POS, RAND_POS ), Game.Random.Float( -RAND_POS, RAND_POS ) );
			var vel = (pos - Position2D) * Game.Random.Float( 2f, 7f );
			Manager.Instance.SpawnCoin( pos, vel, value: Game.Random.Int( CoinValueMin, CoinValueMax ), force: true );
		}

		var health_pack_chance = player != null ? Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.2f, 0.75f ) : 0.1f;
		if ( IsMysteryBox )
		{
			health_pack_chance = 1f;
		}
		else
		{
			if ( Manager.Instance.Difficulty == -1 )
				health_pack_chance *= 1.3f;
			else if ( Manager.Instance.Difficulty >= 3 )
				health_pack_chance *= 0.7f;
		}

		int numHealthPacks = IsMysteryBox ? Game.Random.Int( 1, 2 ) : 1;
		for( int i = 0; i < numHealthPacks; i++ )
		{
			if ( Game.Random.Float( 0f, 1f ) < health_pack_chance )
			{
				var pos = Position2D + new Vector2( Game.Random.Float( -RAND_POS, RAND_POS ), Game.Random.Float( -RAND_POS, RAND_POS ) );
				var vel = (pos - Position2D) * Game.Random.Float( 2f, 6f );
				Manager.Instance.SpawnHealthPack( pos, vel );
			}
		}

		if ( Manager.Instance.TimeSinceMagnet > 50f || IsMysteryBox )
		{
			var magnet_chance = 0.09f * Utils.Map( Manager.Instance.TimeSinceMagnet, 50f, Utils.Map(Manager.Instance.Difficulty, 0, 3, 480f, 750f), 1f, 5.5f, EasingType.Linear );
			if ( IsMysteryBox )
			{
				magnet_chance = 1f;
			}
			else
			{
				if ( Manager.Instance.Difficulty == -1 )
					magnet_chance *= 1.3f;
				else if ( Manager.Instance.Difficulty > 0 )
					magnet_chance *= 0.85f;
				else if ( Manager.Instance.Difficulty > 0 )
					magnet_chance *= 0.5f;
			}

			if ( Game.Random.Float( 0f, 1f ) < magnet_chance )
			{
				var pos = Position2D + new Vector2( Game.Random.Float( -RAND_POS, RAND_POS ), Game.Random.Float( -RAND_POS, RAND_POS ) );
				var vel = (pos - Position2D) * Game.Random.Float( 2f, 6f );
				Manager.Instance.SpawnMagnet( pos, vel );
			}
		}

		//var revive_chance = Manager.Instance.GetPlayers( alive: false ).Count() * 0.4f;
		//if ( Game.Random.Float( 0f, 1f ) < revive_chance )
		//{
		//	var pos = Position2D + new Vector2( Game.Random.Float( -RAND_POS, RAND_POS ), Game.Random.Float( -RAND_POS, RAND_POS ) );
		//	Manager.Instance.SpawnReviveSoul( pos, vel: (pos - Position2D) * Game.Random.Float( 2f, 6f ) );
		//}

		int numGrenadeChances = IsMysteryBox ? Game.Random.Int( 1, 6 ) : Game.Random.Int(1, (Manager.Instance.Difficulty >= 3 ? 5 : 2));
		for( int i = 0; i < numGrenadeChances; i++ )
		{
			var grenade_chance = IsMysteryBox ? 1f : 0.125f;
			if ( player != null && Game.Random.Float( 0f, 1f ) < grenade_chance )
			{
				var pos = Position2D + new Vector2( Game.Random.Float( -RAND_POS, RAND_POS ), Game.Random.Float( -RAND_POS, RAND_POS ) );
				var vel = (pos - Position2D) * Game.Random.Float( 2f, 6f );
				player.SpawnGrenade( pos, vel );
			}
		}

		if( Manager.Instance.Difficulty >= 3 )
		{
			float reroll_chance = IsMysteryBox ? 0.75f : 0.22f;
			int num_chances = IsMysteryBox ? 6 : 2;
			for( int i = 0; i < num_chances; i++ )
			{
				if(Game.Random.Float(0f, 1f) < reroll_chance)
				{
					var pos = Position2D + new Vector2( Game.Random.Float( -RAND_POS, RAND_POS ), Game.Random.Float( -RAND_POS, RAND_POS ) );
					var vel = (pos - Position2D) * Game.Random.Float( 2f, 6f );
					Manager.Instance.SpawnRerollPickup( pos, vel );
				}
			}
		}

		if(IsMysteryBox)
			SpawnMysteryEnemy();
	}

	void SpawnMysteryEnemy()
	{
		List<(TypeDescription Type, float Weight)> enemyTypes = new List<(TypeDescription, float)>
		{
			(TypeLibrary.GetType( typeof( Exploder ) ), 10f),
			(TypeLibrary.GetType( typeof( ExploderElite ) ), 2f),
			(TypeLibrary.GetType( typeof( Spitter ) ), 8f),
			(TypeLibrary.GetType( typeof( SpitterElite ) ), 2f),
			(TypeLibrary.GetType( typeof( Charger ) ), 6f),
			(TypeLibrary.GetType( typeof( ChargerElite ) ), 1f),
			(TypeLibrary.GetType( typeof( Spiker ) ), 6f),
			(TypeLibrary.GetType( typeof( SpikerElite ) ), 0.5f),
			(TypeLibrary.GetType( typeof( Zombie ) ), 0.1f),
			(TypeLibrary.GetType( typeof( ZombieElite ) ), 2f),
			(TypeLibrary.GetType( typeof( Runner ) ), 4f),
			(TypeLibrary.GetType( typeof( RunnerElite ) ), 0.5f)
		};

		if(Manager.Instance.Difficulty >= 2)
		{
			enemyTypes.Add( (TypeLibrary.GetType( typeof( ExploderSpecial ) ), 1f) );
			enemyTypes.Add( (TypeLibrary.GetType( typeof( SpitterSpecial ) ), 1f) );
			enemyTypes.Add( (TypeLibrary.GetType( typeof( SpitterEliteSpecial ) ), 1f) );
			enemyTypes.Add( (TypeLibrary.GetType( typeof( ChargerSpecial ) ), 1f) );
			enemyTypes.Add( (TypeLibrary.GetType( typeof( SpikerSpecial ) ), 1f) );
			enemyTypes.Add( (TypeLibrary.GetType( typeof( RunnerEliteSpecial ) ), 1f) );
		}

		TypeDescription chosenType = null;

		float totalWeight = enemyTypes.Sum( x => x.Weight );
		var rand = Game.Random.Float( 0f, totalWeight );

		for ( int i = enemyTypes.Count - 1; i >= 0; i-- )
		{
			var (type, weight) = enemyTypes[i];
			rand -= weight;

			if ( rand < 0f )
			{
				chosenType = type;
				break;
			}
		}

		var enemy = Manager.Instance.SpawnEnemy( chosenType, Position2D, forceSpawn: true );

		float charmChance = 0.25f;

		if ( Manager.Instance.Difficulty == -1 )
			charmChance *= 2.5f;

		if ( Game.Random.Float( 0f, 1f ) < charmChance )
		{
			enemy.Charm();
			enemy.Flash( 0f );
		}
	}
}