Demos/ComposableHud/SquadHealthView.cs
using System;
using Goo;
using Goo.Animation;
using Sandbox;
using Sandbox.UI;

namespace Sandbox.ComposableHud;

// N ally health bars: each a mock player (avatar icon + name) whose bar flashes on its own damage at an independent, non-harmonic cadence. One view owns N-length spring arrays (the RadialView idiom) so the host sees a single HudElement and never becomes a god-object. Internals: SVG avatar, name label, and a styled bar with two SmoothFloats (smooth health drain + decaying flash).
sealed class SquadHealthView
{
    const int   N           = 6;
    const int   MaxHp       = 100;
    const float AvatarSize  = 30f;
    const float TrackWidth  = 200f;
    const float TrackHeight = 14f;
    const float RowGap      = 8f;
    const float DamageChunk = 0.2f;
    const float DrainSmooth = 0.12f;
    const float FlashSmooth = 0.08f;
    const float FlashPeak   = 0.85f;
    const float ReviveFloor = 0.05f;

    static readonly Color LowHealth  = new( 0.90f, 0.25f, 0.25f );
    static readonly Color HighHealth = new( 0.35f, 0.85f, 0.45f );
    static readonly Color NameColor  = new( 0.92f, 0.94f, 0.98f );
    // Alpha lives in the color, not Container.Opacity (Opacity would fade the name/icon too).
    static readonly Color RowBg      = new Color( 0f, 0f, 0f ).WithAlpha( 0.8f );

    // Every ally is the goo mascot (splat) in a different tint. Tint is a CSS color string
    // (SvgPanel.Color is string, not Color).
    const string Mascot = "goo-blobs/splat.svg";
    readonly record struct Ally( string Name, string Tint );
    static readonly Ally[] Squad =
    {
        new( "Jimmy Weiners",   "#73a0f2" ),
        new( "Jiggalater 9000",  "#73d980" ),
        new( "Peepee Poopoo Terry",  "#f7994d" ),
        new( "Thug blaster4321",  "#f2d966" ),
        new( "Monster Energy Zero Ultra",  "#b07cf2" ),
        new( "hunter42", "#f274a6" ),
    };

    // Distinct, non-harmonic periods (seconds) so the bars flash independently with no visual sync.
    static readonly float[] Period = { 1.7f, 2.3f, 2.9f, 3.7f, 4.3f, 5.1f };

    readonly float[]       _health = new float[N];
    readonly float[]       _timer  = new float[N];
    readonly SmoothFloat[] _shown  = new SmoothFloat[N];
    readonly SmoothFloat[] _flash  = new SmoothFloat[N];

    bool _dirty = true;
    void Invalidate() => _dirty = true;

    public SquadHealthView() => Reset();

    public void Reset()
    {
        for ( int i = 0; i < N; i++ )
        {
            _health[i] = 1f;
            _timer[i]  = i * 0.4f;                   // stagger the first hits
            _shown[i]  = new SmoothFloat( 1f, DrainSmooth );
            _flash[i]  = new SmoothFloat( 0f, FlashSmooth );
        }
        Invalidate();
    }

    public bool Tick( Scene? scene, float dt )
    {
        for ( int i = 0; i < N; i++ )
        {
            _timer[i] += dt;
            if ( _timer[i] >= Period[i] )
            {
                _timer[i] -= Period[i];
                _health[i] = _health[i] - DamageChunk <= ReviveFloor ? 1f : _health[i] - DamageChunk;
                _flash[i].Current = 1f;              // instant peak; decays toward Target 0
                Invalidate();
            }
            _shown[i].Target = _health[i]; _shown[i].Update( dt );
            _flash[i].Target = 0f;         _flash[i].Update( dt );
        }
        if ( NeedsRebuild() ) Invalidate();
        bool d = _dirty; _dirty = false; return d;
    }

    // Idle between hits: once every bar's drain and flash settle, stop rebuilding.
    bool NeedsRebuild()
    {
        for ( int i = 0; i < N; i++ )
            if ( !_shown[i].IsSettled || !_flash[i].IsSettled ) return true;
        return false;
    }

    public Container Build()
    {
        // One dark backing behind the whole stack for bright-scene readability. Alpha is in the color, not Container.Opacity (Opacity would fade the names/icons/numbers too).
        var squad = new Container
        {
            Key             = "squad",
            FlexDirection   = FlexDirection.Column,
            Gap             = RowGap,
            Padding         = 8f,
            BackgroundColor = RowBg,
            BorderRadius    = 8f,
        };
        for ( int i = 0; i < N; i++ )
            squad.Children.Add( Row( i ) );
        return squad;
    }

    Container Row( int i )
    {
        var row = new Container
        {
            Key           = $"bar-{i}",
            FlexDirection = FlexDirection.Row,
            AlignItems    = Align.Center,
            Gap           = 8f,
            PointerEvents = PointerEvents.None,
        };

        row.Children.Add( new Goo.SvgPanel
        {
            Key    = "avatar",
            Path   = Mascot,
            Color  = Squad[i].Tint,
            Width  = AvatarSize,
            Height = AvatarSize,
        } );

        // Name on the left, current/max HP right-aligned on the same line (Row + SpaceBetween).
        int cur = (int)MathF.Round( Math.Clamp( _shown[i].Current, 0f, 1f ) * MaxHp );
        var nameRow = new Container
        {
            Key            = "nameRow",
            FlexDirection  = FlexDirection.Row,
            Width          = TrackWidth,
            JustifyContent = Justify.SpaceBetween,
            AlignItems     = Align.Center,
        };
        nameRow.Children.Add( new Text( Squad[i].Name )  { Key = "name", FontSize = 13f, FontColor = NameColor } );
        nameRow.Children.Add( new Text( $"{cur}/{MaxHp}" ) { Key = "hp", FontSize = 12f, FontColor = NameColor } );

        var meta = new Container { Key = "meta", FlexDirection = FlexDirection.Column, Gap = 3f };
        meta.Children.Add( nameRow );
        meta.Children.Add( Bar( i ) );
        row.Children.Add( meta );

        return row;
    }

    Container Bar( int i )
    {
        float h = Math.Clamp( _shown[i].Current, 0f, 1f );

        var track = new Container
        {
            Key             = "track",
            Position        = PositionMode.Relative,
            Width           = TrackWidth,
            Height          = TrackHeight,
            BackgroundColor = Color.Black.WithAlpha( 0.5f ),
            BorderRadius    = 4f,
            Overflow        = OverflowMode.Hidden,
        };

        // Health fill, smooth-drained via _shown[i]. Round it directly: s&box overflow:hidden
        // doesn't clip absolutely-positioned children to the track's border-radius.
        track.Children.Add( new Container
        {
            Key             = "fill",
            Position        = PositionMode.Absolute,
            Top             = 0,
            Left            = 0,
            Height          = Length.Percent( 100 ),
            Width           = Length.Percent( h * 100f ),
            BorderRadius    = 4f,
            BackgroundColor = Color.Lerp( LowHealth, HighHealth, h ),
        } );

        // Per-instance white damage flash over the whole track; decays independently.
        track.Children.Add( new Container
        {
            Key             = "flash",
            Position        = PositionMode.Absolute,
            Top             = 0,
            Left            = 0,
            Width           = Length.Percent( 100 ),
            Height          = Length.Percent( 100 ),
            BorderRadius    = 4f,
            BackgroundColor = Color.White,
            Opacity         = Math.Clamp( _flash[i].Current, 0f, 1f ) * FlashPeak,
        } );

        return track;
    }
}