Skip to content

Loot — Core

Folder Overview

This folder holds the authored data and the function that turns it into an outcome.

A LootTable is inert. LootRoller.Roll(table, rng) reads it and returns a LootResult describing what was won. Nothing is granted to anybody here — no scene, no owner, no other system is touched.


Purpose

Keeping the roll a pure function of (table, rng) buys two things that matter more than they look:

  • Drop rates are testable without Play Mode. A distribution check runs thousands of rolls in an EditMode test, with no scene to build and no frames to wait for.
  • This assembly compiles when everything else is gone. Nothing in here references Inventory, Currency, Pickups or Health, so a project that deleted all four still builds.

What Lives Here

LootTable

A ScriptableObject holding entries plus how to roll them: Mode, RollsMin/RollsMax, and AllowDuplicates. Read-only once built — nothing in the supported surface mutates an existing table.

LootTable.Create builds one in code, for deterministic tests, tooling and procedural generation. Authoring a .asset is still the recommended route, because odds in code are odds a designer cannot open; the factory exists because the alternative is that a table cannot be built outside this assembly at all. The instance it returns is runtime-only and marked HideFlags.DontSave, so it does not survive a domain reload — build it where you use it rather than caching it across one.

LootEntry / LootEntryKind / LootTableMode

A flat struct with a kind discriminator rather than a polymorphic hierarchy, because Unity serialises it in a list and polymorphic serialisation there costs more than the branch does.

Four kinds: Item, Currency, Table (roll another and fold its awards in), and Nothing — which exists so a weighted table can express "sometimes no drop" as authored intent rather than as an accident.

LootResult / LootItemGrant / LootCurrencyGrant

What a roll produced. A description of awards, not the awards themselves.

LootRoller

The roll itself. Two modes:

Mode Behaviour
Weighted Each roll picks exactly one entry by relative weight. The table always yields as many awards as it has rolls.
IndependentChance Every entry is tested against its own chance. The table may yield nothing, everything, or anything between.

Weights are relative — 1/1/2 behaves identically to 50/50/100.


Important Notes

The Value01() upper bound is handled explicitly in three places

IRandomProvider.Value01() is documented inclusive at both ends, and the two providers shipped in Core/Internal genuinely differ: the seeded one returns [0, 1), Unity's returns [0, 1]. The roller cannot assume a half-open range, so it does not:

  • Chance tests bound 0 and 1 before comparing. A plain roll <= chance would award at chance 0; a plain roll < chance would occasionally refuse at chance 1.
  • Quantity rolls clamp the scaled offset, which would otherwise land one past the end of the range when the draw is exactly 1.
  • Weighted picks fall back to the last eligible entry, which owns the boundary when the roll lands exactly on the running total.

Each of those is one line and each of them is load-bearing. If you replace the RNG, this is the contract your implementation is being read against.

An entry with a missing id warns rather than failing quietly

In weighted mode, an Item entry with no GUID has already consumed its pick by the time anything notices. It awards nothing and behaves as a hidden Nothing entry — which is indistinguishable from bad luck by watching drops. So the roller says so through DevDiagnostics.Warn instead, naming the table. The supported way to express an empty outcome is a Nothing entry, which returns silently.

Quantities behave the same way: a row left at zero awards one, not nothing, so an entry added in the inspector and not yet filled in does something visible rather than silently never appearing.

Nesting is depth-capped and cycle-detected

MaxNestingDepth is 8. Cycle detection is path-based, not global: the same table appearing in two different branches is legitimate, and only a table that reaches itself is a cycle. Either limit produces a warning and an empty award, never a hang.

Determinism is per call-sequence

The same seed replays the same rolls exactly — but changing how many values a roll consumes changes everything after it. That is why the roller draws in a fixed order rather than short-circuiting, and why a change to rolling order is a behavioural change even when the odds are untouched.