-A Guide on Rarities-

Let’s talk about rarities. The rarity system that I’ll go over in this post is a loot table based rarity system meaning

Legendary = 0.1%, Rare = 20%, Common = 75%, etc. 

This isn’t going to be a rarity system like the ones used by those rolling “games” in which the rarities are based off of 1/(input any number here).

Loot tables are very easy, accurate, and manageable. So they’re good for things like daily spin rewards, chest drops, enemy drops, etc.

What are weights?
Weights are basically the rarity you give an item in your loot table, the higher the weight the more common it is.

A good example would be, having a bag(this being our loot table) and putting 50 (Our weight) blue marbles into it, 20 yellow marbles, and 5 green marbles. When we reach into the bag there’s a greater chance to grab the blue marble than anything else.

In code it’d look like this

local LootTable = {
{name = "Blue Marble", weight = 50}, -- Name of item, Amount of weight
{name = "Yellow Marble",  weight = 20},
{name = "Green Marble", weight = 5},
}

How to turn this into percentage?
To get the percentages for each item in our loot table we’ll have to add up all the weights to get a total weight, the reason? Adding up all the marbles then dividing the marble were trying to find we’ll give us a percentage on finding that marble out of all the other marbles.

50 + 20 + 5 = 75

Say we want to find out the percentage of the blue marble

(50/75)*100 = 66.6 -- 50 is the weight of the item, 75 is the total weight of everything in the table, and we times it by 100 to give us a whole number.
66.6% chance to get the blue marble
function RaritySystem.TotalWeight(LootTable) -- Our loottable
	local TotalWeight = 0 -- Base 0
	for i, item in (LootTable) do -- Loops through our table to get the items
		TotalWeight = TotalWeight + item.weight -- Adds item weight to the total weight
	end
	return TotalWeight -- Returns the total weight when we call this function
end

Our total weight is one of the most important processes to a rarity system. It’s what makes a weighted loot table work, adding all the weights gives us a number to compare the items with.

function RaritySystem.CalculateAndDisplayChances(LootTable)
	local TotalWeight = RaritySystem.TotalWeight(LootTable)
	for _, Item in ipairs(LootTable) do
		local chance = (Item.weight / TotalWeight) * 100
		warn(Item.name .. ": " .. tostring(string.format("%.4f", chance)) .. "% chance")
	end
end

How to actually roll the loot table
Okay, now we’re getting to the most complicated part, which isn’t that complicated.

function RaritySystem.SelectRarity()
	local RandomValue
	local SelectedRarity
	local TotalWeight = RaritySystem.TotalWeight(LootTable) -- Send the loottable

You can ignore randomvalue and selectedrarity for now, the first thing we do is get the total weight of the loot table.

function RaritySystem.SelectRarity(Rarities)
	local RandomValue
	local SelectedRarity
	local TotalWeight = RaritySystem.TotalWeight(LootTable)
	repeat
		RandomValue = RandomSeed:NextNumber(0, 1)

Here’s our RandomValue which is a decimal number between 0 and 1, meaning we could get an number like 0.986534, the reason we need this will come up in the next section also pay attention that were putting this in a repeated loop

function RaritySystem.SelectRarity(LootTable)
	local RandomValue
	local SelectedRarity
	local TotalWeight = RaritySystem.TotalWeight(LootTable)
	repeat
		RandomValue = RandomSeed:NextNumber(0, 1)

		for _, Item in (LootTable) do
			if RandomValue <= Item.weight / TotalWeight then
				SelectedRarity = Item 
				break
			else
				RandomValue = RandomValue - Item.weight / TotalWeight
			end
		end

You’ll notice after getting a random number we then loop through the loot table, and the if statement here is one of the most important parts

if RandomValue <= Item.weight / TotalWeight then

So lets put this in perspective lets say the RandomValue = 0.672, we then loop through the table which checks each item to see if its percentage is greater than the items percentage. Aka. Blue marble is 0.66, so it looks like this

if 0.672 <= 0.66 then

Which isn’t true so we go the else part of the if statement

RandomValue = RandomValue - Item.weight / TotalWeight

What this does now is makes

RandomValue = 0.672 - 0.66 = 0.012

Which now we compare our next item which is the yellow marble

if 0.012 <= 20/75 (0.26) then

This statement is true, so the selected item will be the yellow marble and then we break the loop.

Let’s see the whole function

function RaritySystem.SelectRarity(LootTable) -- Our loot table
	local RandomValue -- A randomvalue
	local SelectedRarity -- The item thats selected
	local TotalWeight = RaritySystem.TotalWeight(LootTable) -- Total weight of all items
	repeat
		RandomValue = RandomSeed:NextNumber(0, 1) -- Gets a random number between 0 and 1

		for _, Item in (LootTable) do -- Loops through our loot table
			if RandomValue <= Item.weight / TotalWeight then -- Compares looped item with the random value
				SelectedRarity = Item
				break
			else
				RandomValue = RandomValue - Item.weight / TotalWeight
			end
		end

		task.wait()
	until SelectedRarity ~= nil -- if no selection then repeat

	return SelectedRarity
end

The reason this is in a repeated loop is because there can be rare cases in which no item is selected in that case we’ll just loop until it does.

The whole minusing the random value by the item’s percentage is a little weird to explain but basically think of it like giving the other items in the loot table a better chance to get picked.

There’s a few things not mention in detail but nothing too important also if you’re curious on what’d you need for the green marble to be picked is

if RandomValue(0.94) <= 0.66(Blue marble)
RandomValue = 0.94 - 0.66 = 0.28
...
if RandomValue(0.28) <= 0.26(yellow marble)
RandomValue = 0.28 - 0.26 = 0.02
...
if RandomValue(0.02) <= 0.06(green marble) then
SelectedItem = Green Marble

So you’d need to roll a 0.93 or higher and the green marbles rarity is
6.6% chance.

10 Likes

Now implement luck with this :happy1:

1 Like

Can’t you just say:

local number = math.random(1,100)
if number == 1 then
      print("1%")
elseif number >= 2 and number <= 16 then 
      print("15%")
else 
     print("84%")
end
1 Like

This will get pretty hard to manage when scaling, I do not recommend you do this.

3 Likes

That’s a very good question with a simple fix, if you want to add luck into a system like this it’s as easy as adding a luck multiplier to the RandomValue since the higher the decimal is for the RandomValue the rarer the stuff you can get

local luckMultiplier = 1 + (math.clamp(math.ceil(usersLuck/100), 0, math.huge)) --(1 + 5/100 = 1.05)
RandomValue = RandomValue * luckMultiplier
1 Like

As a previous comment stated this can be a pain to do when you have multiple loot tables. Let’s say you have 20 different mobs in your game and you want to give them all a loot table you wouldn’t want to create a ton of if statements for each mob. Also a rarity system like this is a lot more customizable and modular.

You’d also have to do the calculations for each item you want and math.random(1,100) isn’t as precise as using a random seed which can give you decimal values. Allowing for you to get stuff with a 0.01% chance of dropping, etc.

1 Like

Would such a system work when you are attempting to use rarities up to the 64-bit integer limit? I fear floating-point imprecision would throw off said rarities.

Yes, I also believe that if the rarity chance is insanely low there will be floating point imprecisions, although the size of the impercision I believe wouldn’t be too vast. Hard to tell unless I did some testing with it.

2 Likes