perks/CurseDropPerk.cs

A Perk class 'CurseDropPerk' that causes the player to drop a random perk when they pick up a healthpack. It chooses a non-curse perk if available, otherwise a different curse, removes it from the player, picks a random far position within spawn bounds, and requests the manager to spawn a perk item via RPC. It also sets visual highlight and icon animation values.

Networking
using System;
using System.Linq;
using Sandbox;

[Perk( Rarity.Unique, curse: true, alwaysOfferDebug: false )]
public class CurseDropPerk : Perk
{
	static CurseDropPerk()
	{
		Register<CurseDropPerk>(
			name: "Fumble",
			imagePath: "textures/icons/vector/curse_drop_perk.png",
			description: level => $"Drop a perk to a random location\nwhen you get healthpack"
		);
	}

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

		HighlightColor = new Color( 0.7f, 0.2f, 0.5f );
		HighlightDuration = 0.35f;
		HighlightOpacity = 3f;
	}

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

	public override void OnGainHealthpack( float amount )
	{
		base.OnGainHealthpack( amount );

		var perkTypeToDrop = GetPerkTypeToDrop();
		if ( perkTypeToDrop == null )
			return;

		var typeIdentity = PerkManager.TypeToIdentity( perkTypeToDrop );

		Player.LevelDownPerk( perkTypeToDrop );

		var dropPos = GetRandomFarPosition( 300f );
		Manager.Instance.SpawnPerkItemAtRpc( typeIdentity, Player.Position2D, dropPos );

		Highlight();

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

		// todo: sfx
	}

	private TypeDescription GetPerkTypeToDrop()
	{
		var candidates = Player.Perks
			.Where( kvp => !kvp.Value.IsCurse )
			.Select( kvp => PerkManager.IdentityToType( kvp.Key ) )
			.ToList();

		if ( candidates.Count > 0 )
			return candidates[Game.Random.Int( 0, candidates.Count - 1 )];

		candidates = Player.Perks
			.Where( kvp => kvp.Value.IsCurse && kvp.Value.GetType() != typeof( CurseDropPerk ) )
			.Select( kvp => PerkManager.IdentityToType( kvp.Key ) )
			.ToList();

		if ( candidates.Count > 0 )
			return candidates[Game.Random.Int( 0, candidates.Count - 1 )];

		return null;
	}

	private Vector2 GetRandomFarPosition( float minDist )
	{
		var mgr = Manager.Instance;
		var playerPos = Player.Position2D;

		for ( int i = 0; i < 30; i++ )
		{
			var pos = new Vector2(
				Game.Random.Float( mgr.BOUNDS_MIN_SPAWN.x, mgr.BOUNDS_MAX_SPAWN.x ),
				Game.Random.Float( mgr.BOUNDS_MIN_SPAWN.y, mgr.BOUNDS_MAX_SPAWN.y )
			);
			if ( (pos - playerPos).Length >= minDist )
				return pos;
		}

		return mgr.ClampPosToBounds( playerPos + Utils.GetRandomVector() * minDist );
	}
}