§ How to · with Picoo
How to make a rarity and luck system in Roblox
By Sametcan Tasgiran, Founder & Developer·Published ·Updated
Rarity is the one system where a subtle bug is invisible — the wrong roll still returns an item, it just returns the wrong one slightly too often.
Ask Picoo for a rarity system, drop chances or a luck multiplier and it applies its rarity_roll recipe: weighted tiers, luck that scales weights instead of granting rerolls, odds computed from the roll table, and a server-side roll that shows its result.
Cumulative weight rolling
Five default tiers — Common 1000, Uncommon 250, Rare 60, Epic 12, Legendary 1 — rolled against the total weight with the module's own Random.new() rather than math.random. Weights never need to sum to 100, and you can pass up to 20 tiers of your own.
Luck multipliers
Luck is read from a Luck value in the player's leaderstats (the luck_stat setting picks another name) and clamped between 1 and 100; a player without one rolls at 1. It multiplies the weight of every tier lighter than a quarter of the heaviest one: Rare, Epic and Legendary by default.
Odds from the roll table
Rarity.odds(luck) returns each tier's percentage from the same weights and luck rule the roll uses, and RarityServer.oddsText(player) formats them as text for that player's luck, ready for an odds panel.
Server-side rolls
ServerScriptService.RarityServer rolls with the player's luck. Server code calls _G.RarityServer.roll(player) and gets the tier back; what that tier grants is up to your game.
A reveal on every roll
The rolling player sees the tier name rise over their character in the tier's colour and their camera shakes, while sparkles and a sound play where they stand. The shake, the sparkles and the sound scale with how rare the tier's weight makes it. Pass true as the second argument to roll silently.
Optional pity
Off by default. Set pity_after and a player who goes that many rolls without the rarest tier gets it on the next one. The count is kept in server memory and cleared when the player leaves.
Files Picoo ships for this prompt
4 files · 1059 lines · one recipe call
ReplicatedStorage/Rarity.lua
ModuleScript with the tier table, the weighted roll, the luck rule and the odds functions. 89 lines with the five default tiers; each extra tier adds one.
89 lines
ServerScriptService/RarityServer.lua
Server Script that reads the Luck leaderstat, keeps the pity count, rolls for a player, triggers the reveal and exposes _G.RarityServer.
107 lines
ReplicatedStorage/PicooFeedback.lua
Shared game-feel ModuleScript. RarityServer calls its reveal for the popup, camera shake, sparkles and sound; if the module is missing, rolls still work without them.
851 lines
StarterPlayer/StarterPlayerScripts/PicooFeedbackClient.lua
LocalScript that loads the client half of PicooFeedback so each player receives their popup and camera shake.
12 lines
Sample output: ReplicatedStorage/Rarity.lua
-- WEIGHTS, not percentages. Any scale works and they never need to sum to
-- anything — add an entry and every other tier keeps its relative chance.
Rarity.Tiers = ({
{ id = "common", name = "Common", weight = 1000, color = Color3.fromRGB(170, 170, 170) },
{ id = "uncommon", name = "Uncommon", weight = 250, color = Color3.fromRGB(90, 200, 120) },
{ id = "rare", name = "Rare", weight = 60, color = Color3.fromRGB(80, 140, 240) },
{ id = "epic", name = "Epic", weight = 12, color = Color3.fromRGB(170, 90, 230) },
{ id = "legendary", name = "Legendary", weight = 1, color = Color3.fromRGB(255, 190, 60) },
} :: { Tier })
-- Weights below this are treated as the "rare side" that luck improves.
Rarity.LUCK_THRESHOLD = 250
Rarity.PITY_AFTER = 0
Rarity.RAREST_ID = "legendary"
-- One generator instance for this module. math.random shares state with every
-- other script in the place, so an unrelated randomseed call elsewhere would
-- shift these drop rates as a side effect.
local rng = Random.new()
function Rarity.get(id: string): Tier?
for _, t in Rarity.Tiers do
if t.id == id then return t end
end
return nil
end
-- Luck multiplies the weight of rare tiers. The alternative — an extra roll on
-- a miss — makes the true odds awkward to state, and Roblox requires odds to be
-- disclosed for anything paid.
local function effectiveWeight(tier: Tier, luck: number): number
if tier.weight < Rarity.LUCK_THRESHOLD then
return tier.weight * math.max(1, luck)
end
return tier.weight
end
--- Roll one tier. Call this on the SERVER.
function Rarity.roll(luck: number?): Tier
local l = math.clamp(tonumber(luck) or 1, 1, 100)
local total = 0
for _, t in Rarity.Tiers do
total += effectiveWeight(t, l)
end
local pick = rng:NextNumber() * total
for _, t in Rarity.Tiers do
pick -= effectiveWeight(t, l)
if pick <= 0 then return t end
end
-- Floating-point drift guard. Unreachable in practice, but returning nil here
-- would surface as "sometimes the roll gives nothing".
return Rarity.Tiers[1]
endBuilding a rarity and luck system in Roblox
Rarity is unusual among game systems in that a bug does not crash anything. A broken weighted roll still hands the player an item; it just hands out the wrong one a little too often, and you find out weeks later when the economy is already distorted.
Picoo's rarity_roll recipe is built on weights, not percentages. ReplicatedStorage.Rarity holds five tiers — Common 1000, Uncommon 250, Rare 60, Epic 12 and Legendary 1 — and a roll picks a random point in the total weight, then subtracts each tier's weight until it reaches zero. Nothing has to add up to a hundred, so adding or retuning a tier leaves every other tier's relative chance alone. The roll draws from the module's own Random.new() rather than math.random, whose shared state any other script can reseed.
Luck is where the usual implementation causes trouble. Granting a reroll when the player misses feels natural and makes the true odds genuinely awkward to compute — which matters, because for paid rolls Roblox requires you to disclose them. The recipe scales weights instead: the player's Luck leaderstat, clamped between 1 and 100, multiplies every tier lighter than a quarter of the heaviest one, which with the default table means Rare, Epic and Legendary. The odds after luck are the same one-line calculation as before.
The odds come from the same code. Rarity.odds(luck) runs the same weights through the same luck rule and returns percentages, and RarityServer.oddsText(player) formats them for that player's luck. Hardcoded percentages in a UI start correct and go stale the first time someone retunes a weight; text built from the roll table stays in step with it.
Rolling happens in ServerScriptService.RarityServer, because in a game where the result has value a client-side roll is not randomness, it is a request. Server code calls _G.RarityServer.roll(player) and gets a tier back. By default the roll is also shown: the tier name rises over the player's character in the tier's colour, their camera shakes, and sparkles and a sound play where they stand; the shake, the sparkles and the sound scale with how rare the tier's weight makes it. Passing true as the second argument rolls silently, which is what you want when simulating odds. An optional pity counter, pity_after, hands over the rarest tier after that many misses in a row; it lives in server memory while the player is in the server.
The recipe ends at the tier. What each tier grants, whether a roll costs coins or Robux, saving what was won and an odds panel on screen are follow-up prompts, and the roll and the odds text are already there for them to call.
See more on the Luau generator, the AI Luau script writer, the 3D model generator and the game builder, read how Picoo compares with ChatGPT, or browse the full blog.
Frequently asked
Do I have to show drop rates?+
For anything a player pays Robux for, yes — Roblox requires the odds of paid random-item generators to be disclosed to the player before they buy. The recipe computes those odds from the same weight table and luck rule it rolls with: RarityServer.oddsText(player) returns each tier's percentage as text, so what you show stays in step with what you roll when you retune. Putting that text on screen is a follow-up prompt.
Weights or percentages?+
Weights. Percentages must sum to exactly 100, so adding one item means editing every other number, and the rounding people apply to make it add up quietly shifts the real rates. Weights on any scale need no maintenance — add an entry and the others keep their relative odds.
How should a luck multiplier work?+
Scale the weights of rare entries, not the number of rolls. A luck stat that grants a second roll on a failure changes the effective odds in a way that is genuinely hard to state, which matters if you have to disclose them. The generated module multiplies the weight of every tier lighter than a quarter of the heaviest one by the player's Luck leaderstat, clamped between 1 and 100, so the odds after luck are still one line of arithmetic.
math.random or Random.new?+
Random.new. math.random draws from generator state shared by every script in the place, so anything calling randomseed changes your drop rates as a side effect. A dedicated Random instance is isolated and can be seeded deliberately when you want reproducible tests.
Where should the roll happen?+
On the server, always. A client-side roll is a client-chosen result — and in a game where results have value, that is the whole economy. The generated RarityServer is a server Script: call _G.RarityServer.roll(player) from your server code, and the player's client only draws the popup and camera shake for a result the server already picked.
What happens after the roll?+
RarityServer.roll returns the tier, and your game decides what it is worth. Handing out an item or pet for that tier, charging coins or Robux per roll, saving what was won and an on-screen odds panel are each a follow-up prompt on top of the recipe.
Related Picoo prompts
an egg hatching system in Roblox
Hatching is a rarity roll wrapped in theatre, and the order of operations is what makes it safe.
Roblox pet system
6 files · 1,165 lines · one recipe call. Eggs, rarity, trading and an inventory UI are follow-up prompts that call PetServer.grant and the EquipPet remote.
Roblox inventory system
9 files · 1256 lines · one recipe call. 852 of those lines are PicooFeedback, the shared module behind the item popups.