perks/PerkXpDamage.cs

A Perk class for the game that grants an effect called "XP Shrapnel". When the player gains XP coins it spawns a visual ring, plays a nearby sound, and deals damage to nearby enemies scaled by the XP amount and perk level. It also updates a display stat and highlight visuals.

Networking
using System;
using Sandbox;

[Perk( Rarity.Uncommon, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Aoe })]
public class PerkXpDamage : Perk
{
	private enum Mod { DamageAmount };

	private const float RADIUS = 120f;


	static PerkXpDamage()
	{
		Register<PerkXpDamage>(
			name: "XP Shrapnel",
			imagePath: "textures/icons/vector/xp_damage.png",
			description: level => $"xp coins hurt nearby enemies for\n{GetValue( level, Mod.DamageAmount ).ToString( "0.#" )} dmg per point of xp",
			upgradeDescription: level => $"xp coins hurt nearby enemies for\n{GetValue( level - 1, Mod.DamageAmount ).ToString( "0.#" )}→{GetValue( level, Mod.DamageAmount ).ToString( "0.#" )} dmg per point of xp"
		);
	}

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

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

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

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

	public override void OnGainXpCoin( float xp )
	{
		base.OnGainXpCoin( xp );

		float radius = RADIUS * Player.Stats[PlayerStat.RadiusMultiplier];
		float damage = xp * GetValue( Level, Mod.DamageAmount );

		Manager.Instance.PlaySfxNearbyRpc( "player.shoot", Player.Position2D, pitch: Utils.Map( xp, 1, 16, 1.2f, 1.4f ), volume: Utils.Map( xp, 1, 16, 0.75f, 1f ), maxDist: 300f );

		Manager.Instance.SpawnRingRpc( Player.Position2D, radius, new Color( 0.2f, 0.2f, 1f, 0.2f ), lifetime: Game.Random.Float( 0.2f, 0.3f ), path: "ring_jagged" );

		Manager.Instance.DamageNearbyEnemies( Player.Position2D, radius, damage, force: damage * 0.25f, DamageType.XpShrapnel, Player );

		Highlight();
	}

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