perks/PerkArmorShoot.cs

A Perk class (Armor Flinger) that periodically launches an armor-based bullet when the player has armor. It tracks a timer, computes delay per level, spawns a bullet towards the nearest enemy or a random direction, plays a sound, and updates UI cooldown/visuals.

Networking
using System;
using Sandbox;

[Perk( Rarity.Epic, includedAtStart: false, alwaysOfferDebug: false )]
public class PerkArmorShoot : Perk
{
	private enum Mod { Delay };

	private float _timer;

	static PerkArmorShoot()
	{
		Register<PerkArmorShoot>(
			name: "Armor Flinger",
			imagePath: "textures/icons/vector/armor_shoot.png",
			description: level => $"Launch a bullet-icon with dmg\nequal to your armor-item every {GetValue( level, Mod.Delay ).ToString( "0.#" )}s",
			upgradeDescription: level => $"Launch a bullet-icon with dmg\nequal to your armor-item every {GetValue( level - 1, Mod.Delay ).ToString( "0.#" )}→{GetValue( level, Mod.Delay ).ToString( "0.#" )}s"
		);
	}

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

		ShouldUpdate = true;

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

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

	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Delay:
			default:
				return 6.0f - level * 1.0f;
		}
	}

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

		//if ( Player.IsMoving && Player.Armor > 0 )
		if ( Player.Armor > 0 )
		{
			_timer += dt;
			if ( _timer > GetValue( Level, Mod.Delay ) )
				Shoot();
		}

		//DisplayText = Player.Stats[PlayerStat.Armor] > 0f ? $"{MathX.CeilToInt(GetValue(Level, Mod.Delay) - _timer)}" : " ";
		DisplayCooldown = Player.Armor > 0 ? Utils.Map( _timer, 0f, GetValue( Level, Mod.Delay ), 0f, 1f ) : 0f;

		if ( Player.Armor > 0 )
			DisplayCooldownColor = Color.Lerp( new Color( 0f, 0f, 0f ), new Color( 0.3f, 0.3f, 0.6f ), Utils.Map( _timer, 0f, GetValue( Level, Mod.Delay ), 0f, 1f ) );
	}

	void Shoot()
	{
		if ( Player.Armor <= 0 )
			return;

		var closestEnemy = Manager.Instance.GetClosestEnemy( Player.Position2D, 700f );
		Vector2 dir = closestEnemy != null ? (closestEnemy.Position2D - Player.Position2D).Normal : Utils.GetRandomVector();

		var pos = Player.Position2D + dir * Player.BULLET_SPAWN_OFFSET * Player.Stats[PlayerStat.Scale];
		var damage = (float)Player.Armor;
		damage *= Player.Stats[PlayerStat.BulletDamageMultiplier];
		damage = MathF.Max( damage, Player.MIN_BULLET_DAMAGE );

		// todo: why does this armor bullet look weird?
		Player.SpawnBullet( pos, dir, damage, isFromClip: false, bulletType: BulletType.Armor );

		//Player.LoseArmor( 1, -dir, playSfx: false );

		Manager.Instance.PlaySfxNearbyRpc( "spike.thrust", Player.Position2D, pitch: Game.Random.Float( 1.75f, 1.85f ), volume: 0.65f, maxDist: 320f );

		_timer = 0f;

		Highlight();

		IconScale = Game.Random.Float( 1.2f, 1.3f );
		IconAngleOffset = Game.Random.Float( 10f, 20f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
	}
}