perks/PerkRerollAttackSpeed.cs

A player perk that grants temporary attack/reload/move speed buffs when the player rerolls, at the cost of some self-damage. It tracks queued reroll timestamps, applies multiplicative stat modifiers while active, shows cooldown/display text, and damages the player on each reroll.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Epic, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.SelfDmg })]
public class PerkRerollAttackSpeed : Perk
{
	private enum Mod { Speed, Time, LifeLoss };

	private Queue<float> _rerollTimes = new();

	private float _currStartTime;


	static PerkRerollAttackSpeed()
	{
		Register<PerkRerollAttackSpeed>(
			name: "Stim Injection",
			imagePath: "textures/icons/vector/reroll_attack_speed.png",
			description: level => $"-{(int)GetValue( level, Mod.LifeLoss )} hp and\n+{GetValue( level, Mod.Speed, true )}% attack/reload/move speed for {GetValue( level, Mod.Time ).ToString( "0.#" )}s\nwhen you reroll",
			upgradeDescription: level => $"-{(int)GetValue( level - 1, Mod.LifeLoss )}→-{(int)GetValue( level, Mod.LifeLoss )} hp and +{GetValue( level - 1, Mod.Speed, true )}%→{GetValue( level, Mod.Speed, true )}% attack/reload/move speed for {GetValue( level - 1, Mod.Time ).ToString( "0.#" )}→{GetValue( level, Mod.Time ).ToString( "0.#" )}s when you reroll"
		);
	}

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

		ShouldUpdate = true;

		DisplayCooldownColor = new Color( 0.6f, 0.6f, 1f, 3f );

		HighlightColor = new Color( 0.6f, 0.6f, 1f );
		HighlightDuration = 0.1f;
		HighlightOpacity = 2f;

		// todo: make this rarer and a bit more powerful
	}

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

		Player.Modify( this, PlayerStat.RerollHealDisplay, -GetValue( Level, Mod.LifeLoss ), ModifierType.Add );
	}

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

		if ( _rerollTimes.Count > 0 )
		{
			var rerollTime = _rerollTimes.Peek();
			if ( Time.Now > rerollTime + GetValue( Level, Mod.Time ) )
			{
				_rerollTimes.Dequeue();
				RefreshBuffs();
				_currStartTime = Time.Now;
				DisplayCooldown = 0f;
			}
			else
			{
				DisplayCooldown = Utils.Map( Time.Now, _currStartTime, rerollTime + GetValue( Level, Mod.Time ), 1f, 0f );
			}
		}

		float buffPercent = _rerollTimes.Count * GetValue( Level, Mod.Speed, true );
		DisplayText = _rerollTimes.Count > 0 ? $"{buffPercent}%" : " ";
	}

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

		if ( _rerollTimes.Count == 0 )
			_currStartTime = Time.Now;

		_rerollTimes.Enqueue( Time.Now );
		RefreshBuffs();

		// todo: different sfx?
		Player.Damage( GetValue( Level, Mod.LifeLoss ), DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: 0f, ragdollForce: 0.5f, enemySource: null, enemyType: EnemyType.None );

		Highlight();
	}

	void RefreshBuffs()
	{
		float speed = 1f + _rerollTimes.Count * GetValue( Level, Mod.Speed );
		Player.Modify( this, PlayerStat.AttackSpeed, speed, ModifierType.Mult );
		Player.Modify( this, PlayerStat.ReloadSpeed, speed, ModifierType.Mult );
		Player.Modify( this, PlayerStat.MoveSpeedMultiplier, speed, ModifierType.Mult );

		//float dmgAdd = _rerollTimes.Count * GetValue( Level, Mod.BulletDmgAdd );
		//Player.Modify( this, PlayerStat.BulletDamage, dmgAdd, ModifierType.Add );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Speed:
			default:
				return isPercent
					? 10f + 5f * level
					: 0.10f + 0.05f * level;
			case Mod.Time:
				return 3f + 1f * level;
			//case Mod.BulletDmgAdd:
			//	return 0.5f + 0.3f * level;
			case Mod.LifeLoss:
				return 3f + 3f * level;
		}
	}
}