A rare perk component that heals nearby players when the owner stands still for 2 seconds. It manages readiness and cooldown, displays particle indicators, spawns a visual ring and plays a sound, heals the owner and nearby players via RPCs, and cleans up particles on removal or death.
using Sandbox;
using System;
using System.IO;
[Perk( Rarity.Rare, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Aoe })]
public class PerkBurstHeal : Perk
{
private enum Mod { Cooldown, HpAmount };
private bool _isReady;
private float _stillTimer;
private float _cooldownTimer;
private const float HEAL_RADIUS = 180f;
private const float DISPLAY_RADIUS = 50f;
private ParticleEffect _particleEffect;
private ParticleSpriteRenderer _particleRenderer;
private ParticleEffect _particleEffectBg;
private ParticleSpriteRenderer _particleRendererBg;
private float _bgCurrOpacity;
private const float STILL_TIME = 2f;
static PerkBurstHeal()
{
Register<PerkBurstHeal>(
name: "Burst Heal",
imagePath: "textures/icons/vector/burse_heal.png",
description: level => $"Stop moving for {STILL_TIME}s to heal nearby players for {(int)GetValue( level, Mod.HpAmount)} hp __ (cooldown: {GetValue( level, Mod.Cooldown )}s)",
upgradeDescription: level => $"Stop moving for {STILL_TIME}s to\nheal nearby players for {(int)GetValue( level - 1, Mod.HpAmount )}→{(int)GetValue( level, Mod.HpAmount )} hp\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/burst_heal_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/burst_heal_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;
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.Cooldown:
default:
return 105f - 15f * level;
case Mod.HpAmount:
return 20f + 5f * 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 > STILL_TIME )
{
Shoot();
_stillTimer = 0f;
}
}
float progress = Utils.Map( _stillTimer, 0f, STILL_TIME, 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, 1f, 0.3f, 0.3f ), lifetime: 0.35f, path: "ring2" );
Manager.Instance.PlaySfxNearbyRpc( "heal", Player.Position2D, pitch: Game.Random.Float( 1f, 1.1f ), volume: 1.1f, maxDist: 400f );
Player.Heal( amount: GetValue( Level, Mod.HpAmount ) );
var pos = Player.Position2D;
var radius = GetRadius( visual: false );
var traceResults = Player.Scene.Trace.Sphere( radius, pos, pos ).WithAnyTags( "player" ).HitTriggersOnly().RunAll().ToList();
foreach ( var tr in traceResults )
{
var gameObject = tr.GameObject;
var player = gameObject.GetComponent<Player>();
if ( player.IsDead || !(player.HpPercent < 1f) || player == Player )
continue;
player.HealRpc( amount: GetValue( Level, Mod.HpAmount ), otherPlayerHealer: Player );
}
}
float GetRadius( bool visual = false )
{
return HEAL_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();
}
}