FpsUI/Models/XpModel.cs

A small UI model that tracks recent XP grants for an FPS HUD. Stores a capped list of entries with Id, Amount, Action, Age and a spring-based Pop for animation, updates total XP, adds new entries, ages and evicts expired entries via Tick.

Native Interop
using System.Collections.Generic;
using Goo.Animation;

namespace Goo.FpsUI;

// XP feed logic: a short-lived stack of "+amount XP (action)" grants that pop in and fade out.
// Each entry carries a stable Id (use as the Goo child Key) and a pop spring. Engine-free.
public sealed class XpModel
{
    public float Lifetime   = 1.8f;  // seconds a grant stays on screen
    public int   MaxEntries = 6;     // cap before the oldest is dropped

    public sealed class Entry
    {
        public int         Id;          // stable identity for keying
        public int         Amount;      // XP granted
        public string      Action = ""; // optional reason ("Headshot", "Assist", ...)
        public float       Age;         // seconds since added
        public SpringFloat Pop;         // 0 -> 1 pop-in progress
    }

    readonly List<Entry> _entries = new();
    int _nextId;

    public IReadOnlyList<Entry> Entries => _entries;
    public int Total { get; private set; }  // running XP total (drive a level bar from this later)

    // Award XP. action is an optional label shown to the right of the amount.
    public void Add( int amount, string action = "" )
    {
        Total += amount;
        _entries.Insert( 0, new Entry
        {
            Id = _nextId++, Amount = amount, Action = action ?? "",
            Pop = new SpringFloat( 0f, 18f, 0.38f ) { Target = 1f }, // underdamped: overshoots past 1 for a springy pop
        } );
        while ( _entries.Count > MaxEntries ) _entries.RemoveAt( _entries.Count - 1 );
    }

    public bool Tick( float dt )  // age and pop entries, evict expired, true while anything changes
    {
        bool moving = false;
        for ( int i = _entries.Count - 1; i >= 0; i-- )
        {
            var e = _entries[i];
            e.Age += dt;
            moving |= e.Pop.Tick( dt );
            if ( e.Age >= Lifetime ) { _entries.RemoveAt( i ); moving = true; }
        }
        return moving;
    }
}