perks/PerkMarkEnemy.cs

A gameplay perk class 'PerkMarkEnemy' that marks an enemy when the player touches them. It applies periodic damage to the marked enemy, manages visual highlight properties, enforces a short cooldown between marks, and unmarks previous targets when replaced or when the perk is removed.

Networking
using System;
using Sandbox;

[Perk( Rarity.Epic, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false )]
public class PerkMarkEnemy : Perk
{
	private const float COOLDOWN = 0.1f;

	private Enemy _markedEnemy;
	private TimeSince _timeSinceMark;

	static PerkMarkEnemy()
	{
		Register<PerkMarkEnemy>(
			name: "Mark of Doom",
			imagePath: "textures/icons/vector/mark_enemy.png",
			description: level => $"Touching an enemy marks them\nThe marked enemy takes {GetDamage( level )} dmg/s",
			upgradeDescription: level => $"Touching an enemy marks them\nThe marked enemy\ntakes {GetDamage( level - 1 )}→{GetDamage( level )} dmg/s"
		);
	}

	private static float GetDamage( int level ) => 3f * level;

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

		ShouldUpdate = true;
		_timeSinceMark = COOLDOWN;

		HighlightColor = new Color( 0.75f, 0.4f, 1f );
		HighlightDuration = 0.2f;
		HighlightOpacity = 3f;
	}

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

		DisplayCooldown = _timeSinceMark < COOLDOWN ? Utils.Map( _timeSinceMark, 0f, COOLDOWN, 1f, 0f ) : 0f;
	}

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

		if ( _markedEnemy.IsValid() )
		{
			var status = _markedEnemy.GetUnitStatus<UnitStatusMarked>();
			if ( status != null )
				status.Damage = GetDamage( Level );
		}
	}

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

		if ( other is Enemy enemy && !enemy.IsDying && !enemy.IsMarked && enemy != _markedEnemy && _timeSinceMark > COOLDOWN )
		{
			if ( _markedEnemy.IsValid() )
				_markedEnemy.UnMark();

			_markedEnemy = enemy;
			_timeSinceMark = 0f;

			enemy.Mark( Player, GetDamage( Level ) );

			Highlight();
			IconScale = 1.35f;
			IconAngleOffset = Game.Random.Float( 12f, 18f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
		}
	}

	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		if ( _markedEnemy.IsValid() )
			_markedEnemy.UnMark();
	}
}