perks/PerkTreehuggerVfx.cs

A visual effect component that renders a pulsing, wavy line between a tree and its owning player. It configures a LineRenderer, computes per-point positions with waves, flow and width pulses, and updates a color/alpha gradient each frame.

Native Interop
using System;
using System.Collections.Generic;
using Sandbox;

public sealed class PerkTreehuggerVfx : Component
{
	private const int NUM_POINTS = 8;
	private const float MIN_HEIGHT = 5f;
	private const float TREE_HEIGHT_FACTOR = 0.25f;
	private const float PLAYER_HEIGHT_FACTOR = 0.55f;
	private const float WAVE_SPEED = 12f;
	private const float WAVE_SPEED_2 = 19f;
	private const float WAVE_FREQ = 9f;
	private const float WAVE_FREQ_2 = 16f;
	private const float WAVE_SCALE = 0.03f;
	private const float FORWARD_SCALE = 0.045f;
	private const float FLOW_SPEED = 1.85f;
	private const float FLOW_SURGE_SCALE = 0.07f;
	private const float FLOW_WAVE_SCALE = 0.016f;
	private const float WIDTH_PULSE_SPEED = 9.5f;
	private const float WIDTH_MIN = 1.1f;
	private const float WIDTH_MAX = 4.6f;

	[Property] public LineRenderer LineRenderer { get; set; }

	[Sync] public Player OwnerPlayer { get; set; }
	[Sync] public Tree SourceTree { get; set; }

	private float _phase;
	private bool _rendererConfigured;

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

