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.")
:done()
return nil, 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.")
:done()
return div, 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)
:done()
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)):done()
end
return tr, tostring(tr)
end
---Creates a mw.html.element image 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
---@return string
local function makeImg(name, alt, href, src, width, height)
local image = mw.html.create("td")
:tag("a")
: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()
return image, tostring(image)
end
local tableHtml = mw.html.create("table")
:addClass("wikitable sortable jquery-tablesorter")
:node(makeTr({ "Image", "Item", "Quantity", "Rarity", "Price", "Tradeable", "Leagues" }))
:done()
for _, item in ipairs(loot) do
local item2 = itemData[tostring(item.id)]
local imageHtml = makeImg(
item2.name,
item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""),
"/p/" .. item2.name,
"https://www.play.idlescape.com" .. item2.itemImage,
"45px"
)
local itemHtml = mw.html.create("td")
:tag("a")
:attr("href", "/p/" .. item2.name)
:attr("title", item2.name)
:wikitext(item2.name)
:done()
:done()
local quantityHtml = mw.html.create("td")
:wikitext(item.minAmount .. " - " .. item.maxAmount):done()
local chanceHtml = mw.html.create("td")
:wikitext(toFixed(item.chance * 100, 3) .. "%"):done()
local priceHtml = mw.html.create("td")
:wikitext(item2.value or "-"):done()
local tradeableHtml = mw.html.create("td")
:wikitext(item2.tradeable and "Yes" or "No"):done()
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 = makeImg(
league.name,
league.name,
"/p/" .. league.name,
"https://www.play.idlescape.com" .. league.icon,
"45px"
)
leaguesHtml:node(leagueHtml):done()
else
leaguesHtml:wikitext("?"):done()
end
end
else
leaguesHtml:wikitext("All"):done()
end
local rowHtml = mw.html.create("tr")
:node(imageHtml)
:node(itemHtml)
:node(quantityHtml)
:node(chanceHtml)
:node(priceHtml)
:node(tradeableHtml)
:node(leaguesHtml)
:allDone()
tableHtml:node(rowHtml):done()
end
tableHtml = tableHtml:allDone()
return tableHtml
end
return p