perks/PerkTreehugger.cs

A Legendary perk that heals the player while touching a tree. It tracks collisions with Tree objects, accumulates a touch timer, heals the player every DELAY seconds, updates display values and a VFX GameObject that visually links the player to the tree.

NetworkingFile Access
using System;
using Sandbox;

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

	private const float DELAY = 1f;
	private const string TreehuggerVfxPrefabPath = "prefabs/effects/laser_line_renderer.prefab";

	private float _touchTimer;
	private bool _touchingTreeThing;
	private Tree _tree;

	private bool _isRegenerating;
	private GameObject _treehuggerVfxGo;
	private PerkTreehuggerVfx _treehuggerVfx;

	static PerkTreehugger()
	{
		Register<PerkTreehugger>(
			name: "Treehugger",
			imagePath: "textures/icons/vector/treehugger.png",
			description: level => $"Heal {GetValue( level, Mod.RegenAmount ).ToString("0.#")} hp/s while\ntouching a tree",
			upgradeDescription: level => $"Heal {GetValue( level - 1, Mod.RegenAmount ).ToString( "0.#" )}→{GetValue(level, Mod.RegenAmount).ToString("0.#")} hp/s while\ntouching a tree"
		);
	}

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

		ShouldUpdate = true;
		EnsureTreehuggerVfx();

		HighlightColor = new Color( 0.3f, 1f, 0.3f );
		HighlightDuration = 0.2f;
		HighlightOpacity = 1f;
	}

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

		RefreshDisplay();
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.RegenAmount:
			default:
				return 0.5f + 1.0f * level;
		}
	}

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

		if ( _touchingTreeThing )
		{
			if ( !_tree.IsValid() || (_tree.Position2D - Player.Position2D).LengthSquared > MathF.Pow( Player.Radius + _tree.Radius + 15f, 2f ) )
			{
				_touchingTreeThing = false;
				_tree = null;
			}
		}

		float BUFFER = Player.Radius * 1.2f;
		var pos = Player.Position2D;

		bool touchingBounds = false;
			//(pos.x > Manager.Instance.BOUNDS_MAX.x - BUFFER) ||
			//(pos.x < Manager.Instance.BOUNDS_MIN.x + BUFFER) ||
			//(pos.y > Manager.Instance.BOUNDS_MAX.y - BUFFER) ||
			//(pos.y < Manager.Instance.BOUNDS_MIN.x + BUFFER);

		bool isRegenerating = touchingBounds || _touchingTreeThing;
		if ( isRegenerating )
		{
			_touchTimer += dt;
			if ( _touchTimer > DELAY )
			{
				Player.Heal( GetValue( Level, Mod.RegenAmount ) );
				_touchTimer = 0f;

				Highlight();
			}

			DisplayCooldown = Utils.Map( _touchTimer, 0f, DELAY, 0f, 1f );
		}

		DisplayCooldownColor = isRegenerating ? new Color( 0f, 0f, 0f, 5f ) : new Color( 0f, 0.25f, 0f, 1f );

		if ( isRegenerating != _isRegenerating )
			SetIsRegenerating( isRegenerating );

		UpdateTreehuggerVfx( isRegenerating && _touchingTreeThing ? _tree : null );

		//DebugOverlay.Text($"{touchingBounds}", Player.Position, 0f, float.MaxValue);
	}

	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		if ( _treehuggerVfxGo.IsValid() )
			_treehuggerVfxGo.Destroy();
	}

	void SetIsRegenerating( bool isRegenerating )
	{
		_isRegenerating = isRegenerating;
		RefreshDisplay();
	}

	void RefreshDisplay()
	{
		Player.Modify( this, PlayerStat.HpRegenDisplay, _isRegenerating ? (GetValue( Level, Mod.RegenAmount ) / DELAY) : 0f, ModifierType.Add );
	}

	private void EnsureTreehuggerVfx()
	{
		if ( _treehuggerVfxGo.IsValid() )
			return;

		_treehuggerVfxGo = GameObject.Clone( TreehuggerVfxPrefabPath, new CloneConfig
		{
			StartEnabled = true,
			Transform = new Transform( Player.WorldPosition ),
			Parent = Player.GameObject,
		} );

		_treehuggerVfx = _treehuggerVfxGo.AddComponent<PerkTreehuggerVfx>();
		_treehuggerVfx.LineRenderer = _treehuggerVfxGo.GetComponent<LineRenderer>( includeDisabled: true )
			?? _treehuggerVfxGo.GetComponentInChildren<LineRenderer>( includeDisabled: true, includeSelf: true );
		_treehuggerVfx.OwnerPlayer = Player;
		_treehuggerVfxGo.NetworkSpawn();
	}

	private void UpdateTreehuggerVfx( Tree tree )
	{
		EnsureTreehuggerVfx();

		if ( !_treehuggerVfx.IsValid() )
			return;

		_treehuggerVfx.OwnerPlayer = Player;
		_treehuggerVfx.SourceTree = tree;
	}

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

		if ( _touchingTreeThing )
			return;

		if ( other is Tree tree )
		{
			_tree = tree;
			_touchingTreeThing = true;
		}
	}
}