things/enemies/BossInvincibleGenerator.cs

Enemy subclass representing a boss-linked invincible generator. It renders a dynamic bezier energy line to the boss, manages visual wobble/scrolling, scale pulsing, movement/physics tweaks, sounds on hurt/death, loot/gib spawning, and notifies the boss when destroyed.

Native InteropFile Access
using System;
using Sandbox;
using Sandbox.UI;

public class BossInvincibleGenerator : Enemy
{
	public override EnemyType EnemyType => EnemyType.BossInvincibleGenerator;
	[Property] public LineRenderer LineRenderer { get; set; }
	[Property] public Texture LineTexture { get; set; }

	private SceneLineObject _lineSo;
	private float _wobbleRate;
	private float _wobblePhase;
	private float _scrollRate;
	private Gradient _lineGradient;
	private Vector3 _laggedCtrl1;
	private Vector3 _laggedCtrl2;
	private bool _midpointInitialized;
	private float _scaleBoost = 1f;
	private float _scaleBoostTarget = 1f;
	private float _prevPulseAlpha;
	private Vector3 _baseModelScale;
	private const int LINE_NUM_SEGMENTS = 80;

	public override float GetMaxHealth()
	{
		switch ( Manager.Instance.Difficulty )
		{
			case 0: default: return 600f;
			case 1: return 900f;
			case 2: return 1200f;
		}
	}

	public override Vector3 SpawnScale => new Vector3( 1.5f );
	public override float SpawnZPos => -125f;
	public override bool ShowHealthbar => true;
	public override float HealthbarOffset => 85f;
	public override float HealthbarOpacity => Utils.EasePercent( SpawnProgress, EasingType.QuadIn );
	public override float HealthbarArmorOpacity => Utils.EasePercent( SpawnProgress, EasingType.QuadIn );
	public override bool IsBoss => true;
	public override bool CanHaveTarget => false;
	public override bool CanAttack => false;
	public override bool CanTurn => false;
	public override bool CanBeBackstabbed => false;
	public override bool CanMove => false;

	protected string _debrisName;

	protected override void OnDisabled()
	{
		base.OnDisabled();
		_lineSo?.Delete();
		_lineSo = null;
	}

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

		_baseModelScale = ModelRenderer.LocalScale;

		CoinValueMin = 5;
		CoinValueMax = 8;
		CoinChance = 1f;

		PushStrength = 8000f;
		Weight = 3f;

		_debrisName = "barrel_debris";

		_wobbleRate = Game.Random.Float( 2.5f, 4.5f );
		_wobblePhase = Game.Random.Float( 0f, MathF.PI * 2f );
		_scrollRate = Game.Random.Float( 0.8f, 1.5f );

		_lineGradient = new Gradient();
		_lineGradient.AddColor( 0.0f, new Color( 0.05f, 0.3f, 1f, 0.02f ) );
		_lineGradient.AddColor( 0.14f, new Color( 0.2f, 0.6f, 1f, 1f ) );
		_lineGradient.AddColor( 0.32f, new Color( 0.0f, 0.35f, 0.9f, 0.02f ) );
		_lineGradient.AddColor( 0.5f, new Color( 0.0f, 0.7f, 1f, 0.02f ) );
		_lineGradient.AddColor( 0.66f, new Color( 0.0f, 0.85f, 1f, 1f ) );
		_lineGradient.AddColor( 0.82f, new Color( 0.0f, 0.45f, 1f, 0.02f ) );
		_lineGradient.AddColor( 1.0f, new Color( 0.05f, 0.3f, 1f, 0.02f ) );

