perks/PerkHealDelayed.cs

A Perk class (PerkHealDelayed) that queues delayed healing when the player takes melee damage. It stores heal entries with timestamps, displays cooldown/amount UI, and applies the heal after a configurable delay based on perk level.

NetworkingFile Access
using System;
using Sandbox;

public struct DelayedHealData
{
	public float time;
	public float amount;
}

[Perk( Rarity.Rare, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false )]
public class PerkHealDelayed : Perk
{
	private enum Mod { Delay, Percent, MaxAmmoCount };

	private Queue<DelayedHealData> _delayedHeals = new();

	private float _displayAmount;

	private float _currStartTime;

	static PerkHealDelayed()
	{
		Register<PerkHealDelayed>(
			name: "Delayed Recovery",
			imagePath: "textures/icons/vector/heal_delayed.png",
			//description: level => $"-{GetValue( level, Mod.MaxAmmoCount )} ammo, and when hurt by melee dmg, heal back {(int)GetValue( level, Mod.Percent, true )}% of it after {string.Format( "{0:0.0}", GetValue( level, Mod.Delay ) )}s",
			//upgradeDescription: level => $"-{GetValue( level, Mod.MaxAmmoCount )} ammo, and when hurt by melee dmg, heal back {(int)GetValue( level - 1, Mod.Percent, true )}%→{(int)GetValue( level, Mod.Percent, true )}% of it after {string.Format( "{0:0.0}", GetValue( level - 1, Mod.Delay ) )}→{string.Format( "{0:0.0}", GetValue( level, Mod.Delay ) )}s"
			description: level => $"Heal {(int)GetValue( level, Mod.Percent, true )}% of\nmelee dmg taken after {GetValue( level, Mod.Delay ).ToString("0.#")}s",
			upgradeDescription: level => $"Heal {(int)GetValue( level - 1, Mod.Percent, true )}%→{(int)GetValue( level, Mod.Percent, true )}% of\nmelee dmg taken after {GetValue( level - 1, Mod.Delay ).ToString( "0.#" )}→{GetValue( level, Mod.Delay ).ToString( "0.#" )}s"
		);
	}

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

		ShouldUpdate = true;
		DisplayCooldownColor = new Color( 0f, 1f, 0f, 3f );
		HighlightColor = new Color( 0.5f, 1f, 0.5f );
		HighlightDuration = 0.5f;
		HighlightOpacity = 2f;

		DisplayTextColor = new Color( 0.5f, 1f, 0.5f );
		DisplayTextOpacity = 3f;
	}

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

		//Player.Modify( this, PlayerStat.MaxAmmoCount, -GetValue( Level, Mod.MaxAmmoCount ), ModifierType.Add );
		Player.Modify( this, PlayerStat.DelayedRecoveryPercentDisplay, GetValue( Level, Mod.Percent ), ModifierType.Add );
		Player.Modify( this, PlayerStat.DelayedRecoveryDelayDisplay, GetValue( Level, Mod.Delay ), ModifierType.Add );
	}

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

		float delay = GetValue( Level, Mod.Delay );

		if ( _delayedHeals.Count > 0 )
		{
			var data = _delayedHeals.Peek();
			if ( Time.Now > data.time + delay )
			{
				Player.Heal( data.amount );
				_delayedHeals.Dequeue();
				_currStartTime = Time.Now;

				_displayAmount -= data.amount;

				DisplayText = _displayAmount > 0.1f ? $"{_displayAmount.ToString("0.#")}" : " ";
				DisplayCooldown = 0f;
				Highlight();
			}
			else
			{
				DisplayCooldown = Utils.Map( Time.Now, _currStartTime, data.time + delay, 0f, 1f );
			}
		}
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Delay:
			default:
				return 5.5f - 0.5f * level;
			case Mod.Percent:
				return isPercent
					? 10f + 15f * level
					: 0.1f + 0.15f * level;
			case Mod.MaxAmmoCount:
				return 1;
		}
	}

	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 );

		if ( !Player.IsDamageTypeMelee( damageType ) )
			return;

		float amountToHeal = amount * GetValue( Level, Mod.Percent );

		if ( _delayedHeals.Count == 0 )
			_currStartTime = Time.Now;

		_delayedHeals.Enqueue( new DelayedHealData()
		{
			time = Time.Now,
			amount = amountToHeal
		} );

		_displayAmount += amountToHeal;

		DisplayText = $"{_displayAmount.ToString( "0.#" )}";
	}
}