perks/PerkLightning.cs

A game Perk class 'PerkLightning' that charges and spawns a lightning strike target near the player, shows particle indicators while charging, then spawns a lightning bolt GameObject that deals damage and can chain. It manages particle effects, timing, cooldown display and lifecycle (start, update, die, revive, remove).

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Epic, alwaysOfferDebug: false )]
public class PerkLightning : Perk
{
	private enum Mod { Damage, SpreadLimit };

	private float _shockDelayTimer;

	private const float CHARGE_TIME = 3f;
	private const float DELAY = 0.5f;

	private bool _isShockTargetActive;
	private Vector2 _shockOffset;
	private float _shockTargetTimeStart;

	private GameObject _particles;
	private ParticleEffect _particleRing;
	private ParticleRingEmitter _particleRingEmitter;
	private LightningParticleEffect _lightningParticleEffect;

	static PerkLightning()
	{
		Register<PerkLightning>(
			name: "Thunderstorm",
			imagePath: "textures/icons/vector/shock.png",
			description: level => $"Lightning strikes for {(int)GetValue( level, Mod.Damage )} dmg\nand chains {(int)GetValue( level, Mod.SpreadLimit )} times",
			upgradeDescription: level => $"Lightning strikes for {(int)GetValue( level - 1, Mod.Damage )}→{(int)GetValue( level, Mod.Damage )} dmg\nand chains {(int)GetValue( level - 1, Mod.SpreadLimit )}→{(int)GetValue( level, Mod.SpreadLimit )} times"
		);
	}

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

		ShouldUpdate = true;

		_particles = GameObject.Clone( "prefabs/effects/lightning_target_particles.prefab", new global::Transform( Player.WorldPosition.WithZ( -100f ) ) );
		_particleRing = _particles.GetComponent<ParticleEffect>();
		_particleRing.Alpha = 0f;
		_particleRingEmitter = _particles.GetComponent<ParticleRingEmitter>();
		_particleRingEmitter.Rate = 0;
		_lightningParticleEffect = _particles.GetComponent<LightningParticleEffect>();

		_particles.NetworkSpawn();

		_shockDelayTimer = DELAY;

		DisplayCooldownColor = new Color( 1f, 1f, 1f, 0.15f );

		HighlightColor = new Color( 1f, 1f, 0.8f );
		HighlightDuration = 0.15f;
		HighlightOpacity = 0.75f;
	}

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

	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Damage:
			default:
				return 5f + 5f * level;
			case Mod.SpreadLimit:
				return 2 + 3 * level;
		}
	}

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

		if ( Player.IsDead )
			return;

		if ( !_isShockTargetActive )
		{
			_shockDelayTimer += dt;
			if ( _shockDelayTimer > DELAY )
			{
				_isShockTargetActive = true;
				_shockOffset = Utils.GetRandomVector() * Game.Random.Float( 75f, 170f );
				_shockTargetTimeStart = Time.Now;
			}
		}
		else
		{
			// todo: put cooldown in description, allow cooldown reduction perk to affect it?
			if ( Time.Now > _shockTargetTimeStart + CHARGE_TIME )
			{
				Shock( Player.Position2D + _shockOffset );

				_particles.WorldPosition = _particles.WorldPosition.WithZ( -100f );
				_particleRing.Alpha = 0f;
				_particleRingEmitter.Rate = 0;

				_shockDelayTimer = 0f;
				_isShockTargetActive = false;
				DisplayCooldown = 0f;

				_lightningParticleEffect.ResetEffect(); // affects client only 

				Highlight();
			}
			else
			{
				float progress = Utils.Map( Time.Now - _shockTargetTimeStart, 0f, CHARGE_TIME, 0f, 1f );

				var pos = Player.Position2D + _shockOffset;
				_particles.WorldPosition = new Vector3( pos.x, pos.y, 2f );
				_particleRing.Alpha = Utils.Map( progress, 0f, 1f, 0f, 1f, EasingType.ExpoIn );
				_particleRingEmitter.Radius = Utils.Map( progress, 0f, 1f, 30f, 0f, EasingType.SineIn );
				_particleRingEmitter.Rate = 500;

				DisplayCooldown = Utils.Map( Time.Now, _shockTargetTimeStart, _shockTargetTimeStart + CHARGE_TIME, 0f, 1f );
			}
		}
	}

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

		_particles.WorldPosition = _particles.WorldPosition.WithZ( -100f );
		_particleRing.Alpha = 0f;
		_particleRingEmitter.Rate = 0;

		_lightningParticleEffect.SetVisible( false );
	}

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

		_shockDelayTimer = 0f;

		_lightningParticleEffect.SetVisible( true );
	}

	void Shock( Vector2 pos )
	{
		var lightningBoltGo = GameObject.Clone( $"prefabs/lightning_bolt.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, -100f ) ) } );
		var lightningBolt = lightningBoltGo.GetComponent<LightningBolt>();
		lightningBolt.Shooter = Player;
		lightningBolt.Damage = GetValue( Level, Mod.Damage );
		lightningBolt.SpreadLimit = (int)GetValue( Level, Mod.SpreadLimit );

		lightningBoltGo.NetworkSpawn( Player.Network.Owner );
	}

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

		if ( _particles != null )
			_particles.Destroy();
	}
}