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.
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;
}
}