LootTable.cs
using Sandbox;
using System;
using System.Collections.Generic;

[AssetType( Extension = "loot" )]
public class LootTable : GameResource
{
    [Property] public List<LootEntry> Entries { get; set; } = new();

    /// <summary>Minimum number of rolls per population.</summary>
    [Property, Range( 1, 50 )] public int MinRolls { get; set; } = 1;

    /// <summary>Maximum number of rolls per population.</summary>
    [Property, Range( 1, 50 )] public int MaxRolls { get; set; } = 3;

    /// <summary>
    /// Roll the loot table and return a list of LootResults.
    /// Unique entries can only appear once across all rolls.
    /// </summary>
    public List<LootResult> Roll()
    {
        var results    = new List<LootResult>();
        var usedUnique = new HashSet<int>();
        var rng        = new Random();

        int rolls = rng.Next( MinRolls, MaxRolls + 1 );

        for ( int i = 0; i < rolls; i++ )
        {
            int entryIndex = PickEntryIndex( rng, usedUnique );
            if ( entryIndex < 0 ) break;

            var entry  = Entries[entryIndex];
            int amount = rng.Next( entry.MinAmount, entry.MaxAmount + 1 );
            if ( amount <= 0 || entry.Item == null ) continue;

            if ( entry.Unique )
                usedUnique.Add( entryIndex );

            // Merge with existing result for the same item
            bool merged = false;
            for ( int r = 0; r < results.Count; r++ )
            {
                if ( results[r].Item == entry.Item )
                {
                    results[r] = new LootResult( results[r].Item, results[r].Amount + amount );
                    merged = true;
                    break;
                }
            }
            if ( !merged )
                results.Add( new LootResult( entry.Item, amount ) );
        }

        return results;
    }

    private int PickEntryIndex( Random rng, HashSet<int> usedUnique )
    {
        float totalWeight = 0f;
        for ( int i = 0; i < Entries.Count; i++ )
        {
            if ( Entries[i].Item == null ) continue;
            if ( Entries[i].Unique && usedUnique.Contains( i ) ) continue;
            totalWeight += Entries[i].Weight;
        }

        if ( totalWeight <= 0f ) return -1;

        float roll       = (float)(rng.NextDouble() * totalWeight);
        float cumulative = 0f;

        for ( int i = 0; i < Entries.Count; i++ )
        {
            if ( Entries[i].Item == null ) continue;
            if ( Entries[i].Unique && usedUnique.Contains( i ) ) continue;
            cumulative += Entries[i].Weight;
            if ( roll <= cumulative ) return i;
        }

        return -1;
    }
}

/// <summary>A single resolved result from a LootTable roll.</summary>
public class LootResult
{
    /// <summary>The item that was selected.</summary>
    public InventoryItem Item   { get; }

    /// <summary>How many of the item were rolled.</summary>
    public int           Amount { get; }

    public LootResult( InventoryItem item, int amount )
    {
        Item   = item;
        Amount = amount;
    }
}

public struct LootEntry
{
    [Property] public InventoryItem Item { get; set; }

    [Property, Range( 1, 999 )] public int MinAmount { get; set; }

    [Property, Range( 1, 999 )] public int MaxAmount { get; set; }

    /// <summary>Higher weight = more likely to be picked relative to other entries.</summary>
    [Property, Range( 0f, 100f )] public float Weight { get; set; }

    /// <summary>If true, this entry can only appear once per table roll.</summary>
    [Property] public bool Unique { get; set; }
}