LootPlan - Random loot generation made easy

LootPlan is a lightweight portable ModuleScript I’ve created to solve any loot generation needs you may have. The module has example scripts, but most heavy documentation will be in this post.

The documentation is detailed to make sure nothing is missed, but you can probably get the jist of it by simply skimming over it, or looking at the code examples. The system is quite simple, with the module code only taking up ~150 lines.


Classes

There are two classes of LootPlan; “single” and “multi”.
Each class is fundamentally different in the way it handles loot.
To create a LootPlan, require the module and give it a variable, then use LootPlan.new(“class”)

local LootPlan = require(game.ReplicatedStorage.LootPlan)
local Test = LootPlan.newSingle()

“single” LootPlans

These LootPlans are great for any objects that can only have a single value. For example, you may want to create a mining game where different ores could spawn. Each rock could only have a single type of ore in it, so you would use a “single” LootPlan.

To use LootPlans, you must add loot. In this case, “Loot” is simply a name with a chance attached to it. Example code;

local LootPlan = require(game.ReplicatedStorage.LootPlan)
local OrePlan = LootPlan.newSingle()

-- The first argument is the name, the second is the chance
OrePlan:AddLoot("diamond", 0.01)
OrePlan:AddLoot("gold", 2)
OrePlan:AddLoot("iron", 10)
OrePlan:AddLoot("stone", 100)

To retrieve loot from the LootPlan, simply call “GetRandomLoot”. Optionally, you can set a luck multiplier using the first argument.

local OreType = OrePlan:GetRandomLoot(1) -- Argument is the luck multiplier
print(OreType)

How it works

Chance for “single” LootPlans is calculated relative to the total chance values of all loot.
In this example, the combined chance value is 112.01 (100+10+2+0.01)
That means the Iron ore has a true chance of 10/112.01 = 8.927%
The diamond ore has a true chance of 0.01/112.01 = 0.0089% (approx 1/11000)
You can find the true chance of loot quickly with the function OrePlan:GetTrueLootChance(name)


“multi” LootPlans

This class of LootPlan is ideal for scenarios where you need multiple items at once. For example after killing a mob in an MMO, they can often drop multiple items at a time. For this, you would use a “multi” LootPlan.

Unlike “single” LootPlans, this class returns a table of multiple items.

Like before, we create a plan and add loot to it;

local LootPlan = require(game.ReplicatedStorage.LootPlan)
local DropPlan = LootPlan.newMulti()

-- The arguments follow the same order as before (name, chance)
DropPlan:AddLoot("wizard hat", 0.1)
DropPlan:AddLoot("dagger", 2) 
DropPlan:AddLoot("arrow", 10) 
DropPlan:AddLoot("gold coin", 80)

Similar to the other class, you simply call “GetRandomLoot()” to retrieve random loot. However, instead of returning a single piece of loot, it returns a table of multiple loot.

-- First argument is the number of iterations, second argument is the luck multiplier
-- Both arguments are optional (default to 1)
local Drops = DropPlan:GetRandomLoot(1, 1)
for LootName,LootQuantity in pairs(Drops) do
    print(LootName,"=",LootQuantity)
end

How it works

The difference of “multi” lootplans compared to “single” lootplans is the way the chance works. Instead of being based on the combined chance, it is simply a flat percentage out of 100. A item with a chance of 50 will have a 50% chance of appearing in the loot table, regardless of the chance of other items.


Other functions

LootPlans allow you to change/add/remove loot dynamically with low performance impact. This is useful for any scenario the chance needs to change quickly, such as changing ore chance based on depth.

ChangeLootChance

-- "single" LootPlan example
OrePlan:ChangeLootChance("iron", 20) -- (name, newChance)

-- "multi" LootPlan example
DropPlan:ChangeLootChance("arrow", 20) -- (name, newChance) 

RemoveLoot

-- "single" LootPlan example
OrePlan:RemoveLoot("iron") -- (name)

-- "multi" LootPlan example
DropPlan:RemoveLoot("dagger") -- (name)

GetLootChance

-- Returns the current chance value of the loot

-- "single" LootPlan example
OrePlan:GetLootChance("diamond") -- returns 0.01 in our example

-- "multi" LootPlan example
DropPlan:GetLootChance("wizard hat") -- returns 0.1 in our example

GetTrueLootChance

-- This function only applies to "single" lootplans
-- Returns the true chance of the requested loot adjusted for the total loot value

OrePlan:GetTrueLootChance("iron") -- returns 8.927 from (10/(100+10+2+0.01))*100

AddLootFromTable

-- Adds loot from a table, helpful for convenience and compatibility

-- "single" LootPlan example
local OreChances = {
    ["diamond"] = 0.01;
    ["gold"] = 2;
    ["iron"] = 10;
    ["stone"] = 100;
}
OrePlan:AddLootFromTable(OreChances)

