perks/PerkDrainHealOtherPlayers.cs

A multiplayer-only perk named "Sacrificial Pulse" that periodically damages the owner and heals nearby other players. It checks nearby alive players inside a radius, heals them via HealRpc, spawns a visual ring, plays a sound, and applies self-damage to the perk owner on a 1s interval.

Networking
using Sandbox;
using System;

[Perk( Rarity.Rare, multiplayerMode: MultiplayerMode.OnlyMultiplayer, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.SelfDmg, PerkCategory.Aoe })]
public class PerkDrainHealOtherPlayers : Perk
{
	private enum Mod { DrainAmount, HealAmount };

	public const float HEAL_RADIUS = 150f;

	private TimeSince _timeSinceHeal;


	static PerkDrainHealOtherPlayers()
	{
		Register<PerkDrainHealOtherPlayers>(
			name: "Sacrificial Pulse",
			imagePath: "textures/icons/vector/drain_heal_other_players.png",
			description: level => $"When near other players,\nget -{GetValue( level, Mod.DrainAmount ).ToString("0.#")} hp/s\nand heal them for [+]{GetValue( level, Mod.HealAmount ).ToString("0.#")}[/+] hp/s",
			upgradeDescription: level => $"When near other players,\nget -{GetValue( level - 1, Mod.DrainAmount ).ToString( "0.#" )}→-{GetValue( level, Mod.DrainAmount ).ToString("0.#")} hp/s\nand heal them for {GetValue( level - 1, Mod.HealAmount ).ToString( "0.#" )}→{GetValue( level, Mod.HealAmount ).ToString("0.#")} hp/s"
		);
	}
	
	public override void Start()
	{
		base.Start();

		HighlightColor = new Color( 0.8f, 1f, 0.8f );
		HighlightDuration = 0.4f;
		HighlightOpacity = 1.5f;

		ShouldUpdate = true;
	}

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

		_timeSinceHeal = 0f;
	}

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

		if ( _timeSinceHeal > 1f )
		{
			Vector2 pos = Player.Position2D;
			float radius = HEAL_RADIUS * Player.Stats[PlayerStat.RadiusMultiplier];
			bool didHeal = false;

			foreach ( Player player in Manager.Instance.AlivePlayers )
			{
				if ( player == Player )
					continue;

				if ( (player.Position2D - pos).LengthSquared < MathF.Pow( radius, 2f ) )
				{
					didHeal = true;

					if ( player.Health < player.GetSyncStat( PlayerStat.MaxHp ) )
						player.HealRpc( GetValue( Level, Mod.HealAmount ), playSfx: false, Player );
				}
			}

			if ( didHeal )
			{
				Manager.Instance.SpawnRingRpc( Player.Position2D, radius, new Color( 0.8f, 0.5f, 0.8f, 0.1f ), lifetime: 0.5f, path: "ring2" );

				Player.Damage( GetValue( Level, Mod.DrainAmount ), DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: 0f, ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );

				Manager.Instance.PlaySfxNearbyRpc( "heal", Player.Position2D, pitch: Utils.Map( Player.HpPercent, 1f, 0f, 1.5f, 2f ), volume: 0.4f, maxDist: 240f );

				Highlight();

				IconScale = Game.Random.Float( 1.2f, 1.3f );
				IconAngleOffset = Game.Random.Float( 10f, 20f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
			}

			_timeSinceHeal = 0f;
		}
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.DrainAmount: default:
				return 1f + 1f * level;
			case Mod.HealAmount:
				return 1.2f + 1.1f * level;
		}
	}
}