perks/PerkNextShotDamageMult.cs

An Epic rarity perk class called PerkNextShotDamageMult that accumulates a damage multiplier for the player's next shot every RECHARGE_TIME seconds. It increments a stored multiplier up to a cap, applies it to the player's ShotDamageMult stat, shows UI text and cooldown, plays a sound and a short visual scale/hold animation, and clears the multiplier when the player shoots.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Epic, locked: true, minUnlocksReq: 1, alwaysOfferDebug: false )]
public class PerkNextShotDamageMult : Perk
{
	private enum Mod { DamageMult, MaxDamageMult };

	private const float RECHARGE_TIME = 3f;

	private float _timer;

	private float _currentDamageMult;
	private float _maxDamageMult;

	static PerkNextShotDamageMult()
	{
		Register<PerkNextShotDamageMult>(
			name: "Pumped Up",
			imagePath: "textures/icons/vector/next_bullet_damage_mult.png",
			description: level => $"Every {RECHARGE_TIME}s, your next shot\ngets +{GetValue( level, Mod.DamageMult, true )}% dmg\n(max: [n]+{(int)GetValue( level, Mod.MaxDamageMult, true )}%[/n])",
			upgradeDescription: level => $"Every {RECHARGE_TIME}s, your next shot\ngets +{GetValue( level - 1, Mod.DamageMult, true )}%→+{GetValue( level, Mod.DamageMult, true )}% dmg\n(max: [n]+{(int)GetValue( level, Mod.MaxDamageMult, true )}%[/n])"
		);
	}

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

		ShouldUpdate = true;

		HighlightColor = new Color( 0.6f, 0.6f, 1f );
		HighlightDuration = 0.2f;
		HighlightOpacity = 0.8f;
	}

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

		_maxDamageMult = GetValue( Level, Mod.MaxDamageMult ) - 1f;
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.DamageMult:
			default:
				return isPercent
					? 50 + 50 * level
					: 1.5f + 0.5f * level;
			case Mod.MaxDamageMult:
				return isPercent
					? 1000f
					: 11f;
		}
	}

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

		_timer += dt;
		if ( _timer > RECHARGE_TIME )
		{
			_timer -= RECHARGE_TIME;

			if ( _currentDamageMult < _maxDamageMult )
			{
				_currentDamageMult = Math.Min( _currentDamageMult + (GetValue( Level, Mod.DamageMult ) - 1f), _maxDamageMult );
				Player.Modify( this, PlayerStat.ShotDamageMult, 1f + _currentDamageMult, ModifierType.Mult );

				DisplayText = $"+{_currentDamageMult * 100f:0}%";

				Manager.Instance.PlaySfxNearby( "pumped_up", Player.Position2D, pitch: Game.Random.Float( 0.9f, 1.1f ), volume: 1.8f, maxDist: 200f );

				if ( !(Player.Stats[PlayerStat.PunchBullets] > 0f) )
					Player.SetHoldType( Sandbox.Citizen.CitizenAnimationHelper.HoldTypes.Pistol );

				Player.ScaleHeightRpc( amount: 1.5f, time: Game.Random.Float( 0.05f, 0.06f ) );

				Highlight();
			}
		}

		DisplayCooldown = _currentDamageMult < _maxDamageMult 
			? Utils.Map( _timer, 0f, RECHARGE_TIME, 0f, 1f )
			: 1f;
	}

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

		if ( _currentDamageMult <= 0f )
			return;

		DisplayText = " ";
		Player.StopModifying( this, PlayerStat.ShotDamageMult );

		_currentDamageMult = 0f;

		// todo: bullet spawn position should be different
	}
}