-- "multi" LootPlan example
local DropChances = {
    ["wizard hat"] = 0.1;
    ["dagger"] = 2;
    ["arrow"] = 10;
    ["gold coin"] = 80;
}
DropPlan:AddLootFromTable(DropChances)

Change Log
  • Edit 1: Removed unnecessary quantity functionality from multi lootplans.
  • Edit 2: Changed ‘rarity’ to ‘chance’ to make more logical sense
  • Edit 3: Added “AddLootFromTable” function
  • Edit 4: Added Luck multiplier functionality, provide when calling GetRandomLoot in the first argument for single lootplans and the second argument when using multi lootplans
  • Edit 5: Added support for typed luau, meaning there is now proper auto fill for function names. This change required moving from LootPlan.new("single") to LootPlan.newSingle()

110 Likes

Very useful, it simplifies life a lot instead of having to keep creating lines and lines of code, I haven’t tested it in practice, but I’ve read everything, very cool, good job, thanks for sharing :slight_smile:

2 Likes

hey, is there any way to stop it from pulling duplicates?

This is pretty neat. Is there a way to call GetRandomLoot in a local script while having the loot in a server script so that exploiters can’t mess with the chances?

2 Likes

Yes! By using a Remote Event, you can send a “Player activated” function to a server script where then you can call “GetRandomLoot()”.

Video:

Wiki Explanation:

4 Likes

Does anyone know what the logic is behind the single plan’s math? I’m having a bit of trouble understanding, even after looking at many resources, the way the math was conceived.

The single plan calculates a random value based off of the chances of all loot, while the multiple is just a flat percentage out of 100 - why is the latter math not also present in the former and why is the math chosen the way it is for both plans? A loot with 100% drop chance in a single plan isn’t actually 100% because you wouldn’t be guaranteed it, you would just get it most commonly, which is which flares up my confusion.

Additionally, what if you wanted to use a single plan where there’s a chance you get nothing? Do you literally have to add “Nothing” as a loot? That doesn’t quite make sense - since when you think about %, it’s typically out of 100, so you might be so concerned with trying to fit all your chances out of 100% and not get the most desirable results.

1 Like

Single loot plans calculate odds using the combined value of all of the loot.

The reason it does this instead of a direct percentage is because the developer would have to manually add together all of the chances and make sure it did not exceed 100. Instead, it automatically calculates out of the total combined values of all of the loot, so you don’t have to worry about adding it up manually.

However, if you want to add it up manually, it will work exactly as you desire it to.

Here, I rewrote the OrePlan example to total 100.

local LootPlan = require(game.ReplicatedStorage.LootPlan)
local OrePlan = LootPlan.new("single")

OrePlan:AddLoot("diamond", 0.01)
OrePlan:AddLoot("gold", 1.99)
OrePlan:AddLoot("iron", 10)
OrePlan:AddLoot("stone", 88)

-- 88 + 10 + 1.99 + 0.01 = 100

Since we made sure the values add up to 100, and it finds chance by dividing by the total combined values of the loot, the chances of the loot in this case are the same as their values.

Stone will appear 88% of the time, iron 10%, gold 1.99%, and diamond 0.01%.

1 Like

Thank you for the explanation! I actually just recently got it explained this same way as well.

My main pitfall was thinking about the loot in terms of raw percentages, but I was told it’s better to think about the values being frequencies rather than actual percentages. That meaning: when I fill out drop tables, I’m always thinking about making sure the values I’m inputting don’t exceed 100 when they’re all together, when that’s not how I should be thinking about it.

A helpful article from Gamasutra as linked actually included a single sentence which helped me redefine the way I think about loot tables (it’s also helped me with BTrees, on a separate note):

  • Total probability : First, sum all the weights in the bucket. In the example above, that’s 10+40+50 = 100. They don’t need to add up to 100 since these aren’t percentages.

Appreciate it! Also got some pointers from @TeamSwordphin.

Thank you for this module! I was just wondering if the tables clean themselves up after we’re done creating/using them?

If you set all references to them to nil, lua’s garbage collector will automatically clean them up.

1 Like

For the single plan luck multiplier, does it just multiply the chance of the lowest chance item by whatever number is in the luck multiplier?

very useful for a pet system I wish I knew about this before I spent like 10 hours figuring out how to make one

I’m kinda new to loot tables so this might be a dumb question, but is there a way to add more then one loot table to the single lootplan, as in groups to be called out for example if a player is closer to egg 1 then it randomizes loot table 1 but when the player is closer to egg 2 then it randomizes loot table 2 don’t get me wrong, I know how to do the player is near this egg, but how would you use the 2nd loot table as in trying to randomize loot table 2 then print the name of it i can probably find out the rest i need if I know how this can work thanks in advance. btw I tried experimenting but popped up a error so I figured to ask how to add it or any tips as to how it worked with adding more.