UI/CherryShockHud.razor

Razor UI component that draws the Cherry Shock overlay. It contains a single Frame panel and, each update, reads CherryShockState to pick a framed background image, set opacity, and apply a decaying random shake offset.

File Access
@using Sandbox;
@using Sandbox.UI;
@using System;
@using NZombies;
@inherits PanelComponent

@*
    CHERRY SHOCK — Elemental Pop's reload discharge, as a screen overlay.

    ⛔ AN OVERLAY, NOT A PARTICLE SYSTEM, AND THAT IS A DELIBERATE DEPARTURE. GMod
    does `ParticleEffectAttach( "nz_perks_cherry", PATTACH_ABSORIGIN_FOLLOW, ply, 0 )`
    — a world effect on the player. Three attempts at porting it faithfully failed on
    the same wall: `perks_cherry.pcf` spawns everything at a single point at the
    player's FEET and relies on Source's units and velocities to sweep it past the
    camera, and none of those numbers convert. It was too small, then in the wrong
    place, then horizontal.

    So the effect was redesigned in the browser instead (Docs/vfx/cherry_overlay.html)
    and this plays the result. The trade is stated once, here: a teammate no longer
    sees your discharge, and it is not occluded by geometry.

    ⛔ AND IT IS A BAKED SEQUENCE, NOT PROCEDURAL. 21 frames at 30fps, rendered by
    `Tools/render_cherry_shock.py` at the approved settings. Two reasons: it costs one
    background-image swap per frame instead of forty stroked paths with three glow
    passes each, and a procedural port would be a SECOND implementation of the same
    look that would drift from the mock the first time either was touched.

    ⚠️ THE FRAMES CARRY ALPHA, and the bake depends on that. The design is additive
    — black means "add nothing" — but panels here composite with normal alpha
    blending, so an RGB-on-black frame would draw as a black rectangle over the game.
    See the note in the renderer.
*@

<root class="cherry-shock">
    <div @ref="Frame" class="frame"></div>
</root>

@code
{
    /// <summary>
    /// ⛔ CONSTANT, like DamageOverlay's. Every visible property — the frame image,
    /// opacity and the shake offset — is written to Style per frame. A hash that
    /// tracked the frame index would rebuild the tree 30 times a second for a value
    /// that a style write already handles.
    /// </summary>
    protected override int BuildHash() => 0;

    Panel Frame;

    // ⚠️ NO STATE HERE. Duration, frame count, shake, the switch and the timestamp all
    // live in `CherryShockState`, because gameplay code has to reach them and a
    // generated razor class is not reachable from it. This file is only the drawing.

    protected override void OnUpdate()
    {
        if ( Frame is null ) return;

        var t = CherryShockState.Progress;

        if ( t < 0f || !CherryShockState.ShowOverlay )
        {
            Frame.Style.Opacity = 0f;
            return;
        }

        // ⚠️ CLAMPED, not modulo. At exactly t = 1 the index would land one past the
        // last frame and wrap to zero, flashing the opening frame as the effect ends.
        var index = Math.Clamp( (int)(t * CherryShockState.FrameCount), 0,
            CherryShockState.FrameCount - 1 );

        Frame.Style.Opacity = 1f;
        Frame.Style.SetBackgroundImage( $"ui/nz/cherry/shock_{index:00}.png" );

        var shake = CherryShockState.Shake;
        if ( shake > 0f )
        {
            // A decaying shake: strongest at the strike, gone by the end. Random per
            // frame rather than a sine, because a sine reads as a wobble and this
            // should read as an impact.
            var k = shake * (1f - t);
            Frame.Style.Left = Length.Pixels( Game.Random.Float( -k, k ) );
            Frame.Style.Top = Length.Pixels( Game.Random.Float( -k, k ) );
        }
    }

}