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).
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();
}
}