Loot Table Design: Weighted Drops, Pity Systems & Monte Carlo Simulation
A legendary sword that drops 0.1% of the time sounds exciting on paper. In practice, it means nine players in ten never see it before quitting — and the one who does tells Discord it was "broken RNG." Loot tables are probability contracts with your audience. Get them wrong and no amount of combat polish saves the economy.
This guide covers weighted random tables, nested loot pools, pity and bad-luck protection, and how to Monte Carlo test your design in the Loot Table Simulator before exporting JSON to Godot or Unity. Pair drop rates with a validated XP curve so reward pacing feels intentional, not arbitrary.
Anatomy of a Loot Table
Every loot table is a list of entries, each with:
- Item reference — ID, prefab, resource path
- Weight — relative probability (not necessarily percentage)
- Quantity range — min/max stack size
- Conditions — player level, biome, kill streak, first-clear flag
Weighted random selection:
totalWeight = sum(entry.weight for entry in table)
roll = random(0, totalWeight)
cumulative = 0
for entry in table:
cumulative += entry.weight
if roll <= cumulative:
return entry.item
Weights of [70, 25, 5] give roughly 70% / 25% / 5% — no need to normalize to 100.
Rarity Tiers and Player Expectation
Players mentally map tiers even when you hide the math:
| Tier | Typical weight share | Player expectation | | --- | --- | --- | | Common | 60–75% | Fodder, crafting mats, gold | | Uncommon | 20–30% | Sidegrade gear, consumables | | Rare | 5–10% | Build-enabling drops | | Epic / Legendary | 0.5–2% | Chase items, cosmetics, trophies |
Critical rule: chase items must be possible within a normal session length. If average dungeon runs are 12 minutes, a 0.01% drop needs a pity system or it is effectively nonexistent.
Nested Tables (Loot Within Loot)
Boss chests often roll twice:
- Pool roll: Guaranteed 2–4 items from "dungeon_loot_common"
- Bonus roll: 15% chance to add one item from "dungeon_loot_rare"
Structure nested tables as separate JSON resources the Loot Table Simulator can chain. Godot example:
func roll_table(table_id: String) -> Array[Item]:
var results: Array[Item] = []
var table = LootDB.get_table(table_id)
for rule in table.rules:
if randf() <= rule.chance:
results.append_array(_roll_weighted(rule.sub_table))
return results
Pity Systems and Bad-Luck Protection
Gacha games popularized hard pity (guaranteed rare at roll N) and soft pity (ramps weight after M rolls). For indie RPGs, lighter approaches work:
- Kill counter: After 50 kills without a rare, next kill uses a 5× weight multiplier
- Unique pool depletion: Diablo-style — each drop removes item from pool until exhausted
- Bad-luck token: +1% legendary chance per boss kill without legendary, resets on drop
Simulate pity in the browser tool by enabling counter rules and running 10,000 iterations — inspect the distribution, not just the mean.
Balancing Drops Against XP/Hour
Loot value only lands if players earn enough attempts per hour:
expectedLegendariesPerHour = (killsPerHour * legendaryDropRate) + pityBonus
If your XP curve targets 45 minutes per level and legendaries should appear once per level, solve for drop rate:
dropRate = 1 / (killsPerLevel * averageAttemptsPerKill)
Adjust item power, not just drop rate, when chase items feel weak — a 2% drop of a useless sword still feels like a slap.
Godot 4 Export Workflow
- Build table in Loot Table Simulator
- Export Godot Resource JSON
- Load at runtime:
# loot_manager.gd
@export var tables: Array[LootTableResource]
func drop_from_enemy(enemy_type: String) -> Array[ItemStack]:
var table = _get_table(enemy_type)
return LootRoller.roll(table, _pity_state.get(enemy_type))
Keep server-authoritative rolls for multiplayer — client-side randf() is trivially exploited.
Unity ScriptableObject Export
The simulator exports Unity-compatible weighted lists. Attach to enemy prefabs via LootDropper component. Use Random.State seeding for reproducible debug sessions.
Common Mistakes
- Publishing raw percentages that do not match weights: Rounding 0.333% across three items creates player distrust when datamined
- Independent rolls on multi-drop chests: Without diminishing returns, variance spikes — players get 4 legendaries or zero
- Same table for all enemy tiers: Elite enemies should pull from shifted weight columns, not identical tables
- No simulation before ship: Always Monte Carlo 10k+ runs; mean drop rate hides long dry streaks
Related Tools and Guides
- RPG XP Curve Balancing Guide
- Procedural Level Generation Guide — place chests in generated dungeons
- Game Dev Lab
FAQ
Weighted random vs true percentage?
Weighted is easier to rebalance — double every weight and probabilities stay the same. Percentages tempt designers to force sums to exactly 100.000%.
How many rolls should I simulate?
10,000 minimum for rare items below 1%. 100,000 for sub-0.1% chase drops if you skip pity.
Should duplicates convert to currency?
Yes for collectible-heavy games — reduces frustration without inflating drop rates. Document conversion in tooltips.
Conclusion
Loot tables are where game design meets statistics. Weighted pools, nested rolls, and pity counters give you control; Monte Carlo simulation gives you confidence. Build in the browser, export to engine, tune with real session data.
Simulate your tables: Loot Table Simulator →