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