perks/PerkFreezeArmor.cs

PerkFreezeArmor is a Perk implementation that attaches a frost-armor visual to the player and lets the player freeze and damage the next enemy they touch while the perk is active. It manages cooldown, visual state, applying freeze and damage to enemies on collision, and reacts to death/revive/remove.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Epic, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Freeze })]
public class PerkFreezeArmor : Perk
{
	private enum Mod { Cooldown, Damage };

	private bool _isActive;
	private TimeSince _timeSinceActivate;

	private GameObject _freezeArmorGo;

	static PerkFreezeArmor()
	{
		Register<PerkFreezeArmor>(
			name: "Frozen Armor",
			imagePath: "textures/icons/vector/freeze_armor.png",
			description: level => $"Freeze and do {GetValue( level, Mod.Damage ).ToString( "0.#" )} dmg to the next enemy you touch __ (cooldown: {GetValue( level, Mod.Cooldown ).ToString( "0.#" )}s)",
			upgradeDescription: level => $"Freeze and do {GetValue( level - 1, Mod.Damage ).ToString( "0.#" )}→{GetValue( level, Mod.Damage ).ToString( "0.#" )} dmg to the next enemy you touch\n(cooldown: {GetValue( level - 1, Mod.Cooldown ).ToString( "0.#" )}→{GetValue( level, Mod.Cooldown ).ToString("0.#")}s)"
		);
	}

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

		_freezeArmorGo = GameObject.Clone( "prefabs/effects/frost_armor.prefab", new CloneConfig { StartEnabled = true, Parent = Player.GameObject } );
		_freezeArmorGo.LocalPosition = new Vector3( 0f, 0f, 6f );
	}

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

		_isActive = true;
	}

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

		Player.Modify( this, PlayerStat.FreezeOnMeleeCooldownDisplay, GetValue( Level, Mod.Cooldown ), ModifierType.Add );
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( !_isActive )
			return;

		if ( other is Enemy enemy && !enemy.IsDying )
		{
			enemy.Freeze( playerSource: Player, enemySource: null, Player.Stats[PlayerStat.FreezeTimeScale], Player.Stats[PlayerStat.FreezeLifetime] );

			Vector2 dir = (enemy.Position2D - Player.Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR
				? (enemy.Position2D - Player.Position2D).Normal
				: Utils.GetRandomVector();

			var hitPos = enemy.WorldPosition - (Vector3)dir * enemy.Radius;

			var damage = GetValue( Level, Mod.Damage );

			//Player.GetAdditionalDamageToEnemy( ref damage, enemy, DamageType.Freeze, dir, canBackstab: true );

			enemy.DamageRpc( damage, Player, DamageType.FrostArmor, hitPos, force: dir * 5f, isCrit: false, shouldFlinch: true );

			//Player.OnDamageEnemy( enemy, damage, DamageType.Freeze, dir, isCrit: false );

			BecomeNotReady();
		}
	}

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

		float cooldown = GetValue( Level, Mod.Cooldown );

		if ( _timeSinceActivate > cooldown )
		{
			BecomeReady();
		}
		else
		{
			DisplayCooldown = Utils.Map( _timeSinceActivate, 0f, cooldown, 1f, 0f );
		}
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Cooldown:
			default:
				return 5f - 1f * level;
			case Mod.Damage:
				switch( level)
				{
					case 1: default: return 20f;
					case 2: return 50f;
					case 3: return 75f;
					case 4: return 100f;
				}
		}
	}

	void BecomeReady()
	{
		_isActive = true;
		ShouldUpdate = false;
		DisplayText = " ";
		DisplayCooldown = 0f;

		HighlightColor = new Color( 1f, 1f, 1f );
		HighlightDuration = 0.15f;
		HighlightOpacity = 0.8f;
		Highlight();

		if ( _freezeArmorGo.IsValid() )
			_freezeArmorGo.Enabled = true;
	}

	void BecomeNotReady()
	{
		_timeSinceActivate = 0f;
		_isActive = false;
		ShouldUpdate = true;
		DisplayText = "❌";

		HighlightColor = new Color( 0.35f, 0.5f, 1f );
		HighlightDuration = 0.25f;
		HighlightOpacity = 2f;
		Highlight();

		if ( _freezeArmorGo.IsValid() )
			_freezeArmorGo.Enabled = false;
	}

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

		BecomeNotReady();
	}

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

		BecomeNotReady();
	}

	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		if ( _freezeArmorGo.IsValid() )
			_freezeArmorGo.Destroy();
	}
}