paid_random_policy.luau
Luau, 78 lines from Game design overview
--!nonstrict
-- Server: PolicyService gate for paid random items (crates, eggs, spins).
-- A failed lookup counts as restricted and is retried after RETRY seconds.
local PolicyService = game:GetService("PolicyService")
local Players = game:GetService("Players")
local Policy = {}
local RETRY = 60
local cache = {} -- player -> {restricted = bool, failed = bool, at = number}
function Policy.lookup(player)
local c = cache[player]
if c and (not c.failed or os.clock() - c.at < RETRY) then return c end
local ok, info = pcall(PolicyService.GetPolicyInfoForPlayerAsync, PolicyService, player)
if ok and type(info) == "table" then
c = { restricted = info.ArePaidRandomItemsRestricted == true, failed = false, at = os.clock() }
else
c = { restricted = true, failed = true, at = os.clock() }
end
cache[player] = c
return c
end
function Policy.restricted(player): boolean
return Policy.lookup(player).restricted
end
-- Use this in EVERY path that can start a paid random item:
-- the crate-open remote, the generic "use product" remote, gift paths.
function Policy.guard(player, productDef)
if productDef.paidRandom and Policy.restricted(player) then
return false, "This isn't available for your account"
end
return true
end
-- Client asks this; keep asking every RETRY s while `failed` is true.
function Policy.forClient(player)
local c = Policy.lookup(player)
return { restricted = c.restricted, failed = c.failed }
end
Players.PlayerRemoving:Connect(function(p) cache[p] = nil end)
-- Weighted roll with pity, on the server. Odds text must come from `items`.
-- items = { {id="a", weight=60, rare=false}, {id="b", weight=5, rare=true}, ... }
function Policy.roll(items, pityCount, pityAt, rng)
rng = rng or Random.new()
if pityAt and pityCount + 1 >= pityAt then
local rares, w = {}, 0
for _, it in ipairs(items) do if it.rare then table.insert(rares, it); w += it.weight end end
local r = rng:NextNumber() * w
for _, it in ipairs(rares) do r -= it.weight; if r <= 0 then return it, 0 end end
end
local total = 0
for _, it in ipairs(items) do total += it.weight end
local r = rng:NextNumber() * total
for _, it in ipairs(items) do
r -= it.weight
if r <= 0 then return it, (it.rare and 0 or pityCount + 1) end
end
return items[#items], pityCount + 1
end
-- Odds for display, computed from the same table the roll uses.
function Policy.oddsText(items)
local total = 0
for _, it in ipairs(items) do total += it.weight end
local out = {}
for _, it in ipairs(items) do
table.insert(out, { id = it.id, percent = math.floor(it.weight / total * 1000 + 0.5) / 10 })
end
return out
end
return Policy