Module:Drop Sources
Revision as of 21:31, 22 April 2025 by Demcookies (talk | contribs) (Fix rename [Toggle all drops] to [Collapse] / [Expand] and style to look more like a button. Add darker background to rows that are from specific leagues)
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) if loot.allowedLeagues then rowHtml:css("background-color", "rgba(0, 0, 0, 0.15)") end 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 function createCustomCollapsible(collapsed, className) return mw.html.create("span") :attr("id", "mw-customcollapsible-" .. className) :css("font-weight", "700") :addClass("mw-collapsible " .. (collapsed and " mw-collapsed" or "")) :wikitext("[") :node(createSpan( collapsed and "Collapse" or "Expand", { ["role"] = "button" }, { --["cursor"] = "pointer", --["font-weight"] = "700", ["color"] = "#8cabe6" }, "mw-customtoggle-" .. className )) :wikitext("]") 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(createCustomCollapsible( false, className )) :node(createCustomCollapsible( true, 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