Difference between revisions of "Module:Loot table"
Jump to navigation
Jump to search
Demcookies (talk | contribs) (Fix remove multiple return values from makeElement functions) |
Demcookies (talk | contribs) (Fix incorrect handling of combat locations (quick fix before sleep, could probably use some refactoring)) |
||
(17 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
− | |||
local findId = require("Module:FindId") | local findId = require("Module:FindId") | ||
local lootData = mw.loadData("Module:Loot/data") | local lootData = mw.loadData("Module:Loot/data") | ||
− | local | + | local itemsData = mw.loadData("Module:Items/data") |
− | local | + | local leaguesData = mw.loadData("Module:Leagues/data") |
+ | local locationData = mw.loadData("Module:Location/data") | ||
+ | local locationLootData = mw.loadData("Module:LocationLoot/data") | ||
+ | |||
+ | ---Creates a deep copy of a tables and functions. | ||
+ | ---Does not work with functions with upvalues and maybe userdata. | ||
+ | ---@generic T | ||
+ | ---@param t T The value to copy. | ||
+ | ---@return T # A new value that is a deep copy of the original. | ||
+ | local function copyTable(t) | ||
+ | if type(t) ~= "table" then | ||
+ | return t | ||
+ | elseif type(t) == "function" then | ||
+ | return loadstring(string.dump(t)) | ||
+ | end | ||
+ | local copy = {} | ||
+ | for k, v in pairs(t) do | ||
+ | copy[k] = copyTable(v) | ||
+ | end | ||
+ | return copy | ||
+ | end | ||
+ | |||
+ | ---Formats a number up to a given number of decimal places, removing trailing zeros. | ||
+ | ---@param num number The number to format. | ||
+ | ---@param digits number The number of decimal places to include (must be a non-negative integer). | ||
+ | ---@return string # The formatted number as a string. | ||
+ | local function toFixed(num, digits) | ||
+ | digits = math.max(0, math.floor(digits)) | ||
+ | local formatted = string.format("%." .. digits .. "f", num):gsub("%.?0+$", "") | ||
+ | return formatted | ||
+ | end | ||
+ | |||
+ | ---Creates a mw.html 'DIV' element containing an "error" message. | ||
+ | ---@param message string Message as wikitext. | ||
+ | ---@return table|mw.html # The message wrapped in a mw.html DIV element as red text. | ||
+ | local function createErrorMessage(message) | ||
+ | local e = mw.html.create("div") | ||
+ | :css("color", "red") | ||
+ | :wikitext(message) | ||
+ | return e | ||
+ | end | ||
---Finds the source ID for a given name. | ---Finds the source ID for a given name. | ||
---@param name string The name of the source. | ---@param name string The name of the source. | ||
− | ---@return number|nil The source ID (monsterID or locationID), or nil if the source is not found. | + | ---@return number|nil # The source ID (monsterID or locationID), or nil if the source is not found. |
− | ---@return string| | + | ---@return string|nil # The source type ("monster" or "location"), or nil if the source is not found. |
local function findSourceId(name) | local function findSourceId(name) | ||
− | + | for _, source in ipairs({ "monster", "location" }) do | |
− | + | local id = findId._findId({ name, source }) | |
− | + | if id ~= "id not found" then | |
− | + | return id, source | |
+ | end | ||
end | end | ||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | --- | + | ---Finds the first source in the lootData that matches the given sourceId. |
− | ---@ | + | ---@param id string|number The ID of the source to find. |
− | ---@ | + | ---@param sourceType string String representing the type of the source ("monster" or "location"). |
− | ---@ | + | ---@param locationId string|number|nil Optional ID for the location to find the source from. Use `id` and `sourceType` for location loot. |
− | ---@ | + | ---@return LootDrop[]|nil # The source data, or nil if the source is empty or not found. |
− | ---@ | + | ---@return string|nil # The locationId where the source was found (`id` if `sourceType` is "location"), or nil if the source is not found. |
+ | local function getFirstSourceMatch(id, sourceType, locationId) | ||
+ | id = tostring(id) | ||
+ | locationId = locationId and tostring(locationId) or nil | ||
− | + | if sourceType == "location" then | |
− | + | local location = lootData[id] | |
− | + | if not location then | |
− | + | return | |
− | + | end | |
− | + | local source = location["-1"] | |
− | + | if not source then | |
− | + | return | |
− | + | end | |
− | + | return source, id | |
− | + | end | |
− | + | ||
− | + | --Needed for 'override' monsters that share ID with other monsters | |
+ | -- otherwise the loot would be from first matching ID and could be wrong | ||
+ | if locationId then | ||
+ | local location = lootData[locationId] | ||
+ | for sourceId, source in pairs(location) do | ||
+ | if sourceId == id then | ||
+ | return source, locationId | ||
+ | end | ||
end | end | ||
− | + | return | |
end | end | ||
− | |||
− | |||
− | + | for locationId2, location in pairs(lootData) do | |
− | + | for sourceId, source in pairs(location) do | |
− | + | if sourceId == id then | |
− | + | return source, locationId2 | |
− | + | end | |
− | + | end | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | |||
end | end | ||
--TODO: Add option to show percentages as fractions | --TODO: Add option to show percentages as fractions | ||
− | ---Converts a floating-point number to a fraction. | + | ---Converts a floating-point number to a approximation of fraction. |
---@param num number The floating-point number to convert. | ---@param num number The floating-point number to convert. | ||
---@param maxDenominator number The maximum denominator for the fraction. | ---@param maxDenominator number The maximum denominator for the fraction. | ||
− | ---@return number The numerator of the fraction. | + | ---@return number # The numerator of the fraction. |
− | ---@return number The denominator of the fraction. | + | ---@return number # The denominator of the fraction. |
local function floatToFraction(num, maxDenominator) | local function floatToFraction(num, maxDenominator) | ||
local sign = num < 0 and -1 or 1 | local sign = num < 0 and -1 or 1 | ||
Line 81: | Line 117: | ||
for d = 1, maxDenominator do | for d = 1, maxDenominator do | ||
− | local n = math.floor(num*d + 0.5) | + | local n = math.floor(num * d + 0.5) |
local approx = n / d | local approx = n / d | ||
local difference = math.abs(num - approx) | local difference = math.abs(num - approx) | ||
Line 94: | Line 130: | ||
end | end | ||
− | ---Converts a floating-point number to a fraction string. | + | ---Converts a floating-point number to a approximation of fraction as string. |
---@param num number The floating-point number to convert. | ---@param num number The floating-point number to convert. | ||
---@param maxDenominator number The maximum denominator for the fraction. | ---@param maxDenominator number The maximum denominator for the fraction. | ||
− | ---@return string The fraction string. | + | ---@return string # The fraction as string ('numerator/denominator'). |
local function floatToFractionString(num, maxDenominator) | local function floatToFractionString(num, maxDenominator) | ||
local numerator, denominator = floatToFraction(num, maxDenominator) | local numerator, denominator = floatToFraction(num, maxDenominator) | ||
Line 106: | Line 142: | ||
end | end | ||
− | ---Formats a | + | local function normalizeSortString(str) |
− | ---@param | + | local function normalizeNumber(n) |
− | ---@param | + | local base = 1000000 |
− | ---@return string | + | local num = tonumber(n) or -base |
− | local function | + | return base + num |
− | + | end | |
− | + | ||
− | + | local function normalizeRange(s) | |
− | + | local from, to = string.match(s, "^(.-)–(.-)$") | |
+ | if from and from ~= "" and to and to ~= "" then | ||
+ | return normalizeNumber(from) .. "–" .. normalizeNumber(to) | ||
+ | else | ||
+ | return normalizeNumber(s) | ||
+ | end | ||
+ | end | ||
+ | |||
+ | str = str:gsub(",", "") | ||
+ | return normalizeRange(str) | ||
+ | end | ||
+ | |||
+ | ---Formats the given chance as a string presentation of percentage or 10k/100k fraction. | ||
+ | ---@param chance number The chance to format. 1 equals 100%. | ||
+ | ---@return string # The formatted chance as a string. | ||
+ | local function formatRarity(chance) | ||
+ | local sign = chance < 0 and "-" or "" | ||
+ | local abs = math.abs(chance) | ||
+ | |||
+ | if abs < 0.001 then | ||
+ | return sign .. toFixed(abs * 10000, 1) .. "/10k" | ||
+ | elseif abs < 0.0001 then | ||
+ | return sign .. toFixed(abs * 100000, 1) .. "/100k" | ||
+ | end | ||
+ | |||
+ | return toFixed(chance * 100, 3) .. "%" | ||
+ | end | ||
+ | |||
+ | ---Finds the ID of a monster that is overridden by another monster with the given name. | ||
+ | ---@param name string The name of the monster that overrides another monster. | ||
+ | ---@return string|nil # The ID of the monster that is overridden, or nil if not found. | ||
+ | local function findIdByOverrideName(name) | ||
+ | local monsterStatsData = mw.loadData("Module:Monsters_stats/data") | ||
+ | for id, monster in pairs(monsterStatsData) do | ||
+ | for overrideName, overrideMonster in pairs(monster.overrides or {}) do | ||
+ | if overrideName == name then | ||
+ | return id | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | end | ||
+ | |||
+ | local function createDottedTooltip(name, message) | ||
+ | return string.format('<span class="rt-commentedText tooltip tooltip-dotted" title="%s">%s</span>', message, name) | ||
+ | end | ||
+ | |||
+ | ---@param name string Wikitext. | ||
+ | ---@return table|mw.html # mw.html TH element. | ||
+ | local function createTh(name) | ||
+ | return mw.html.create("th") | ||
+ | :addClass("headerSort") | ||
+ | :attr("tabindex", 0) | ||
+ | :attr("title", "Sort ascending") | ||
+ | :wikitext(name) | ||
+ | end | ||
+ | |||
+ | ---@param names string[] Array of wikitext. | ||
+ | ---@return mw.html # TR element containing names as TH elements. | ||
+ | local function createThs(names) | ||
+ | local tr = mw.html.create("tr") | ||
+ | |||
+ | for _, name in ipairs(names) do | ||
+ | if name ~= "" then | ||
+ | tr:node(createTh(name)) | ||
+ | end | ||
+ | end | ||
+ | |||
+ | return tr | ||
+ | end | ||
+ | |||
+ | ---Creates a mw.html TABLE element with the given columns and caption. | ||
+ | ---@param columns string[] Array of column names. | ||
+ | ---@param caption string|nil Optional caption for the table. | ||
+ | ---@param collapsed boolean|nil If true, the table will be collapsed by default. | ||
+ | ---@return table|mw.html # mw.html TABLE element. | ||
+ | local function createTable(columns, caption, collapsed) | ||
+ | local element = mw.html.create("table") | ||
+ | :addClass("wikitable sortable jquery-tablesorter mw-collapsible" .. (collapsed and " mw-collapsed" or "")) | ||
+ | |||
+ | if caption then | ||
+ | element | ||
+ | :tag("caption") | ||
+ | :css("width", "fit-content") --Might not be needed, depends on the wiki css | ||
+ | :wikitext(caption .. " ") | ||
+ | :done() | ||
+ | end | ||
+ | |||
+ | return element:node(createThs(columns)) | ||
+ | end | ||
+ | |||
+ | ---Creates an Wikitext link with an image. | ||
+ | ---@param title string The title of the link. | ||
+ | ---@param imageAttributes table The html attributes for the image. | ||
+ | ---@return string # Wikitext link with the image. | ||
+ | local function createWikitextImage(title, imageAttributes) | ||
+ | local img = mw.html.create("img") | ||
+ | :attr(imageAttributes) | ||
+ | img = tostring(img):gsub("<img(.-) */>", "<img%1>") | ||
+ | |||
+ | return string.format('[[%s|%s]]', title, img) | ||
+ | end | ||
+ | |||
+ | ---Formats the value of an item as a string. | ||
+ | ---@param item table|Item Item table from Module:Items/data. | ||
+ | ---@param min number|nil The minimum amount of the item. Default 1 if both min & max are missing. | ||
+ | ---@param max number|nil The maximum amount of the item. | ||
+ | ---@return string # The formatted value of the item as a string, or '-' if no value. | ||
+ | local function createValueText(item, min, max) | ||
+ | if not (min or max) then min = 1 end | ||
+ | |||
+ | local lang = mw.language.getContentLanguage() | ||
+ | local value = item.id == 1 and 1 or item.value | ||
+ | |||
+ | if value then | ||
+ | local minValue = min and min * value | ||
+ | local maxValue = max and max * value | ||
+ | |||
+ | if minValue == maxValue then | ||
+ | return minValue and lang:formatNum(minValue) or "NA" | ||
+ | elseif minValue and maxValue then | ||
+ | return lang:formatNum(minValue) .. "–" .. lang:formatNum(maxValue) | ||
+ | elseif minValue or maxValue then | ||
+ | return lang:formatNum(minValue or maxValue) | ||
+ | end | ||
+ | end | ||
+ | |||
+ | return "NA" | ||
+ | end | ||
+ | |||
+ | ---Formats the given min & max values as a string. | ||
+ | ---@param min number|nil The minimum value. | ||
+ | ---@param max number|nil The maximum value. | ||
+ | ---@return string # The formatted range as a string. | ||
+ | local function createRangeText(min, max) | ||
+ | if not (min or max) then min = 1 end | ||
+ | |||
+ | local lang = mw.language.getContentLanguage() | ||
+ | if min == max then | ||
+ | return min and lang:formatNum(min) or "NA" | ||
+ | elseif min and max then | ||
+ | return lang:formatNum(min) .. "–" .. lang:formatNum(max) | ||
+ | end | ||
+ | |||
+ | return lang:formatNum(min or max) | ||
end | end | ||
− | function | + | local function createFrequencyText(min, max) |
− | + | if not min then min = 0 end | |
− | + | ||
+ | if min == max then | ||
+ | return tostring(min) | ||
+ | elseif min and max then | ||
+ | return min .. "–" .. max | ||
+ | end | ||
+ | |||
+ | return tostring(min or max) | ||
end | end | ||
− | function | + | ---Calculates the basic frequencies of an items. |
− | local | + | ---@param items table[] The list of items to calculate frequencies for. |
− | + | ---@param skill string The skill to use for the calculation. | |
− | + | ---@param bonus number|nil The bonus rarity, used to normalize nodes with Bonus Rarity requirements. | |
− | + | ---@return number[] # The list of frequencies for each item. | |
+ | ---@return number # The total frequency of the items. | ||
+ | local function getFrequencies(items, skill, bonus) | ||
+ | bonus = bonus or 0 | ||
+ | local frequencies = {} | ||
+ | local totalFrequency = 0 | ||
+ | |||
+ | for _, item in ipairs(items) do | ||
+ | local frequency | ||
+ | if skill == "fishing" then | ||
+ | frequency = item.frequency or 0 | ||
+ | frequency = frequency + bonus | ||
+ | if frequency > 0 then | ||
+ | --frequency = frequency * (1 + 1/360) -- multiplier for effective level of 1 | ||
+ | if item.maxFrequency and frequency > item.maxFrequency then | ||
+ | local bonusRate = ((frequency - item.maxFrequency) * item.maxFrequency) / 50 | ||
+ | frequency = item.maxFrequency + bonusRate | ||
+ | end | ||
+ | end | ||
+ | else | ||
+ | frequency = math.max(0, math.min(item.frequency or 0, item.maxFrequency or math.huge)) | ||
+ | end | ||
+ | |||
+ | table.insert(frequencies, frequency) | ||
+ | totalFrequency = totalFrequency + math.max(0, frequency or 0) | ||
end | end | ||
− | local | + | return frequencies, totalFrequency |
− | + | end | |
− | + | ||
− | + | ---Creates a mw.html TABLE elements for gathering location drops. | |
− | + | ---@param sourceId string|number The ID of the location. | |
− | + | ---@param imageWidth number|string The width of the item image. | |
− | + | ---@param imageHeight number|string The height of the item image. | |
+ | ---@param collapsed boolean|nil If true, the table will be collapsed by default. | ||
+ | ---@return table|mw.html # An empty mw.html containing the created TABLE elements. | ||
+ | local function createGatheringTable(sourceId, imageWidth, imageHeight, collapsed) | ||
+ | sourceId = tostring(sourceId) | ||
+ | |||
+ | local location = locationData[sourceId] | ||
+ | local locationLoot = copyTable(locationLootData[sourceId]) | ||
+ | local name = locationData[sourceId].name | ||
+ | if not (location and locationLoot) then | ||
+ | return createErrorMessage( | ||
+ | "No loot found for location: '" .. name .. "'. The Module:LocationLoot/data may be outdated." | ||
+ | ) | ||
end | end | ||
− | --- | + | --location's node list from Module:LocationLoot/data |
− | ---@ | + | --nodes are in 'nodes' if location has nodes (foraging, fishing) or 'loot' if not (mining) |
− | local | + | ---@type table<string, LocationLoot[]> |
− | + | local lootNodes = locationLoot.nodes or (locationLoot.loot and {[location.name] = locationLoot.loot} or nil) | |
− | + | if not lootNodes then | |
− | + | return createErrorMessage( | |
− | + | "No loot found for location: '" .. name .. "'. The Module:LocationLoot/data may be outdated." | |
− | + | ) | |
− | |||
− | |||
end | end | ||
− | + | local function keyPos(tbl, key, val) | |
− | + | for i, v in ipairs(tbl) do | |
− | local function | + | if v[key] == val then return i end |
− | |||
− | for | ||
− | |||
end | end | ||
− | return | + | return 0 |
+ | end | ||
+ | local nodes = location.nodes -- location's node list from Module:Location/data | ||
+ | ---@type {name: string, nodes: LocationLoot[]}[] | ||
+ | local lootNodeArray = {} | ||
+ | for k,v in pairs(lootNodes) do | ||
+ | table.insert(lootNodeArray, {name = k, nodes = v}) | ||
end | end | ||
+ | table.sort(lootNodeArray, function(a, b) | ||
+ | return keyPos(nodes, "nodeID", a.name) < keyPos(nodes, "nodeID", b.name) | ||
+ | end) | ||
+ | |||
+ | local action = (location.actionType or ""):lower():gsub("action%-", "") | ||
+ | local container = mw.html.create() | ||
+ | local rarityLabel = action == "fishing" | ||
+ | and createDottedTooltip( | ||
+ | "Frequency", | ||
+ | "Min–max frequency of the item. Negative minimum frequency is the item's inversed Rarity." | ||
+ | .. " Drop chance is calculated from item's frequency and total frequency of the node's items." | ||
+ | .. " Frequncies depends on effective level, Bonus Rarity and Deadliest Catch & Fiber Finder enchantments." | ||
+ | ) or "Rarity" | ||
+ | local reqRarityLabel = action == "fishing" | ||
+ | and createDottedTooltip( | ||
+ | "Req. Rarity", | ||
+ | "To gather an item, Fishing Bonus Rarity must be greater than the required Rarity." | ||
+ | .. " For example, a Bonus Rarity of exactly 5 cannot gather an item with Rarity 5," | ||
+ | .. " but a Bonus Rarity of 5.1 can." | ||
+ | ) | ||
+ | or "" | ||
+ | local columns = { | ||
+ | " ", | ||
+ | "Item", | ||
+ | "Quantity", | ||
+ | rarityLabel, | ||
+ | reqRarityLabel, | ||
+ | "Vendor value" | ||
+ | } | ||
− | + | for i, node in ipairs(lootNodeArray) do | |
− | + | local tableHtml = createTable(columns, node.name, collapsed) | |
− | + | local frequencies, totalFrequency = getFrequencies(node.nodes, action) | |
− | + | ||
− | + | for ii, item in ipairs(node.nodes) do | |
− | + | local item_ = itemsData[tostring(item.id)] | |
− | + | local src = item_.itemImage | |
− | + | src = src:sub(1, 1) ~= "/" and "/" .. src or src | |
− | + | ||
− | : | + | local frequencyPercent = math.max(0, frequencies[ii] / totalFrequency) |
− | : | + | local minBonusRarity = (item.frequency or 0) <= 0 and (math.abs(item.frequency) or 0) or 0 |
− | + | local valueText = createValueText(item_, item.minAmount, item.maxAmount) | |
− | + | local rarityText = action == "fishing" | |
− | + | and createFrequencyText(item.frequency, item.maxFrequency) | |
− | + | or formatRarity(frequencyPercent) | |
− | + | local rarityValue = action == "fishing" | |
− | + | and createFrequencyText(item.frequency, item.maxFrequency) | |
− | : | + | or string.format("%f", frequencyPercent) |
− | + | ||
+ | local imageHtml = mw.html.create("td") | ||
+ | :wikitext(createWikitextImage( | ||
+ | item_.name, | ||
+ | { | ||
+ | src = "https://www.play.idlescape.com" .. src, | ||
+ | alt = item_.name .. (item_.extraTooltip and ". " .. item_.extraTooltip or ""), | ||
+ | width = imageWidth, | ||
+ | height = imageHeight | ||
+ | } | ||
+ | )) | ||
+ | local itemHtml = mw.html.create("td") | ||
+ | :wikitext('[[' .. item_.name .. ']]') | ||
+ | local quantityHtml = mw.html.create("td") | ||
+ | :css("text-align", "center") | ||
+ | :wikitext(createRangeText(item.minAmount, item.maxAmount)) | ||
+ | local rarityHtml = mw.html.create("td") | ||
+ | :css("text-align", "center") | ||
+ | :wikitext(rarityText) | ||
+ | :attr("data-sort-value", normalizeSortString(rarityValue)) | ||
+ | local reqBonusRarityHtml = action == "fishing" | ||
+ | and mw.html.create("td") | ||
+ | :css("text-align", "center") | ||
+ | :wikitext(tostring(minBonusRarity)) | ||
+ | or nil | ||
+ | local valueHtml = mw.html.create("td") | ||
+ | :css("text-align", "right") | ||
+ | :attr("data-sort-value", normalizeSortString(valueText)) | ||
+ | :wikitext(valueText) | ||
+ | |||
+ | local row = mw.html.create("tr") | ||
+ | :node(imageHtml) | ||
+ | :node(itemHtml) | ||
+ | :node(quantityHtml) | ||
+ | :node(rarityHtml) | ||
+ | :node(reqBonusRarityHtml) | ||
+ | :node(valueHtml) | ||
+ | |||
+ | tableHtml:node(row) | ||
+ | end | ||
+ | container:node(tableHtml) | ||
end | end | ||
− | + | return container | |
− | + | end | |
− | |||
− | + | ---Creates TABLE elements for monster and combat location drops. | |
− | + | ---@param sourceId string|number The ID of the source (monster or location). | |
− | + | ---@param sourceName string The name of the source. | |
− | + | ---@param sourceType string The type of the source ("monster" or "location"). | |
− | + | ---@param overrideLocId string|number|nil The ID of the location to override the sourceId. | |
− | " | + | ---@param imageWidth string|number The width of the item image. |
− | " | + | ---@param imageHeight string|number The height of the item image. |
− | " | + | ---@param collapsed boolean|nil If true, the table will be collapsed by default. |
+ | ---@return table|mw.html # An empty mw.html containing the created TABLE elements. | ||
+ | local function createMonsterTable(sourceId, sourceName, sourceType, overrideLocId, imageWidth, imageHeight, collapsed) | ||
+ | local loot = getFirstSourceMatch(sourceId, sourceType, overrideLocId) | ||
+ | if not loot then | ||
+ | return createErrorMessage( | ||
+ | "No loot found for monster or location named '" .. sourceName .. | ||
+ | "'. The Module:Loot/data may be outdated. <br>Please add loot drops to Module:Loot/data " .. | ||
+ | "if possible or try manually making a table.</div>" | ||
) | ) | ||
+ | end | ||
+ | loot = copyTable(loot) --loot data is read-only from Module so we need to copy it | ||
+ | table.sort(loot, function(a, b) | ||
+ | if a.allowedLeagues and not b.allowedLeagues then | ||
+ | return false | ||
+ | elseif not a.allowedLeagues and b.allowedLeagues then | ||
+ | return true | ||
+ | else | ||
+ | return a.chance > b.chance | ||
+ | end | ||
+ | end) | ||
+ | |||
+ | --TODO: Add option to show percentages as fractions (open options from the gear icon) | ||
+ | -- Saving preferences would require special permissions? MAybe with mw.user or javascript to localStorage? | ||
+ | |||
+ | local tables = { | ||
+ | ["Normal drops"] = {}, | ||
+ | ["Junk drops"] = {}, | ||
+ | ["League specific drops"] = {} | ||
+ | } | ||
+ | |||
+ | for _, drop in ipairs(loot) do | ||
+ | local item = itemsData[tostring(drop.id)] | ||
+ | --IS itemList.ts has some incorrect URIs, e.g. for Feather | ||
+ | local src = item.itemImage | ||
+ | src = src:sub(1, 1) ~= "/" and "/" .. src or src | ||
+ | local valueText = createValueText(item, drop.minAmount, drop.maxAmount) | ||
+ | |||
+ | local imageHtml = mw.html.create("td") | ||
+ | :wikitext(createWikitextImage( | ||
+ | item.name, | ||
+ | { | ||
+ | src = "https://www.play.idlescape.com" .. src, | ||
+ | alt = item.name .. (item.extraTooltip and ". " .. item.extraTooltip or ""), | ||
+ | width = imageWidth, | ||
+ | height = imageHeight | ||
+ | } | ||
+ | )) | ||
local itemHtml = mw.html.create("td") | local itemHtml = mw.html.create("td") | ||
− | :wikitext( | + | :wikitext('[[' .. item.name .. ']]') |
local quantityHtml = mw.html.create("td") | local quantityHtml = mw.html.create("td") | ||
− | : | + | :css("text-align", "center") |
+ | :wikitext(createRangeText(drop.minAmount, drop.maxAmount)) | ||
local chanceHtml = mw.html.create("td") | local chanceHtml = mw.html.create("td") | ||
− | : | + | :css("text-align", "center") |
− | + | :attr("data-sort-value", string.format("%f", drop.chance)) | |
− | :wikitext( | + | :wikitext(formatRarity(drop.chance)) |
− | local | + | local valueHtml = mw.html.create("td") |
− | : | + | :css("text-align", "right") |
− | + | :attr("data-sort-value", normalizeSortString(valueText)) | |
− | --TODO: Add indicator if league is | + | :wikitext(valueText) |
− | if | + | local leagueHtml |
− | for _, leagueId in ipairs( | + | --TODO: Add indicator if league is inactive, the field is present in |
− | local league = | + | -- leagueList.ts / Module:Leagues/data but some are incorrect |
+ | if drop.allowedLeagues then | ||
+ | leagueHtml = mw.html.create("td") | ||
+ | :css("text-align", "center") | ||
+ | for _, leagueId in ipairs(drop.allowedLeagues) do | ||
+ | local league = leaguesData[tostring(leagueId)] | ||
if league then | if league then | ||
− | + | leagueHtml:wikitext(createWikitextImage( | |
league.name, | league.name, | ||
− | + | { | |
− | + | src = "https://www.play.idlescape.com" .. league.icon, | |
− | + | alt = league.name, | |
− | + | width = imageWidth, | |
− | ) | + | height = imageHeight |
− | + | } | |
+ | )) | ||
else | else | ||
− | + | leagueHtml:wikitext("?") | |
end | end | ||
end | end | ||
− | |||
− | |||
end | end | ||
− | local | + | |
+ | local row = mw.html.create("tr") | ||
:node(imageHtml) | :node(imageHtml) | ||
:node(itemHtml) | :node(itemHtml) | ||
:node(quantityHtml) | :node(quantityHtml) | ||
:node(chanceHtml) | :node(chanceHtml) | ||
− | :node( | + | :node(valueHtml) |
− | :node( | + | |
− | + | if leagueHtml then | |
+ | row:node(leagueHtml) | ||
+ | end | ||
+ | |||
+ | local caption = "Normal drops" | ||
+ | if drop.allowedLeagues then | ||
+ | caption = "League specific drops" | ||
+ | elseif item.class == "junk" then | ||
+ | caption = "Junk drops" | ||
+ | end | ||
+ | |||
+ | table.insert(tables[caption], row) | ||
+ | end | ||
+ | |||
+ | local tableHtml = mw.html.create() | ||
− | tableHtml:node( | + | for caption, rowList in pairs(tables) do |
+ | if next(rowList) ~= nil then | ||
+ | local isLeagues = caption == "League specific drops" | ||
+ | local columns = { | ||
+ | " ", | ||
+ | "Item", | ||
+ | "Quantity", | ||
+ | "Rarity", | ||
+ | "Vendor value", | ||
+ | isLeagues and "Leagues" or nil | ||
+ | } | ||
+ | local tableHtml2 = createTable(columns, caption, collapsed) | ||
+ | for _, row in ipairs(rowList) do | ||
+ | tableHtml2:node(row) | ||
+ | end | ||
+ | tableHtml:node(tableHtml2) | ||
+ | end | ||
end | end | ||
return tableHtml | return tableHtml | ||
+ | end | ||
+ | |||
+ | |||
+ | function p.lootTable(frame) | ||
+ | local args = frame:getParent().args | ||
+ | return tostring(p._lootTable(args)) | ||
+ | end | ||
+ | |||
+ | function p._lootTable(args) | ||
+ | local sourceName = args.name or args.title or mw.title.getCurrentTitle().text | ||
+ | local collapsed = args.collapsed or false | ||
+ | local imageWidth = args.width == nil and 30 or args.width | ||
+ | local imageHeight = args.height == nil and imageWidth or args.height | ||
+ | --Some monsters use same id as other monsters so we need to specify location for them | ||
+ | local overrideLocId = args["zone"] and findSourceId(args["zone"]) | ||
+ | |||
+ | local sourceId | ||
+ | local sourceType | ||
+ | if overrideLocId then | ||
+ | sourceId = findIdByOverrideName(sourceName) | ||
+ | sourceType = "monster" | ||
+ | else | ||
+ | sourceId, sourceType = findSourceId(sourceName) | ||
+ | |||
+ | end | ||
+ | if not (sourceId and sourceType) then | ||
+ | return createErrorMessage( | ||
+ | "Found no monsters overridden by '" .. sourceName .. | ||
+ | "'. The Modules: Monsters_stats/data or Loot/data may be outdated." | ||
+ | ) | ||
+ | end | ||
+ | |||
+ | local location = sourceType == "location" and locationData[tostring(sourceId)] or nil | ||
+ | local isCombatLocation = location and location.actionType == "Action-Combat" | ||
+ | |||
+ | if sourceType == "monster" or isCombatLocation then | ||
+ | return createMonsterTable(sourceId, sourceName, sourceType, overrideLocId, imageWidth, imageHeight, collapsed) | ||
+ | elseif sourceType == "location" then | ||
+ | return createGatheringTable(sourceId, imageWidth, imageHeight, collapsed) | ||
+ | end | ||
end | end | ||
return p | return p |
Latest revision as of 00:49, 22 May 2025
local p = {} local findId = require("Module:FindId") local lootData = mw.loadData("Module:Loot/data") local itemsData = mw.loadData("Module:Items/data") local leaguesData = mw.loadData("Module:Leagues/data") local locationData = mw.loadData("Module:Location/data") local locationLootData = mw.loadData("Module:LocationLoot/data") ---Creates a deep copy of a tables and functions. ---Does not work with functions with upvalues and maybe userdata. ---@generic T ---@param t T The value to copy. ---@return T # A new value that is a deep copy of the original. local function copyTable(t) if type(t) ~= "table" then return t elseif type(t) == "function" then return loadstring(string.dump(t)) end local copy = {} for k, v in pairs(t) do copy[k] = copyTable(v) end return copy end ---Formats a number up to a given number of decimal places, removing trailing zeros. ---@param num number The number to format. ---@param digits number The number of decimal places to include (must be a non-negative integer). ---@return string # The formatted number as a string. local function toFixed(num, digits) digits = math.max(0, math.floor(digits)) local formatted = string.format("%." .. digits .. "f", num):gsub("%.?0+$", "") return formatted end ---Creates a mw.html 'DIV' element containing an "error" message. ---@param message string Message as wikitext. ---@return table|mw.html # The message wrapped in a mw.html DIV element as red text. local function createErrorMessage(message) local e = mw.html.create("div") :css("color", "red") :wikitext(message) return e end ---Finds the source ID for a given name. ---@param name string The name of the source. ---@return number|nil # The source ID (monsterID or locationID), or nil if the source is not found. ---@return string|nil # The source type ("monster" or "location"), or nil if the source is not found. local function findSourceId(name) for _, source in ipairs({ "monster", "location" }) do local id = findId._findId({ name, source }) if id ~= "id not found" then return id, source end end end ---Finds the first source in the lootData that matches the given sourceId. ---@param id string|number The ID of the source to find. ---@param sourceType string String representing the type of the source ("monster" or "location"). ---@param locationId string|number|nil Optional ID for the location to find the source from. Use `id` and `sourceType` for location loot. ---@return LootDrop[]|nil # The source data, or nil if the source is empty or not found. ---@return string|nil # The locationId where the source was found (`id` if `sourceType` is "location"), or nil if the source is not found. local function getFirstSourceMatch(id, sourceType, locationId) id = tostring(id) locationId = locationId and tostring(locationId) or nil if sourceType == "location" then local location = lootData[id] if not location then return end local source = location["-1"] if not source then return end return source, id end --Needed for 'override' monsters that share ID with other monsters -- otherwise the loot would be from first matching ID and could be wrong if locationId then local location = lootData[locationId] for sourceId, source in pairs(location) do if sourceId == id then return source, locationId end end return end for locationId2, location in pairs(lootData) do for sourceId, source in pairs(location) do if sourceId == id then return source, locationId2 end end end end --TODO: Add option to show percentages as fractions ---Converts a floating-point number to a approximation of fraction. ---@param num number The floating-point number to convert. ---@param maxDenominator number The maximum denominator for the fraction. ---@return number # The numerator of the fraction. ---@return number # The denominator of the fraction. local function floatToFraction(num, maxDenominator) local sign = num < 0 and -1 or 1 num = math.abs(num) local bestNumerator, bestDenominator = 1, 1 local minDifference = math.huge for d = 1, maxDenominator do local n = math.floor(num * d + 0.5) local approx = n / d local difference = math.abs(num - approx) if difference < minDifference then bestNumerator, bestDenominator = n, d minDifference = difference end end return sign * bestNumerator, bestDenominator end ---Converts a floating-point number to a approximation of fraction as string. ---@param num number The floating-point number to convert. ---@param maxDenominator number The maximum denominator for the fraction. ---@return string # The fraction as string ('numerator/denominator'). local function floatToFractionString(num, maxDenominator) local numerator, denominator = floatToFraction(num, maxDenominator) if denominator == 1 then return tostring(numerator) end return string.format("%d/%d", numerator, denominator) end local function normalizeSortString(str) local function normalizeNumber(n) local base = 1000000 local num = tonumber(n) or -base return base + num end local function normalizeRange(s) local from, to = string.match(s, "^(.-)–(.-)$") if from and from ~= "" and to and to ~= "" then return normalizeNumber(from) .. "–" .. normalizeNumber(to) else return normalizeNumber(s) end end str = str:gsub(",", "") return normalizeRange(str) end ---Formats the given chance as a string presentation of percentage or 10k/100k fraction. ---@param chance number The chance to format. 1 equals 100%. ---@return string # The formatted chance as a string. local function formatRarity(chance) local sign = chance < 0 and "-" or "" local abs = math.abs(chance) if abs < 0.001 then return sign .. toFixed(abs * 10000, 1) .. "/10k" elseif abs < 0.0001 then return sign .. toFixed(abs * 100000, 1) .. "/100k" end return toFixed(chance * 100, 3) .. "%" end ---Finds the ID of a monster that is overridden by another monster with the given name. ---@param name string The name of the monster that overrides another monster. ---@return string|nil # The ID of the monster that is overridden, or nil if not found. local function findIdByOverrideName(name) local monsterStatsData = mw.loadData("Module:Monsters_stats/data") for id, monster in pairs(monsterStatsData) do for overrideName, overrideMonster in pairs(monster.overrides or {}) do if overrideName == name then return id end end end end local function createDottedTooltip(name, message) return string.format('<span class="rt-commentedText tooltip tooltip-dotted" title="%s">%s</span>', message, name) end ---@param name string Wikitext. ---@return table|mw.html # mw.html TH element. local function createTh(name) return mw.html.create("th") :addClass("headerSort") :attr("tabindex", 0) :attr("title", "Sort ascending") :wikitext(name) end ---@param names string[] Array of wikitext. ---@return mw.html # TR element containing names as TH elements. local function createThs(names) local tr = mw.html.create("tr") for _, name in ipairs(names) do if name ~= "" then tr:node(createTh(name)) end end return tr end ---Creates a mw.html TABLE element with the given columns and caption. ---@param columns string[] Array of column names. ---@param caption string|nil Optional caption for the table. ---@param collapsed boolean|nil If true, the table will be collapsed by default. ---@return table|mw.html # mw.html TABLE element. local function createTable(columns, caption, collapsed) local element = mw.html.create("table") :addClass("wikitable sortable jquery-tablesorter mw-collapsible" .. (collapsed and " mw-collapsed" or "")) if caption then element :tag("caption") :css("width", "fit-content") --Might not be needed, depends on the wiki css :wikitext(caption .. " ") :done() end return element:node(createThs(columns)) end ---Creates an Wikitext link with an image. ---@param title string The title of the link. ---@param imageAttributes table The html attributes for the image. ---@return string # Wikitext link with the image. local function createWikitextImage(title, imageAttributes) local img = mw.html.create("img") :attr(imageAttributes) img = tostring(img):gsub("<img(.-) */>", "<img%1>") return string.format('[[%s|%s]]', title, img) end ---Formats the value of an item as a string. ---@param item table|Item Item table from Module:Items/data. ---@param min number|nil The minimum amount of the item. Default 1 if both min & max are missing. ---@param max number|nil The maximum amount of the item. ---@return string # The formatted value of the item as a string, or '-' if no value. local function createValueText(item, min, max) if not (min or max) then min = 1 end local lang = mw.language.getContentLanguage() local value = item.id == 1 and 1 or item.value if value then local minValue = min and min * value local maxValue = max and max * value if minValue == maxValue then return minValue and lang:formatNum(minValue) or "NA" elseif minValue and maxValue then return lang:formatNum(minValue) .. "–" .. lang:formatNum(maxValue) elseif minValue or maxValue then return lang:formatNum(minValue or maxValue) end end return "NA" end ---Formats the given min & max values as a string. ---@param min number|nil The minimum value. ---@param max number|nil The maximum value. ---@return string # The formatted range as a string. local function createRangeText(min, max) if not (min or max) then min = 1 end local lang = mw.language.getContentLanguage() if min == max then return min and lang:formatNum(min) or "NA" elseif min and max then return lang:formatNum(min) .. "–" .. lang:formatNum(max) end return lang:formatNum(min or max) end local function createFrequencyText(min, max) if not min then min = 0 end if min == max then return tostring(min) elseif min and max then return min .. "–" .. max end return tostring(min or max) end ---Calculates the basic frequencies of an items. ---@param items table[] The list of items to calculate frequencies for. ---@param skill string The skill to use for the calculation. ---@param bonus number|nil The bonus rarity, used to normalize nodes with Bonus Rarity requirements. ---@return number[] # The list of frequencies for each item. ---@return number # The total frequency of the items. local function getFrequencies(items, skill, bonus) bonus = bonus or 0 local frequencies = {} local totalFrequency = 0 for _, item in ipairs(items) do local frequency if skill == "fishing" then frequency = item.frequency or 0 frequency = frequency + bonus if frequency > 0 then --frequency = frequency * (1 + 1/360) -- multiplier for effective level of 1 if item.maxFrequency and frequency > item.maxFrequency then local bonusRate = ((frequency - item.maxFrequency) * item.maxFrequency) / 50 frequency = item.maxFrequency + bonusRate end end else frequency = math.max(0, math.min(item.frequency or 0, item.maxFrequency or math.huge)) end table.insert(frequencies, frequency) totalFrequency = totalFrequency + math.max(0, frequency or 0) end return frequencies, totalFrequency end ---Creates a mw.html TABLE elements for gathering location drops. ---@param sourceId string|number The ID of the location. ---@param imageWidth number|string The width of the item image. ---@param imageHeight number|string The height of the item image. ---@param collapsed boolean|nil If true, the table will be collapsed by default. ---@return table|mw.html # An empty mw.html containing the created TABLE elements. local function createGatheringTable(sourceId, imageWidth, imageHeight, collapsed) sourceId = tostring(sourceId) local location = locationData[sourceId] local locationLoot = copyTable(locationLootData[sourceId]) local name = locationData[sourceId].name if not (location and locationLoot) then return createErrorMessage( "No loot found for location: '" .. name .. "'. The Module:LocationLoot/data may be outdated." ) end --location's node list from Module:LocationLoot/data --nodes are in 'nodes' if location has nodes (foraging, fishing) or 'loot' if not (mining) ---@type table<string, LocationLoot[]> local lootNodes = locationLoot.nodes or (locationLoot.loot and {[location.name] = locationLoot.loot} or nil) if not lootNodes then return createErrorMessage( "No loot found for location: '" .. name .. "'. The Module:LocationLoot/data may be outdated." ) end local function keyPos(tbl, key, val) for i, v in ipairs(tbl) do if v[key] == val then return i end end return 0 end local nodes = location.nodes -- location's node list from Module:Location/data ---@type {name: string, nodes: LocationLoot[]}[] local lootNodeArray = {} for k,v in pairs(lootNodes) do table.insert(lootNodeArray, {name = k, nodes = v}) end table.sort(lootNodeArray, function(a, b) return keyPos(nodes, "nodeID", a.name) < keyPos(nodes, "nodeID", b.name) end) local action = (location.actionType or ""):lower():gsub("action%-", "") local container = mw.html.create() local rarityLabel = action == "fishing" and createDottedTooltip( "Frequency", "Min–max frequency of the item. Negative minimum frequency is the item's inversed Rarity." .. " Drop chance is calculated from item's frequency and total frequency of the node's items." .. " Frequncies depends on effective level, Bonus Rarity and Deadliest Catch & Fiber Finder enchantments." ) or "Rarity" local reqRarityLabel = action == "fishing" and createDottedTooltip( "Req. Rarity", "To gather an item, Fishing Bonus Rarity must be greater than the required Rarity." .. " For example, a Bonus Rarity of exactly 5 cannot gather an item with Rarity 5," .. " but a Bonus Rarity of 5.1 can." ) or "" local columns = { " ", "Item", "Quantity", rarityLabel, reqRarityLabel, "Vendor value" } for i, node in ipairs(lootNodeArray) do local tableHtml = createTable(columns, node.name, collapsed) local frequencies, totalFrequency = getFrequencies(node.nodes, action) for ii, item in ipairs(node.nodes) do local item_ = itemsData[tostring(item.id)] local src = item_.itemImage src = src:sub(1, 1) ~= "/" and "/" .. src or src local frequencyPercent = math.max(0, frequencies[ii] / totalFrequency) local minBonusRarity = (item.frequency or 0) <= 0 and (math.abs(item.frequency) or 0) or 0 local valueText = createValueText(item_, item.minAmount, item.maxAmount) local rarityText = action == "fishing" and createFrequencyText(item.frequency, item.maxFrequency) or formatRarity(frequencyPercent) local rarityValue = action == "fishing" and createFrequencyText(item.frequency, item.maxFrequency) or string.format("%f", frequencyPercent) local imageHtml = mw.html.create("td") :wikitext(createWikitextImage( item_.name, { src = "https://www.play.idlescape.com" .. src, alt = item_.name .. (item_.extraTooltip and ". " .. item_.extraTooltip or ""), width = imageWidth, height = imageHeight } )) local itemHtml = mw.html.create("td") :wikitext('[[' .. item_.name .. ']]') local quantityHtml = mw.html.create("td") :css("text-align", "center") :wikitext(createRangeText(item.minAmount, item.maxAmount)) local rarityHtml = mw.html.create("td") :css("text-align", "center") :wikitext(rarityText) :attr("data-sort-value", normalizeSortString(rarityValue)) local reqBonusRarityHtml = action == "fishing" and mw.html.create("td") :css("text-align", "center") :wikitext(tostring(minBonusRarity)) or nil local valueHtml = mw.html.create("td") :css("text-align", "right") :attr("data-sort-value", normalizeSortString(valueText)) :wikitext(valueText) local row = mw.html.create("tr") :node(imageHtml) :node(itemHtml) :node(quantityHtml) :node(rarityHtml) :node(reqBonusRarityHtml) :node(valueHtml) tableHtml:node(row) end container:node(tableHtml) end return container end ---Creates TABLE elements for monster and combat location drops. ---@param sourceId string|number The ID of the source (monster or location). ---@param sourceName string The name of the source. ---@param sourceType string The type of the source ("monster" or "location"). ---@param overrideLocId string|number|nil The ID of the location to override the sourceId. ---@param imageWidth string|number The width of the item image. ---@param imageHeight string|number The height of the item image. ---@param collapsed boolean|nil If true, the table will be collapsed by default. ---@return table|mw.html # An empty mw.html containing the created TABLE elements. local function createMonsterTable(sourceId, sourceName, sourceType, overrideLocId, imageWidth, imageHeight, collapsed) local loot = getFirstSourceMatch(sourceId, sourceType, overrideLocId) if not loot then return createErrorMessage( "No loot found for monster or location named '" .. sourceName .. "'. The Module:Loot/data may be outdated. <br>Please add loot drops to Module:Loot/data " .. "if possible or try manually making a table.</div>" ) end loot = copyTable(loot) --loot data is read-only from Module so we need to copy it table.sort(loot, function(a, b) if a.allowedLeagues and not b.allowedLeagues then return false elseif not a.allowedLeagues and b.allowedLeagues then return true else return a.chance > b.chance end end) --TODO: Add option to show percentages as fractions (open options from the gear icon) -- Saving preferences would require special permissions? MAybe with mw.user or javascript to localStorage? local tables = { ["Normal drops"] = {}, ["Junk drops"] = {}, ["League specific drops"] = {} } for _, drop in ipairs(loot) do local item = itemsData[tostring(drop.id)] --IS itemList.ts has some incorrect URIs, e.g. for Feather local src = item.itemImage src = src:sub(1, 1) ~= "/" and "/" .. src or src local valueText = createValueText(item, drop.minAmount, drop.maxAmount) local imageHtml = mw.html.create("td") :wikitext(createWikitextImage( item.name, { src = "https://www.play.idlescape.com" .. src, alt = item.name .. (item.extraTooltip and ". " .. item.extraTooltip or ""), width = imageWidth, height = imageHeight } )) local itemHtml = mw.html.create("td") :wikitext('[[' .. item.name .. ']]') local quantityHtml = mw.html.create("td") :css("text-align", "center") :wikitext(createRangeText(drop.minAmount, drop.maxAmount)) local chanceHtml = mw.html.create("td") :css("text-align", "center") :attr("data-sort-value", string.format("%f", drop.chance)) :wikitext(formatRarity(drop.chance)) local valueHtml = mw.html.create("td") :css("text-align", "right") :attr("data-sort-value", normalizeSortString(valueText)) :wikitext(valueText) local leagueHtml --TODO: Add indicator if league is inactive, the field is present in -- leagueList.ts / Module:Leagues/data but some are incorrect if drop.allowedLeagues then leagueHtml = mw.html.create("td") :css("text-align", "center") for _, leagueId in ipairs(drop.allowedLeagues) do local league = leaguesData[tostring(leagueId)] if league then leagueHtml:wikitext(createWikitextImage( league.name, { src = "https://www.play.idlescape.com" .. league.icon, alt = league.name, width = imageWidth, height = imageHeight } )) else leagueHtml:wikitext("?") end end end local row = mw.html.create("tr") :node(imageHtml) :node(itemHtml) :node(quantityHtml) :node(chanceHtml) :node(valueHtml) if leagueHtml then row:node(leagueHtml) end local caption = "Normal drops" if drop.allowedLeagues then caption = "League specific drops" elseif item.class == "junk" then caption = "Junk drops" end table.insert(tables[caption], row) end local tableHtml = mw.html.create() for caption, rowList in pairs(tables) do if next(rowList) ~= nil then local isLeagues = caption == "League specific drops" local columns = { " ", "Item", "Quantity", "Rarity", "Vendor value", isLeagues and "Leagues" or nil } local tableHtml2 = createTable(columns, caption, collapsed) for _, row in ipairs(rowList) do tableHtml2:node(row) end tableHtml:node(tableHtml2) end end return tableHtml end function p.lootTable(frame) local args = frame:getParent().args return tostring(p._lootTable(args)) end function p._lootTable(args) local sourceName = args.name or args.title or mw.title.getCurrentTitle().text local collapsed = args.collapsed or false local imageWidth = args.width == nil and 30 or args.width local imageHeight = args.height == nil and imageWidth or args.height --Some monsters use same id as other monsters so we need to specify location for them local overrideLocId = args["zone"] and findSourceId(args["zone"]) local sourceId local sourceType if overrideLocId then sourceId = findIdByOverrideName(sourceName) sourceType = "monster" else sourceId, sourceType = findSourceId(sourceName) end if not (sourceId and sourceType) then return createErrorMessage( "Found no monsters overridden by '" .. sourceName .. "'. The Modules: Monsters_stats/data or Loot/data may be outdated." ) end local location = sourceType == "location" and locationData[tostring(sourceId)] or nil local isCombatLocation = location and location.actionType == "Action-Combat" if sourceType == "monster" or isCombatLocation then return createMonsterTable(sourceId, sourceName, sourceType, overrideLocId, imageWidth, imageHeight, collapsed) elseif sourceType == "location" then return createGatheringTable(sourceId, imageWidth, imageHeight, collapsed) end end return p