perks/PerkPreventDeathOver1Hp.cs

A perk class 'PerkPreventDeathOver1Hp' that grants a one-time automatic save from death when the player would die while above 1 HP. It tracks readiness with a cooldown, shows UI cooldown text, applies/removes a player stat modifier when ready/used, plays sound and floater effects, and exposes Activate/BecomeReady methods.

Networking
using System;
using Sandbox;

[Perk( Rarity.Epic, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.OneHpLeft })]
public class PerkPreventDeathOver1Hp : Perk
{
	private enum Mod { RechargeTime, };

	public bool IsReady { get; set; }

	private float _timer;


	static PerkPreventDeathOver1Hp()
	{
		Register<PerkPreventDeathOver1Hp>(
			name: "Last Stand",
			imagePath: "textures/icons/vector/prevent_death_over_1hp.png",
			description: level => $"If you would die while over 1 hp, instead go to 1 hp __ (cooldown: {(int)GetValue( level, Mod.RechargeTime )}s)",
			upgradeDescription: level => $"If you would die while over 1 hp, instead go to 1 hp\n(cooldown: {(int)GetValue( level - 1, Mod.RechargeTime )}→{(int)GetValue( level, Mod.RechargeTime )}s)"
		);
	}

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

	}

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

		BecomeReady( playSfx: Level > 1 );
	}

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

	}

	public override void Update( float dt )
	{
		if ( !IsReady )
		{
			_timer += dt;
			if ( _timer > GetValue( Level, Mod.RechargeTime ) )
			{
				BecomeReady( playSfx: true );
			}
			else
			{
				DisplayText = $"{MathX.CeilToInt( GetValue( Level, Mod.RechargeTime ) - _timer )}";
				DisplayCooldown = Utils.Map( _timer, 0f, GetValue( Level, Mod.RechargeTime ), 0f, 1f );
			}
		}
	}

	public void BecomeReady( bool playSfx = false )
	{
		if ( playSfx && !IsReady )
			Manager.Instance.PlaySfxUI( "last_stand_ready", pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 0.75f );

		IsReady = true;
		DisplayText = " ";
		ShouldUpdate = false;
		DisplayCooldown = 0f;

		Player.Modify( this, PlayerStat.PreventDeathOver1Hp, 1f, ModifierType.Add );

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

	public void Activate()
	{
		IsReady = false;
		ShouldUpdate = true;
		_timer = 0f;

		Player.StopModifying( this, PlayerStat.PreventDeathOver1Hp );

		Manager.Instance.SpawnFloaterTextRpc( Player.WorldPosition.WithZ( 65f ), "SAVED!", color: new Color( 0.75f, 0.5f, 1f, 0.8f ), size: 1.5f, floaterType: FloaterType.PositiveMessage );

		Manager.Instance.PlaySfxNearbyRpc( "last_stand_activate", Player.Position2D, pitch: Game.Random.Float( 0.8f, 0.85f ), volume: 2f, maxDist: 400f );

		HighlightColor = new Color( 1f, 1f, 0f );
		HighlightDuration = 0.35f;
		HighlightOpacity = 3f;
		Highlight();
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.RechargeTime:
			default:
				return 25 - 5 * level;
		}
	}
}