perks/CurseStunTimer.cs

A Perk class for a game that periodically stuns the player. It tracks time since last stun, updates a UI display (cooldown, text, icon) and triggers a 2s stun with visual/audio feedback when the cooldown elapses.

Networking
using Sandbox;
using System;
using System.Numerics;
using System.Reflection;
using static Sandbox.VertexLayout;

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

	private TimeSince _timeSinceStunned;

	static CurseStunTimer()
	{
		Register<CurseStunTimer>(
			name: "Recurring Paralysis",
			imagePath: "textures/icons/vector/curse_stun_timer.png",
			description: level => $"Every [-]{GetValue( level, Mod.Cooldown )}s[/-], get stunned for 2s"
		);
	}

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

		DisplayCooldownColor = new Color( 0.8f, 0.5f, 0.5f, 0.6f );

		ShouldUpdate = true;
		_timeSinceStunned = 0f;
	}

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

	}

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

		DisplayCooldown = Utils.Map( _timeSinceStunned, 0f, GetValue( Level, Mod.Cooldown ), 0f, 1f );
		DisplayText = $"{MathX.CeilToInt( GetValue( Level, Mod.Cooldown ) - _timeSinceStunned )}";

		if ( Player.IsInvincible || Player.IsStunned )
			return;

		if ( _timeSinceStunned > GetValue( Level, Mod.Cooldown ) )
		{
			_timeSinceStunned = 0f;
			DisplayCooldown = 0f;

			if ( !Player.IsDead )
			{
				Player.Stun( null, null, 2f );

				HighlightColor = new Color( 1f, 0.25f, 0.3f );
				HighlightDuration = 0.5f;
				HighlightOpacity = 4f;
				Highlight();

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

				Manager.Instance.PlaySfxNearbyRpc( "fear", Player.Position2D, pitch: Game.Random.Float( 5.35f, 5.55f ), volume: 0.5f, maxDist: 350f );
			}
		}
	}

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