perks/PerkRegenLimitedReserve.cs

A player perk class "PerkRegenLimitedReserve" that grants a limited, delayed health regeneration reserve. It tracks total healed amount, applies periodic heals while the player is not at full HP up to a per-level cap, updates UI display text and cooldown, and modifies a displayed HP regen value.

Networking
using System;
using Sandbox;

[Perk( Rarity.Epic, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false )]
public class PerkRegenLimitedReserve : Perk
{
	private enum Mod { RegenAmount, HpTotal };

	private const float DELAY = 1f;

	private bool _isRegenerating;

	private float _totalHealed;
	private TimeSince _timeSinceHeal;

	static PerkRegenLimitedReserve()
	{
		Register<PerkRegenLimitedReserve>(
			name: "Picnic Basket",
			imagePath: "textures/icons/vector/regen_limited_reserve.png",
			description: level => $"+{GetValue( level, Mod.RegenAmount ).ToString( "0.#" )} hp/s regen\n(limited to {(int)GetValue( level, Mod.HpTotal )} hp total)",
			upgradeDescription: level => $"+{GetValue( level - 1, Mod.RegenAmount ).ToString( "0.#" )}→{GetValue( level, Mod.RegenAmount ).ToString( "0.#" )} hp/s regen\n(limited to {(int)GetValue( level - 1, Mod.HpTotal )}→{(int)GetValue( level, Mod.HpTotal )} hp total)"
		);
	}

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

		HighlightColor = new Color( 0.3f, 1f, 0.3f );
		HighlightDuration = 0.2f;
		HighlightOpacity = 1f;

		_totalHealed = 0f;

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

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

		_timeSinceHeal = 0f;
	}

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

		ShouldUpdate = true;
		SetIsRegenerating( Player.HpPercent < 1f );

		var hpTotal = GetValue( Level, Mod.HpTotal );
		DisplayText = $"{Math.Ceiling( hpTotal - _totalHealed )}";
		DisplayCooldown = Utils.Map( _totalHealed, 0f, hpTotal, 1f, 0f );
	}

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

		var hpTotal = GetValue( Level, Mod.HpTotal );
		bool isRegenerating = _totalHealed < hpTotal && Player.HpPercent < 1f;

		if ( isRegenerating )
		{
			if ( _timeSinceHeal > DELAY )
			{
				var missingHp = Player.Stats[PlayerStat.MaxHp] - Player.Health;
				var regenAmount = GetValue( Level, Mod.RegenAmount );
				var amountToHeal = Math.Min( regenAmount, missingHp );
				amountToHeal = Math.Min( amountToHeal, hpTotal - _totalHealed );

				Player.Heal( amountToHeal );

				Highlight();

				_timeSinceHeal = 0f;

				_totalHealed += amountToHeal;

				if( _totalHealed >= hpTotal )
				{
					isRegenerating = false;
					DisplayText = "❌";
					DisplayCooldown = 0f;

					ShouldUpdate = false;
				}
				else
				{
					DisplayText = $"{Math.Ceiling( hpTotal - _totalHealed )}";
					DisplayCooldown = Utils.Map( _totalHealed, 0f, hpTotal, 1f, 0f);
				}
			}
		}

		if ( isRegenerating != _isRegenerating )
			SetIsRegenerating( isRegenerating );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.RegenAmount:
			default:
				return 1.0f + 1.0f * level;
			case Mod.HpTotal:
				return 50f + 150f * level;
		}
	}

	void SetIsRegenerating( bool isRegenerating )
	{
		_isRegenerating = isRegenerating;
		RefreshDisplay();
	}

	void RefreshDisplay()
	{
		Player.Modify( this, PlayerStat.HpRegenDisplay, _isRegenerating ? (GetValue( Level, Mod.RegenAmount ) / DELAY) : 0f, ModifierType.Add );
	}
}