Module:Drop Sources

From Idlescape Wiki
Revision as of 20:37, 21 April 2025 by Demcookies (talk | contribs) (Add error if no id found for given item name)
Jump to navigation Jump to search

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