perks/CurseSpawnSpikerHead.cs

A unique curse perk that periodically spawns a 'Spiker head' near the player. It tracks a cooldown, visual crack marker, and triggers RPCs on the Manager to show cracks and then spawn the spiker head shortly after.

Networking
using System;
using Sandbox;

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

	private TimeSince _timeSinceSpawn;

	private Vector2 _cracksPos;
	private TimeSince _timeSinceCracks;
	private bool _isShooting;

	static CurseSpawnSpikerHead()
	{
		Register<CurseSpawnSpikerHead>(
			name: "Nemesis Spikes",
			imagePath: "textures/icons/vector/curse_spawn_spiker_head.png",
			description: level => $"Spawn a Spiker head\nnearby every [-]{(int)GetValue( level, Mod.Time )}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 );

		_timeSinceSpawn = 0f;
	}

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

	}

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

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

		if( _isShooting && _timeSinceCracks > 0.7f )
		{
			Manager.Instance.SpawnSpikerHeadRpc(
				_cracksPos
			);

			_isShooting = false;
		}

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

			_cracksPos = Manager.Instance.ClampPosToBounds( Player.Position2D + Utils.GetRandomVectorInCone( dir, 60f ) * Player.Radius + Player.Velocity * Game.Random.Float( 0.3f, 3f ) );
			Manager.Instance.SpawnSpikerHeadCracksRpc( _cracksPos, color: new Color( 0.09f, 0.02f, 0.01f ) );

			_isShooting = true;
			_timeSinceCracks = 0f;

			_timeSinceSpawn = 0f;

			Highlight();

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

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

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

				//switch( level )
				//{
				//	case 1: default: return 7f;
				//	case 2: return 3f;
				//}
		}
	}
}