Difference between revisions of "Module:Loot table"
Jump to navigation
Jump to search
Demcookies (talk | contribs) (Fix return the table as a string and not mw.html element) |
Demcookies (talk | contribs) (Fix change html / wikitext parsing.) |
||
Line 5: | Line 5: | ||
local leagueData = mw.loadData("Module:Leagues/data") | local leagueData = mw.loadData("Module:Leagues/data") | ||
+ | |||
+ | ---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. | ---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|mw.html | + | ---@return string|mw.html # The source type ("monster" or "location"), or mw.html element with error message 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 | ||
− | + | local errorMessage = createErrorMessage("No monster or location named '" .. | |
− | + | name .. "'. The Module:Loot/data may be outdated.") | |
− | + | return nil, errorMessage | |
− | |||
− | |||
end | end | ||
Line 34: | Line 43: | ||
---@param id string|number # The ID of the item to find. | ---@param id string|number # The ID of the item to find. | ||
---@return Item? # The item data, or nil if the item is not found. | ---@return Item? # The item data, or nil if the item is not found. | ||
− | ---@return string? # The monsterId where the item was found ("-1" for locations), or nil if the item | + | ---@return string? # The monsterId where the item was found ("-1" for locations), or nil if the item is not found. |
− | ---@return string? # The locationId where the item was found, or nil if the item | + | ---@return string? # The locationId where the item was found, or nil if the item is not found. |
local function getFirstLootMatch(loot, id) | local function getFirstLootMatch(loot, id) | ||
id = tostring(id) | id = tostring(id) | ||
− | + | for locationId, location in pairs(loot) 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 | end | ||
− | |||
end | end | ||
Line 52: | Line 61: | ||
---@param loot table # The loot table from Module:Loot/data. | ---@param loot table # The loot 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. | ||
− | ---@return Item[]? # The source data, or nil if the source | + | ---@return Item[]? # The source data, or nil if the source is not found. |
− | ---@return string? # The locationId where the source was found, or nil if the source | + | ---@return string? # The locationId where the source was found, or nil if the source is not found. |
local function getFirstSourceMatch(loot, id) | local function getFirstSourceMatch(loot, id) | ||
id = tostring(id) | id = tostring(id) | ||
− | + | for locationId, location in pairs(loot) do | |
− | + | for sourceId, source in pairs(location) do | |
− | + | if sourceId == id then | |
− | + | return source, locationId | |
− | + | 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 80: | Line 89: | ||
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 93: | Line 102: | ||
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 115: | ||
---Formats a number up to a given number of decimal places, removing trailing zeros. | ---Formats a number up to a given number of decimal places, removing trailing zeros. | ||
− | ---@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)) | |
− | + | local formatted = string.format("%." .. digits .. "f", num) | |
− | + | formatted = formatted:gsub("%.?0+$", "") | |
− | + | return formatted | |
end | end | ||
+ | |||
function p.lootTable(frame) | function p.lootTable(frame) | ||
− | + | local args = frame:getParent().args | |
− | + | local output = tostring(p._lootTable(args)) | |
+ | return args.raw and output or frame:preprocess("<html>" .. output .. "</html>") | ||
end | end | ||
− | function p._lootTable( | + | function p._lootTable(args) |
− | local sourceName = | + | local sourceName = args.name or args.title or mw.title.getCurrentTitle().text |
local sourceId, sourceTypeOrError = findSourceId(sourceName) | local sourceId, sourceTypeOrError = findSourceId(sourceName) | ||
if not sourceId then | if not sourceId then | ||
Line 137: | Line 148: | ||
end | end | ||
− | ---@param name | + | local lang = mw.language.getContentLanguage() |
− | ---@return mw.html. | + | |
− | local function | + | ---@param name string # Wikitext. |
+ | ---@return mw.html # 'TH' element. | ||
+ | local function createTh(name) | ||
local th = mw.html.create("th") | local th = mw.html.create("th") | ||
:addClass("headerSort") | :addClass("headerSort") | ||
:attr("tabindex", 0) | :attr("tabindex", 0) | ||
− | |||
:attr("title", "Sort ascending") | :attr("title", "Sort ascending") | ||
:wikitext(name) | :wikitext(name) | ||
Line 149: | Line 161: | ||
end | end | ||
− | ---@param names | + | ---@param names string[] # Array of wikitext. |
− | ---@return mw.html. | + | ---@return mw.html # 'TR' element containing names as 'TH' elements. |
− | local function | + | local function createThs(names) |
local tr = mw.html.create("tr") | local tr = mw.html.create("tr") | ||
for _, name in ipairs(names) do | for _, name in ipairs(names) do | ||
− | tr:node( | + | tr:node(createTh(name)) |
end | end | ||
return tr | return tr | ||
end | end | ||
− | ---Creates a | + | ---Creates a 'A' element containing an image. |
---@param name string # The name of the item. | ---@param name string # The name of the item. | ||
− | + | ---@param src string # The URI to the item image. | |
− | ---@param src string # The URI | + | ---@param width? string|integer # The width of the item image as integer. |
− | ---@param width string # The width of the item image as | + | ---@param height? string|integer # The height of the item image as integer. |
− | ---@param height string | + | ---@return mw.html # mw.html 'A' element containing an image. |
− | ---@return mw.html.element | + | local function createImg(name, src, alt, width, height) |
− | local function | + | local e = mw.html.create("a") |
− | local | + | :attr("href", tostring(mw.uri.localUrl(name))) |
+ | :attr("title", name) | ||
+ | :tag("img") | ||
:attr("src", src) | :attr("src", src) | ||
:attr("alt", alt) | :attr("alt", alt) | ||
− | :attr("width", width | + | if width then |
+ | e:attr("width", width) | ||
+ | end | ||
if height then | if height then | ||
− | + | e:attr("height", height) | |
+ | end | ||
+ | return e:done() | ||
+ | end | ||
+ | |||
+ | ---Formats the value of an item as string. | ||
+ | ---@param item table # Item table from Module:Items/data | ||
+ | ---@return string # The formatted value of the item as string, or '-' if no value. | ||
+ | local function createValueText(item) | ||
+ | if item.id == 1 then -- Gold | ||
+ | return "1" | ||
end | end | ||
− | + | return item.value and lang:formatNum(item.value) or "-" | |
− | + | end | |
− | + | ||
− | + | ---Formats the Item drop quantities as string. | |
− | + | ---@param item Item # Item table from Module:Loot/data | |
− | + | ---@return string # The formatted quantity range as string. | |
+ | local function createQuantityText(item) | ||
+ | if item.minAmount == item.maxAmount then | ||
+ | return tostring(item.minAmount) | ||
+ | end | ||
+ | return lang:formatNum(item.minAmount) .. " - " .. lang:formatNum(item.maxAmount) | ||
end | end | ||
local tableHtml = mw.html.create("table") | local tableHtml = mw.html.create("table") | ||
:addClass("wikitable sortable jquery-tablesorter") | :addClass("wikitable sortable jquery-tablesorter") | ||
− | :node( | + | :node(createThs({ "Image", "Item", "Quantity", "Rarity", "Value", "Tradeable", "Leagues" })) |
for _, item in ipairs(loot) do | for _, item in ipairs(loot) do | ||
Line 191: | Line 222: | ||
local src = item2.itemImage | local src = item2.itemImage | ||
src = src:sub(1, 1) ~= "/" and "/" .. src or src | src = src:sub(1, 1) ~= "/" and "/" .. src or src | ||
− | local imageHtml = | + | local imageHtml = mw.html.create("td") |
− | item2.name, | + | :node(createImg( |
− | + | item2.name, | |
− | + | "https://www.play.idlescape.com" .. src, | |
− | + | item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""), | |
− | + | args.width or 45, | |
− | + | args.height or nil | |
+ | )) | ||
local itemHtml = mw.html.create("td") | local itemHtml = mw.html.create("td") | ||
:tag("a") | :tag("a") | ||
− | :attr("href", | + | :attr("href", tostring(mw.uri.localUrl(item2.name))) |
:attr("title", item2.name) | :attr("title", item2.name) | ||
:wikitext(item2.name) | :wikitext(item2.name) | ||
:done() | :done() | ||
local quantityHtml = mw.html.create("td") | local quantityHtml = mw.html.create("td") | ||
− | :wikitext(item | + | :wikitext(createQuantityText(item)) |
local chanceHtml = mw.html.create("td") | local chanceHtml = mw.html.create("td") | ||
:wikitext(toFixed(item.chance * 100, 3) .. "%") | :wikitext(toFixed(item.chance * 100, 3) .. "%") | ||
local priceHtml = mw.html.create("td") | local priceHtml = mw.html.create("td") | ||
− | :wikitext(item2 | + | :wikitext(createValueText(item2)) |
local tradeableHtml = mw.html.create("td") | local tradeableHtml = mw.html.create("td") | ||
:wikitext(item2.tradeable and "Yes" or "No") | :wikitext(item2.tradeable and "Yes" or "No") | ||
− | local | + | local leagueHtml = mw.html.create("td") |
− | --TODO: Add indicator if league is | + | --TODO: Add indicator if league is inactive, the field is present in |
+ | -- leagueList.ts / Module:Leagues/data but some are incorrect | ||
if item.allowedLeagues then | if item.allowedLeagues then | ||
for _, leagueId in ipairs(item.allowedLeagues) do | for _, leagueId in ipairs(item.allowedLeagues) do | ||
local league = leagueData[tostring(leagueId)] | local league = leagueData[tostring(leagueId)] | ||
if league then | if league then | ||
− | + | leagueHtml:node(createImg( | |
league.name, | league.name, | ||
+ | "https://www.play.idlescape.com" .. league.icon, | ||
league.name, | league.name, | ||
− | + | args.width or 45, | |
− | + | args.height or nil | |
− | + | )) | |
− | |||
− | |||
− | ) | ||
else | else | ||
− | + | leagueHtml:wikitext("?") | |
end | end | ||
end | end | ||
else | else | ||
− | + | leagueHtml:wikitext("All") | |
end | end | ||
+ | |||
local rowHtml = mw.html.create("tr") | local rowHtml = mw.html.create("tr") | ||
:node(imageHtml) | :node(imageHtml) | ||
Line 241: | Line 273: | ||
:node(priceHtml) | :node(priceHtml) | ||
:node(tradeableHtml) | :node(tradeableHtml) | ||
− | :node( | + | :node(leagueHtml) |
tableHtml:node(rowHtml) | tableHtml:node(rowHtml) | ||
end | end | ||
− | return | + | return tableHtml |
end | end | ||
return p | return p |
Revision as of 16:26, 3 April 2025
local p = {} local findId = require("Module:FindId") local lootData = mw.loadData("Module:Loot/data") local itemData = mw.loadData("Module:Items/data") local leagueData = mw.loadData("Module:Leagues/data") ---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|mw.html # The source type ("monster" or "location"), or mw.html element with error message 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 local errorMessage = createErrorMessage("No monster or location named '" .. name .. "'. The Module:Loot/data may be outdated.") return nil, errorMessage end ---@class Item ---@field allowedLeagues? number[] # The leagues in which the item can be found. Found in all leagues if nil. ---@field id integer # The item ID. ---@field chance number # The base chance of the item being dropped. ---@field minAmount integer # The minimum base amount of the item that can be dropped. ---@field maxAmount integer # The maximum base amount of the item that can be dropped. ---Finds the first item in the loot table that matches the given item ID. ---@param loot table # The loot table from Module:Loot/data. ---@param id string|number # The ID of the item to find. ---@return Item? # The item data, or nil if the item is not found. ---@return string? # The monsterId where the item was found ("-1" for locations), or nil if the item is not found. ---@return string? # The locationId where the item was found, or nil if the item is not found. local function getFirstLootMatch(loot, id) id = tostring(id) for locationId, location in pairs(loot) 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 loot table that matches the given sourceId. ---@param loot table # The loot table from Module:Loot/data. ---@param id string|number # The ID of the source to find. ---@return Item[]? # The source data, or nil if the source is not found. ---@return string? # The locationId where the source was found, or nil if the source is not found. local function getFirstSourceMatch(loot, id) id = tostring(id) for locationId, location in pairs(loot) do for sourceId, source in pairs(location) do if sourceId == id then return source, locationId 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) formatted = formatted:gsub("%.?0+$", "") return formatted end function p.lootTable(frame) local args = frame:getParent().args local output = tostring(p._lootTable(args)) return args.raw and output or frame:preprocess("<html>" .. output .. "</html>") end function p._lootTable(args) local sourceName = args.name or args.title or mw.title.getCurrentTitle().text local sourceId, sourceTypeOrError = findSourceId(sourceName) if not sourceId then return sourceTypeOrError end local loot = getFirstSourceMatch(lootData, sourceId) if not loot then local div = mw.html.create("div") :css("color", "red") :wikitext("No loot found for monster or location named '" .. sourceName .. "' The Module:Loot/data may be outdated.") return div end local lang = mw.language.getContentLanguage() ---@param name string # Wikitext. ---@return 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 '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 ---Formats the value of an item as string. ---@param item table # Item table from Module:Items/data ---@return string # The formatted value of the item as string, or '-' if no value. local function createValueText(item) if item.id == 1 then -- Gold return "1" end return item.value and lang:formatNum(item.value) or "-" end ---Formats the Item drop quantities as string. ---@param item Item # Item table from Module:Loot/data ---@return string # The formatted quantity range as string. local function createQuantityText(item) if item.minAmount == item.maxAmount then return tostring(item.minAmount) end return lang:formatNum(item.minAmount) .. " - " .. lang:formatNum(item.maxAmount) end local tableHtml = mw.html.create("table") :addClass("wikitable sortable jquery-tablesorter") :node(createThs({ "Image", "Item", "Quantity", "Rarity", "Value", "Tradeable", "Leagues" })) for _, item in ipairs(loot) do local item2 = itemData[tostring(item.id)] --IS itemList.ts has some incorrect URIs, e.g. for Feather local src = item2.itemImage src = src:sub(1, 1) ~= "/" and "/" .. src or src local imageHtml = mw.html.create("td") :node(createImg( item2.name, "https://www.play.idlescape.com" .. src, item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""), args.width or 45, args.height or nil )) local itemHtml = mw.html.create("td") :tag("a") :attr("href", tostring(mw.uri.localUrl(item2.name))) :attr("title", item2.name) :wikitext(item2.name) :done() local quantityHtml = mw.html.create("td") :wikitext(createQuantityText(item)) local chanceHtml = mw.html.create("td") :wikitext(toFixed(item.chance * 100, 3) .. "%") local priceHtml = mw.html.create("td") :wikitext(createValueText(item2)) local tradeableHtml = mw.html.create("td") :wikitext(item2.tradeable and "Yes" or "No") local leagueHtml = mw.html.create("td") --TODO: Add indicator if league is inactive, the field is present in -- leagueList.ts / Module:Leagues/data but some are incorrect if item.allowedLeagues then for _, leagueId in ipairs(item.allowedLeagues) do local league = leagueData[tostring(leagueId)] if league then leagueHtml:node(createImg( league.name, "https://www.play.idlescape.com" .. league.icon, league.name, args.width or 45, args.height or nil )) else leagueHtml:wikitext("?") end end else leagueHtml:wikitext("All") end local rowHtml = mw.html.create("tr") :node(imageHtml) :node(itemHtml) :node(quantityHtml) :node(chanceHtml) :node(priceHtml) :node(tradeableHtml) :node(leagueHtml) tableHtml:node(rowHtml) end return tableHtml end return p