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