Programming Loot Tables

Intro
I’m making this tutorial because I noticed there aren’t many inventory/item related tutorials on the devforum, and I really want to change that

What we are making
We are going to create a Items module to store all of our item information, a LootTables module to store all of our loot tables, and a ItemService module that uses both of the first two modules to get random items

we are creating the Items module separate from the LootTables module so we have the option to make multiple loot tables

Tutorial
Firstly, if we want to make loot tables, we are going to need some loot!

We can store all of our item information in Modules

Items Module
-- Module named Items in ReplicatedStorage

-- you can create as many items as you want, and give the items any value's you want
-- make sure to keep it as an array (linear and no missing indexes, e.g 1, 2, 3, 4, etc)


-- PRACTICE: try adding one or more items of your own
local Items = {
	[1] = {
		Name = "Iron",
		Description = "Used to make iron equipment",
	},
	[2] = {
		Name = "Iron Sword",
		Description = "Watch out it's sharp",
	},
	[3] = {
		Name = "Coal",
		Description = "Used as a fuel source",
	}

}

return Items

now to create a loot table, we can also use Modules for this

Loot Table Module
-- Module named LootTables in ReplicatedStorage
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Items = require(ReplicatedStorage.Items) -- get the Items module

--[[ 
	Weight must be in order from greatest to least
	
	The higher the weight, the more likely it is to be choosen 
	(relative to other items in loottable)
--]] 

-- PRACTICE: if you created one of your own item(s) in the item module, add it here
local LootTables = {
	["LootTableA"] = {
		{Item = Items[3], Weight = 125}, -- coal, has the highest weight (most common)
		{Item = Items[1], Weight = 80}, -- iron sword
		{Item = Items[2], Weight = 45} -- iron, with the lowest weight (the rarest)
	},
	["LootTableB"] = {
		{Item = Items[1], Weight = 500}, -- iron sword, and weight is way higher than others, so really common 
		{Item = Items[3], Weight = 75},
		{Item = Items[2], Weight = 35}
	}
}

return LootTables

so now that we’ve create both of those information modules, we can create a module to get random items from the loot tables we’ve just created

ItemService Module
-- Module named ItemService in ServerScriptService

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LootTables = require(ReplicatedStorage.LootTables)


local ItemService = {LootTables = LootTables}

-- gets the total weight, used to generate a random number
local function getTotalWeightOfLootTable(lootTable)
	local weight = 0
	for _, v in ipairs(lootTable) do
		weight = weight + v.Weight
	end
	return weight
end

function ItemService:GetRandomItemFromLootTable(lootTable)
	local totalWeight = getTotalWeightOfLootTable(lootTable)
	local randomNumber = math.random(totalWeight)	
	--[[ 
		loops through the lootTable, if an item is less or equal to the weight of the current entry
		it will return the item, otherwise it will subtract the weight and continue the process
		until an item is returned
	--]]	
	for _, entry in ipairs(lootTable) do
		if randomNumber <= entry.Weight then -- weight is less than random number so returns item
			return entry.Item
		else -- no item, subtracts the weight from randomNumber
			randomNumber = randomNumber - entry.Weight
		end
	end
end

return ItemService

that’s it, we created a loot table system, and now we can get random items as shown below

local ServerScriptService = game:GetService("ServerScriptService")
local ItemService = require(ServerScriptService.ItemService)

local lootTableA = ItemService.LootTables.LootTableA

local randomItem = ItemService:GetRandomItemFromLootTable(lootTableA)
print(randomItem.Name)
Examples of what it can do

image

this is my first tutorial, so feel free to let me know how I can improve this post, and I will do so, depending on how this tutorial is received I will possibly add more to this tutorial

Update #1

Using indexes for loot tables may be bothersome or confusing, so in this first update we will make a function that can get an item by it’s name instead of index

we need to edit the Items module

creating a new table that that has an items name as the key and value as the item, is better than looping through the entire Items table and checking the name as we only have to iterate through a table once


local Items = {
	[1] = {
		Name = "Iron Sword",
		Description = "Watch out it's sharp",
	},
	
	[2] = {
		Name = "Iron",
		Description = "Used to make iron equipment",
	},
}

local ItemsByName = {}

for i, item in ipairs(Items) do
	ItemsByName[item.Name] = item
end

function Items:GetItemByName(itemName)
	return ItemsByName[itemName]
end

return Items

now in our LootTables module we can do

["LootTableA"] = {
    {Item = Items:GetItemByName("Iron"), Weight = 2}, 
    {Item = Items:GetItemByName("Coal"), Weight = 1}, 
    {Item = Items:GetItemByName("Iron Sword"), Weight = 1}
}

it’s a lot easier to read to me, but this step is optional and opinion based

File: LootTablePlace.rbxl (19.5 KB)

58 Likes

What is the benefit of using weight over let’s say percentages? For example, an item might have a 50% chance of dropping.

Percentages tend to have to be exact numbers e.g (50%, 25%, 10%, 10%, 5%) and add up to a total of 100%

which might not be a problem for small loot tables, but what if you have 10 items or 100 or even 1000

weighted system is more straight forward imo, and likely better in performance too

	["LootTableB"] = {
		{Item = Items[1], Weight = 500}, 
		{Item = Items[3], Weight = 344},
		{Item = Items[1], Weight = 77},
		{Item = Items[2], Weight = 26},
		{Item = Items[3], Weight = 16},
		{Item = Items[2], Weight = 13},
		{Item = Items[1], Weight = 7},
		{Item = Items[3], Weight = 7},
		{Item = Items[3], Weight = 7},
		{Item = Items[2], Weight = 5},
		{Item = Items[1], Weight = 1},
	}

