Skip to content

Instantly share code, notes, and snippets.

@ahmoin
Created January 6, 2026 00:35
Show Gist options
  • Select an option

  • Save ahmoin/3b42477528e6ed281f931de9a2ab78f5 to your computer and use it in GitHub Desktop.

Select an option

Save ahmoin/3b42477528e6ed281f931de9a2ab78f5 to your computer and use it in GitHub Desktop.
--!strict
local ServerStorage = game:GetService("ServerStorage")
local Workspace = game:GetService("Workspace")
local LootTables = require(ServerStorage:WaitForChild("Modules"):WaitForChild("LootTables"))
type Entry<T> = { isValue: true, weight: number, tier: number, value: T }
type LootTable<T> = {
maxWeight: number,
maxTier: number,
entries: { Entry<T> },
entry: (LootTable<T>, weight: number, tier: number, value: T) -> LootTable<T>,
roll: (LootTable<T>, luck: number?, seed: number?) -> T,
probabilities: (LootTable<T>, luck: number?) -> { { chance: number, value: T } },
findEntry: (self: LootTable<T>, value: T) -> Entry<T>?,
}
local function rollLootTable<T>(lootTable: LootTable<T>, luck: number?, seed: number?): T
local actualLuck = luck or 0
local totalWeight = 0
for _, entry in lootTable.entries do
totalWeight += entry.weight + actualLuck * entry.tier
end
local random = Random.new(seed)
local roll = (random:NextNumber()) * totalWeight
for _, entry in lootTable.entries do
roll -= entry.weight + actualLuck * entry.tier
if roll < 0 then
return entry.value
end
end
error("Unable to roll a value from loot table")
end
local function setInterval(callback: (number, ...any) -> nil, intervalSeconds: number, ...: any)
local cleared = false
local function call(scheduledTime: number, ...: any)
if cleared then
return
end
local deltaTime = os.clock() - scheduledTime
task.spawn(callback, deltaTime, ...)
task.delay(intervalSeconds, call, os.clock(), ...)
end
local function clear()
cleared = true
end
task.delay(intervalSeconds, call, os.clock(), ...)
return clear
end
export type StoreData = {
name: string,
interval: number,
products: {
{
itemName: string,
price: number,
minimumQuantity: number,
quantity: number,
}
},
}
local Store = {}
Store.__index = Store
export type ClassType = typeof(setmetatable(
{} :: {
-- onRestock: Signal.ClassType,
_lastRestock: number,
_lastSeed: number,
_lootTable: LootTable<string>,
_storeData: StoreData,
_clearInterval: (() -> ())?,
_quantities: { [string]: number },
},
Store
))
function Store.new(storeData: StoreData): ClassType
local self = {
-- onRestock = Signal.new(),
_lootTable = LootTables[storeData.name],
_lastSeed = -1,
_lastRestock = Workspace:GetServerTimeNow() // storeData.interval * storeData.interval,
_storeData = storeData,
_clearInterval = nil :: (() -> ())?,
_quantities = {} :: { [string]: number },
}
setmetatable(self, Store)
self:_generateQuantities()
self._clearInterval = setInterval(function()
local nextRestock = self._lastRestock + storeData.interval
if nextRestock <= Workspace:GetServerTimeNow() then
self:restock()
self._lastRestock = nextRestock
end
end, 1) :: () -> ()
return self
end
function Store.getData(self: ClassType)
return self._storeData
end
function Store.getQuantities(self: ClassType): { [string]: number }
local seed = (Workspace:GetServerTimeNow() // self._storeData.interval)
if seed == self._lastSeed and self._quantities then
return self._quantities
end
return self:_generateQuantities()
end
function Store.destroy(self: ClassType)
if self._clearInterval then
self._clearInterval()
self._clearInterval = nil
end
end
function Store.restock(self: ClassType, isPaidRestock: boolean?)
local quantities = self:_generateQuantities(if isPaidRestock then math.random(0, 1_000_000) else 0)
-- self.onRestock:Fire(quantities)
end
function Store._getProductInStore(
self: ClassType,
productName: string
): {
itemName: string,
price: number,
minimumQuantity: number,
quantity: number,
}?
for _, product in self._storeData.products do
if product.itemName == productName then
return product
end
end
return nil
end
function Store._generateQuantities(self: ClassType, offset: number?): { [string]: number }
offset = if offset then offset else 0
local productNames = {}
local seed = (Workspace:GetServerTimeNow() // self._storeData.interval) + offset
for index = 1, #self._storeData.products do
local choice = rollLootTable(self._lootTable, 0, seed + index)
if not table.find(productNames, choice) then
table.insert(productNames, choice)
end
end
local random = Random.new(seed)
local quantities = {}
for _, productName in productNames do
local productDataInStore = self:_getProductInStore(productName)
if not productDataInStore then
warn("productDataInStore not found for productName", productName)
continue
end
quantities[productName] = random:NextInteger(1, productDataInStore.quantity)
end
for _, product in self._storeData.products do
if not quantities[product.itemName] then
quantities[product.itemName] = product.minimumQuantity
end
end
self._lastSeed = seed
self._quantities = quantities
return quantities
end
return Store
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment