things/items/PerkItem.cs

An in-game Item subclass representing a perk pickup. It handles visual rotation/hover, configuration via RPC Setup, attraction to nearby players, collision collection logic that grants a perk (including curses) and plays SFX and chat messages.

NetworkingHttp Calls
using System;
using Sandbox;

public class PerkItem : Item
{
	[Property] public PerkWorldPanel PerkWorldPanel { get; set; }
	[Property, Hide] public int PerkTypeIdentity { get; set; }
	[Property, Hide] public string IconPath { get; set; }
	[Property, Hide] public Rarity PerkRarity { get; set; }
	[Property, Hide] public bool Curse { get; set; }

	private float _rotateTimeOffset;
	private float _personalRotateSpeed;

	[Sync] public bool ShouldHidePanel { get; set; }

	public override Vector3 SpawnScale => new Vector3( 1.2f );
	protected override float AttractRangeFactor => 1.25f;
	protected override float AttractStrengthFactor => 0.5f;

	public Player PlayerToTarget { get; set; }

	protected override void OnStart()
	{
		base.OnStart();

		Lifetime = 90f;
		ShouldCheckBounds = true;
		PushStrength = 500f;

		BaseZPos = 13f;
		WorldPosition = WorldPosition.WithZ( BaseZPos );

		Manager.Instance.PlaySfxNearby( "scuffle", Position2D, pitch: Game.Random.Float( 1.4f, 1.45f ), volume: 1.1f, maxDist: 400f );

		if ( IsProxy )
			return;

		Deceleration = 0.92f;

		_rotateTimeOffset = Game.Random.Float( 0f, 10f );
		_personalRotateSpeed = Game.Random.Float( 3f, 7f );
	}

	[Rpc.Broadcast]
	public void Setup( int typeIdent )
	{
		PerkTypeIdentity = typeIdent;

		var type = TypeLibrary.GetTypeByIdent( typeIdent );
		IconPath = Perk.GetImagePath( type.TargetType );
		var attrib = type.GetAttribute<PerkAttribute>();
		PerkRarity = attrib.Rarity;
		Curse = attrib.Curse;
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"{TypeLibrary.GetTypeByIdent( PerkTypeIdentity )}\n{PerkRarity}", new global::Transform( WorldPosition ) );

		if ( Manager.Instance.IsGameOver )
			return;

		if ( IsProxy )
			return;

		if ( PlayerToTarget.IsValid() && !PlayerToTarget.IsDying && !IsInTheAir )
			Velocity += (PlayerToTarget.Position2D - Position2D).Normal * 400f * Time.Delta;

		WorldRotation = Rotation.From( new Angles( Utils.FastSin( _rotateTimeOffset + Time.Now * _personalRotateSpeed ) * 15f, 0f, -30f + Utils.FastSin( _rotateTimeOffset + Time.Now * _personalRotateSpeed * 0.75f ) * 5f ) );

		if ( !IsInTheAir )
			WorldPosition = WorldPosition.WithZ( BaseZPos + 4f + Utils.Map( Utils.FastSin( _rotateTimeOffset + Time.Now * _personalRotateSpeed * 2f ), -1f, 1f, -1f, 1f, EasingType.QuadIn ) * 4f );
	}

	public override void SetVisible( bool visible )
	{
		base.SetVisible( visible );

		ShouldHidePanel = !visible;
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( CantBeCollected )
			return;

		if ( other is Player player )
		{
			if ( !player.IsDead )
			{
				var type = PerkManager.IdentityToType( PerkTypeIdentity );
				var attrib = type.GetAttribute<PerkAttribute>();
				bool isCurse = attrib.Curse;

				// check if already at max level before adding
				bool wasAlreadyMaxLevel = false;
				if( isCurse )
				{
					var existingLevel = player.GetPerkLevel( type );
					var maxLevel = PerkManager.GetMaxLevelForRarity( attrib.Rarity );
					wasAlreadyMaxLevel = existingLevel >= maxLevel;
				}

				player.AddPerkRpc( PerkTypeIdentity );

				if( isCurse && !wasAlreadyMaxLevel )
				{
					player.AddLocalChatMessageRpc( "You got cursed!", from: "", endPerkIdents: new List<int>() { PerkTypeIdentity } );
				}

				// todo: different sfx for curses
				Manager.Instance.PlaySfxNearbyRpc( "heal", player.Position2D, pitch: Game.Random.Float( 1.9f, 2f ), volume: 1.1f, maxDist: 350f );

				player.CollectItemEffect();

				Remove();
			}
		}
	}
}