		if ( !Application.IsDedicatedServer )
		{
			LineRenderer.GameObject.Enabled = false;

			_lineSo = new SceneLineObject( Scene.SceneWorld );
			_lineSo.RenderingEnabled = false;
			_lineSo.Flags.IsOpaque = false;
			_lineSo.Flags.IsTranslucent = true;
			_lineSo.Flags.CastShadows = true;
			_lineSo.StartCap = SceneLineObject.CapStyle.Rounded;
			_lineSo.EndCap = SceneLineObject.CapStyle.Rounded;
			_lineSo.Face = SceneLineObject.FaceMode.Camera;
			_lineSo.Material = Material.Load( "materials/shockwave.vmat" );
			_lineSo.Attributes.Set( "g_flEmissiveScale", 3f );
			_lineSo.Attributes.Set( "BaseTexture", LineTexture ?? Texture.White );
			_lineSo.RenderingEnabled = true;
		}

		if ( IsProxy )
			return;

		Deceleration = 2f;
	}

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

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"{Radius}", new global::Transform( WorldPosition ) );

		if ( _lineSo is not null && Manager.Instance.Boss.IsValid() )
		{
			Vector3 a = WorldPosition.WithZ( WorldPosition.z + 40f );
			Vector3 b = Manager.Instance.Boss.WorldPosition.WithZ( Manager.Instance.Boss.WorldPosition.z + 50f );

			// Physics-lag two cubic bezier control points independently
			Vector3 trueCtrl1 = Vector3.Lerp( a, b, 1f / 3f );
			Vector3 trueCtrl2 = Vector3.Lerp( a, b, 2f / 3f );
			if ( !_midpointInitialized ) { _laggedCtrl1 = trueCtrl1; _laggedCtrl2 = trueCtrl2; _midpointInitialized = true; }
			_laggedCtrl1 = Vector3.Lerp( _laggedCtrl1, trueCtrl1, Time.Delta * 0.4f );
			_laggedCtrl2 = Vector3.Lerp( _laggedCtrl2, trueCtrl2, Time.Delta * 0.4f );

			_lineSo.Transform = global::Transform.Zero;

			float lineLength = (b - a).Length;
			const float ENDPOINT_FADE_DIST = 280f;
			const float STRIPE_WORLD_SIZE = 400f; // world units per gradient cycle — keeps scroll speed constant regardless of line length
			float uvScale = lineLength / STRIPE_WORLD_SIZE;

			float alphaPulse = Utils.Map( MathF.Sin( Time.Now * 1.8f + _wobblePhase ), -1f, 1f, 0.7f, 1.3f );
			float baseAlpha = 1.8f * alphaPulse;

			_lineSo.StartLine();
			for ( int i = 0; i <= LINE_NUM_SEGMENTS; i++ )
			{
				float t = (float)i / LINE_NUM_SEGMENTS;
				float tc = 1f - t;

				// Cubic bezier: endpoints locked, two lagged control points give trailing arc
				Vector3 basePos = tc*tc*tc*a + 3f*tc*tc*t*_laggedCtrl1 + 3f*tc*t*t*_laggedCtrl2 + t*t*t*b;

				// Local tangent + perpendicular from cubic bezier derivative
				Vector3 tangent = (3f*tc*tc*(_laggedCtrl1 - a) + 6f*tc*t*(_laggedCtrl2 - _laggedCtrl1) + 3f*t*t*(b - _laggedCtrl2)).Normal;
				var perp = new Vector3( -tangent.y, tangent.x, 0f );

				float envelope = MathF.Sin( t * MathF.PI );
				float wobble = (
					MathF.Sin( t * MathF.PI * 2.5f - Time.Now * _wobbleRate + _wobblePhase ) * 35f
				  + MathF.Sin( t * MathF.PI * 5.5f - Time.Now * _wobbleRate * 1.6f + _wobblePhase * 1.4f ) * 15f
				) * envelope;

				float sampledT = ((t * uvScale - Time.Now * _scrollRate) % 1.0f + 1.0f) % 1.0f;
				Color gradientColor = _lineGradient.Evaluate( sampledT );

				// Brighter/thicker at opaque gradient stops, thinner/dimmer at gaps
				float boost = Utils.Map( gradientColor.a, 0.02f, 1f, 1f, 2.5f );

				// Wider and fully transparent over a fixed world-space distance from each endpoint
				float distFromEndpoint = MathF.Min( t, 1f - t ) * lineLength;
				float fadeT = Math.Clamp( distFromEndpoint / ENDPOINT_FADE_DIST, 0f, 1f );
				float endWidthMult = 1f + (1f - fadeT) * 5f; // 6x wider right at endpoints, normal beyond fade zone
				float endAlphaMult = fadeT; // fully transparent at endpoints, opaque beyond fade zone

				float widthWobble = MathF.Sin( t * MathF.PI * 4f - Time.Now * _wobbleRate * 2.2f + _wobblePhase * 1.2f ) * 0.6f;
				float width = MathF.Max( (1.5f + widthWobble) * boost * endWidthMult, 0.6f );

				Color pointColor = gradientColor.WithAlpha( gradientColor.a * baseAlpha * boost * endAlphaMult );

				float uv = t * uvScale - Time.Now * _scrollRate;
				_lineSo.AddLinePoint( basePos + perp * wobble, Vector3.Up, pointColor, width, uv );
			}
			_lineSo.EndLine();
		}

		if ( !IsSpawning && !IsDying )
		{
			float t0SampledT = ( (-Time.Now * _scrollRate) % 1.0f + 1.0f ) % 1.0f;
			float pulseAlpha = _lineGradient.Evaluate( t0SampledT ).a;

			if ( pulseAlpha > 0.5f && _prevPulseAlpha <= 0.5f )
				_scaleBoostTarget = 1.25f;

			_prevPulseAlpha = pulseAlpha;
			_scaleBoostTarget += (1f - _scaleBoostTarget) * Time.Delta * 4f;
			float scaleRate = _scaleBoostTarget > _scaleBoost ? 8f : 3f;
			_scaleBoost += (_scaleBoostTarget - _scaleBoost) * Time.Delta * scaleRate;
			ModelRenderer.LocalScale = _baseModelScale * _scaleBoost;
		}

		if ( IsProxy || IsDying )
			return;

		if ( IsSpawning )
			return;

		if ( Manager.Instance.IsWindActive )
			Velocity += (Manager.Instance.GlobalWindForce / (Weight * 7f)) * Time.Delta;

		Velocity *= Math.Max( 1f - Time.Delta * Deceleration * Manager.Instance.GlobalFrictionModifier, 0f );

		WorldPosition += (Vector3)Velocity * Time.Delta;

		WorldRotation = WorldRotation.RotateAroundAxis( Vector3.Up, Time.Delta * -75f );
	}

	public override void Flinch( float time, Vector2 dir )
	{
		base.Flinch( time, dir );

		ModelRenderer.LocalRotation = new Angles(Game.Random.Float(-10f, 10f), 0f, Game.Random.Float(-10f, 10f));
	}

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

		ModelRenderer.LocalRotation = new Angles(0f, 0f, 0f);
	}

	public override void SetAnim( string name, bool forceRestart = false )
	{
		return;
	}

	public override void PlayHurtSfx( float damage, DamageType damageType, Vector3 hitPos, Player player, DamageResultFlags damageFlags )
	{
		// sfx
		if ( _realTimeSinceHurtSfx > 0.0175f )
		{
			if ( damage == 0f )
			{
				Manager.Instance.PlaySfxNearby( "crystal_hit", hitPos, pitch: Game.Random.Float( 1.45f, 1.5f ), volume: 1.7f, maxDist: 300f );
			}
			else if ( damageType == DamageType.Punch ) { Manager.Instance.PlaySfxNearby( "crystal_hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 1.1f, 1.25f, EasingType.SineIn ), volume: 1.7f, maxDist: 400f ); }
			//else if ( damageType == DamageType.DashSlash ) { Manager.Instance.PlaySfxNearby( "player.dash.slash.hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 0.9f, 1.1f, EasingType.SineIn ) * Game.Random.Float( 0.95f, 1.05f ), volume: 0.8f, maxDist: 350f ); }
			else if ( damageType == DamageType.Fire ) { Manager.Instance.PlaySfxNearby( "burn_2", hitPos, pitch: Game.Random.Float( 1.15f, 1.35f ), volume: 0.7f, maxDist: 300f ); }
			else if ( damageType == DamageType.Poison ) { Manager.Instance.PlaySfxNearby( "poisoned", hitPos, pitch: Game.Random.Float( 1.55f, 1.65f ), volume: 0.35f, maxDist: 300f ); }
			else if ( damageType == DamageType.SpikerHead ) { Manager.Instance.PlaySfxNearby( "spike.stab", hitPos, pitch: Game.Random.Float( 0.95f, 1f ), volume: 0.8f, maxDist: 300f ); }
			else if ( damageType == DamageType.SpitterProjectile || damageType == DamageType.SpitterProjectileHoming ) { Manager.Instance.PlaySfxNearby( "splash", hitPos, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f ); }
			else if ( damageType == DamageType.Aoe || damageType == DamageType.BulletSplash ) { /* no sfx */ }
			else if ( damageType == DamageType.Radiation ) { /* no sfx */ }
			else if ( damageType == DamageType.Shock ) { /* no sfx */ }
			else if ( damageType == DamageType.Explosion ) { /* no sfx */ }
			else if ( damageType == DamageType.JumpFinish ) { Manager.Instance.PlaySfxNearby( "slam", hitPos, pitch: Game.Random.Float( 0.85f, 0.95f ), volume: 0.8f, maxDist: 150f ); }
			else { Manager.Instance.PlaySfxNearby( "crystal_hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 1f, 1.25f, EasingType.SineIn ) * Game.Random.Float( 0.95f, 1.05f ), volume: 1.7f, maxDist: 400f ); }
			// todo: OrbitingBlade hit sfx

			_realTimeSinceHurtSfx = 0f;
		}
	}

	public override void Die( Vector2 dir, float force, Player player, DamageType damageType )
	{
		base.Die( dir, force, player, damageType );

		if ( IsProxy )
			return;

		Manager.Instance.Boss?.InvincibleGeneratorDestroyed();
	}

	protected override void DropLoot( Player player )
	{
		base.DropLoot( player );

		// todo: sfx

		// todo: spawn a stage event, or a miniboss, or something else exciting
	}

	protected override void PlayDeathSfx( Vector2 pos )
	{
		Manager.Instance.PlaySfxNearby( "crystal_break", pos, pitch: Game.Random.Float( 0.75f, 0.8f ), volume: 1.2f, maxDist: 700f );
	}

	protected override void SpawnGibs( Vector2 dir, float force, DamageType damageType )
	{
		//GameObject.Clone( $"prefabs/effects/diamond_debris.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( WorldPosition.WithZ( Game.Random.Float( 40f, 60f ) ), Rotation.Identity ) } );

		SpawnGibs( "fragment", Game.Random.Int( 4, 6 ), force, damageType );
		SpawnGibs( "fragment_2", Game.Random.Int( 3, 6 ), force, damageType );
		SpawnGibs( "fragment_3", Game.Random.Int( 3, 6 ), force, damageType );
	}

	void SpawnGibs( string name, int count, float force, DamageType damageType )
	{
		for ( int i = 0; i < count; i++ )
		{
			SpawnGoreGib(
				$"{GibFolder}/{name}",
				localPos: new Vector3( 0f, 0, Game.Random.Float( 20f, 45f ) ) + Vector3.Random * Game.Random.Float( 0f, 20f ),
				localRot: Rotation.Random,
				scaleMultiplier: Game.Random.Float( 2f, 2.5f ),
				dir: Vector3.Random,
				force: force * Game.Random.Float( 0.15f, 1.8f ),
				ModelRenderer.Tint,
				damageType
			);
		}
	}
}