Difference between revisions of "Module:Loot table"
Jump to navigation
Jump to search
(Monster Drops) |
Demcookies (talk | contribs) (Add basic / initial Module functionality) |
||
Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
local findId = require("Module:FindId") | local findId = require("Module:FindId") | ||
− | local | + | local lootData = mw.loadData("Module:Loot/data") |
− | + | local itemData = mw.loadData("Module:Items/data") | |
− | local | + | local leagueData = mw.loadData("Module:Leagues/data") |
− | local | ||
− | |||
− | local function | + | |
− | return | + | ---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 The source type ("monster" or "location"), or 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 div = mw.html.create("div") | ||
+ | :css("color", "red") | ||
+ | :wikitext("No monster or location named '" .. name .. "' The Module:Loot/data may be outdated.") | ||
+ | return nil, tostring(div) | ||
end | end | ||
− | local function | + | ---@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 was not found. | ||
+ | ---@return string? # The locationId where the item was found, or nil if the item was 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 | ||
+ | return | ||
end | end | ||
− | --- | + | ---Finds the first source in the loot table that matches the given sourceId. |
− | ---@param | + | ---@param loot table # The loot table from Module:Loot/data. |
− | ---@return string | + | ---@param id string|number # The ID of the source to find. |
− | local function | + | ---@return Item[]? # The source data, or nil if the source was not found. |
− | + | ---@return string? # The locationId where the source was found, or nil if the source was 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 | |
+ | return | ||
end | end | ||
− | local function | + | --TODO: Add option to show percentages as fractions |
− | local | + | |
− | local | + | ---Converts a floating-point number to a fraction. |
− | local | + | ---@param num number The floating-point number to convert. |
− | for | + | ---@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 | ||
end | end | ||
− | + | ||
− | + | return sign * bestNumerator, bestDenominator | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | --- | + | ---Converts a floating-point number to a fraction string. |
− | ---@param | + | ---@param num number The floating-point number to convert. |
− | ---@return | + | ---@param maxDenominator number The maximum denominator for the fraction. |
− | local function | + | ---@return string The fraction string. |
− | local | + | local function floatToFractionString(num, maxDenominator) |
− | if | + | local numerator, denominator = floatToFraction(num, maxDenominator) |
− | return | + | if denominator == 1 then |
− | + | return tostring(numerator) | |
− | |||
end | end | ||
+ | return string.format("%d/%d", numerator, denominator) | ||
end | end | ||
− | local function | + | ---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 | end | ||
− | + | function p.lootTable(frame) | |
− | + | local args = frame:getParent().args | |
− | + | return p._lootTable(args) | |
− | |||
− | |||
end | end | ||
− | + | function p._lootTable(_args) | |
− | local | + | local sourceName = _args["name"] or _args["title"] or mw.title.getCurrentTitle().text |
− | if | + | 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 tostring(div) | ||
end | end | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | local function | + | ---@param name any |
− | + | ---@return mw.html.element | |
− | + | ---@return string | |
− | + | local function makeTh(name) | |
− | + | local th = mw.html.create("th") | |
− | end | + | :addClass("headerSort") |
+ | :attr("tabindex", 0) | ||
+ | :attr("role", "columnheader button") | ||
+ | :attr("title", "Sort ascending") | ||
+ | :wikitext(name) | ||
+ | return th, tostring(th) | ||
+ | end | ||
− | local function | + | ---@param names any[] |
− | + | ---@return mw.html.element | |
− | + | ---@return string | |
− | : | + | local function makeTr(names) |
− | + | local tr = mw.html.create("tr") | |
− | + | for _, name in ipairs(names) do | |
− | + | tr:node(makeTh(name)) | |
− | + | end | |
− | + | return tr, tostring(tr) | |
− | + | end | |
− | |||
− | end | ||
− | |||
− | + | ---Creates a mw.html.element image & text container for loot tables. | |
− | + | ---@param name string # The name of the item. | |
− | + | ---@param href string # The URI of the item page. Should be "/p/" .. name | |
− | + | ---@param src string # The URI of the item image. | |
− | + | ---@param width string # The width of the item image as css param. Default is "auto". | |
− | + | ---@param height string? # The height of the item image as css param. Default is "auto". | |
− | + | ---@return mw.html.element | |
− | + | local function makeImageContainer(name, alt, href, src, width, height) | |
− | + | return mw.html.create("td") | |
− | + | :tag("span") | |
− | + | :css("white-space", "nowrap") | |
− | + | :tag("a") | |
− | + | :addClass("mw-redirect") | |
− | + | :attr("href", href) | |
− | + | :attr("title", name) | |
− | + | :tag("img") | |
− | + | :attr("src", src) | |
− | + | :attr("alt", alt) | |
− | + | :attr("width", width or "auto") | |
− | + | :attr("height", height or "auto") | |
− | |||
− | : | ||
− | : | ||
− | : | ||
− | : | ||
− | |||
:done() | :done() | ||
+ | :wikitext(name) | ||
+ | :allDone() | ||
end | end | ||
− | |||
− | |||
− | + | local tableHtml = mw.html.create("table") | |
− | + | :addClass("wikitable sortable jquery-tablesorter") | |
− | end | + | :tag("thead") |
+ | :node(makeTr({ "Item", "Quantity", "Rarity", "Price", "League" })) | ||
+ | :done() | ||
+ | :tag("tbody") | ||
+ | |||
+ | for _, item in ipairs(loot) do | ||
+ | local item2 = itemData[tostring(item.id)] | ||
+ | local imageHtml = makeImageContainer( | ||
+ | item2.name, | ||
+ | item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""), | ||
+ | "/p/" .. item2.name, | ||
+ | "https://www.play.idlescape.com" .. item2.itemImage, | ||
+ | "45px" | ||
+ | ) | ||
+ | local quantityHtml = mw.html.create("td") | ||
+ | :wikitext(item.minAmount .. " - " .. item.maxAmount) | ||
+ | local chanceHtml = mw.html.create("td") | ||
+ | :wikitext(toFixed(item.chance * 100, 3) .. "%") | ||
+ | local priceHtml = mw.html.create("td") | ||
+ | :wikitext(itemData[tostring(item.id)].value or "-") | ||
+ | local tradeableHtml = mw.html.create("td") | ||
+ | :wikitext(itemData[tostring(item.id)].tradeable or false) | ||
+ | local leaguesHtml = mw.html.create("td") | ||
+ | --TODO: Add indicator if league is active, the field is present in Module:Leagues/data but is incorrect | ||
+ | if item.allowedLeagues then | ||
+ | for _, leagueId in ipairs(item.allowedLeagues) do | ||
+ | local league = leagueData[tostring(leagueId)] | ||
+ | if league then | ||
+ | local leagueHtml = makeImageContainer( | ||
+ | league.name, | ||
+ | league.name, | ||
+ | "/p/" .. league.name, | ||
+ | "https://www.play.idlescape.com" .. league.icon, | ||
+ | "45px" | ||
+ | ) | ||
+ | leaguesHtml:node(leagueHtml) | ||
+ | else | ||
+ | leaguesHtml:wikitext("?") | ||
+ | end | ||
+ | end | ||
+ | else | ||
+ | leaguesHtml:wikitext("All") | ||
+ | end | ||
+ | local itemHtml = mw.html.create("tr") | ||
+ | :node(imageHtml) | ||
+ | :node(quantityHtml) | ||
+ | :node(chanceHtml) | ||
+ | :node(priceHtml) | ||
+ | :node(tradeableHtml) | ||
+ | :node(leaguesHtml) | ||
+ | :allDone() | ||
− | + | tableHtml:node(itemHtml) | |
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
− | + | ||
− | + | return tostring(tableHtml) | |
end | end | ||
return p | return p |
Revision as of 20:11, 1 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") ---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 The source type ("monster" or "location"), or 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 div = mw.html.create("div") :css("color", "red") :wikitext("No monster or location named '" .. name .. "' The Module:Loot/data may be outdated.") return nil, tostring(div) 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 was not found. ---@return string? # The locationId where the item was found, or nil if the item was 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 return 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 was not found. ---@return string? # The locationId where the source was found, or nil if the source was 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 return end --TODO: Add option to show percentages as fractions ---Converts a floating-point number to a 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 fraction string. ---@param num number The floating-point number to convert. ---@param maxDenominator number The maximum denominator for the fraction. ---@return string The fraction string. 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 return p._lootTable(args) 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 tostring(div) end ---@param name any ---@return mw.html.element ---@return string local function makeTh(name) local th = mw.html.create("th") :addClass("headerSort") :attr("tabindex", 0) :attr("role", "columnheader button") :attr("title", "Sort ascending") :wikitext(name) return th, tostring(th) end ---@param names any[] ---@return mw.html.element ---@return string local function makeTr(names) local tr = mw.html.create("tr") for _, name in ipairs(names) do tr:node(makeTh(name)) end return tr, tostring(tr) end ---Creates a mw.html.element image & text container for loot tables. ---@param name string # The name of the item. ---@param href string # The URI of the item page. Should be "/p/" .. name ---@param src string # The URI of the item image. ---@param width string # The width of the item image as css param. Default is "auto". ---@param height string? # The height of the item image as css param. Default is "auto". ---@return mw.html.element local function makeImageContainer(name, alt, href, src, width, height) return mw.html.create("td") :tag("span") :css("white-space", "nowrap") :tag("a") :addClass("mw-redirect") :attr("href", href) :attr("title", name) :tag("img") :attr("src", src) :attr("alt", alt) :attr("width", width or "auto") :attr("height", height or "auto") :done() :wikitext(name) :allDone() end local tableHtml = mw.html.create("table") :addClass("wikitable sortable jquery-tablesorter") :tag("thead") :node(makeTr({ "Item", "Quantity", "Rarity", "Price", "League" })) :done() :tag("tbody") for _, item in ipairs(loot) do local item2 = itemData[tostring(item.id)] local imageHtml = makeImageContainer( item2.name, item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""), "/p/" .. item2.name, "https://www.play.idlescape.com" .. item2.itemImage, "45px" ) local quantityHtml = mw.html.create("td") :wikitext(item.minAmount .. " - " .. item.maxAmount) local chanceHtml = mw.html.create("td") :wikitext(toFixed(item.chance * 100, 3) .. "%") local priceHtml = mw.html.create("td") :wikitext(itemData[tostring(item.id)].value or "-") local tradeableHtml = mw.html.create("td") :wikitext(itemData[tostring(item.id)].tradeable or false) local leaguesHtml = mw.html.create("td") --TODO: Add indicator if league is active, the field is present in Module:Leagues/data but is incorrect if item.allowedLeagues then for _, leagueId in ipairs(item.allowedLeagues) do local league = leagueData[tostring(leagueId)] if league then local leagueHtml = makeImageContainer( league.name, league.name, "/p/" .. league.name, "https://www.play.idlescape.com" .. league.icon, "45px" ) leaguesHtml:node(leagueHtml) else leaguesHtml:wikitext("?") end end else leaguesHtml:wikitext("All") end local itemHtml = mw.html.create("tr") :node(imageHtml) :node(quantityHtml) :node(chanceHtml) :node(priceHtml) :node(tradeableHtml) :node(leaguesHtml) :allDone() tableHtml:node(itemHtml) end return tostring(tableHtml) end return p