perks/PerkSpear.cs

A perk component named PerkSpear that charges over several shots and then spawns a piercing spear prefab. It tracks shots, displays cooldown/countdown text, computes spear damage per pierce from player max HP, clones and initializes a spear GameObject, network-spawns it, plays an SFX and resets the charge.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Epic, locked: true, minUnlocksReq: 5, alwaysOfferDebug: false )]
public class PerkSpear : Perk
{
	private enum Mod { NumShotsRequired, HpPercentPerPierce, BulletDamage };

	private int _shotCounter;

	static PerkSpear()
	{
		Register<PerkSpear>(
			name: "Spear Thrower",
			imagePath: "textures/icons/vector/spear.png",
			description: level => $"Every {GetValue( level, Mod.NumShotsRequired )} shots, launch a\npiercing spear that gains dmg equal to +{GetValue( level, Mod.HpPercentPerPierce, true ).ToString( "0.#" )}% of your max hp\neach pierce",
			upgradeDescription: level => $"Every {GetValue( level - 1, Mod.NumShotsRequired )}→{GetValue( level, Mod.NumShotsRequired )} shots, launch a piercing spear that gains dmg equal to {GetValue( level - 1, Mod.HpPercentPerPierce, true ).ToString("0.#")}%→{GetValue( level, Mod.HpPercentPerPierce, true ).ToString("0.#")}% of your max hp each pierce",
			descriptionLineHeight: 10f
		);
	}

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

		DisplayCooldownColor = new Color( 1f, 1f, 0.3f, 0.6f );

		HighlightColor = new Color( 0.6f, 0.6f, 1f );
		HighlightDuration = 0.25f;
		HighlightOpacity = 2f;
	}

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

		DisplayText = $"{GetValue( Level, Mod.NumShotsRequired ) - _shotCounter}";
		DisplayCooldown = Utils.Map( _shotCounter, 0f, GetValue( Level, Mod.NumShotsRequired ), 0f, 1f );

		//Player.Modify( this, PlayerStat.BulletDamage, -GetValue( Level, Mod.BulletDamage ), ModifierType.Add );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.NumShotsRequired:
			default:
				return 42 - 8 * level;
			case Mod.HpPercentPerPierce:
				return isPercent ? 0.5f + 0.5f * level : 0.005f + 0.005f * level;
			//case Mod.BulletDamage:
			//	return 0.5f + 0.5f * level;
		}
	}

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

		_shotCounter++;
		if ( _shotCounter >= GetValue( Level, Mod.NumShotsRequired ) )
		{
			Shoot();
		}
		else
		{
			// todo: maybe add a "clink" sfx for the final few shots, to give feedback that you're charging up the spear?
		}

		DisplayText = $"{GetValue( Level, Mod.NumShotsRequired ) - _shotCounter}";
		DisplayCooldown = Utils.Map( _shotCounter, 0f, GetValue( Level, Mod.NumShotsRequired ), 0f, 1f );
	}

	public void Shoot()
	{
		var dir = Player.FacingDir;
		var pos = Player.Position2D + dir * Player.BULLET_SPAWN_OFFSET;
		var zPos = Player.WorldPosition.z + 50f * Player.Stats[PlayerStat.Scale];
		var spearGo = GameObject.Clone( "prefabs/spear.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, -100f ), Rotation.From( 0f, -Utils.GetAngleDegreesFromVector( dir ), 0f ) ) } );
		var spear = spearGo.Components.Get<Spear>( true );

		spear.BaseZPos = zPos;
		spear.Velocity = dir * 180f;
		spear.Shooter = Player;
		spear.Damage = 1f;
		spear.DamagePerPierce = Player.Stats[PlayerStat.MaxHp] * GetValue( Level, Mod.HpPercentPerPierce );
		spear.Lifetime = 2.8f;

		spear.Init();

		spearGo.NetworkSpawn( Player.Network.Owner );

		Manager.Instance.PlaySfxNearbyRpc( "spike.thrust", pos, pitch: Game.Random.Float( 0.94f, 0.98f ), volume: 0.7f, maxDist: 400f );

		_shotCounter = 0;

		Highlight();

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