compared to

	["LootTableB"] = {
		{Item = Items[1], Percent = 50}, 
		{Item = Items[3], Percent = 25},
		{Item = Items[1], Percent = 10}, 
		{Item = Items[2], Percent = 10},
		{Item = Items[2], Percent = 5}
	}

and if I wanted to add a item to the first LootTable it’s as easy as copy pasting and setting the weight to a number

with the second LootTable, I’d have to change every single percent when I add a item to make it add up to 100%

3 Likes

I agree with your points, but I feel like percentage or more comprehensible. With weights, it would be hard to tell how often an item would really drop.

EDIT: With probabilities you could always have multiple items drop. You can think of probabilities like a dice roll. For each item in the loot table, roll the dice and see if that item should drop.

2 Likes

can just have the weights add up to 100 then

	["LootTableB"] = {
		{Item = Items[1], Weight = 50}, 
		{Item = Items[2], Weight = 25},
		{Item = Items[3], Weight = 25},
	}
local ServerScriptService = game:GetService("ServerScriptService")
local ItemService = require(ServerScriptService.ItemService)

local lootTableB = ItemService.LootTables.LootTableB

local items = {}

-- created 10k items
for i = 1, 10000 do
	local randomItem = ItemService:GetRandomItemFromLootTable(lootTableB)
	items[randomItem.Name] = items[randomItem.Name] and items[randomItem.Name] + 1 or 1
end

-- converted to percentage
for i, v in pairs(items) do
	print(i, "%" .. tostring(v/100))
end

image

	["LootTableB"] = {
		{Item = Items[1], Weight = 50}, 
		{Item = Items[2], Weight = 40},
		{Item = Items[3], Weight = 9},
		{Item = Items[4], Weight = 1},
	}

image
etc

and @Nezuo no

	["LootTableB"] = {
		{Item = Items[1], Weight = 1}, 
		{Item = Items[2], Weight = 1},
	}

image

and

	["LootTableB"] = {
		{Item = Items[1], Weight = 1}, 
		{Item = Items[1], Weight = 1}, 
		{Item = Items[2], Weight = 1},
	}

image

1 Like

Yes, I removed that edit, after using my brain I realized I was wrong.

1 Like

Alright, enough criticism, I have some suggestions now. You might want to consider adding the ability of setting a weight for nothing to drop so that loot table doesn’t always guarantee an item. Another addition would be to make the loot table not drop items but groups of items. So you can make one entry in a loot table drop 2 items at once.

3 Likes

I also created a similar “tutorial” in a reply to a topic:

2 Likes

I remember i asked this question before :laughing:

Thanks for making this thread, so if i want to make a more advanced loot system, i could search this! :+1:

1 Like

This tutorial will be super helpful for when I start making a loot system for my game. There’s barely anything anywhere about this so it really is the one of the best resources right now to make a good loot system. Also I like the weight a lot because I’ve worked with percentage in the past and it gets really hectic whenever you have mass items and or need to add or remove anything in the table. Hope to see more of these unique tutorials from you in the future!

1 Like

How can I use weight but in another way? I mean, that instead of the higher number will get a higher chance to be choosen, the lowest number will get a higher chance?

1 Like

You can add an indirection layer around the module’s API that turns each weight into 1/weight before passing it through to the actual method, to accomplish something like that.

3 Likes

How would this work if we wanted to make an item like SUPER rare? For example, in Bubble Gum simulator they have some items that have a percentage of 0.001%

1 Like

in that case I’d likely just make the overall weights bigger numbers

e.g

	["LootTableB"] = {
		{Item = Items[3], Weight = 700},
		{Item = Items[1], Weight = 300},
		{Item = Items[2], Weight = 1}
	}

image

	["LootTableB"] = {
		{Item = Items[3], Weight = 7000},
		{Item = Items[1], Weight = 3000},
		{Item = Items[2], Weight = 1}
	}

image

also if you want to do any tests yourself with percentages this is what I’m using

local items = {}
-- creates x amount items
local count = 10000 -- reduce this count if you get lag when trying to run this
for i = 1, count do
	local randomItem = ItemService:GetRandomItemFromLootTable(lootTableB)
	items[randomItem.Name] = items[randomItem.Name] and items[randomItem.Name] + 1 or 1
end

-- converted to percentage
for i, v in pairs(items) do
	print(i, tostring(math.floor(v/count*100*100)/100) .. "%")
end

(making weights lower than 1 might work like .3 etc, but don’t take my word for it test it out yourself and see if you run into any issues as I haven’t done enough testing to know if any bugs will happen if you do that (inaccuracy)) I ran into bugs with this, so I don’t recommend doing it, just make the weights higher

2 Likes

I loved it

But, I was thinking, and if you added the possibility of creating a group of Items that has the ID [1] and all the items within that group had the same chance of being Dropped, and another possible update would be to get items from a folder and add them to GroupID [1] with a simple GetChildren ().

the logic behind my idea:

Imagine an RPG, the first map is a forest, and several items there have the same chance to drop in the Normal rarity, some in Uncommon Rarity, and only a few in the magic rarity

add all common items in a folder, and place that folder as GroupID [1], and give GroupID [1] the Weight = 100, GroupID [2], Weight = 50 and GroupID [3], Weight = 25 … and it’s done :grinning: I would particularly love it, for the ease, and for learning how to do it, your tutorial helped me a lot to understand how the tables work :slight_smile: thanks

if you could do a tutorial on how to execute my idea i would be very grateful

(sorry for my english, maybe i’m really bad at it, i don’t know, almost nobody speaks english where i live)

1 Like

For anyone who wants this to work with chances less that 1, simply do

local rng = Random.new()

local random = rng:NextNumber(0, TotalWeight)

This will work with decimals now :smiley:

1 Like