Created
January 6, 2026 00:35
-
-
Save ahmoin/3b42477528e6ed281f931de9a2ab78f5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --!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