FpsUI/Widgets/StaminaWidget.cs

UI widget for a stamina (secondary) bar in an FPS UI. Contains a stateless StaminaView that constructs the bar visuals and a StaminaWidget component that updates stamina state each tick, handles sprint input, gating via PlayerController, and builds the UI container.

NetworkingFile Access
using System;
using Goo;
using Sandbox;
using Sandbox.UI;

namespace Goo.FpsUI;

// Stateless presenter: the secondary (stamina/armor) bar.
static class StaminaView
{
    public const float TrackW = 320f, TrackH = 10f;

    public static Container Build( StaminaModel m, FpsTheme t )
    {
        float frac = m.ShownFraction;
        Color fill = Color.Lerp( t.Warn, t.Good, frac );

        var track = new Container
        {
            Key = "stTrack", Position = PositionMode.Relative,
            Width = TrackW, Height = TrackH, BackgroundColor = t.TrackBg,
            BorderRadius = t.Radius, Overflow = OverflowMode.Hidden,
            Children = { Parts.FillRect( "fill", frac, fill, t.Radius ) },
        };
        if ( m.Flash > 0.001f ) track.Children.Add( Parts.Overlay( "flash", Color.White, m.Flash * 0.8f, t.Radius ) );
        return track;
    }
}

// Standalone stamina bar. Call Sprinting(bool) from your movement code each frame.
public sealed partial class StaminaWidget : GooPanel<Container>
{
    [Property, Range( 1f, 1000f )] public float MaxStamina { get; set; } = 100f; // full-bar value
    [Property, Range( 0.05f, 2f )] public float DrainRate { get; set; } = 0.55f;  // fraction of max drained per second while sprinting
    [Property] public PlayerController? Player { get; set; }                      // player to gate sprint on when stamina is empty (null = skip gating)

    readonly StaminaModel _m = new();
    readonly FpsTheme _t = new();
    bool _booted;
    bool _sprintBlocked;
    float _cachedRunSpeed;

    void Boot() { _m.MaxStamina = MaxStamina; _m.Reset(); _booted = true; }

    public void Sprinting( bool on ) => _m.SetSprinting( on ); // toggle sprint drain

    // Demo-only seam: implemented in FpsDemo.cs, compiles out when that file is deleted.
    partial void StepDemo( float dt, ref bool active );

    protected override bool Tick( float dt )
    {
        if ( !_booted ) Boot();
        _m.DrainRate = DrainRate;  // live-tunable
        bool demo = false;
        StepDemo( dt, ref demo );
        if ( !demo )
        {
            Sprinting( Sandbox.Input.Down( "run" ) );
            FpsInput.ApplySprintGate( Player, _m.Stamina, ref _sprintBlocked, ref _cachedRunSpeed );
        }
        bool moving = _m.Tick( dt );
        return demo || moving;
    }

    protected override Container Build()
    {
        if ( !_booted ) Boot();
        var root = Parts.Root( "fpsStamina" );
        // Sits just above where the health bar would be (health height + gap).
        root.Children.Add( Parts.Anchor( "a", Parts.Corner.BottomLeft, _t.Margin + HealthView.TrackH + 10f, Parts.Panel( "bg", _t, StaminaView.Build( _m, _t ) ) ) );
        return root;
    }
}