Code/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; }
}