UI/CherryShockState.cs

Static state holder for the CherryShock HUD overlay. Stores playback duration and frame count, controls whether the overlay is shown, tracks when the effect was fired, exposes progress, shake strength, and a console command to toggle/configure and trigger the overlay.

Http CallsFile Access
using Sandbox;

namespace NZombies;

/// <summary>
/// When Elemental Pop's discharge fired, for <c>CherryShockHud</c> to play.
///
/// ⛔ A STATE CLASS RATHER THAN A CALL INTO THE PANEL, matching `PowerupBannerState`
/// and `HudState`. Two reasons, and the second is the one that matters:
///   • a generated razor class is not reachable from gameplay code without adding a
///     namespace to it, and nothing else in this project does that;
///   • the panel is CLIENT-SIDE and may not exist when the perk fires — mid-load, in
///     the lobby, on a spectator. A call into a panel that is not there is a crash or
///     a silent no-op depending on the day; a timestamp nothing reads simply expires.
///
/// ⚠️ STATICS SURVIVE A HOTLOAD (INSTRUCTIONS.md §1), so `FiredAt` seeds to a large
/// negative rather than 0 — at 0 the overlay would consider itself mid-burst for the
/// first frames of every session, because `Time.Now` also starts near zero.
/// </summary>
public static class CherryShockState
{
	/// <summary>
	/// Playback length, and it MUST equal the renderer's `duration`.
	///
	/// ⚠️ A const, not a tunable. The length is baked into the frames — driving
	/// playback from a knob would stretch or truncate a fixed sequence, which reads as
	/// the animation stuttering rather than as a shorter effect. Changing it means
	/// re-running `Tools/render_cherry_shock.py`.
	/// </summary>
	public const float Duration = 0.70f;

	/// <summary>Frames the bake produced. Must match the files in ui/nz/cherry.</summary>
	public const int FrameCount = 21;

	/// <summary>
	/// Screen shake in pixels at full strength.
	///
	/// ⚠️ APPLIED TO THE PANEL, NOT BAKED INTO THE FRAMES. Baking it would slide the
	/// overlay against a still world, which reads as the image slipping rather than as
	/// the player being hit. Shaking the panel is the cheap approximation; the honest
	/// version is a camera shake, which this project does not have yet.
	/// </summary>
	public static float Shake { get; set; } = 3.5f;

	/// <summary>Master switch — `nz_perk_pop_hud 0`.</summary>
	public static bool ShowOverlay { get; set; } = true;

	/// <summary>When the current burst started, or long ago if none.</summary>
	public static float FiredAt { get; private set; } = -999f;

	/// <summary>Trigger the overlay. Called from PerkEffects.ElementalPop.</summary>
	public static void Fire() => FiredAt = Time.Now;

	/// <summary>
	/// Cancel a pending burst.
	///
	/// ⚠️ NOT called `Reset`. `Component` has one, and although this class is not a
	/// component the name has already caused that exact collision twice in this project
	/// (`DamageOverlay.Enabled`, `PackAPunch.Reset`).
	/// </summary>
	public static void ClearPending() => FiredAt = -999f;

	/// <summary>How far through the sequence, or -1 when idle.</summary>
	public static float Progress
	{
		get
		{
			var since = Time.Now - FiredAt;
			return since < 0f || since > Duration ? -1f : since / Duration;
		}
	}

	/// <summary>
	/// `nz_perk_pop_hud [0/1] [shake]` — toggle the overlay, set the shake, and fire
	/// one so it can be looked at without needing a zombie or a reload.
	/// </summary>
	[ConCmd( "nz_perk_pop_hud" )]
	public static void HudCmd( int on = -1, float shake = -1f )
	{
		if ( on >= 0 ) ShowOverlay = on != 0;
		if ( shake >= 0f ) Shake = shake;

		Fire();
		Log.Info( $"[nz-perk] pop hud: {(ShowOverlay ? "on" : "off")}"
			+ $", shake {Shake:0.#}px, {FrameCount} frames over {Duration:0.##}s — fired one" );
	}
}