perks/PerkPacifistXp.cs

Perk class for a "Pacifist" legendary perk. It tracks time since the player last damaged a counted enemy and awards +2 XP periodically when the player goes a configured number of seconds without hurting enemies, plus visual and sound feedback.

Networking
using System;
using Sandbox;

[Perk( Rarity.Legendary, locked: true, alwaysOfferDebug: false )]
public class PerkPacifistXp : Perk
{
	private enum Mod { Time };

	private TimeSince _timeSince;

	static PerkPacifistXp()
	{
		Register<PerkPacifistXp>(
			name: "Pacifist",
			imagePath: "textures/icons/vector/pacifist_xp.png",
			description: level => $"+2 xp when you don't hurt any enemies for {(int)GetValue( level, Mod.Time )}s",
			upgradeDescription: level => $"+2 xp when you don't hurt any enemies for {(int)GetValue( level - 1, Mod.Time )}→{(int)GetValue( level, Mod.Time )}s"
		);
	}

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

		ShouldUpdate = true;
		_timeSince = 0f;

		DisplayCooldownColor = new Color( 0.4f, 0.4f, 1f, 3f );
	}

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

	}

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

		if ( _timeSince > GetValue( Level, Mod.Time ) )
		{
			float value = 2f * Player.Stats[PlayerStat.XpGainMultiplier];

			if ( Manager.Instance.IsCommunismActive )
				Manager.Instance.AddCommunismXp( value, XpSource.Passive, spawnFloater: true, playSfx: true );
			else
				Player.AddXpRpc( value, XpSource.Passive );

			// todo: is this needed?
			Manager.Instance.PlaySfxNearby( "xp", Player.Position2D, pitch: Game.Random.Float( 0.4f, 0.6f ), volume: 0.2f, maxDist: 200f );

			_timeSince = 0f;

			HighlightColor = new Color( 0.5f, 0.5f, 1f );
			HighlightDuration = 0.25f;
			HighlightOpacity = 3f;
			Highlight();
		}

		DisplayText = $"{Math.Round( GetValue( Level, Mod.Time ) - _timeSince )}";
		DisplayCooldown = Utils.Map( _timeSince, 0f, GetValue( Level, Mod.Time ), 0f, 1f );
	}

	public override void OnDamageEnemy( Enemy enemy, float amount, DamageType damageType, Vector2 dir, bool isCrit )
	{
		base.OnDamageEnemy( enemy, amount, damageType, dir, isCrit );

		if ( !enemy.CountsAsKill )
			return;

		_timeSince = 0f;

		HighlightColor = new Color( 1f, 0.3f, 0.3f );
		HighlightDuration = 0.15f;
		HighlightOpacity = 1f;
		Highlight();
	}

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