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