UI model for a health bar. It stores MaxHealth and current Health, and animates displayed fill, a trailing ghost chip, and hit/heal flashes using SpringFloat and DecayFloat dampers. Exposes computed properties (fractions, numeric HP, critical state), methods to Reset, Damage, Heal, and Tick the animations.
using System;
using Goo.Animation;
namespace Goo.FpsUI;
// Health bar logic: a springy fill, a trailing "ghost" chip revealing recently-lost HP,
// and white hit / green heal flashes. Engine-free so it unit-tests headlessly.
public sealed class HealthModel
{
public float MaxHealth = 100f; // full-bar value
public float Health { get; private set; } = 100f; // current HP
SpringFloat _shown = new( 1f, 14f, 0.55f ); // displayed fill fraction (punchy overshoot)
DecayFloat _ghost = new( 1f, 0.5f ); // lagging chip behind the fill
DecayFloat _hit = new( 0f, 0.11f ); // white damage flash
DecayFloat _heal = new( 0f, 0.16f ); // green heal flash
public float ShownFraction => Math.Clamp( _shown.Current, 0f, 1.05f ); // animated fill 0..1
public float GhostFraction => Math.Clamp( _ghost.Current, 0f, 1.05f ); // lost-HP chip extent
public float HitFlash => Math.Clamp( _hit.Current, 0f, 1f ); // 1 on hit, decays to 0
public float HealFlash => Math.Clamp( _heal.Current, 0f, 1f ); // 1 on heal, decays to 0
public int Number => Math.Max( 0, (int)MathF.Round( _shown.Current * MaxHealth ) ); // rolling HP number
public bool Critical => ShownFraction <= 0.25f; // low-HP warning state
float Frac => MaxHealth > 0f ? Health / MaxHealth : 0f;
// Re-sync to full and re-seed dampers, call after setting MaxHealth.
public void Reset()
{
Health = MaxHealth;
_shown = new SpringFloat( 1f, 14f, 0.55f );
_ghost = new DecayFloat( 1f, 0.5f );
_hit = new DecayFloat( 0f, 0.11f );
_heal = new DecayFloat( 0f, 0.16f );
}
public void Damage( float amount ) // apply damage and light the hit flash
{
if ( amount <= 0f ) return;
Health = Math.Clamp( Health - amount, 0f, MaxHealth );
_hit.Current = 1f;
}
public void Heal( float amount ) // apply healing and light the heal flash
{
if ( amount <= 0f ) return;
Health = Math.Clamp( Health + amount, 0f, MaxHealth );
_heal.Current = 1f;
_ghost.Current = MathF.Max( _ghost.Current, Frac ); // no stale chip after a heal
}
public bool Tick( float dt ) // advance dampers, true while anything still moves
{
_shown.Target = Frac;
bool moving = _shown.Tick( dt );
_ghost.Target = _shown.Current;
_hit.Target = 0f; _heal.Target = 0f;
moving |= _ghost.Tick( dt ) | _hit.Tick( dt ) | _heal.Tick( dt );
return moving;
}
}