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.
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;
}
}
}