FpsUI/Models/HealthModel.cs

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.

Native Interop
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;
    }
}