Difference between revisions of "Module:Loot table"
Jump to navigation
Jump to search
Demcookies (talk | contribs) (Fix type heigth to height) |
Demcookies (talk | contribs) (Update split main, junk and league drops to seperate tables. Add captions to the tables. Change Vendor value from single item value to quantity * value range.) |
||
(3 intermediate revisions by the same user not shown) | |||
Line 2: | Line 2: | ||
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") |
+ | |||
+ | ---Creates a deep copy of a table. | ||
+ | ---@param t table | ||
+ | ---@return table | ||
+ | local function copyTable(t) | ||
+ | local copy = {} | ||
+ | for k, v in pairs(t) do | ||
+ | if type(v) == "table" then | ||
+ | copy[k] = copyTable(v) | ||
+ | else | ||
+ | copy[k] = v | ||
+ | end | ||
+ | end | ||
+ | return copy | ||
+ | end | ||
---Creates a mw.html 'DIV' element containing an "error" message. | ---Creates a mw.html 'DIV' element containing an "error" message. | ||
---@param message string Message as wikitext. | ---@param message string Message as wikitext. | ||
− | ---@return mw.html The message wrapped in a mw.html DIV element as red text. | + | ---@return mw.html # The message wrapped in a mw.html DIV element as red text. |
local function createErrorMessage(message) | local function createErrorMessage(message) | ||
local e = mw.html.create("div") | local e = mw.html.create("div") | ||
Line 18: | Line 33: | ||
---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|nil The source type ("monster" or "location"), 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) | local function findSourceId(name) | ||
for _, source in ipairs({ "monster", "location" }) do | for _, source in ipairs({ "monster", "location" }) do | ||
Line 29: | Line 44: | ||
end | end | ||
− | + | ---Finds the first item in the drop table that matches the given item ID. | |
− | + | ---@param drop table The drop table from Module:Loot/data. | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | ---Finds the first item in the | ||
− | ---@param | ||
---@param id string|number The ID of the item to find. | ---@param id string|number The ID of the item to find. | ||
---@return Item|nil The item data, or nil if the item is not found. | ---@return Item|nil The item data, or nil if the item is not found. | ||
− | ---@return string|nil The monsterId where the item was found ("-1" for locations), or nil if the item is not found. | + | ---@return string|nil # The monsterId where the item was found ("-1" for locations), or nil if the item is not found. |
− | ---@return string|nil The locationId where the item was found, or nil if the item is not found. | + | ---@return string|nil # The locationId where the item was found, or nil if the item is not found. |
− | local function getFirstLootMatch( | + | local function getFirstLootMatch(drop, id) |
id = tostring(id) | id = tostring(id) | ||
− | for locationId, location in pairs( | + | for locationId, location in pairs(drop) do |
for sourceId, source in pairs(location) do | for sourceId, source in pairs(location) do | ||
for _, item in ipairs(source) do | for _, item in ipairs(source) do | ||
Line 55: | Line 63: | ||
end | end | ||
− | ---Finds the first source in the | + | ---Finds the first source in the drop table that matches the given sourceId. |
− | ---@param | + | ---@param drop table|Drop The drop table from Module:Loot/data. |
---@param id string|number The ID of the source to find. | ---@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 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. | ---@param locationId string|number|nil Optional ID for the location to find the source from. Use `id` and `sourceType` for location loot. | ||
− | ---@return Item[]|nil The source data, or nil if the source is not found. | + | ---@return Item[]|nil # The source data, or nil if the source is 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. | + | ---@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( | + | local function getFirstSourceMatch(drop, id, sourceType, locationId) |
id = tostring(id) | id = tostring(id) | ||
locationId = locationId and tostring(locationId) or nil | locationId = locationId and tostring(locationId) or nil | ||
if sourceType == "location" then | if sourceType == "location" then | ||
− | local location = | + | local location = drop[id] |
if not location then | if not location then | ||
return | return | ||
Line 81: | Line 89: | ||
-- otherwise the loot would be from first matching ID and could be wrong | -- otherwise the loot would be from first matching ID and could be wrong | ||
if locationId then | if locationId then | ||
− | local location = | + | local location = drop[locationId] |
for sourceId, source in pairs(location) do | for sourceId, source in pairs(location) do | ||
if sourceId == id then | if sourceId == id then | ||
Line 90: | Line 98: | ||
end | end | ||
− | for | + | for locationId2, location in pairs(drop) do |
for sourceId, source in pairs(location) do | for sourceId, source in pairs(location) do | ||
if sourceId == id then | if sourceId == id then | ||
− | return source, | + | return source, locationId2 |
end | end | ||
end | end | ||
Line 104: | Line 112: | ||
---@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 130: | Line 138: | ||
---@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 as string ('numerator/denominator'). | + | ---@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 142: | Line 150: | ||
---@param num number The number to format. | ---@param num number The number to format. | ||
---@param digits number The number of decimal places to include (must be a non-negative integer). | ---@param digits number The number of decimal places to include (must be a non-negative integer). | ||
− | ---@return string The formatted number as a string. | + | ---@return string # The formatted number as a string. |
local function toFixed(num, digits) | local function toFixed(num, digits) | ||
digits = math.max(0, math.floor(digits)) | digits = math.max(0, math.floor(digits)) | ||
Line 151: | Line 159: | ||
---Finds the ID of a monster that is overridden by another monster with the given name. | ---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. | ---@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. | + | ---@return string|nil # The ID of the monster that is overridden, or nil if not found. |
local function findIdByOverrideName(name) | local function findIdByOverrideName(name) | ||
local monsterStatsData = mw.loadData("Module:Monsters_stats/data") | local monsterStatsData = mw.loadData("Module:Monsters_stats/data") | ||
Line 161: | Line 169: | ||
end | end | ||
end | end | ||
+ | end | ||
+ | |||
+ | ---@param name string Wikitext. | ||
+ | ---@return mw.html # mw.html TH element. | ||
+ | local function createTh(name) | ||
+ | local th = mw.html.create("th") | ||
+ | :addClass("headerSort") | ||
+ | :attr("tabindex", 0) | ||
+ | :attr("title", "Sort ascending") | ||
+ | :wikitext(name) | ||
+ | return th | ||
+ | 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 | ||
+ | tr:node(createTh(name)) | ||
+ | end | ||
+ | return tr | ||
+ | end | ||
+ | |||
+ | ---Creates a mw.html A element containing an image. | ||
+ | ---@param name string The name of the item. | ||
+ | ---@param src string The URI to the item image. | ||
+ | ---@param width? string|integer The width of the item image as integer. | ||
+ | ---@param height? string|integer The height of the item image as integer. | ||
+ | ---@return mw.html # mw.html A element containing an image. | ||
+ | local function createImg(name, src, alt, width, height) | ||
+ | local e = mw.html.create("a") | ||
+ | :attr("href", tostring(mw.uri.localUrl(name))) | ||
+ | :attr("title", name) | ||
+ | :tag("img") | ||
+ | :attr("src", src) | ||
+ | :attr("alt", alt) | ||
+ | if width then | ||
+ | e:attr("width", width) | ||
+ | end | ||
+ | if height then | ||
+ | e:attr("height", height) | ||
+ | end | ||
+ | return e:done() | ||
+ | end | ||
+ | |||
+ | local function createImgText(name, src, alt, width, height) | ||
+ | return string.format( | ||
+ | '[[%s|<img src="%s" alt="%s"%s%s>]]', | ||
+ | name, | ||
+ | src, | ||
+ | alt, | ||
+ | width and ' width="' .. width .. '"' or "", | ||
+ | height and ' height="' .. height .. '"' or "" | ||
+ | ) | ||
+ | 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 string. | ||
+ | ---@param item table|Item Item table from Module:Items/data | ||
+ | ---@param drop table|Drop Drop table from Module:Loot/data | ||
+ | ---@return string # The formatted value of the item as string, or '-' if no value. | ||
+ | local function createValueText(item, drop) | ||
+ | local lang = mw.language.getContentLanguage() | ||
+ | local value = item.id == 1 and 1 or item.value | ||
+ | if value then | ||
+ | local minValue = drop.minAmount and drop.minAmount * value | ||
+ | local maxValue = drop.maxAmount and drop.maxAmount * value | ||
+ | if minValue == maxValue then | ||
+ | return lang:formatNum(minValue) | ||
+ | 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 Item drop quantities as string. | ||
+ | ---@param drop table|Drop Item drop table from Module:Loot/data | ||
+ | ---@return string # The formatted quantity range as string. | ||
+ | local function createQuantityText(drop) | ||
+ | local lang = mw.language.getContentLanguage() | ||
+ | if drop.minAmount == drop.maxAmount then | ||
+ | return lang:formatNum(drop.minAmount) | ||
+ | end | ||
+ | return lang:formatNum(drop.minAmount) .. "–" .. lang:formatNum(drop.maxAmount) | ||
end | end | ||
Line 194: | Line 299: | ||
end | end | ||
+ | ---@diagnostic disable-next-line: param-type-mismatch | ||
local loot = getFirstSourceMatch(lootData, sourceId, sourceType, locationId) | local loot = getFirstSourceMatch(lootData, sourceId, sourceType, locationId) | ||
if not loot then | if not loot then | ||
return createErrorMessage( | return createErrorMessage( | ||
"No loot found for monster or location named '" .. sourceName .. | "No loot found for monster or location named '" .. sourceName .. | ||
− | "' The Module:Loot/data may be outdated." | + | "'. 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 | end | ||
+ | --loot data is read-only from Module so we need to copy it | ||
+ | loot = copyTable(loot) | ||
+ | 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) | ||
− | |||
local imageWidth = args.width == nil and 30 or args.width | local imageWidth = args.width == nil and 30 or args.width | ||
local imageHeight = args.height == nil and imageWidth or args.height | local imageHeight = args.height == nil and imageWidth or args.height | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
--TODO: Add option to show percentages as fractions (open options from the gear icon) | --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? | -- Saving preferences would require special permissions? MAybe with mw.user or javascript to localStorage? | ||
− | |||
− | |||
− | |||
− | |||
− | for _, | + | local rows = { |
− | local | + | ["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 | --IS itemList.ts has some incorrect URIs, e.g. for Feather | ||
− | local src = | + | local src = item.itemImage |
src = src:sub(1, 1) ~= "/" and "/" .. src or src | src = src:sub(1, 1) ~= "/" and "/" .. src or src | ||
local imageHtml = mw.html.create("td") | local imageHtml = mw.html.create("td") | ||
:wikitext(createWikitextImage( | :wikitext(createWikitextImage( | ||
− | + | item.name, | |
{ | { | ||
src = "https://www.play.idlescape.com" .. src, | src = "https://www.play.idlescape.com" .. src, | ||
− | alt = | + | alt = item.name .. (item.extraTooltip and ". " .. item.extraTooltip or ""), |
width = imageWidth, | width = imageWidth, | ||
height = imageHeight | height = imageHeight | ||
Line 312: | Line 348: | ||
)) | )) | ||
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") | :css("text-align", "center") | ||
− | :wikitext(createQuantityText( | + | :wikitext(createQuantityText(drop)) |
local chanceHtml = mw.html.create("td") | local chanceHtml = mw.html.create("td") | ||
:css("text-align", "center") | :css("text-align", "center") | ||
− | :wikitext(toFixed( | + | :wikitext(toFixed(drop.chance * 100, 3) .. "%") |
− | local | + | local valueHtml = mw.html.create("td") |
:css("text-align", "right") | :css("text-align", "right") | ||
− | :wikitext(createValueText( | + | :wikitext(createValueText(item, drop)) |
local tradeableHtml = mw.html.create("td") | local tradeableHtml = mw.html.create("td") | ||
:css("text-align", "center") | :css("text-align", "center") | ||
− | :wikitext(( | + | :wikitext((item.id == 1 or item.tradeable) and "Yes" or "No") --Gold is tradeable |
− | local leagueHtml | + | local leagueHtml |
− | |||
--TODO: Add indicator if league is inactive, the field is present in | --TODO: Add indicator if league is inactive, the field is present in | ||
-- leagueList.ts / Module:Leagues/data but some are incorrect | -- leagueList.ts / Module:Leagues/data but some are incorrect | ||
− | if | + | if drop.allowedLeagues then |
− | for _, leagueId in ipairs( | + | leagueHtml = mw.html.create("td") |
− | local league = | + | :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( | leagueHtml:wikitext(createWikitextImage( | ||
Line 346: | Line 383: | ||
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(tradeableHtml) | :node(tradeableHtml) | ||
− | |||
− | tableHtml: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(rows[caption], row) | ||
+ | end | ||
+ | |||
+ | ---@param caption string|nil Optional caption for the table. | ||
+ | ---@param isLeagues boolean|nil If true, show leagues in the table. | ||
+ | ---@return table|mw.html # mw.html TABLE element. | ||
+ | local function createTable(caption, isLeagues) | ||
+ | local columns = { | ||
+ | " ", | ||
+ | "Item", | ||
+ | "Quantity", | ||
+ | "Rarity", | ||
+ | "Vendor value", | ||
+ | "Tradeable", | ||
+ | isLeagues and "Leagues" or nil | ||
+ | } | ||
+ | local element = mw.html.create("table") | ||
+ | :addClass("wikitable sortable jquery-tablesorter mw-collapsible") | ||
+ | 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 | ||
+ | |||
+ | local tableHtml = mw.html.create() | ||
+ | |||
+ | for caption, rowList in pairs(rows) do | ||
+ | if next(rowList) ~= nil then | ||
+ | local isLeagues = caption == "League specific drops" | ||
+ | local tableHtml2 = createTable(caption, isLeagues) | ||
+ | for _, row in ipairs(rowList) do | ||
+ | tableHtml2:node(row) | ||
+ | end | ||
+ | tableHtml:node(tableHtml2) | ||
+ | end | ||
end | end | ||
Latest revision as of 21:15, 27 April 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") ---Creates a deep copy of a table. ---@param t table ---@return table local function copyTable(t) local copy = {} for k, v in pairs(t) do if type(v) == "table" then copy[k] = copyTable(v) else copy[k] = v end end return copy end ---Creates a mw.html 'DIV' element containing an "error" message. ---@param message string Message as wikitext. ---@return 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 item in the drop table that matches the given item ID. ---@param drop table The drop table from Module:Loot/data. ---@param id string|number The ID of the item to find. ---@return Item|nil The item data, or nil if the item is not found. ---@return string|nil # The monsterId where the item was found ("-1" for locations), or nil if the item is not found. ---@return string|nil # The locationId where the item was found, or nil if the item is not found. local function getFirstLootMatch(drop, id) id = tostring(id) for locationId, location in pairs(drop) do for sourceId, source in pairs(location) do for _, item in ipairs(source) do if item.id == id then return item, sourceId, locationId end end end end end ---Finds the first source in the drop table that matches the given sourceId. ---@param drop table|Drop The drop table from Module:Loot/data. ---@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 Item[]|nil # The source data, or nil if the source is 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(drop, id, sourceType, locationId) id = tostring(id) locationId = locationId and tostring(locationId) or nil if sourceType == "location" then local location = drop[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 = drop[locationId] for sourceId, source in pairs(location) do if sourceId == id then return source, locationId end end return end for locationId2, location in pairs(drop) 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 ---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 ---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 ---@param name string Wikitext. ---@return mw.html # mw.html TH element. local function createTh(name) local th = mw.html.create("th") :addClass("headerSort") :attr("tabindex", 0) :attr("title", "Sort ascending") :wikitext(name) return th 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 tr:node(createTh(name)) end return tr end ---Creates a mw.html A element containing an image. ---@param name string The name of the item. ---@param src string The URI to the item image. ---@param width? string|integer The width of the item image as integer. ---@param height? string|integer The height of the item image as integer. ---@return mw.html # mw.html A element containing an image. local function createImg(name, src, alt, width, height) local e = mw.html.create("a") :attr("href", tostring(mw.uri.localUrl(name))) :attr("title", name) :tag("img") :attr("src", src) :attr("alt", alt) if width then e:attr("width", width) end if height then e:attr("height", height) end return e:done() end local function createImgText(name, src, alt, width, height) return string.format( '[[%s|<img src="%s" alt="%s"%s%s>]]', name, src, alt, width and ' width="' .. width .. '"' or "", height and ' height="' .. height .. '"' or "" ) 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 string. ---@param item table|Item Item table from Module:Items/data ---@param drop table|Drop Drop table from Module:Loot/data ---@return string # The formatted value of the item as string, or '-' if no value. local function createValueText(item, drop) local lang = mw.language.getContentLanguage() local value = item.id == 1 and 1 or item.value if value then local minValue = drop.minAmount and drop.minAmount * value local maxValue = drop.maxAmount and drop.maxAmount * value if minValue == maxValue then return lang:formatNum(minValue) 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 Item drop quantities as string. ---@param drop table|Drop Item drop table from Module:Loot/data ---@return string # The formatted quantity range as string. local function createQuantityText(drop) local lang = mw.language.getContentLanguage() if drop.minAmount == drop.maxAmount then return lang:formatNum(drop.minAmount) end return lang:formatNum(drop.minAmount) .. "–" .. lang:formatNum(drop.maxAmount) 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 --Some monsters use same id as other monsters so we need to specify location for them local locationId = args["zone"] and findSourceId(args["zone"]) local sourceId local sourceType if locationId then sourceId = findIdByOverrideName(sourceName) sourceType = "monster" else sourceId, sourceType = findSourceId(sourceName) end if not sourceId then return locationId and createErrorMessage( "Found no monsters overridden by '" .. sourceName .. "'. The Modules: Monsters_stats/data or Loot/data may be outdated." ) or createErrorMessage( "No monster or location named '" .. sourceName .. "'. The Module:Loot/data may be outdated. <br>Try adding parameter 'location' to the template, e.g.:" .. "<br><code>{{Loot_table|location=Palace of Flame}}</code>." ) end ---@diagnostic disable-next-line: param-type-mismatch local loot = getFirstSourceMatch(lootData, sourceId, sourceType, locationId) 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 data is read-only from Module so we need to copy it loot = copyTable(loot) 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) local imageWidth = args.width == nil and 30 or args.width local imageHeight = args.height == nil and imageWidth or args.height --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 rows = { ["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 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(createQuantityText(drop)) local chanceHtml = mw.html.create("td") :css("text-align", "center") :wikitext(toFixed(drop.chance * 100, 3) .. "%") local valueHtml = mw.html.create("td") :css("text-align", "right") :wikitext(createValueText(item, drop)) local tradeableHtml = mw.html.create("td") :css("text-align", "center") :wikitext((item.id == 1 or item.tradeable) and "Yes" or "No") --Gold is tradeable 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) :node(tradeableHtml) 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(rows[caption], row) end ---@param caption string|nil Optional caption for the table. ---@param isLeagues boolean|nil If true, show leagues in the table. ---@return table|mw.html # mw.html TABLE element. local function createTable(caption, isLeagues) local columns = { " ", "Item", "Quantity", "Rarity", "Vendor value", "Tradeable", isLeagues and "Leagues" or nil } local element = mw.html.create("table") :addClass("wikitable sortable jquery-tablesorter mw-collapsible") 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 local tableHtml = mw.html.create() for caption, rowList in pairs(rows) do if next(rowList) ~= nil then local isLeagues = caption == "League specific drops" local tableHtml2 = createTable(caption, isLeagues) for _, row in ipairs(rowList) do tableHtml2:node(row) end tableHtml:node(tableHtml2) end end return tableHtml end return p