Effects/PulseTelegraph.cs

A local visual effect component that draws a charged pulse telegraph: a static rim on the ground and a closing inner ring that contracts over time. It creates LineRenderer child objects, lays out circle vertices (optionally tracing to ground), and updates color/width over its Life before self-destroying.

File Access
using Sandbox;
using System;
using System.Collections.Generic;

namespace NZombies;

/// <summary>
/// THE WARNING BEFORE A PULSE — a ring on the floor at the pulse's full reach, brightening and thickening as it charges, and a
/// second one closing in from it on the one who will fire it. Oberon's pulse (`OberonBoss`, basalt's boss fight, phase 2 on):
/// *"a pulse around it that has the effect of the shrieker, stunning the players"*.
///
/// ⛔ THE RIM IS DRAWN AT THE REACH AND NOTHING ELSE, `Vortex`'s contract: standing outside it has to be safe, and a ring drawn
/// generously is a lie the attack does not tell. `ShockRing` would not do — it grows from nothing and fades as it goes, the
/// opposite of a warning, and its life is one number for every ring in the game.
///
/// ⚠️ LOCAL, AND IT DESTROYS ITSELF: every machine draws its own from `NZNet.OberonPulseCharge`, as `ShockRing.Fire` does.
/// </summary>
public sealed class PulseTelegraph : Component
{
	/// <summary>The warning's colour: the Shrieker's pale violet, since what lands is its daze.</summary>
	public static Color Colour => new( 0.78f, 0.62f, 1f );

	/// <summary>
	/// This warning's colour — <see cref="Colour"/> unless the caller names one (2026-10-06: the Revelations Margwa's amber pulse;
	/// Oberon's calls name none and stay violet).
	/// </summary>
	public Color Tint { get; set; } = Colour;

	/// <summary>How far it reaches, and how long it charges, in units and seconds.</summary>
	public float Radius { get; set; } = 500f;
	public float Life { get; set; } = 1.2f;

	LineRenderer _rim, _closing;
	Vector3 _at;
	float _born;

	/// <summary>Draw one here, for this long. LOCAL.</summary>
	public static PulseTelegraph Fire( Vector3 at, float radius, float seconds, Color? colour = null )
	{
		var scene = Game.ActiveScene;
		if ( !scene.IsValid() ) return null;

		var go = scene.CreateObject();
		go.Name = "nz_pulse_telegraph";
		go.Flags |= GameObjectFlags.NotSaved;
		go.NetworkMode = NetworkMode.Never;   // ⛔ THIS MACHINE'S OWN — out of a joiner's snapshot, where it would stand frozen (NZNetListener)
		go.WorldPosition = PitVisual.GroundAt( at );

		var t = go.Components.Create<PulseTelegraph>();
		t.Radius = MathF.Max( 16f, radius );
		t.Life = MathF.Max( 0.1f, seconds );
		t.Tint = colour ?? Colour;
		return t;
	}

	protected override void OnStart()
	{
		_born = Time.Now;
		_at = WorldPosition;
		_rim = Line();
		_closing = Line();
		Lay( _rim, Radius, trace: true );
		Lay( _closing, Radius, trace: false );
	}

	LineRenderer Line()
	{
		var go = Scene.CreateObject();
		go.Flags |= GameObjectFlags.NotSaved;
		go.NetworkMode = NetworkMode.Never;   // ⛔ THIS MACHINE'S OWN — out of a joiner's snapshot, where it would stand frozen (NZNetListener)
		go.SetParent( GameObject, false );

		var l = go.Components.Create<LineRenderer>();
		l.UseVectorPoints = true;
		l.Additive = true;
		l.Lighting = false;
		l.CastShadows = false;
		l.Color = Tint.WithAlpha( 0.3f );
		l.Width = 2f;
		return l;
	}

	/// <summary>
	/// A closed circle of this radius round the middle, lifted 2u off the floor (`ShockRing.Rebuild`): each point traced onto
	/// the floor if `trace` — the rim, laid once — or level with the middle — the closing ring, laid every frame, where 48
	/// traces a frame would buy nothing on the arena's flat floor.
	/// </summary>
	void Lay( LineRenderer l, float r, bool trace )
	{
		if ( !l.IsValid() ) return;

		const int n = 48;
		var pts = new List<Vector3>( n + 1 );
		for ( var i = 0; i <= n; i++ )
		{
			var a = i / (float)n * MathF.PI * 2f;
			var p = _at + new Vector3( MathF.Cos( a ), MathF.Sin( a ), 0f ) * r;
			pts.Add( (trace ? PitVisual.GroundAt( p ) : p) + Vector3.Up * 2f );
		}
		l.VectorPoints = pts;
	}

	protected override void OnUpdate()
	{
		var t = (Time.Now - _born) / Life;
		if ( t >= 1f ) { GameObject.Destroy(); return; }

		// the rim brightens and thickens as the charge builds; the second ring closes in from it on him
		if ( _rim.IsValid() )
		{
			_rim.Color = Tint.WithAlpha( 0.3f + 0.7f * t );
			_rim.Width = 2f + 5f * t;
		}
		if ( _closing.IsValid() )
		{
			Lay( _closing, Radius * (1f - t), trace: false );
			_closing.Color = Tint.WithAlpha( 0.8f * (1f - t * 0.5f) );
			_closing.Width = 3f;
		}
	}
}