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.
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.#" )}";
}
}