perks/CurseChainGround.cs

A unique perk class 'CurseChainGround' that periodically tethers the player to a nearby ground position. It tracks cooldown and time-since-last-tether, updates display values, and triggers visual/audio feedback and camera shake when the chain is applied.

NetworkingFile Access
using Sandbox;
using System;
using System.Numerics;
using System.Reflection;

[Perk( Rarity.Unique, curse: true, alwaysOfferDebug: false )]
public class CurseChainGround : Perk
{
	private enum Mod { Time, Cooldown };

	private TimeSince _timeSinceTether;

	static CurseChainGround()
	{
		Register<CurseChainGround>(
			name: "Tethered",
			imagePath: "textures/icons/vector/curse_chain_ground.png",
			description: level => $"Every [-]{GetValue( level, Mod.Cooldown )}s[/-], get\nchained for [-]{GetValue( level, Mod.Time )}s[/-]"
		);
	}

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

		DisplayCooldownColor = new Color( 1f, 0.5f, 0.4f, 0.6f );

		ShouldUpdate = true;
		_timeSinceTether = 0f;
	}

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

	}

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

		DisplayCooldown = Utils.Map( _timeSinceTether, 0f, GetValue( Level, Mod.Cooldown ), 0f, 1f );
		DisplayText = $"{MathX.CeilToInt( GetValue( Level, Mod.Cooldown ) - _timeSinceTether )}";

		if ( _timeSinceTether > GetValue( Level, Mod.Cooldown ) )
		{
			_timeSinceTether = 0f;
			DisplayCooldown = 0f;

			if ( !Player.IsDead )
			{
				var pos = Manager.Instance.ClampPosToBounds( Player.Position2D + Player.FacingDir * 25f );
				Player.Chain( anchorUnit: null, chainPos: pos, chainLength: 150f, lifetime: GetValue( Level, Mod.Time ) );

				HighlightColor = new Color( 1f, 0.25f, 0.3f );
				HighlightDuration = 0.5f;
				HighlightOpacity = 4f;
				Highlight();

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

				Manager.Instance.RequestTimeScale( startTimeScale: 0f, endTimeScale: 1f, duration: 0.15f, priority: 10 );

				Player.ShakeRpc( startStrength: 1f, endStrength: 0f, time: 0.15f, easingType: EasingType.SineInOut );
			}
		}
	}

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