perks/PerkBulletHitStreak.cs

A Perk class for a game, named Deadly Accuracy. It tracks a bullet-hit streak and provides an added damage modifier per stacked shot, displays streak info and plays a fail SFX when a bullet icon hits the ground.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Epic, locked: true, alwaysOfferDebug: false )]
public class PerkBulletHitStreak : Perk
{
	private enum Mod { BulletHitStreakDmg };

	private const int MAX_STREAK = 100;

	private TimeSince _timeSinceHitGroundSfx;

	static PerkBulletHitStreak()
	{
		Register<PerkBulletHitStreak>(
			name: "Deadly Accuracy",
			imagePath: "textures/icons/vector/bullet_hit_streak.png",
			description: level => $"+{GetValue( level, Mod.BulletHitStreakDmg ).ToString("0.###")} bullet dmg when you shoot\n(max: {MAX_STREAK} stacks)\nReset when bullet-icon hits the ground",
			upgradeDescription: level => $"+{GetValue( level - 1, Mod.BulletHitStreakDmg ).ToString( "0.###" )}→{GetValue(level, Mod.BulletHitStreakDmg).ToString("0.###")} bullet dmg when\nyou shoot (max: {MAX_STREAK} stacks)\nReset when bullet-icon hits the ground"
		);
	}

	public override void Start()
	{
		base.Start();
		DisplayCooldownColor = new Color( 0.5f, 0.5f, 1f, 3f );
		HighlightColor = new Color( 1f, 0f, 0f );
		HighlightDuration = 0.25f;
		HighlightOpacity = 2f;
	}

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

		Player.Modify( this, PlayerStat.BulletHitStreakDmg, GetValue( Level, Mod.BulletHitStreakDmg ), ModifierType.Add );
	}

	public override void OnShoot()
	{
		RefreshDisplayText();
	}

	public override void OnBulletHitGround( Bullet bullet )
	{
		RefreshDisplayText();

		// todo: halve stacks instead of canceling all buffs?

		if( _timeSinceHitGroundSfx > 0.1f )
		{
			Manager.Instance.PlaySfxNearby( "bullet_hit_streak_fail", bullet.Position2D, pitch: Game.Random.Float( 1f, 1.2f ), volume: 1.3f, maxDist: 400f );
			
			_timeSinceHitGroundSfx = 0f;

			Highlight();
		}
	}

	void RefreshDisplayText()
	{
		DisplayText = Player.Stats[PlayerStat.NumShotsWithoutBulletHitGround] > 0 ? string.Format( "{0:0.00}", MathF.Min( (int)Player.Stats[PlayerStat.NumShotsWithoutBulletHitGround], MAX_STREAK ) * GetValue( Level, Mod.BulletHitStreakDmg ) ) : " ";
		DisplayCooldown = Utils.Map( Player.Stats[PlayerStat.NumShotsWithoutBulletHitGround], 0f, MAX_STREAK, 0f, 1f );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.BulletHitStreakDmg:
			default:
				return 0.01f + level * 0.015f;
		}
	}
}