Player component that applies a temporary daze effect: slows the local player and requests screen blur. It stores duration, strength curve (hold then fade), and exposes static helpers to apply, clear, and query speed/blur. Also provides console commands to self-daze and clear all dazes.
using Sandbox;
using System;
using System.Linq;
namespace NZombies;
/// <summary>
/// WHAT A SONIC WAVE LEAVES BEHIND — the player is slowed hard and their vision goes soft.
///
/// ⛔ THE SHRIEKER DOES NOT DAMAGE YOU, IT TAKES YOUR LEGS AND YOUR EYES. That is the whole enemy,
/// and it is why this is a status rather than a hit: being at 35% speed with a blurred screen while
/// a horde closes is worse than losing health, and unlike health it is not something Juggernog or a
/// medkit answers. The only answers are killing the Shrieker or not being where the wave is going.
///
/// ⚠️ A COMPONENT ON THE PLAYER, NOT A STATIC. Every other player-side modifier in this project is
/// read off the player — `PerkEffects.SpeedMultiplier` is a product of component lookups — and a
/// static would mean one global daze shared by everybody in a four-player game. It also removes
/// itself, so nothing has to remember to clean up.
///
/// ⚠️ AND IT IS APPLIED LOCALLY ON EACH MACHINE, not replicated. `SonicWave` is broadcast and every
/// machine runs its own copy along the same straight line at the same speed, so each one reaches the
/// same verdict about its own local player — which is better than host authority here, because the
/// thing being modified is movement, and movement is owned by the client doing the moving.
/// </summary>
public sealed class SonicDaze : Component
{
/// <summary>
/// What the player's speed is multiplied by at full strength.
///
/// ⚠️ 0.35 IS "A LOT SLOWER" AND IS MEANT TO BE ALARMING, but it is deliberately not 0. A
/// player who cannot move at all is a player watching their own death, which is a worse
/// experience than a hard fight — they should always be able to back out of a room, just not
/// outrun anything while doing it.
/// </summary>
[Property] public float SlowTo { get; set; } = 0.35f;
/// <summary>How blurred the screen gets at full strength, in pixels of backdrop blur.</summary>
[Property] public float BlurPixels { get; set; } = 11f;
/// <summary>How long the whole thing lasts.</summary>
[Property] public float Seconds { get; set; } = 4f;
TimeUntil _until;
/// <summary>
/// 1 while it is at full strength, easing to 0 as it wears off.
///
/// ⛔ IT HOLDS FOR THE FIRST HALF AND FADES OVER THE SECOND, rather than decaying from the
/// moment it lands. A linear decay makes the strongest instant the one you never see — you are
/// already recovering before you have registered being hit — and a hard snap back at the end
/// reads as a bug. Holding then easing gives the hit a weight and the recovery a shape.
/// </summary>
public float Strength
{
get
{
var life = MathF.Max( 0.05f, Seconds );
var left = (float)_until;
if ( left <= 0f ) return 0f;
var fade = life * 0.5f;
return left >= fade ? 1f : ( left / fade ).Clamp( 0f, 1f );
}
}
protected override void OnUpdate()
{
// ⚠️ IT DELETES ITSELF. The alternative is every reader checking whether the thing it found
// has expired, which is the shape that eventually gets one reader wrong.
if ( _until <= 0f ) Destroy();
}
/// <summary>
/// Daze a player, or extend a daze they already have.
///
/// ⚠️ IT EXTENDS RATHER THAN STACKS, AND ONLY UPWARDS — upstream's `UpdateDuration` refuses to
/// shorten an existing effect (`if self.statusEnd - CurTime() > newtime then return end`) and
/// this keeps that. Two Shriekers screaming should not compound into a ten-second blind; the
/// longer of the two is the honest answer and it keeps the worst case bounded.
/// </summary>
public static void Apply( NZPlayer player, float seconds, float slowTo = -1f )
{
if ( !player.IsValid() || seconds <= 0f ) return;
var d = player.Components.GetOrCreate<SonicDaze>();
if ( slowTo >= 0f ) d.SlowTo = slowTo;
if ( (float)d._until < seconds )
{
d.Seconds = seconds;
d._until = seconds;
}
}
/// <summary>
/// The speed multiplier a dazed player is walking under — 1 when they are not.
///
/// ⚠️ READ BY `PerkEffects.SpeedMultiplier`, which is the single term NZPlayer's walk and run
/// AND Stamina's sprint all multiply by. Applying it at any one of those three would leave the
/// other two untouched, so a dazed player could still sprint away at full speed.
/// </summary>
public static float SpeedScale( NZPlayer player )
{
if ( !player.IsValid() ) return 1f;
var d = player.Components.Get<SonicDaze>( FindMode.EverythingInSelf );
if ( !d.IsValid() ) return 1f;
// Lerp UP from the slow toward 1 as it wears off, so the legs come back with the eyes.
return MathX.Lerp( 1f, MathF.Max( 0.05f, d.SlowTo ), d.Strength );
}
/// <summary>How much blur the local player's screen wants, in pixels. 0 when clear.</summary>
public static float BlurFor( NZPlayer player )
{
if ( !player.IsValid() ) return 0f;
var d = player.Components.Get<SonicDaze>( FindMode.EverythingInSelf );
return d.IsValid() ? d.BlurPixels * d.Strength : 0f;
}
// ── commands ─────────────────────────────────────────────────────────────
/// <summary>
/// `nz_daze [seconds] [slowto]` — daze yourself, to look at it without a Shrieker.
///
/// ⚠️ THE ONLY WAY TO SEE THE EFFECT ON DEMAND. The wave has to be fired by a live Shrieker at
/// a target in a 90–325u band, which is a lot of setup for "is the blur the right strength" —
/// and a look you cannot get easily is a look nobody takes.
/// </summary>
[ConCmd( "nz_daze" )]
public static void DazeCmd( float seconds = 4f, float slowTo = 0.35f )
{
var p = NZPlayer.Local;
if ( !p.IsValid() ) { Log.Warning( "[nz-daze] no local player" ); return; }
Apply( p, seconds, slowTo );
Log.Info( $"[nz-daze] {seconds:0.#}s at x{slowTo:0.##} speed"
+ $" — blur {BlurFor( p ):0.#}px, speed x{SpeedScale( p ):0.##}" );
}
/// <summary>`nz_daze_clear` — end it now.</summary>
[ConCmd( "nz_daze_clear" )]
public static void ClearCmd()
{
var n = Game.ActiveScene?.GetAllComponents<SonicDaze>().ToList();
foreach ( var d in n ?? Enumerable.Empty<SonicDaze>().ToList() ) d.Destroy();
Log.Info( $"[nz-daze] cleared {n?.Count ?? 0}" );
}
}