perks/CurseSpawnZombies.cs

A Perk component named CurseSpawnZombies that periodically spawns a temporary zombie near the player. It tracks time since last spawn, triggers a spawn via Manager.Instance.SpawnEnemyRpc when the interval elapses, and updates UI display properties like icon and cooldown.

Networking
using System;
using Sandbox;

[Perk( Rarity.Unique, curse: true, alwaysOfferDebug: false )]
public class CurseSpawnZombies : Perk
{
	private enum Mod { Time };

	private TimeSince _timeSinceZombie;

	static CurseSpawnZombies()
	{
		Register<CurseSpawnZombies>(
			name: "Unmarked Graves",
			imagePath: "textures/icons/vector/curse_spawn_zombie.png",
			description: level => $"Spawn a temporary Zombie\nnearby every [-]{GetValue( level, Mod.Time ).ToString( "0.#" )}s[/-]"
		);
	}

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

		ShouldUpdate = true;

		HighlightColor = new Color( 0.5f, 0f, 0f );
		HighlightDuration = 0.75f;
		HighlightOpacity = 3.5f;

		DisplayCooldownColor = new Color( 0.75f, 0.2f, 0.25f );

		_timeSinceZombie = 0f;
	}

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

	}

	public override void Update( float dt )
	{
		base.Update( dt );

		var timeReq = GetValue( Level, Mod.Time );

		if(_timeSinceZombie > timeReq)
		{
			var dir = Player.MoveVector.LengthSquared > 0f
				? Player.MoveVector
				: Utils.GetRandomVector();

			Manager.Instance.SpawnEnemyRpc( 
				EnemyType.ZombieTemporary, 
				Player.Position2D + Utils.GetRandomVectorInCone( dir, 60f ) * Player.Radius + Player.Velocity * Game.Random.Float( 1f, 2.5f ) 
			);

			_timeSinceZombie = 0f;

			Highlight();

			IconScale = Game.Random.Float( 1.1f, 1.2f );
			IconAngleOffset = Game.Random.Float( 5f, 8f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
		}

		DisplayText = Level > 1 
			? " " 
			: $"{MathX.CeilToInt( timeReq - _timeSinceZombie )}";
		DisplayCooldown = Utils.Map( _timeSinceZombie, 0f, timeReq, 0f, 1f );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Time:
			default:
				return 3f;
		}
	}
}