Module:Drop Sources
Revision as of 20:37, 21 April 2025 by Demcookies (talk | contribs) (Add error if no id found for given item name)
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" })
if itemId == "id not found" then
return "No id found for item '" .. name .. "'."
end
-- { 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
p.fid = findId._findId
return p