Difference between revisions of "Module:Drop Sources"
Jump to navigation
Jump to search
Demcookies (talk | contribs) (Feature collapse multiple drops from the same source under single row, can be toggled. Sorting the table also affects the collapsed rows. Rows expand relative to the sort order and not just under the "parent" row.) |
Demcookies (talk | contribs) m (Fix make whole [Toggle all drops] cell clickable) |
||
| Line 308: | Line 308: | ||
:attr("colspan", 3) | :attr("colspan", 3) | ||
:css("text-align", "center") | :css("text-align", "center") | ||
| + | :addClass("mw-customtoggle-" .. className) | ||
:node(createSpan( | :node(createSpan( | ||
"[Toggle all drops]", | "[Toggle all drops]", | ||
Revision as of 20:33, 22 April 2025
local p = {}
local findId = require("Module:FindId")
local lootData = mw.loadData("Module:Loot/data")
local itemsData = mw.loadData("Module:Items/data")
local leaguesData = mw.loadData("Module:Leagues/data")
local locationData = mw.loadData("Module:Location/data")
local monstersData = mw.loadData("Module:Monsters/data")
local monstersStatsData = mw.loadData("Module:Monsters stats/data")
---@class LootData
---@field id number The ID of the item.
---@field chance number The chance of the item dropping.
---@field minAmount number The minimum amount of the item that can drop.
---@field maxAmount number The maximum amount of the item that can drop.
---@field allowedLeagues number[]|nil The IDs of the leagues that allow the item to drop, or nil if allowed everywhere.
---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
---Formats the given number range.
---@param min number The minimum of the range.
---@param max number The maximum of the range.
---@return string # The formatted range as string.
local function toNumberRange(min, max)
local lang = mw.language.getContentLanguage()
if min == max then
return lang:formatNum(min)
end
return lang:formatNum(min) .. "–" .. lang:formatNum(max)
end
---Calculates the length of a table.
---@param t table
---@return number
local function getTableLength(t)
local count = 0
for _ in pairs(t) do
count = count + 1
end
return count
end
---Creates a deep copy of a table.
---@param t table
---@return table
local function copyTable(t)
local copy = {}
for k, v in pairs(t) do
if type(v) == "table" then
copy[k] = copyTable(v)
else
copy[k] = v
end
end
return copy
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 img = mw.html.create("img")
:attr(imageAttributes)
img = tostring(img):gsub("<img(.-) */>", "<img%1>")
return string.format('[[%s|%s]]', title, img)
end
---Finds locations, sources and `LootData` for a given item ID.
---{ locationId = { sourceId = { LootData1, LootData2, ... }, ... }, ... }
---@param itemId string|number The ID of the item to search for.
---@return table<string, table<string, LootData[]>>|nil # A table containing locations, sources and `LootData` for the item, or nil if not found.
local function getSources(itemId)
itemId = tostring(itemId)
local sources = {}
for locationId, location in pairs(lootData) do
for sourceId, source in pairs(location) do
for _, drop in ipairs(source) do
if tostring(drop.id) == itemId then
sources[locationId] = sources[locationId] or {}
sources[locationId][sourceId] = sources[locationId][sourceId] or {}
table.insert(sources[locationId][sourceId], drop)
end
end
end
end
return sources
end
---Gets monster or location object based on the source ID and optionally location ID for locations and override monster.
---@param sourceId string|number The ID of the source, -1 for locations.
---@param locationId string|number|nil The ID of the location (optional), required for locations.
---@return table|nil # The stats of the monster or nil if not found.
local function getSource(sourceId, locationId)
sourceId = tostring(sourceId)
if locationId then
locationId = tostring(locationId)
--If sourceId is "-1", it means we are looking for a location
if sourceId == "-1" then
local location = locationData[locationId]
if location then
return copyTable(location)
end
end
local baseMonster = monstersStatsData[sourceId]
for _, monster in pairs(baseMonster.overrides or {}) do
for _, location in ipairs(monster.locations or {}) do
if location and tostring(location.id) == locationId then
return copyTable(monster)
end
end
end
end
--Tries to get the stats of base monster if not a location or no override is found
-- e.g. no locationId or was given but no ovverides for the monster
return copyTable(monstersStatsData[sourceId])
end
---Converts location-source-loot table to name-sourceStats-loot table.
---@param sources table<string, table<string, LootData[]>> # The drop sources for the item.
---@return table<string, table> # A table containing the sources and their loot data and and optional Invalid IDs table.
--[[
return {
["Source Name"] = {
_loot = { LootData1, LootData2, ... },
_id = sourceId,
`unpack key-value pairs from Module monstersStatsData or locationData for the source`
},
...,
["Invalid ID"] = { invalidSourceId1, invalidSourceId2, ... }
}
]]
local function getSourceList(sources)
local sourceList = {}
for locationId, sources_ in pairs(sources) do
for sourceId, loot in pairs(sources_) do
local source = getSource(sourceId, locationId)
if source then
local name = source.name
sourceList[name] = sourceList[name] or source
sourceList[name]._loot = sourceList[name]._loot or loot
sourceList[name]._id = sourceList[name]._id or sourceId
else
sourceList["Invalid ID"] = sourceList["Invalid ID"] or {}
table.insert(sourceList["Invalid ID"], sourceId)
end
end
end
return sourceList
end
---Creates a mw.html TR element containing monster and loot data.
---@param name string The name of the monster or location.
---@param src string The source of the image.
---@param loot LootData The loot data for the item.
---@param imageWidth string|number The width of the image.
---@param imageHeight string|number The height of the image.
---@return mw.html|table # mw.html TR element containing monster and loot data.
local function createMonsterLootRow(name, src, loot, imageWidth, imageHeight)
local imageHtml = mw.html.create("td")
:wikitext(createWikitextImage(
name,
{
src = "https://www.play.idlescape.com" .. src,
alt = name,
width = imageWidth,
height = imageHeight
}
))
local itemHtml = mw.html.create("td")
:wikitext('[[' .. name .. ']]')
local quantityHtml = mw.html.create("td")
:css("text-align", "center")
:wikitext(toNumberRange(loot.minAmount, loot.maxAmount))
local chanceHtml = mw.html.create("td")
:css("text-align", "center")
:wikitext(toFixed(loot.chance * 100, 3) .. "%")
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 loot.allowedLeagues then
for _, leagueId in ipairs(loot.allowedLeagues) do
local league = leaguesData[tostring(leagueId)]
if league then
leagueHtml:wikitext(createWikitextImage(
league.name,
{
src = "https://www.play.idlescape.com" .. league.icon,
alt = league.name,
width = 30,
height = 30
}
))
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(leagueHtml)
return rowHtml
end
---Creates a table of drop sources for a given item.
---@param sources table<string, table<string, LootData[]>> # The drop sources for the item.
---@param imageWidth string|number # The width of the images in the table.
---@param imageHeight string|number # The height of the images in the table.
local function createDropSourceTable(sources, imageWidth, imageHeight)
---@param names string[] Array of wikitext.
---@return mw.html|table # 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(mw.html.create("th")
:addClass("headerSort")
:attr("tabindex", 0)
:attr("title", "Sort ascending")
:wikitext(name)
)
end
return tr
end
---Creates a mw.html SPAN element with optional text, attributes, CSS and class.
---@param text string|nil The text to be added to the span.
---@param attr table|nil The attributes to be added to the span.
---@param css table|nil The CSS styles to be added to the span.
---@param class string|nil The classes to be added to the span.
---@return table|mw.html # mw.html SPAN element.
local function createSpan(text, attr, css, class)
local span = mw.html.create("span")
if attr then span:attr(attr) end
if css then span:css(css) end
if class then span:addClass(class) end
if text then span:wikitext(text) end
return span
end
local tableHtml = mw.html.create("table")
:addClass("wikitable sortable mw-collapsible")
--[[ Adding caption and collapsing the table did sift the content after the table under infobox
(normally it would occupy the space next to it), so captions are disabled for now
:tag("caption")
:wikitext("Monsters and locations")
:done()
]]
:node(createThs({ " ", "Source", "Quantity", "Rarity", "Leagues" }))
for name, source in pairs(getSourceList(sources)) do
local rows = {}
if name == "Invalid ID" then
table.insert(
rows,
mw.html.create("tr")
:tag("td")
:attr("colspan", 5)
:wikitext("No sources with IDs: " .. table.concat(source, ", "))
:done()
)
else
local _loot = source._loot
local multiDrop = getTableLength(_loot) > 1
local relatedKey = itemsData[tostring(source.relatedKey)]
local relatedKeyImage = relatedKey and relatedKey.itemImage
local src = source.imageOverride or relatedKeyImage or (monstersData[tostring(source._id)] or {}).image
src = src and src:sub(1, 1) ~= "/" and "/" .. src or src
local className = name:gsub(" ", "_"):gsub("[^%w_]", "")
if multiDrop then
table.insert(
rows,
mw.html.create("tr")
:tag("td")
:wikitext(createWikitextImage(
name,
{
src = "https://www.play.idlescape.com" .. src,
alt = name,
width = imageWidth,
height = imageHeight
}
))
:done()
:tag("td")
:wikitext("[[" .. name .. "]]")
:done()
:tag("td")
:attr("colspan", 3)
:css("text-align", "center")
:addClass("mw-customtoggle-" .. className)
:node(createSpan(
"[Toggle all drops]",
{ ["role"] = "button" },
{
["cursor"] = "pointer",
["font-weight"] = "700",
["color"] = "var(--color-progressive,#36c)"
},
"mw-customtoggle-" .. className
))
:node(createSpan(
"▼",
{ ["id"] = "mw-customcollapsible-" .. className },
{ ["float"] = "right" },
"mw-collapsible"
))
:node(createSpan(
"▲",
{ ["id"] = "mw-customcollapsible-" .. className },
{ ["float"] = "right" },
"mw-collapsible mw-collapsed"
))
:done()
)
end
for _, loot in ipairs(_loot) do
local row = createMonsterLootRow(name, src, loot, imageWidth, imageHeight)
if multiDrop then
row
:addClass("mw-collapsible mw-collapsed")
:attr("id", "mw-customcollapsible-" .. className)
end
table.insert(rows, row)
end
end
for _, row in ipairs(rows) do
tableHtml:node(row)
end
end
return tableHtml
end
function p.dropSources(frame)
local args = frame:getParent().args or {}
return p._dropSources(args)
end
function p._dropSources(args)
local name = args["name"] or args["title"] or mw.title.getCurrentTitle().text
local itemId = findId._findId({ name, "item" })
if itemId == "id not found" then
return "No id found for item '" .. name .. "'."
end
local sources = getSources(itemId)
if not sources then
return "No sources found for '" .. name .. "', Modules: Loot/data or Monsters_stats/data may be outdated."
end
local html = createDropSourceTable(sources, args["width"] or 30, args["height"] or 30)
return html
end
return p