perks/PerkMoveToShoot.cs

A player perk class 'PerkMoveToShoot' that triggers a spawned bullet when the player has moved a configured distance. It tracks accumulated travel distance, spawns a bullet and plays sound/animation when threshold is exceeded, and updates UI cooldown visuals.

NetworkingFile Access
using System;
using Sandbox;

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

	private float _currDist;

	static PerkMoveToShoot()
	{
		Register<PerkMoveToShoot>(
			name: "Pedal Powered",
			imagePath: "textures/icons/vector/move_to_shoot.png",
			description: level => $"Launch a bullet-icon when you move {GetValue( level, Mod.RequiredDist, true ).ToString("0.##")}m",
			upgradeDescription: level => $"Launch a bullet-icon when\nyou move {GetValue( level - 1, Mod.RequiredDist, true ).ToString( "0.##" )}→{GetValue( level, Mod.RequiredDist, true ).ToString( "0.##" )}m"
		);
	}

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

		ShouldUpdate = true;

		HighlightColor = new Color( 0f, 0f, 0f );
		HighlightDuration = 0.15f;
		HighlightOpacity = 1f;

		DisplayCooldownColor = new Color( 0f, 0f, 0.2f );

		//DisplayTextColor = new Color( 0.75f, 0.75f, 1f );
	}

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

	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.RequiredDist:
			default:
				return (1.45f - 0.25f * level) * (isPercent ? 1f : Utils.Meter2Unit);
		}
	}

	public override void Update( float dt )
	{
		base.Update( dt );
		//DisplayText = $"{string.Format( "{0:0.0}", GetValue( Level, Mod.RequiredDist ) - _currDist )}";
		//DisplayTextOpacity = Utils.Map( _currDist, 0f, GetValue( Level, Mod.RequiredDist ), 0f, 2.5f, EasingType.ExpoOut );
		DisplayCooldown = Utils.Map( _currDist, 0f, GetValue( Level, Mod.RequiredDist ), 0f, 1f );
		DisplayCooldownColor = Color.Lerp( new Color( 0f, 0f, 0.3f ), new Color( 0.3f, 0.3f, 0.6f ), Utils.Map( _currDist, 0f, GetValue( Level, Mod.RequiredDist ), 0f, 1f ) );

		if ( Player.TotalVelocity.LengthSquared > 0f )
		{
			_currDist += Player.TotalVelocity.Length * dt;
			if ( _currDist > GetValue( Level, Mod.RequiredDist ) )
			{
				float damage = Player.GetBulletDamage( isFromClip: true, isLastAmmo: false );
				var dir = Player.TotalVelocity.Normal;
				var pos = Player.Position2D + dir * Player.BULLET_SPAWN_OFFSET * Player.Stats[PlayerStat.Scale];
				Player.SpawnBullet( pos, dir, damage, isFromClip: false );

				Manager.Instance.PlaySfxNearbyRpc( "player.shoot", pos, Game.Random.Float(0.8f, 0.9f), volume: 0.9f, maxDist: 300f );

				Player.DodgeDuckRpc( dir, time: Game.Random.Float( 0.05f, 0.15f ) );

				_currDist = 0f;

				Highlight();
			}
		}
	}
}