perks/PerkFocusFire.cs

A Perk class for an 'Epic' rarity called Focus Fire. It tracks consecutive hits on the same enemy and grants multiplicative attack and reload speed buffs based on stacks, updates UI display text, highlight and cooldown visuals.

using System;
using Sandbox;

[Perk( Rarity.Epic, locked: true, alwaysOfferDebug: false )]
public class PerkFocusFire : Perk
{
	private enum Mod { Speed, MaxStacks };

	private Enemy _currHitEnemy;
	private int _hitCounter;

	static PerkFocusFire()
	{
		Register<PerkFocusFire>(
			name: "Focus Fire",
			imagePath: "textures/icons/vector/focus_fire.png",
			description: level => $"+{GetValue( level, Mod.Speed, isPercent: true )}% attack/reload speed when you hit the same enemy again (max: +{GetValue( level, Mod.MaxStacks ) * GetValue( level, Mod.Speed, isPercent: true )}%)",
			//upgradeDescription: level => $"+{GetValue( level - 1, Mod.Speed, isPercent: true )}%→{GetValue( level, Mod.Speed, isPercent: true )}% attack/reload speed when you hit the same enemy again\n(max: +{GetValue( level - 1, Mod.MaxStacks ) * GetValue( level - 1, Mod.Speed, isPercent: true )}%→{GetValue( level, Mod.MaxStacks ) * GetValue( level, Mod.Speed, isPercent: true )}%)"
			upgradeDescription: level => $"+{GetValue( level, Mod.Speed, isPercent: true )}% attack/reload speed when you hit the same enemy again\n(max: +{GetValue( level - 1, Mod.MaxStacks ) * GetValue( level - 1, Mod.Speed, isPercent: true )}%→{GetValue( level, Mod.MaxStacks ) * GetValue( level, Mod.Speed, isPercent: true )}%)"
		);
	}

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

		HighlightColor = new Color( 1f, 0.5f, 0.5f );
		HighlightDuration = 0.2f;
		HighlightOpacity = 1.5f;
	}

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

		DisplayCooldownColor = new Color( 0f, 0.7f, 1f, 3f );
	}

	public override void OnDamageEnemy( Enemy enemy, float amount, DamageType damageType, Vector2 dir, bool isCrit )
	{
		base.OnDamageEnemy( enemy, amount, damageType, dir, isCrit );

		if ( damageType != DamageType.Bullet && damageType != DamageType.Punch )
			return;

		if ( enemy == _currHitEnemy )
		{
			_hitCounter++;
		}
		else
		{
			_currHitEnemy = enemy;
			_hitCounter = 0;

			Highlight();
		}

		RefreshBuffs();
	}

	void RefreshBuffs()
	{
		int maxStacks = (int)GetValue( Level, Mod.MaxStacks );
		int currStacks = Math.Min( _hitCounter, maxStacks );
		float speed = 1f + currStacks * GetValue( Level, Mod.Speed );

		Player.Modify( this, PlayerStat.AttackSpeed, speed, ModifierType.Mult );
		Player.Modify( this, PlayerStat.ReloadSpeed, speed, ModifierType.Mult );

		DisplayText = currStacks > 0 ? $"{currStacks * GetValue( Level, Mod.Speed, isPercent: true )}%" : " ";
		DisplayCooldown = Utils.Map( currStacks, 0f, maxStacks, 0f, 1f );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Speed:
			default:
				//return isPercent
				//	? 2f + 1f * level
				//	: 0.02f + 0.01f * level;
				return isPercent
					? 2f
					: 0.02f;
			case Mod.MaxStacks:
				return 5 + 5 * level;
		}
	}
}