perks/PerkWrapArenaEdge.cs

A player perk named Wraparound that teleports the player to the opposite side of the arena when they touch the bounds, with a cooldown and visual highlight effects. It checks player 2D position against arena bounds and calls Player.Teleport to move them, tracking cooldown via TimeSince.

NetworkingFile Access
using System;
using Sandbox;

[Perk( Rarity.Legendary, alwaysOfferDebug: false )]
public class PerkWrapArenaEdge : Perk
{
	private enum Mod { Cooldown };

	private TimeSince _timeSinceWarping;

	private bool _isActive;

	static PerkWrapArenaEdge()
	{
		Register<PerkWrapArenaEdge>(
			name: "Wraparound",
			imagePath: "textures/icons/vector/wrap_arena_edge.png",
			description: level => $"Teleport when you touch the fence __ (cooldown: {GetValue( level, Mod.Cooldown )}s)",
			upgradeDescription: level => $"Teleport when you touch the fence __ (cooldown: {GetValue( level - 1, Mod.Cooldown )}→{GetValue( level, Mod.Cooldown )}s)"
		);
	}

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

		ShouldUpdate = true;
		_timeSinceWarping = 60f;

		// todo: too annoying to teleport without expecting it?
		// add a indicator circle or something
	}

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

		_isActive = true;
	}

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

	}

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

		float cooldown = GetValue( Level, Mod.Cooldown );

		if ( _isActive && !Player.IsInTheAir )
		{
			var pos = Player.Position2D;
			var amount = 1.15f;
			var minX = Manager.Instance.BOUNDS_MIN.x - Player.BoundsExpand + Player.Radius * amount;
			var maxX = Manager.Instance.BOUNDS_MAX.x + Player.BoundsExpand - Player.Radius * amount;
			var minY = Manager.Instance.BOUNDS_MIN.y - Player.BoundsExpand + Player.Radius * amount;
			var maxY = Manager.Instance.BOUNDS_MAX.y + Player.BoundsExpand - Player.Radius * amount;

			if ( pos.x > maxX && pos.y > maxY )			WarpTo( new Vector2( minX, minY ) );
			else if ( pos.x > maxX && pos.y < minY )	WarpTo( new Vector2( minX, maxY ) );
			else if ( pos.x < minX && pos.y > maxY )	WarpTo( new Vector2( maxX, minY ) );
			else if ( pos.x < minX && pos.y < minY )	WarpTo( new Vector2( maxX, maxY ) );
			else if ( pos.x > maxX )					WarpTo( new Vector2( minX, pos.y ) );
			else if ( pos.x < minX )					WarpTo( new Vector2( maxX, pos.y ) );
			else if ( pos.y > maxY )					WarpTo( new Vector2( pos.x, minY ) );
			else if ( pos.y < minY )					WarpTo( new Vector2( pos.x, maxY ) );
		}
		else
		{
			if ( _timeSinceWarping > cooldown )
			{
				_isActive = true;

				HighlightColor = new Color( 0.6f, 0.6f, 1f );
				HighlightDuration = 0.25f;
				HighlightOpacity = 1.2f;
				Highlight();
			}
		}

		DisplayText = !_isActive ? $"{MathX.CeilToInt( cooldown - _timeSinceWarping )}" : " ";
		DisplayCooldown = !_isActive ? Utils.Map( _timeSinceWarping, 0f, cooldown, 1f, 0f ) : 0f;
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Cooldown:
			default:
				return level == 1 ? 60f : 30f;
		}
	}

	void WarpTo( Vector2 pos )
	{
		Player.Teleport( pos );

		_timeSinceWarping = 0f;
		_isActive = false;

		HighlightColor = new Color( 0.1f, 0.2f, 1f );
		HighlightDuration = 0.5f;
		HighlightOpacity = 4f;
		Highlight();
	}
}