---@alias OxSelectorItem {[1]: number, [2]: any} ---@alias OxSelectorSet OxSelectorItem[] ---@class OxSelector: OxClass ---@field private sets OxSelectorSet | table ---@field private totalWeights table local OxSelector = lib.class("OxSelector") local DEFAULT_SET = 'default' local deepClone = lib.table.deepclone local function calculateTotalWeight(set) local total = 0 for i = 1, #set do local item = set[i] assert(type(item) == "table", "Each OxSelectorItem must be a table") local weight = item[1] assert(type(weight) == "number" and weight >= 0, "weight must be 0 or more") total += weight end return total end ---@param sets OxSelectorSet | table function OxSelector:constructor(sets) if type(sets) ~= "table" then lib.print.error("Invalid sets provided to OxSelector constructor") end if lib.table.type(sets) == "array" then sets = { [DEFAULT_SET] = sets } end self.private.totalWeights = {} self.private.sets = {} for setName, set in pairs(sets) do assert(type(set) == "table" and lib.table.type(set) == "array", "Each set must be an array of OxSelectorItem") assert(#set > 0, "Each set must contain at least one OxSelectorItem") self.private.totalWeights[setName] = calculateTotalWeight(set) end self.private.sets = sets end --- Get a random non-weighted item from a specific set ---@param setName? string ---@return OxSelectorItem? function OxSelector:getRandom(setName) local set = (setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET] if not set then return nil end local item = set[math.random(#set)][2] return type(item) == "table" and deepClone(item) or item end --- Get a random weighted item from a specific set ---@param setName? string ---@return OxSelectorItem? function OxSelector:getRandomWeighted(setName) local set = (setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET] if not set then return nil end local totalWeight = self.private.totalWeights[setName or DEFAULT_SET] if totalWeight == 0 then return nil end local randomWeight = math.random() * totalWeight local cumulativeWeight = 0 for i = 1, #set do cumulativeWeight = cumulativeWeight + set[i][1] if randomWeight <= cumulativeWeight then local item = set[i][2] return type(item) == "table" and deepClone(item) or item end end return nil end --- get multiple non-weighted random items from a specific set ---@param setName? string ---@param count number ---@return OxSelectorItem[] function OxSelector:getRandomAmount(setName, count) assert(type(count) == "number" and count > 0, "Count must be a positive number") local items = {} for _ = 1, count do local item = self:getRandom(setName) if item then table.insert(items, item) end end return items end --- get multiple weighted random items from a specific set ---@param setName? string ---@param count number ---@return OxSelectorItem[] function OxSelector:getRandomWeightedAmount(setName, count) assert(type(count) == "number" and count > 0, "Count must be a positive number") local items = {} for _ = 1, count do local item = self:getRandomWeighted(setName) if item then table.insert(items, item) end end return items end --- get all items from a specific set ---@param setName? string ---@return OxSelectorItem[] function OxSelector:getSet(setName) return deepClone((setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET]) end --- get all sets ---@return table function OxSelector:getAllSets() return deepClone(self.private.sets) end --- add a new set ---@param setName string ---@param items OxSelectorItem[] function OxSelector:addSet(setName, items) assert(type(setName) == "string", "setName must be a string") if self.private.sets[setName] then lib.print.error("Selector set '" .. setName .. "' already exists.") return end assert(type(items) == "table" and lib.table.type(items) == "array", "items must be an array") assert(#items > 0, "set must contain at least one OxSelectorItem") self.private.totalWeights[setName] = calculateTotalWeight(items) self.private.sets[setName] = items end --- update an existing set ---@param setName string ---@param newItems OxSelectorItem[] function OxSelector:updateSet(setName, newItems) assert(type(setName) == "string", "setName must be a string") if not self.private.sets[setName] then lib.print.error("Selector set '" .. setName .. "' does not exist.") return end assert(type(newItems) == "table" and lib.table.type(newItems) == "array", "newItems must be an array") assert(#newItems > 0, "set must contain at least one OxSelectorItem") self.private.totalWeights[setName] = calculateTotalWeight(newItems) self.private.sets[setName] = newItems end --- remove a set ---@param setName string function OxSelector:removeSet(setName) assert(type(setName) == "string", "setName must be a string") self.private.totalWeights[setName] = nil self.private.sets[setName] = nil end lib.selector = OxSelector return lib.selector