perks/PerkAcidBlood.cs

A player perk class 'PerkAcidBlood' that spawns an acid puddle when the player is hit. It manages perk registration, cooldown, visual particle prefab, activation state, and RPC spawn parameters for the puddle damage and lifetime.

NetworkingFile Access
using Sandbox;
using System;
using System.Numerics;

[Perk( Rarity.Epic, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Aoe })]
public class PerkAcidBlood : Perk
{
	private enum Mod { DmgPercent, Cooldown };

	private TimeSince _timeSinceBlood;

	private bool _isActive;

	private GameObject _particlesGo;
	private ParticleEffect _particleEffect;
	private ParticleEmitter _particleEmitter;

	static PerkAcidBlood()
	{
		Register<PerkAcidBlood>(
			name: "Acid Blood",
			imagePath: "textures/icons/vector/acid_blood.png",
			description: level => $"When hit, spawn acid that\ndoes {(int)GetValue( level, Mod.DmgPercent, true )}% of the hit's dmg\n(cooldown: {GetValue( level, Mod.Cooldown )}s)",
			upgradeDescription: level => $"When hit, spawn acid that\ndoes {(int)GetValue( level - 1, Mod.DmgPercent, true )}%→{(int)GetValue( level, Mod.DmgPercent, true )}% of the hit's dmg\n(cooldown: {GetValue( level - 1, Mod.Cooldown )}→{GetValue( level, Mod.Cooldown )}s)"
		);
	}

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

		DisplayCooldownColor = new Color( 0.7f, 0.7f, 1f, 0.5f );

		_particlesGo = GameObject.Clone( "prefabs/effects/acid_blood.prefab", new CloneConfig { StartEnabled = true, Parent = Player.GameObject } );
		_particlesGo.LocalPosition = new Vector3( 0f, 0f, 35f );

		_particleEffect = _particlesGo.GetComponent<ParticleEffect>();
		_particleEmitter = _particlesGo.GetComponent<ParticleEmitter>();
	}

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

		BecomeReady();
	}

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

	}

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

		var cooldown = GetValue( Level, Mod.Cooldown );
		if ( _timeSinceBlood > cooldown )
		{
			BecomeReady();
		}
		else
		{
			DisplayCooldown = Utils.Map( _timeSinceBlood, 0f, cooldown, 1f, 0f );
		}
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.DmgPercent:
			default:
				return isPercent
					? 25f * level
					: 0.25f * level;
			case Mod.Cooldown:
				return 14f - level * 2f;
		}
	}

	public override void OnHit( float amount, DamageType damageType, bool isSelfInflicted, Vector2 dir, float force, Enemy enemySource, EnemyType enemyType, float previousHealth )
	{
		base.OnHit( amount, damageType, isSelfInflicted, dir, force, enemySource, enemyType, previousHealth );

		if ( damageType == DamageType.Self )
			return;

		if ( !_isActive )
			return;

		var dmg = Math.Max( amount * GetValue( Level, Mod.DmgPercent ), 1f );

		var pos = Player.Position2D - dir * Player.Radius;
		var scale = 1.5f * Player.Stats[PlayerStat.RadiusMultiplier];
		var lifetime = Game.Random.Float( 10f, 11f );
		Manager.Instance.SpawnAcidPuddleRpc( pos, lifetime, dmg, scale, new Color( 0.1f, 0.2f, 0.6f ), new Color( 0.3f, 0.3f, 0.9f ),
			damagePlayers: false, damageEnemies: true, playerSource: Player, enemySource: null, enemyType: EnemyType.None );

		_timeSinceBlood = 0f;

		HighlightColor = new Color( 0.3f, 0.4f, 1f );
		HighlightDuration = 0.3f;
		HighlightOpacity = 4f;
		Highlight();

		IconScale = Game.Random.Float( 1.2f, 1.3f );
		IconAngleOffset = Game.Random.Float( 10f, 20f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);

		// todo: sfx

		BecomeNotReady();
	}

	void BecomeReady()
	{
		_isActive = true;
		DisplayCooldown = 0f;
		ShouldUpdate = false;

		HighlightColor = new Color( 0.5f, 0.5f, 1f );
		HighlightDuration = 0.2f;
		HighlightOpacity = 2f;
		Highlight();

		IconScale = Game.Random.Float( 1.05f, 1.1f );
		IconAngleOffset = Game.Random.Float( 3f, 6f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);

		if ( _particlesGo.IsValid() )
			_particlesGo.Enabled = true;

		Player.ScaleHeightRpc( amount: 1.5f, time: Game.Random.Float( 0.05f, 0.06f ) );

		// todo: sfx
	}

	void BecomeNotReady()
	{
		_isActive = false;
		ShouldUpdate = true;
		DisplayCooldown = 1f;

		if ( _particleEmitter.IsValid() && _particleEffect.IsValid() )
			_particleEmitter.Emit( _particleEffect );

		if( _particlesGo.IsValid() )
			_particlesGo.Enabled = false;
	}

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

		BecomeNotReady();
	}

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

		BecomeNotReady();
	}

	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		if ( _particlesGo.IsValid() )
			_particlesGo.Destroy();
	}
}