perks/PerkPunchReload.cs

A Perk class for a game that grants a chance to reload 1 ammo when punching the same enemy repeatedly. It tracks the last hit enemy and consecutive punches, maps those to a chance between starting and final values, triggers a reload on success, and updates visual highlights and display text.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Uncommon, includedAtStart: false, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false )]
public class PerkPunchReload : Perk
{
	private enum Mod { StartingChance, FinalChance, NumStacks };

	private Enemy _lastHitEnemy;
	private int _numConsecutiveHits;

	static PerkPunchReload()
	{
		Register<PerkPunchReload>(
			name: "Relentless Fists",
			imagePath: "textures/icons/vector/punch_reload.png",
			description: level => $"{GetValue( level, Mod.StartingChance, true ).ToString( "0.#" )}% chance to reload 1 ammo on punching the same enemy\n(up to {(int)GetValue( level, Mod.FinalChance, true )}% chance\nat {(int)GetValue( level, Mod.NumStacks )} punches)",
			upgradeDescription: level => $"{GetValue( level - 1, Mod.StartingChance, true ).ToString( "0.#" )}%→{GetValue( level, Mod.StartingChance, true ).ToString( "0.#" )}% chance to reload 1 ammo on punching the same enemy\n(up to {(int)GetValue( level - 1, Mod.FinalChance, true )}→{(int)GetValue( level, Mod.FinalChance, true )}% chance\nat {(int)GetValue( level - 1, Mod.NumStacks )}→{(int)GetValue( level, Mod.NumStacks )} punches)",
			descriptionLineHeight: 9.5f
		);
	}

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

		DisplayCooldownColor = new Color( 0.4f, 0.4f, 1f, 0.5f );
	}

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

	}

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

		if( damageType != DamageType.Punch )
			return;

		if( enemy != _lastHitEnemy )
		{
			_lastHitEnemy = enemy;
			_numConsecutiveHits = 0;

			HighlightColor = new Color( 1f, 0f, 0f );
			HighlightDuration = 0.25f;
			HighlightOpacity = 2f;
			Highlight();
		}
		else
		{
			var maxStacks = (int)GetValue( Level, Mod.NumStacks );
			_numConsecutiveHits = Math.Min( _numConsecutiveHits + 1, maxStacks );

			var chance = Utils.Map( _numConsecutiveHits, 0, maxStacks, GetValue( Level, Mod.StartingChance ), GetValue( Level, Mod.FinalChance ) );
			if ( Game.Random.Float( 0f, 1f ) < chance )
			{
				Player.ReloadAmmoAmount( 1 );

				HighlightColor = new Color( 0.75f, 0.75f, 1f );
				HighlightDuration = 0.15f;
				HighlightOpacity = 2f;
				Highlight();
			}
		}

		RefreshText();
	}

	void RefreshText()
	{
		if ( _numConsecutiveHits == 0 )
			DisplayText = " "; 
		else
			DisplayText = $"{(int)Math.Round( Utils.Map( _numConsecutiveHits, 1, (int)GetValue( Level, Mod.NumStacks ), GetValue( Level, Mod.StartingChance, true ), GetValue( Level, Mod.FinalChance, true ) ) )}%";

		DisplayCooldown = Utils.Map( _numConsecutiveHits, 0, (int)GetValue( Level, Mod.NumStacks ), 0f, 1f );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.StartingChance:
			default:
				return isPercent
					? 4f + 1f * level
					: 0.04f + 0.01f * level;
			case Mod.FinalChance:
				return isPercent
					? 20f + 10f * level
					: 0.20f + 0.10f * level;
			case Mod.NumStacks:
				return 32 - level * 2;
		}
	}
}