Difference between revisions of "Module:Drop Sources"

From Idlescape Wiki
Jump to navigation Jump to search
(Update name of imported module)
(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 img = require("Module:Img")
+
local lootData = mw.loadData("Module:Loot/data")
local monstersDropsData = mw.loadData("Module:Monsters drops/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 monstersData = mw.loadData("Module:Monsters/data")
 +
local monstersStatsData = mw.loadData("Module:Monsters stats/data")
  
local function pairsByKeys(t, f)
+
 
    local a = {}
+
---@class LootData
    local orgi_key_type
+
---@field id number The ID of the item.
    local orgi_key_numbered
+
---@field chance number The chance of the item dropping.
    for n in pairs(t) do
+
---@field minAmount number The minimum amount of the item that can drop.
        if tonumber(n) == nil then
+
---@field maxAmount number The maximum amount of the item that can drop.
            table.insert(a, n)
+
---@field allowedLeagues number[]|nil The IDs of the leagues that allow the item to drop, or nil if allowed everywhere.
            orgi_key_type = "word"
+
 
        elseif type(n) == "number" then
+
 
            table.insert(a, n)
+
---Formats a number up to a given number of decimal places, removing trailing zeros.
            orgi_key_type = "int"
+
---@param num number The number to format.
        elseif type(n) == "string" and type(tonumber(n) == "number") then
+
---@param digits number The number of decimal places to include (must be a non-negative integer).
            orgi_key_type = "number"
+
---@return string # The formatted number as a string.
            table.insert(a, tonumber(n))
+
local function toFixed(num, digits)
        end
+
    digits = math.max(0, math.floor(digits))
    end
+
    local formatted = string.format("%." .. digits .. "f", num):gsub("%.?0+$", "")
    table.sort(a, f)
+
    return formatted
    local key
+
end
    local value
+
 
    local i = 0            -- iterator variable
+
---Formats the given number range.
    local iter = function() -- iterator function
+
---@param min number The minimum of the range.
        i = i + 1
+
---@param max number The maximum of the range.
        if a[i] == nil then
+
---@return string # The formatted range as string.
            return nil
+
local function toNumberRange(min, max)
        elseif orgi_key_type == "word" or orgi_key_type == "int" then
+
    local lang = mw.language.getContentLanguage()
            key = a[i]
+
    if min == max then
            value = t[a[i]]
+
        return lang:formatNum(min)
        elseif orgi_key_type == "number" then
 
            key = tostring(a[i])
 
            value = t[tostring(a[i])]
 
        end
 
        return key, value
 
 
     end
 
     end
     return iter
+
     return lang:formatNum(min) .. "–" .. lang:formatNum(max)
 
end
 
end
  
local function round(num)
+
---Creates an Wikitext link with an image.
     return tostring(math.floor(num * 1000 + 0.5) / 1000)
+
---@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 getMonsters(itemId)
+
---Finds locations, sources and `LootData` for a given item ID.
     local a = {}
+
---{locationId = { sourceId = { LootData1, LootData2, ... }, ... }, ... }
     local monsters = {}
+
---@param itemId string|number The ID of the item to search for.
     for monsterId, monsterDrops in pairsByKeys(monstersDropsData) do
+
---@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(monsterDrops) do
+
local function getSources(itemId)
            if drop["id"] == itemId then
+
     itemId = tostring(itemId)
                if not monsters[monsterId] then
+
     local sources = {}
                    monsters[monsterId] = {}
+
     for locationId, location in pairs(lootData) do
                     table.insert(monsters[monsterId],drop)
+
         for sourceId, source in pairs(location) do
                else
+
            for _, drop in ipairs(source) do
                    table.insert(monsters[monsterId],drop)
+
                if tostring(drop.id) == itemId then
 +
                    sources[locationId] = sources[locationId] or {}
 +
                     sources[locationId][sourceId] = drop
 
                 end
 
                 end
 
             end
 
             end
 
         end
 
         end
 
     end
 
     end
     return monsters
+
     return sources
 
end
 
end
  
local function getMonsterName(monsterId)
+
---Gets monster or location object based on the source ID and optionally location ID for locations and override monster.
     local monster = monstersData[tostring(monsterId)]
+
---@param sourceId string|number The ID of the source, -1 for locations.
     if monster then
+
---@param locationId string|number|nil The ID of the location (optional), required for locations.
         return monster["name"]
+
---@return table|nil # The stats of the monster or nil if not found.
    else
+
local function getSource(sourceId, locationId)
         return "Module:Monsters/data out of date"
+
     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 genTable(monsters)
+
---Creates a mw.html TR element containing monster and loot data.
     local s = "{|class=\"wikitable sortable\"\n"
+
---@param name string The name of the monster or location.
     s = s .. "!colspan=2|Monster\n"
+
---@param src string The source of the image.
    s = s .. "!Quantity\n"
+
---@param loot LootData The loot data for the item.
     s = s .. "!Rarity\n"
+
---@param imageWidth string|number The width of the image.
    s = s .. "|-\n"
+
---@param imageHeight string|number The height of the image.
    for id, monsterDrops in pairsByKeys(monsters) do
+
---@return mw.html|table # mw.html TR element containing monster and loot data.
         if #monsterDrops == 1 then
+
local function createMonsterLootRow(name, src, loot, imageWidth, imageHeight)
            s = s .. "|".. img._img({getMonsterName(id),"40"}) .. "\n"
+
     local imageHtml = mw.html.create("td")
            s = s .. "|[[" .. getMonsterName(id) .. "]]\n"
+
        :wikitext(createWikitextImage(
            if monsterDrops[1]["minAmount"] == monsterDrops[1]["maxAmount"] then
+
            name,
                s = s .. "|style=\"text-align:center;\"|" .. tostring(monsterDrops[1]["minAmount"]) .. "\n"
+
            {
 +
                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
                 s = s ..
+
                 leagueHtml:wikitext("?")
                "|style=\"text-align:center;\"|" .. tostring(monsterDrops[1]["minAmount"]) .. "-" .. tostring(monsterDrops[1]["maxAmount"]) .. "\n"
 
 
             end
 
             end
            s = s .. "|style=\"text-align:center;\"|" .. round(monsterDrops[1]["chance"] * 100) .. "%\n"
+
        end
            s = s .. "|-\n"
+
    else
         elseif #monsterDrops > 1 then
+
        leagueHtml:wikitext("All")
             s = s .. "|rowspan=" .. tostring(#monsterDrops) .."\"|" .. img._img({getMonsterName(id),"40"}) .. "\n"
+
    end
            s = s .. "|rowspan=" .. tostring(#monsterDrops) .. "\"|[[" .. getMonsterName(id) .. "]]\n"
+
 
            for _, drop in ipairs(monsterDrops) do
+
    local rowHtml = mw.html.create("tr")
                 if drop["minAmount"] == drop["maxAmount"] then
+
        :node(imageHtml)
                     s = s .. "|style=\"text-align:center;\"|" .. tostring(drop["minAmount"]) .. "\n"
+
        :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
                     s = s ..
+
                     row = mw.html.create("tr")
                    "|style=\"text-align:center;\"|" .. tostring(drop["minAmount"]) .. "-" .. tostring(drop["maxAmount"]) .. "\n"
+
                        :tag("td")
 +
                        :wikitext("No source with ID " .. sourceId)
 +
                        :done()
 
                 end
 
                 end
                 s = s .. "|style=\"text-align:center;\"|" .. round(drop["chance"] * 100) .. "%\n"
+
                 tableHtml:node(row)
                s = s .. "|-\n"
 
 
             end
 
             end
 
         end
 
         end
 
     end
 
     end
    s = s .. "|}"
+
 
     return s
+
     return tableHtml
 
end
 
end
 +
  
 
function p.dropSources(frame)
 
function p.dropSources(frame)
     return p._dropSources(frame:getParent().args)
+
     local args = frame:getParent().args or {}
 +
    return p._dropSources(args)
 
end
 
end
  
function p._dropSources(_args)
+
function p._dropSources(args)
     local itemId = findId._findId({ _args[1], "item" })
+
    local name = args["name"] or args["title"] or mw.title.getCurrentTitle().text
     local monsters = getMonsters(itemId)
+
     local itemId = findId._findId({ name, "item" })
     local s = genTable(monsters)
+
    -- { locationId = { sourceId = { drop1, drop2, ... }, ... }, ... }
     return s
+
     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