perks/CurseSpawnSpikerHead.cs

A unique curse perk that periodically spawns a "Spiker head" near the player. It tracks cooldown time, shows crack indicators, requests crack and spawn effects via Manager RPCs, and updates HUD display properties like icon scale, angle, highlight and cooldown visuals.

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;
				//}
		}
	}
}