Code/Demos/ComposableHud/PlayerHealthView.cs
using System;
using System.Linq;
using Goo;
using Goo.Animation;
using Sandbox;
using Sandbox.UI;

namespace Sandbox.ComposableHud;

// Prominent player HP bar that mirrors a real PlayerHealth component (damaged by engine TriggerHurt volumes), found in the scene via Scene.GetAllComponents. Falls back to a self-contained demo if no PlayerHealth is present: press H to take damage, J to heal. Smooth drain + a white flash on each hit.
sealed class PlayerHealthView
{
    const float BarWidth     = 360f;
    const float BarHeight    = 22f;
    const float MaxFallback  = 100f;
    const float DrainSmooth  = 0.14f;
    const float FlashSmooth  = 0.09f;
    const float FlashPeak    = 0.8f;
    const float DebugHit     = 12f;
    const float DebugHeal    = 18f;

    static readonly Color LowHealth  = new( 0.90f, 0.22f, 0.22f );
    static readonly Color HighHealth = new( 0.35f, 0.85f, 0.45f );
    static readonly Color NameColor  = new( 0.95f, 0.97f, 1f );

    SmoothFloat _shown = new( 1f, DrainSmooth );
    SmoothFloat _flash = new( 0f, FlashSmooth );
    float _cur  = MaxFallback;
    float _max  = MaxFallback;
    float _last = MaxFallback;
    bool _hPrev, _jPrev;
    bool _dirty = true;
    void Invalidate() => _dirty = true;

    public void Reset()
    {
        _shown = new SmoothFloat( 1f, DrainSmooth );
        _flash = new SmoothFloat( 0f, FlashSmooth );
        _cur = _max = _last = MaxFallback;
        _hPrev = _jPrev = false;
        Invalidate();
    }

    public bool Tick( Scene? scene, float dt )
    {
        var hp = scene?.GetAllComponents<PlayerHealth>().FirstOrDefault();
        if ( hp is not null )
        {
            _max = MathF.Max( 1f, hp.MaxHealth );
            _cur = Math.Clamp( hp.Health, 0f, _max );
        }
        else
        {
            // Standalone demo when no PlayerHealth is wired up: H hurts, J heals.
            bool h = Sandbox.Input.Keyboard.Down( "H" );
            bool j = Sandbox.Input.Keyboard.Down( "J" );
            if ( h && !_hPrev ) _cur = Math.Clamp( _cur - DebugHit,  0f, _max );
            if ( j && !_jPrev ) _cur = Math.Clamp( _cur + DebugHeal, 0f, _max );
            _hPrev = h; _jPrev = j;
        }

        if ( _cur < _last - 0.01f ) { _flash.Current = 1f; Invalidate(); }   // flash on damage
        _last = _cur;

        _shown.Target = _cur / _max; _shown.Update( dt );
        _flash.Target = 0f;          _flash.Update( dt );

        if ( !_shown.IsSettled || !_flash.IsSettled ) Invalidate();
        bool d = _dirty; _dirty = false; return d;
    }

    public Container Build()
    {
        float frac = Math.Clamp( _shown.Current, 0f, 1f );

        var root = new Container
        {
            Key           = "player-hp",
            FlexDirection = FlexDirection.Column,
            Gap           = 4f,
            PointerEvents = PointerEvents.None,
        };

        // "Player" left, current/max right-aligned on the same line.
        var label = new Container
        {
            Key            = "label",
            FlexDirection  = FlexDirection.Row,
            Width          = BarWidth,
            JustifyContent = Justify.SpaceBetween,
            AlignItems     = Align.Center,
        };
        label.Children.Add( new Text( "Player" ) { Key = "name", FontSize = 15f, FontColor = NameColor } );
        label.Children.Add( new Text( $"{(int)MathF.Round( _cur )}/{(int)MathF.Round( _max )}" )
                            { Key = "hp", FontSize = 14f, FontColor = NameColor } );
        root.Children.Add( label );

        var track = new Container
        {
            Key             = "track",
            Position        = PositionMode.Relative,
            Width           = BarWidth,
            Height          = BarHeight,
            BackgroundColor = Color.Black.WithAlpha( 0.55f ),
            BorderRadius    = 5f,
            Overflow        = OverflowMode.Hidden,
        };
        // Round the fill/flash themselves: s&box overflow:hidden doesn't clip absolutely-
        // positioned children to the track's border-radius, so the corners must be set here.
        track.Children.Add( new Container
        {
            Key             = "fill",
            Position        = PositionMode.Absolute,
            Top             = 0,
            Left            = 0,
            Height          = Length.Percent( 100 ),
            Width           = Length.Percent( frac * 100f ),
            BorderRadius    = 5f,
            BackgroundColor = Color.Lerp( LowHealth, HighHealth, frac ),
        } );
        track.Children.Add( new Container
        {
            Key             = "flash",
            Position        = PositionMode.Absolute,
            Top             = 0,
            Left            = 0,
            Width           = Length.Percent( 100 ),
            Height          = Length.Percent( 100 ),
            BorderRadius    = 5f,
            BackgroundColor = Color.White,
            Opacity         = Math.Clamp( _flash.Current, 0f, 1f ) * FlashPeak,
        } );
        root.Children.Add( track );

        return root;
    }
}