perks/PerkShootOnlyWhenClick.cs

A player perk class named PerkShootOnlyWhenClick (Trigger Discipline) that accumulates bonus shot damage over time when the player is not shooting or reloading, shows that as UI (icon, text, cooldown color/scale), and applies the accumulated damage to the next shot. It resets the accumulation on shoot and modifies player stats while active.

Networking
using System;
using Sandbox;

[Perk( Rarity.Unique, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false )]
public class PerkShootOnlyWhenClick : Perk
{
	private enum Mod { DamageGainSpeed, MaxDamage };

	private float _accumulatedDamage;
	private int _currDmgInt;

	static PerkShootOnlyWhenClick()
	{
		Register<PerkShootOnlyWhenClick>(
			name: "Trigger Discipline",
			imagePath: "textures/icons/vector/shoot_only_when_click.png",
			description: level => $"Shoot with left-click\nWhile not shooting or reloading,\n+{GetValue( level, Mod.DamageGainSpeed ).ToString("0.##")} dmg/s for next shot (max: [n]+{(int)GetValue( level, Mod.MaxDamage )}[/n])"
		);
	}

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

		ShouldUpdate = true;
	}

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

		Player.Modify( this, PlayerStat.OnlyShootWithMouse1, 1f, ModifierType.Add );
		DisplayCooldownColor = new Color( 0.4f, 0.4f, 1f );
	}

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

		if ( !Player.IsReloading )
		{
			var maxDmg = GetValue( Level, Mod.MaxDamage );
			_accumulatedDamage = Math.Min( _accumulatedDamage + GetValue( Level, Mod.DamageGainSpeed ) * dt, maxDmg );
			Player.Modify( this, PlayerStat.ShotDamageAdd, _accumulatedDamage, ModifierType.Add );

			DisplayCooldown = Utils.Map( _accumulatedDamage, 0f, maxDmg, 0f, 1f );
		}
		
		DisplayText = $"+{_accumulatedDamage.ToString("0.#")}";

		var targetScale = Input.Down( "click" )
			? 1f
			: 1.3f;
		IconScale = MathX.Lerp( IconScale, targetScale, Time.Delta * 20f );

		var newDmg = _accumulatedDamage.FloorToInt();
		if( newDmg != _currDmgInt )
		{
			if(!Input.Down( "click" ) )
			{
				IconScale *= 1.1f;
				Manager.Instance.PlaySfxUI( "click", pitch: Utils.Map( _currDmgInt, 1, 50, 1f, 3f ), volume: Utils.Map( _currDmgInt, 1, 20, 0.05f, 0.3f ) );
			}

			_currDmgInt = newDmg;
		}

		// todo: different holdtype while not shooting
	}

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

		_accumulatedDamage = 0f;
		Player.StopModifying( this, PlayerStat.ShotDamageAdd );
		DisplayCooldown = 0f;
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.DamageGainSpeed:
			default:
				return 1f;
			case Mod.MaxDamage:
				return 30f;
		}
	}
}