A Perk component (PerkRadiation) that periodically damages nearby enemies, can heal nearby players, and may damage the owner. It spawns and controls a particle GameObject, computes pulse hits via sphere traces, applies damage/heal/repel, and updates visual timing and stats.
using System;
using Sandbox;
[Perk( Rarity.Rare, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Aoe, PerkCategory.Radiation, PerkCategory.SelfDmg, PerkCategory.OneHpLeft })]
public class PerkRadiation : Perk
{
public override float ImportanceMultiplier => 1.15f;
private enum Mod { Damage, SelfDamageChance };
private float _timer;
private const float DAMAGE_RADIUS = 130f;
//private Particles _particles;
private GameObject _radiationGo; // destroyed on Restart because it has ParticleEffect
private ParticleSpriteRenderer _particleRenderer;
private RadiationParticleEffect _radiationParticleEffect;
private float _baseDelay;
private float _baseLifetime;
private float _baseRate;
static PerkRadiation()
{
Register<PerkRadiation>(
name: "Radiation",
imagePath: "textures/icons/vector/radiation.png",
description: level => $"Periodically do [+]{ GetValue( level, Mod.Damage ).ToString( "0.#" )}[/+] dmg to nearby enemies, with [-]{GetValue( level, Mod.SelfDamageChance, true )}%[/-] chance to hurt yourself too (this dmg can't kill you)",
upgradeDescription: level => $"Periodically do {GetValue( level - 1, Mod.Damage ).ToString( "0.#" )}→{GetValue( level, Mod.Damage ).ToString( "0.#" )} dmg to nearby enemies, with {GetValue( level - 1, Mod.SelfDamageChance, true )}%→{GetValue( level, Mod.SelfDamageChance, true )}% chance to hurt yourself too (this dmg can't kill you)",
descriptionLineHeight: 10.5f
);
}
public override void Start()
{
base.Start();
ShouldUpdate = true;
_radiationGo = GameObject.Clone( "prefabs/radiation.prefab", new CloneConfig { StartEnabled = true, Parent = Player.GameObject } );
_radiationGo.LocalPosition = new Vector3( 0f, 0f, 5f );
_radiationParticleEffect = _radiationGo.GetComponent<RadiationParticleEffect>();
_particleRenderer = _radiationGo.GetComponent<ParticleSpriteRenderer>();
_particleRenderer.Scale = GetRadius( visual: true );
_baseDelay = Player.Stats[PlayerStat.RadiationDelay];
_baseLifetime = _radiationParticleEffect.ParticleEffect.Lifetime.ConstantValue;
_baseRate = _radiationParticleEffect.SphereEmitter.Rate.ConstantValue;
_radiationGo.NetworkSpawn();
}
public override void Refresh()
{
base.Refresh();
// todo: display radiation damage / delay on stats panel
RefreshHpRegenDisplay();
}
void RefreshHpRegenDisplay()
{
var averageSelfDps = (GetValue( Level, Mod.Damage ) * GetValue( Level, Mod.SelfDamageChance )) / Player.Stats[PlayerStat.RadiationDelay];
Player.Modify( this, PlayerStat.HpRegenDisplay, -averageSelfDps, ModifierType.Add );
// todo: need to refresh if you get perk that lowers self-dmg
// todo: decrease max hp over time instead?
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.Damage:
default:
return 1.1f + 0.6f * level;
case Mod.SelfDamageChance:
return isPercent
? 29f - 2f * level
: 0.29f - 0.02f * level;
}
}
public override void Update( float dt )
{
base.Update( dt );
float delay = Player.Stats[PlayerStat.RadiationDelay];
//Gizmo.Draw.Color = Color.Red.WithAlpha(0.1f);
//Gizmo.Draw.LineSphere( Player.WorldPosition.WithZ( 50f ), GetRadius(), rings: 32 );
_timer += dt;
if ( _timer > delay )
{
Pulse();
_timer = 0f;
}
DisplayCooldown = Utils.Map( _timer, 0f, delay, 0f, 1f );
}
void Pulse()
{
float damage = GetValue( Level, Mod.Damage );
float heal = Player.Stats[PlayerStat.RadiationHealAmount];
float repel = Player.Stats[PlayerStat.RadiationRepelAmount];
var radius = GetRadius();
var pos = Player.Position2D;
var numHits = 0;
var averagePos = Vector2.Zero;
var traceResults = Player.Scene.Trace.Sphere( radius, pos, pos ).WithAnyTags( "enemy", "player" ).HitTriggersOnly().RunAll().ToList();
foreach ( var tr in traceResults )
{
var gameObject = tr.GameObject;
if( gameObject.Tags.Has("enemy"))
{
var enemy = gameObject.GetComponent<Enemy>();
if ( !enemy.IsValid() || enemy.IsDying || enemy.IsInTheAir || (enemy.IsSpawning && !enemy.AlmostFinishedSpawning) )
continue;
Vector2 dir = (enemy.Position2D - pos).Normal;
var hitPos = enemy.Position2D;
if ( damage < 1f )
{
if ( Game.Random.Float( 0f, 1f ) < damage )
damage = 1f;
else
continue;
}
enemy.DamageRpc( damage, Player, DamageType.Radiation, hitPos, force: dir * repel, isCrit: false, shouldFlinch: false );
numHits++;
averagePos += hitPos;
}
else if ( Player.Stats[PlayerStat.RadiationHealAmount] > 0f && gameObject.Tags.Has( "player" ) && gameObject != Player.GameObject )
{
var player = gameObject.GetComponent<Player>();
if ( player.IsValid() && !player.IsDead && player.Health < player.GetSyncStat( PlayerStat.MaxHp ) )
{
player.HealRpc( heal, otherPlayerHealer: Player );
}
}
}
if( numHits > 0 )
Manager.Instance.PlaySfxNearbyRpc( "enemy.hit", averagePos / numHits, pitch: Game.Random.Float( 0.9f, 1f ), volume: 0.5f, maxDist: 250f );
if ( Game.Random.Float( 0f, 1f ) < GetValue( Level, Mod.SelfDamageChance ) )
{
Player.Damage( damage, DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: 0f, ragdollForce: 0.2f, enemySource: null, enemyType: EnemyType.None, cantKill: true );
}
}
float GetRadius( bool visual = false )
{
//return DAMAGE_RADIUS * Player.Stats[PlayerStat.RadiusMultiplier] * (Utils.MapReturn( _timer, 0f, Player.Stats[PlayerStat.RadiationDelay], 1f, 0.9f, EasingType.QuadInOut )) * (visual ? 1.4f : 1f);
return DAMAGE_RADIUS * Player.Stats[PlayerStat.RadiusMultiplier] * (visual ? 2.5f : 1f);
}
public override void OnAddPerkAfter( TypeDescription type )
{
base.OnAddPerkAfter( type );
if ( type.TargetType == typeof( PerkRadiusMultiplier ) )
{
_radiationParticleEffect.SetRadius( GetRadius( visual: true ) );
}
else if ( type.TargetType == typeof( PerkRadiationRepel) || type.TargetType == typeof( PerkRadiationDelay ) )
{
float change = Player.Stats[PlayerStat.RadiationDelay] / _baseDelay;
_radiationParticleEffect.SetTiming( lifetime: _baseLifetime * change, rate: _baseRate / change );
//RefreshHpRegenDisplay();
}
else if ( type.TargetType == typeof( PerkRadiationHeal ) )
{
_radiationParticleEffect.ShowHealing();
}
}
public override void OnDie()
{
base.OnDie();
_radiationParticleEffect.SetVisible( false );
}
public override void OnRevive()
{
base.OnRevive();
_radiationParticleEffect.SetVisible( true );
}
public override void Remove( bool restart = false )
{
base.Remove( restart );
if( _radiationGo != null )
_radiationGo.Destroy();
}
}