Difference between revisions of "Module:Infobox Monster"
Jump to navigation
Jump to search
Demcookies (talk | contribs) (Fix fetch override info automatically, no need for "override" parameter.) |
Demcookies (talk | contribs) (Fix correct handling of monsters that share an id with other monsters; needs to be included in the table "overrides" key points in the monster's Module:Monsters_stats/data table.) |
||
| 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 | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | -- | + | ---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 | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | return | ||
end | 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 | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | if | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
end | end | ||
| − | |||
end | end | ||
| − | |||
end | end | ||
| − | + | ---@class Item | |
| − | + | ---@field allowedLeagues number[]|nil 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|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. | |
| − | local function | + | ---@return string|nil The locationId where the item was found, or nil if the item is not found. |
| − | + | local function getFirstLootMatch(loot, id) | |
| − | for | + | 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 | ||
| − | + | ---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. | |
| − | + | ---@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(loot, id, sourceType, locationId) | ||
| + | id = tostring(id) | ||
| + | locationId = locationId and tostring(locationId) or nil | ||
| − | local | + | if sourceType == "location" then |
| − | + | local location = loot[id] | |
| − | + | if not location then | |
| − | + | return | |
| − | + | end | |
| − | + | local source = location["-1"] | |
| − | end | + | if not source then |
| + | return | ||
| + | end | ||
| + | return source, id | ||
| + | end | ||
| − | local | + | -- 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 = loot[locationId] | |
| − | + | for sourceId, source in pairs(location) do | |
| − | + | if sourceId == id then | |
| − | end | + | return source, locationId |
| + | end | ||
| + | end | ||
| + | return | ||
| + | end | ||
| − | + | 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 | ||
| − | + | --TODO: Add option to show percentages as fractions | |
| − | |||
| − | |||
| − | |||
| − | local function | + | ---Converts a floating-point number to a approximation of fraction. |
| − | local | + | ---@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 | + | local bestNumerator, bestDenominator = 1, 1 |
| − | local | + | local minDifference = math.huge |
| − | |||
| − | |||
| − | + | for d = 1, maxDenominator do | |
| − | + | local n = math.floor(num * d + 0.5) | |
| − | + | local approx = n / d | |
| − | local | + | local difference = math.abs(num - approx) |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | if difference < minDifference then | |
| − | + | bestNumerator, bestDenominator = n, d | |
| − | + | minDifference = difference | |
| − | |||
| − | |||
| − | if | ||
| − | |||
| − | |||
| − | |||
| − | |||
end | end | ||
end | end | ||
| − | |||
| − | |||
| − | + | return sign * bestNumerator, bestDenominator | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
end | end | ||
| − | local function | + | ---Converts a floating-point number to a approximation of fraction as string. |
| − | local | + | ---@param num number The floating-point number to convert. |
| − | if | + | ---@param maxDenominator number The maximum denominator for the fraction. |
| − | return | + | ---@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 | end | ||
| − | + | return string.format("%d/%d", numerator, denominator) | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
end | end | ||
| − | --- 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)) | digits = math.max(0, math.floor(digits)) | ||
| Line 225: | Line 149: | ||
end | 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 | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | if | ||
| − | |||
| − | |||
end | end | ||
end | end | ||
end | end | ||
| − | |||
end | 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["location"] and findSourceId(args["location"]) | |
| − | + | 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." | ||
| + | ) | ||
end | end | ||
| − | |||
| − | |||
| − | local | + | local loot = getFirstSourceMatch(lootData, sourceId, sourceType, locationId) |
| − | + | if not loot then | |
| − | if | + | return createErrorMessage( |
| − | + | "No loot found for monster or location named '" .. sourceName .. | |
| − | + | "' The Module:Loot/data may be outdated." | |
| − | + | ) | |
| − | |||
| − | |||
| − | |||
end | end | ||
| − | |||
| − | |||
| − | + | local lang = mw.language.getContentLanguage() | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | local | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | --- | + | ---@param name string Wikitext. |
| − | ---@ | + | ---@return mw.html mw.html TH element. |
| − | + | local function createTh(name) | |
| − | + | local th = mw.html.create("th") | |
| − | local function | + | :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 | end | ||
| + | return tr | ||
end | 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 | + | ---@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") | |
| − | local | + | :attr("src", src) |
| − | + | :attr("alt", alt) | |
| − | + | if width then | |
| − | + | e:attr("width", width) | |
| − | + | end | |
| − | + | if height then | |
| − | + | e:attr("height", height) | |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
end | end | ||
| + | return e:done() | ||
end | end | ||
| − | local | + | 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 ' heigth="' .. height .. '"' or "" | |
| − | + | ) | |
end | end | ||
| − | local | + | ---Creates an Wikitext link with an image. |
| − | + | ---@param title string The title of the link. | |
| − | end | + | ---@param imageAttributes table The html attributes for the image. |
| + | ---@return string Wikitext link with the image. | ||
| + | local function createWikitextImage(title, imageAttributes) | ||
| + | local image = mw.html.create("img"):attr(imageAttributes) | ||
| + | return string.format('[[%s|%s]]', title, tostring(image)) | ||
| + | end | ||
| − | --- | + | ---Formats the value of an item as string. |
| − | ---@param | + | ---@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, has no value in the data | |
| − | - | + | return "1" |
| − | + | end | |
| − | local function | + | return item.value and lang:formatNum(item.value) or "-" |
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
end | end | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | + | ---Formats the Item drop quantities as string. | |
| − | if | + | ---@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 | end | ||
| + | return lang:formatNum(item.minAmount) .. "–" .. lang:formatNum(item.maxAmount) | ||
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 tableHtml = mw.html.create("table") | ||
| + | :addClass("wikitable sortable jquery-tablesorter") | ||
| + | --:node(createThs({ "⚙️", "Item", "Quantity", "Rarity", "Value", "Tradeable", "Leagues" })) | ||
| + | :node(createThs({ " ", "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") | |
| − | for | + | :wikitext(createWikitextImage( |
| − | + | item2.name, | |
| − | + | { | |
| + | src="https://www.play.idlescape.com" .. src, | ||
| + | alt=item2.name .. (item2.extraTooltip and ".\n" .. item2.extraTooltip or ""), | ||
| + | width=args.width or 30, | ||
| + | heigth=args.height or nil | ||
| + | } | ||
| + | )) | ||
| + | local itemHtml = mw.html.create("td") | ||
| + | :wikitext('[[' .. item2.name .. ']]') | ||
| + | local quantityHtml = mw.html.create("td") | ||
| + | :css("text-align", "center") | ||
| + | :wikitext(createQuantityText(item)) | ||
| + | local chanceHtml = mw.html.create("td") | ||
| + | :css("text-align", "center") | ||
| + | :wikitext(toFixed(item.chance * 100, 3) .. "%") | ||
| + | local priceHtml = mw.html.create("td") | ||
| + | :css("text-align", "right") | ||
| + | :wikitext(createValueText(item2)) | ||
| + | local tradeableHtml = mw.html.create("td") | ||
| + | :css("text-align", "center") | ||
| + | :wikitext(item2.tradeable and "Yes" or "No") | ||
| + | local leagueHtml = mw.html.create("td") | ||
| + | :css("text-align", "center") | ||
| + | --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:wikitext(createWikitextImage( | ||
| + | league.name, | ||
| + | { | ||
| + | src="https://www.play.idlescape.com" .. league.icon, | ||
| + | alt=league.name, | ||
| + | width=args.width or 30, | ||
| + | heigth=args.height or nil | ||
| + | } | ||
| + | )) | ||
| + | else | ||
| + | leagueHtml:wikitext("?") | ||
| + | end | ||
end | end | ||
| + | else | ||
| + | leagueHtml:wikitext("All") | ||
end | 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 | end | ||
| − | + | return tableHtml | |
| − | return | ||
end | end | ||
return p | return p | ||
Revision as of 02:38, 10 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|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
---@class Item
---@field allowedLeagues number[]|nil 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|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(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.
---@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(loot, id, sourceType, locationId)
id = tostring(id)
locationId = locationId and tostring(locationId) or nil
if sourceType == "location" then
local location = loot[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 = loot[locationId]
for sourceId, source in pairs(location) do
if sourceId == id then
return source, locationId
end
end
return
end
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):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
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["location"] and findSourceId(args["location"])
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."
)
end
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."
)
end
local lang = mw.language.getContentLanguage()
---@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 ' heigth="' .. 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 image = mw.html.create("img"):attr(imageAttributes)
return string.format('[[%s|%s]]', title, tostring(image))
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, has no value in the data
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
--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 tableHtml = mw.html.create("table")
:addClass("wikitable sortable jquery-tablesorter")
--:node(createThs({ "⚙️", "Item", "Quantity", "Rarity", "Value", "Tradeable", "Leagues" }))
:node(createThs({ " ", "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")
:wikitext(createWikitextImage(
item2.name,
{
src="https://www.play.idlescape.com" .. src,
alt=item2.name .. (item2.extraTooltip and ".\n" .. item2.extraTooltip or ""),
width=args.width or 30,
heigth=args.height or nil
}
))
local itemHtml = mw.html.create("td")
:wikitext('[[' .. item2.name .. ']]')
local quantityHtml = mw.html.create("td")
:css("text-align", "center")
:wikitext(createQuantityText(item))
local chanceHtml = mw.html.create("td")
:css("text-align", "center")
:wikitext(toFixed(item.chance * 100, 3) .. "%")
local priceHtml = mw.html.create("td")
:css("text-align", "right")
:wikitext(createValueText(item2))
local tradeableHtml = mw.html.create("td")
:css("text-align", "center")
:wikitext(item2.tradeable and "Yes" or "No")
local leagueHtml = mw.html.create("td")
:css("text-align", "center")
--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:wikitext(createWikitextImage(
league.name,
{
src="https://www.play.idlescape.com" .. league.icon,
alt=league.name,
width=args.width or 30,
heigth=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