perks/PerkShieldDeath.cs

A Mythic perk class 'PerkShieldDeath' that prevents player death by restoring 1 health, granting a shield and a short invulnerability window, then starts a cooldown before it can trigger again. It manages timers, UI display state, sounds, chat/floater messages, and perk registration metadata.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Mythic, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Shield })]
public class PerkShieldDeath : Perk
{
	private enum Mod { RechargeTime, InvulnTime, };

	public bool IsReady { get; set; }

	private float _timer;

	private float _invulnTimer;
	private bool _isInvuln;


	static PerkShieldDeath()
	{
		Register<PerkShieldDeath>(
			name: "Saving Grace",
			imagePath: "textures/icons/vector/death_shield.png",
			description: level => $"Instead of dying, get a shield and become invulnerable for {GetValue( level, Mod.InvulnTime ).ToString( "0.#" )}s\n(cooldown: {(int)GetValue( level, Mod.RechargeTime )}s)",
			upgradeDescription: level => $"Instead of dying, get a shield and become invulnerable for {GetValue( level - 1, Mod.InvulnTime ).ToString("0.#")}→{GetValue( level, Mod.InvulnTime ).ToString("0.#")}s\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 ( _isInvuln )
		{
			_invulnTimer += dt;
			if ( _invulnTimer > GetValue( Level, Mod.InvulnTime ) )
			{
				//Player.StopModifying( this, PlayerStat.InvulnAmount );
				_isInvuln = false;
			}
			else
			{
				DisplayCooldownColor = new Color( 1f, 1f, 0f, 5f );
				DisplayCooldown = Utils.Map( _invulnTimer, 0f, GetValue( Level, Mod.InvulnTime ), 1f, 0f );
			}
		}

		if ( !IsReady )
		{
			_timer += dt;
			if ( _timer > GetValue( Level, Mod.RechargeTime ) )
			{
				BecomeReady( playSfx: true );
			}
			else
			{
				DisplayText = $"{MathX.CeilToInt( GetValue( Level, Mod.RechargeTime ) - _timer )}";

				if ( !_isInvuln )
				{
					DisplayCooldownColor = new Color( 0.6f, 0.5f, 0.5f, 2.5f );
					DisplayCooldown = Utils.Map( _timer, 0f, GetValue( Level, Mod.RechargeTime ), 1f, 0f );
				}
			}
		}
	}

	public void BecomeReady( bool playSfx = false )
	{
		IsReady = true;
		DisplayText = "✔️";
		ShouldUpdate = false;
		DisplayCooldown = 0f;

		if ( playSfx )
			Manager.Instance.PlaySfxUI( "saving_grace_ready", pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 0.75f );

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

	public override bool TryPreventDeath()
	{
		if ( !IsReady )
			return false;

		Activate();
		return true;
	}

	public void Activate()
	{
		Player.Health = 1f;

		if ( !Player.IsShielded )
			Player.GainShield();

		IsReady = false;
		ShouldUpdate = true;
		_timer = 0f;
		//Player.Modify( this, PlayerStat.InvulnAmount, 1f, ModifierType.Add );
		Player.BecomeInvincible( GetValue( Level, Mod.InvulnTime ) );
		_isInvuln = true;
		_invulnTimer = 0f;

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

		Manager.Instance.Chat.AddLocalChatMessage( $"{Perk.GetRichTextToken( GetType() )} Saved from death!", from: "" );

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

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.RechargeTime:
			default:
				return 80f - 20f * level;
			case Mod.InvulnTime:
				return 0.5f + 0.5f * level;
		}
	}
}