perks/PerkHurtUniqueChoice.cs

A Perk component called PerkHurtUniqueChoice (Spirit Quest). It tracks damage taken and every configured amount of damage awards the player a pending Unique perk choice, updates icon/cooldown display, and triggers highlight visuals.

Networking
using System;
using Sandbox;

[Perk( Rarity.Unique, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false )]
public class PerkHurtUniqueChoice : Perk
{
	private enum Mod { ReqDmg };

	private float _dmgTaken;

	static PerkHurtUniqueChoice()
	{
		Register<PerkHurtUniqueChoice>(
			name: "Spirit Quest",
			imagePath: "textures/icons/vector/hurt_unique_choice.png",
			description: level => $"Every {(int)GetValue( level, Mod.ReqDmg )} dmg taken,\nsee a Unique perk choice",
			upgradeDescription: level => $"Every {(int)GetValue( level - 1, Mod.ReqDmg )}→{(int)GetValue( level, Mod.ReqDmg )} dmg taken,\nsee a Unique perk choice"
		);
	}

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

		DisplayCooldownColor = new Color( 1f, 1f, 0.3f, 0.5f );
	}

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

		CheckDmg();
		RefreshIcon();
	}

	void CheckDmg()
	{
		var dmgReq = GetValue( Level, Mod.ReqDmg );
		if ( _dmgTaken >= dmgReq )
		{
			_dmgTaken -= dmgReq;
			Player.NumPendingUniqueChoices++;

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

			// todo: sfx
		}
	}

	public override void OnHit( float amount, DamageType damageType, bool isSelfInflicted, Vector2 dir, float force, Enemy enemySource, EnemyType enemyType, float previousHealth )
	{
		base.OnHit( amount, damageType, isSelfInflicted, dir, force, enemySource, enemyType, previousHealth );

		_dmgTaken += amount;

		CheckDmg();
		RefreshIcon();
	}

	public override void OnPerkChoicesRefreshed( int numUniqueConsumed )
	{
		base.OnPerkChoicesRefreshed( numUniqueConsumed );

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

		RefreshIcon();
	}

	void RefreshIcon()
	{
		var pending = Player.NumPendingUniqueChoices;
		DisplayText = pending > 0 ? $"+{pending}" : "";
		DisplayCooldown = Utils.Map( _dmgTaken, 0f, GetValue( Level, Mod.ReqDmg ), 0f, 1f );
	}

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