		_phase = Game.Random.Float( 0f, MathF.PI * 2f );
		TryEnsureLineRenderer();
	}

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

		if ( !TryEnsureLineRenderer() )
			return;

		if ( !OwnerPlayer.IsValid() )
		{
			GameObject.Destroy();
			return;
		}

		if ( !SourceTree.IsValid() || SourceTree.IsDying || OwnerPlayer.IsDead )
		{
			LineRenderer.Enabled = false;
			return;
		}

		var start = SourceTree.WorldPosition + new Vector3( 0f, 0f, Math.Max( SourceTree.Height * TREE_HEIGHT_FACTOR, MIN_HEIGHT ) );
		var end = OwnerPlayer.WorldPosition + new Vector3( 0f, 0f, Math.Max( OwnerPlayer.Height * PLAYER_HEIGHT_FACTOR, MIN_HEIGHT ) );
		var delta = end - start;
		var length = delta.Length;

		if ( length <= 0f )
		{
			LineRenderer.Enabled = false;
			return;
		}

		LineRenderer.Enabled = true;
		EnsureLinePoints();

		var dir = delta / length;
		var perp = new Vector3( -dir.y, dir.x, 0f );
		var time = Time.Now;
		var travel = 0.04f + ((time * FLOW_SPEED + _phase * 0.11f) % 1f) * 0.92f;
		LineRenderer.Width = Utils.Map( MathF.Sin( time * WIDTH_PULSE_SPEED + _phase * 0.8f ), -1f, 1f, WIDTH_MIN, WIDTH_MAX, EasingType.SineInOut );

		for ( int i = 0; i < NUM_POINTS; i++ )
		{
			var t = i / (float)(NUM_POINTS - 1);
			var basePos = Vector3.Lerp( start, end, t );
			var envelope = MathF.Sin( t * MathF.PI );
			var offsetScale = length * WAVE_SCALE * envelope * Utils.Map( t, 0f, 1f, 0.35f, 1.55f, EasingType.QuadOut );
			var forwardScale = length * FORWARD_SCALE * envelope * Utils.Map( t, 0f, 1f, 0.35f, 1.75f, EasingType.QuadOut );
			var wave = MathF.Sin( t * WAVE_FREQ - time * WAVE_SPEED + _phase );
			wave += MathF.Sin( t * WAVE_FREQ_2 - time * WAVE_SPEED_2 + _phase * 1.23f ) * 0.35f;
			var forwardWave = MathF.Sin( t * WAVE_FREQ_2 - time * (WAVE_SPEED + 3.5f) + _phase * 0.8f );
			var packet = MathF.Max( 1f - MathF.Abs( t - travel ) / 0.22f, 0f );
			packet *= packet;
			var flowWave = MathF.Sin( (t - travel) * MathF.PI * 2.8f - time * 4.2f + _phase * 0.45f );

			var pos = basePos
				+ perp * wave * offsetScale
				+ dir * (forwardWave * forwardScale
					+ flowWave * length * FLOW_WAVE_SCALE * envelope
					+ packet * length * FLOW_SURGE_SCALE * Utils.Map( t, 0f, 1f, 0.15f, 1f, EasingType.QuadOut ));
			LineRenderer.VectorPoints[i] = pos.WithZ( Math.Max( pos.z, MIN_HEIGHT ) );
		}

		UpdateGradient( time, travel );
	}

	private bool TryEnsureLineRenderer()
	{
		if ( LineRenderer == null || !LineRenderer.IsValid() )
			LineRenderer = GameObject.GetComponent<LineRenderer>( includeDisabled: true )
				?? GameObject.GetComponentInChildren<LineRenderer>( includeDisabled: true, includeSelf: true );

		if ( LineRenderer == null || !LineRenderer.IsValid() )
			return false;

		if ( !_rendererConfigured )
		{
			LineRenderer.UseVectorPoints = true;
			LineRenderer.Additive = true;
			LineRenderer.Opaque = false;
			LineRenderer.CastShadows = false;
			LineRenderer.Lighting = false;
			LineRenderer.DepthFeather = 8f;
			LineRenderer.FogStrength = 0f;
			LineRenderer.Face = SceneLineObject.FaceMode.Camera;
			LineRenderer.StartCap = SceneLineObject.CapStyle.Rounded;
			LineRenderer.EndCap = SceneLineObject.CapStyle.Rounded;
			LineRenderer.Enabled = false;
			_rendererConfigured = true;
		}

		EnsureLinePoints();
		return true;
	}

	private void EnsureLinePoints()
	{
		if ( LineRenderer.VectorPoints != null && LineRenderer.VectorPoints.Count == NUM_POINTS )
			return;

		LineRenderer.VectorPoints = new List<Vector3>( NUM_POINTS );
		for ( int i = 0; i < NUM_POINTS; i++ )
		{
			LineRenderer.VectorPoints.Add( Vector3.Zero );
		}
	}

	private void UpdateGradient( float time, float travel )
	{
		var pulse = Utils.Map( MathF.Sin( time * 8.5f + _phase ), -1f, 1f, 1.15f, 1.7f );
		var leadPulse = Utils.Map( MathF.Sin( time * 14f + _phase * 1.2f ), -1f, 1f, 1.2f, 2f ) * pulse;
		var packetStart = Math.Max( travel - 0.18f, 0.04f );
		var packetLeadIn = Math.Max( packetStart - 0.09f, 0.03f );
		var packetEnd = Math.Min( travel + 0.09f, 0.98f );

		var gradient = new Gradient();
		gradient.AddAlpha( 0f, 0f );
		gradient.AddAlpha( 0.03f, 0.42f * pulse );
		gradient.AddAlpha( packetLeadIn, 0.62f * pulse );
		gradient.AddAlpha( packetStart, 0.95f * pulse );
		gradient.AddAlpha( travel, 1.95f * leadPulse );
		gradient.AddAlpha( packetEnd, 1.15f * leadPulse );
		gradient.AddAlpha( 1f, 0f );

		gradient.AddColor( 0f, new Color( 0.08f, 0.22f, 0.03f, 0f ) );
		gradient.AddColor( 0.08f, new Color( 0.35f, 1.2f, 0.12f, 0.5f * pulse ) );
		gradient.AddColor( packetLeadIn, new Color( 0.6f, 2f, 0.2f, 0.8f * pulse ) );
		gradient.AddColor( packetStart, new Color( 0.95f, 2.1f, 0.35f, 1f * pulse ) );
		gradient.AddColor( travel, new Color( 1.8f, 2.8f, 0.65f, 1.5f * leadPulse ) );
		gradient.AddColor( packetEnd, new Color( 1.2f, 2.5f, 0.48f, 1.15f * leadPulse ) );
		gradient.AddColor( 1f, new Color( 1.2f, 3.6f, 0.48f, 0f ) );

		LineRenderer.Color = gradient;
	}
}