Difference between revisions of "Module:Drop Sources"
Jump to navigation
Jump to search
(Update name of imported module) |
Demcookies (talk | contribs) (Refactor complete rewrite of the module. Add elite challenges, dungeons and override monsters) |
||
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 | + | 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 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 | end | ||
− | return | + | return lang:formatNum(min) .. "–" .. lang:formatNum(max) |
end | end | ||
− | local function | + | ---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 | end | ||
− | local function | + | ---Finds locations, sources and `LootData` for a given item ID. |
− | + | ---{locationId = { sourceId = { LootData1, LootData2, ... }, ... }, ... } | |
− | local | + | ---@param itemId string|number The ID of the item to search for. |
− | for | + | ---@return table<string, table<string, LootData>>|nil # A table containing locations, sources and `LootData` for the item, or nil if not found. |
− | for _, drop in ipairs( | + | 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] = drop | ||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
− | return | + | return sources |
end | end | ||
− | local function | + | ---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. | |
− | if | + | ---@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) | |
− | return | + | 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 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 monster | ||
+ | end | ||
+ | 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 monstersStatsData[sourceId] | ||
end | end | ||
− | local function | + | ---Creates a mw.html TR element containing monster and loot data. |
− | local | + | ---@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) .. "%") | ||
+ | :css("text-align", "center") | ||
+ | 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 | else | ||
− | + | leagueHtml:wikitext("?") | |
− | |||
end | 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 | ||
+ | |||
+ | local seen = {} | ||
+ | local tableHtml = mw.html.create("table") | ||
+ | :addClass("wikitable sortable mw-collapsible") | ||
+ | :node(createThs({ " ", "Source", "Quantity", "Rarity", "Leagues" })) | ||
+ | |||
+ | for locationId, sources_ in pairs(sources) do | ||
+ | for sourceId, loot in pairs(sources_) do | ||
+ | local source = getSource(sourceId, locationId) | ||
+ | local hash = tostring(source and source.name) | ||
+ | if not seen[hash] then | ||
+ | seen[hash] = true | ||
+ | local row | ||
+ | if source then | ||
+ | local name = source.name | ||
+ | local relatedKey = itemsData[tostring(source.relatedKey)] | ||
+ | local relatedKeyImage = relatedKey and relatedKey.itemImage | ||
+ | local src = source.imageOverride or relatedKeyImage or monstersData[sourceId].image | ||
+ | src = src and src:sub(1, 1) ~= "/" and "/" .. src or src | ||
+ | row = createMonsterLootRow(name, src, loot, imageWidth, imageHeight) | ||
else | else | ||
− | + | row = mw.html.create("tr") | |
− | + | :tag("td") | |
+ | :wikitext("No source with ID " .. sourceId) | ||
+ | :done() | ||
end | end | ||
− | + | tableHtml:node(row) | |
− | |||
end | end | ||
end | end | ||
end | end | ||
− | + | ||
− | return | + | return tableHtml |
end | end | ||
+ | |||
function p.dropSources(frame) | function p.dropSources(frame) | ||
− | + | local args = frame:getParent().args or {} | |
+ | return p._dropSources(args) | ||
end | end | ||
− | function p._dropSources( | + | function p._dropSources(args) |
− | local itemId = findId._findId({ | + | local name = args["name"] or args["title"] or mw.title.getCurrentTitle().text |
− | local | + | local itemId = findId._findId({ name, "item" }) |
− | local | + | -- { locationId = { sourceId = { drop1, drop2, ... }, ... }, ... } |
− | return | + | 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 | end | ||
+ | |||
return p | return p |
Revision as of 20:30, 21 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 ---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] = 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 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 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 monstersStatsData[sourceId] 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) .. "%") :css("text-align", "center") 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 local seen = {} local tableHtml = mw.html.create("table") :addClass("wikitable sortable mw-collapsible") :node(createThs({ " ", "Source", "Quantity", "Rarity", "Leagues" })) for locationId, sources_ in pairs(sources) do for sourceId, loot in pairs(sources_) do local source = getSource(sourceId, locationId) local hash = tostring(source and source.name) if not seen[hash] then seen[hash] = true local row if source then local name = source.name local relatedKey = itemsData[tostring(source.relatedKey)] local relatedKeyImage = relatedKey and relatedKey.itemImage local src = source.imageOverride or relatedKeyImage or monstersData[sourceId].image src = src and src:sub(1, 1) ~= "/" and "/" .. src or src row = createMonsterLootRow(name, src, loot, imageWidth, imageHeight) else row = mw.html.create("tr") :tag("td") :wikitext("No source with ID " .. sourceId) :done() end tableHtml:node(row) end 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" }) -- { locationId = { sourceId = { drop1, drop2, ... }, ... }, ... } 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