FpsUI/Models/StaminaModel.cs

Model for a stamina bar used in the FPS UI. Tracks current stamina, handles draining while sprinting and passive regeneration, and exposes animated values for UI display using SpringFloat and DecayFloat.

Native Interop
using System;
using Goo.Animation;

namespace Goo.FpsUI;

// Secondary bar under health: drains while sprinting and regenerates otherwise. Engine-free.
public sealed class StaminaModel
{
    public float MaxStamina = 100f;                  // full-bar value
    public float DrainRate  = 0.55f;                 // fraction of max stamina drained per second while sprinting
    public float Stamina { get; private set; } = 100f;   // current value

    bool        _sprinting;
    SpringFloat _shown = new( 1f, 18f, 0.85f );      // displayed fill
    DecayFloat  _flash = new( 0f, 0.12f );           // blip when it empties

    public float ShownFraction => Math.Clamp( _shown.Current, 0f, 1f ); // animated fill 0..1
    public float Flash         => Math.Clamp( _flash.Current, 0f, 1f ); // 1 on empty, decays
    public bool  Sprinting     => _sprinting;

    public void Reset()
    {
        Stamina = MaxStamina;
        _shown = new SpringFloat( 1f, 18f, 0.85f );
        _flash = new DecayFloat( 0f, 0.12f );
    }

    public void SetSprinting( bool on ) => _sprinting = on;          // call from game code each frame
    public void SetStamina( float v ) => Stamina = Math.Clamp( v, 0f, MaxStamina ); // set directly (custom)

    public bool Tick( float dt )
    {
        // Drain whenever the sprint key is held (pinned at 0 once gassed), regenerate only when not sprinting.
        // Regenerating at 0 while still held would bounce stamina above 0 each frame and flip the sprint gate
        // on and off, letting a gassed player keep sprinting. Holding sprint must keep it at 0.
        float prev = Stamina;
        Stamina = _sprinting
            ? MathF.Max( 0f, Stamina - MaxStamina * DrainRate * dt )
            : MathF.Min( MaxStamina, Stamina + MaxStamina * 0.30f * dt );
        if ( prev > 0f && Stamina <= 0f ) _flash.Current = 1f;  // gassed-out blip
        _shown.Target = MaxStamina > 0f ? Stamina / MaxStamina : 0f;
        _flash.Target = 0f;
        bool moving = _shown.Tick( dt ) | _flash.Tick( dt );
        return moving || _sprinting;
    }
}