Game/2D/UI/PopupLayer.razor

UI component (Razor) for the popup layer. It exposes a singleton Instance and methods to spawn floating text popups, rotated popups, multiplier popups, cell flash elements, and simple explosion particles by creating GameObject instances and attaching UI component items (FloatPopupItem, GridCellFlash).

Native Interop
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent

<root></root>

@code {
    public static PopupLayer Instance { get; private set; }

    protected override void OnStart() => Instance = this;

    public void SpawnPopup( string text, string cssClass, float xPct, float yPx, float lifetime = 1.4f )
    {
        var go = new GameObject( true, "Popup" );
        go.Parent = GameObject;

        var item = go.Components.Create<FloatPopupItem>();
        item.Text     = text;
        item.CssClass = cssClass;
        item.XPct     = xPct;
        item.YPx      = yPx;
        item.Lifetime = lifetime;
    }

    public void SpawnCellFlash( float screenX, float screenY, float cellSize, string cssClass, float duration )
    {
        var go = new GameObject( true, "CellFlash" );
        go.Parent = GameObject;

        var flash = go.Components.Create<GridCellFlash>();
        flash.CssClass = cssClass;
        flash.Duration = duration;
        flash.ScreenX  = screenX;
        flash.ScreenY  = screenY;
        flash.CellSize = cellSize;
    }

    public void SpawnMultiplier(string val, string label, string cssClass, float lifetime = 1.2f)
    {
        var rng = new System.Random();
        
        float baseX = 38f + (float)(rng.NextDouble() * 8f - 4f);
        float baseY = 340f + (float)(rng.NextDouble() * 40f - 20f);
        float rot   = (float)(rng.NextDouble() * 30f - 15f);
        
        SpawnPopupRotated(val,   $"mult-val-popup {cssClass}",   baseX, baseY,        rot,        lifetime);
        SpawnPopupRotated(label, $"mult-label-popup {cssClass}", baseX, baseY + 110f, rot * 0.6f, lifetime);
    }

    public void SpawnPopupRotated(string text, string cssClass, float xPct, float yPx, float rotation, float lifetime = 1.4f)
    {
        var go = new GameObject(true, "Popup");
        go.Parent = GameObject;

        var item = go.Components.Create<FloatPopupItem>();
        item.Text     = text;
        item.CssClass = cssClass;
        item.XPct     = xPct;
        item.YPx      = yPx;
        item.Rot = rotation;
        item.Lifetime = lifetime;
    }
    
    public void SpawnExplosionParticles(float xPct, float yPx)
    {
        var rng = new System.Random();
        
        // Sparks flying in random directions
        for (int i = 0; i < 6; i++)
        {
            float ox  = (float)(rng.NextDouble() * 10f - 5f);
            float oy  = (float)(rng.NextDouble() * 10f - 5f);
            float rot = (float)(rng.NextDouble() * 360f);
            SpawnPopupRotated("✦", "spark-particle", xPct + ox, yPx + oy, rot, 0.6f);
        }
        
        // Smoke puff rising
        SpawnPopup("💨", "smoke-particle", xPct, yPx - 10f, 0.8f);
    }
}