A legendary perk component that triggers a Frost Nova when the player stands still long enough. It spawns particle indicators, tracks readiness and cooldown, expands a visual indicator while the player is still, and on trigger freezes nearby enemies and plays effects.
using System;
using Sandbox;
[Perk( Rarity.Legendary, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Aoe, PerkCategory.Freeze })]
public class PerkFrostNova : Perk
{
private enum Mod { TriggerTime, Cooldown };
private bool _isReady;
private float _stillTimer;
private float _cooldownTimer;
private const float FREEZE_RADIUS = 210f;
private const float DISPLAY_RADIUS = 50f;
private ParticleEffect _particleEffect;
private ParticleSpriteRenderer _particleRenderer;
private ParticleEffect _particleEffectBg;
private ParticleSpriteRenderer _particleRendererBg;
private float _bgCurrOpacity;
static PerkFrostNova()
{
Register<PerkFrostNova>(
name: "Frost Nova",
imagePath: "textures/icons/vector/frost_nova.png",
description: level => $"Stop moving for {GetValue( level, Mod.TriggerTime )}s to\nfreeze nearby enemies\n(cooldown: {GetValue( level, Mod.Cooldown )}s)",
upgradeDescription: level => $"Stop moving for {GetValue( level - 1, Mod.TriggerTime )}→{GetValue( level, Mod.TriggerTime )}s to\nfreeze nearby enemies\n(cooldown: {GetValue( level - 1, Mod.Cooldown )}→{GetValue( level, Mod.Cooldown )}s)"
);
}
public override void Start()
{
base.Start();
ShouldUpdate = true;
var particleGo = GameObject.Clone( "prefabs/effects/frost_nova_indicator.prefab", new CloneConfig { StartEnabled = true, Parent = Player.GameObject } );
particleGo.LocalPosition = new Vector3( 0f, 0f, 4f );
_particleEffect = particleGo.GetComponent<ParticleEffect>();
_particleRenderer = particleGo.GetComponent<ParticleSpriteRenderer>();
_particleRenderer.Scale = 0f;
var particleBgGo = GameObject.Clone( "prefabs/effects/frost_nova_indicator_bg.prefab", new CloneConfig { StartEnabled = true, Parent = Player.GameObject } );
particleBgGo.LocalPosition = new Vector3( 0f, 0f, 3.5f );
_particleEffectBg = particleBgGo.GetComponent<ParticleEffect>();
_particleRendererBg = particleBgGo.GetComponent<ParticleSpriteRenderer>();
_particleRendererBg.Scale = DISPLAY_RADIUS * Player.LocalScale.x;
HighlightColor = new Color( 0.7f, 0.7f, 1f );
HighlightDuration = 0.25f;
HighlightOpacity = 4f;
}
public override void IncreaseLevel()
{
base.IncreaseLevel();
_isReady = true;
_stillTimer = 0f;
_bgCurrOpacity = 0f;
}
public override void Refresh()
{
base.Refresh();
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.TriggerTime:
default:
return 1.5f - 0.5f * level;
case Mod.Cooldown:
return 45f - 15f * level;
}
}
public override void Update( float dt )
{
base.Update( dt );
var displayRadius = DISPLAY_RADIUS * Player.LocalScale.x;
if ( _isReady )
{
if ( Player.IsMoving )
{
_stillTimer = 0f;
}
else
{
_stillTimer += dt;
if ( _stillTimer > GetValue( Level, Mod.TriggerTime ) )
{
Shoot();
_stillTimer = 0f;
}
}
float progress = Utils.Map( _stillTimer, 0f, GetValue( Level, Mod.TriggerTime ), 0f, 1f );
_particleRenderer.Scale = progress * displayRadius;
_particleEffect.Alpha = Utils.Map( progress, 0f, 1f, 0f, 1f, EasingType.QuadOut );
}
else
{
_cooldownTimer += dt;
if ( _cooldownTimer > GetValue( Level, Mod.Cooldown ) )
{
_isReady = true;
Highlight();
}
_particleRenderer.Scale = 0f;
_particleEffect.Alpha = 0f;
}
float bgTargetOpacity = !_isReady ? 0f : (Player.IsMoving ? 0.1f : 0.4f);
_bgCurrOpacity = Utils.DynamicEaseTo( _bgCurrOpacity, bgTargetOpacity, 0.2f, dt );
_particleRendererBg.Scale = displayRadius;
_particleEffectBg.Alpha = _bgCurrOpacity;
DisplayText = _isReady ? " " : $"{MathX.CeilToInt( GetValue( Level, Mod.Cooldown ) - _cooldownTimer )}";
DisplayCooldown = _isReady ? 0f : Utils.Map( _cooldownTimer, 0f, GetValue( Level, Mod.Cooldown ), 1f, 0f );
//Utils.DrawCircle(Player.Position2D, GetRadius(visual: false), 20, 0f, Color.Blue);
}
void Shoot()
{
_isReady = false;
_cooldownTimer = 0f;
Manager.Instance.SpawnRingRpc( Player.Position2D, GetRadius(visual: true), new Color( 0.3f, 0.3f, 1f, 0.3f ), lifetime: 0.35f, path: "ring_spiky" );
Manager.Instance.PlaySfxNearbyRpc( "frozen", Player.Position2D, pitch: Game.Random.Float( 1.3f, 1.35f ), volume: 0.8f, maxDist: 350f );
var pos = Player.Position2D;
var radius = GetRadius( visual: false );
var traceResults = Player.Scene.Trace.Sphere( radius, pos, pos ).WithAnyTags( "enemy" ).HitTriggersOnly().RunAll().ToList();
foreach ( var tr in traceResults )
{
var gameObject = tr.GameObject;
var enemy = gameObject.GetComponent<Enemy>();
if ( enemy.IsDying || enemy.IsInTheAir || (enemy.IsSpawning && !enemy.AlmostFinishedSpawning) )
continue;
Vector2 dir = (enemy.Position2D - pos).Normal;
var hitPos = enemy.Position2D;
enemy.Freeze( playerSource: Player, enemySource: null, Player.Stats[PlayerStat.FreezeTimeScale], Player.Stats[PlayerStat.FreezeLifetime], playSfx: false );
}
}
float GetRadius( bool visual = false )
{
return FREEZE_RADIUS * Player.Stats[PlayerStat.RadiusMultiplier] * (visual ? 1.1f : 1f);
}
public override void OnDie()
{
base.OnDie();
_particleEffect.Alpha = 0f;
_particleEffectBg.Alpha = 0f;
}
public override void Remove( bool restart = false )
{
base.Remove( restart );
if ( _particleEffect != null )
_particleEffect.Destroy();
if ( _particleEffectBg != null )
_particleEffectBg.Destroy();
}
}