Difference between revisions of "Module:Loot table"

From Idlescape Wiki
Jump to navigation Jump to search
(Monster Drops)
 
(Add basic / initial Module functionality)
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 itemsData = mw.loadData("Module:Items/data")
+
local itemData = mw.loadData("Module:Items/data")
local monstersDropsData = mw.loadData("Module:Monsters drops/data")
+
local leagueData = mw.loadData("Module:Leagues/data")
local monstersData = mw.loadData("Module:Monsters/data")
 
local pageName = mw.title.getCurrentTitle().fullText
 
  
local function round(num)
+
 
     return tostring(math.floor(num * 1000 + 0.5) / 1000)
+
---Finds the source ID for a given name.
 +
---@param name string The name of the source.
 +
---@return number|nil The source ID (monsterID or locationID), or nil if the source is not found.
 +
---@return string The source type ("monster" or "location"), or error message if the source is not found.
 +
local function findSourceId(name)
 +
  for _, source in ipairs({ "monster", "location" }) do
 +
     local id = findId._findId({ name, source })
 +
    if id ~= "id not found" then
 +
      return id, source
 +
    end
 +
  end
 +
  local div = mw.html.create("div")
 +
    :css("color", "red")
 +
    :wikitext("No monster or location named '" .. name .. "' The Module:Loot/data may be outdated.")
 +
  return nil, tostring(div)
 
end
 
end
  
local function addSeparator(num)
+
---@class Item
     return tostring(tonumber(num)):reverse():gsub("(%d%d%d)", "%1,"):gsub(",(%-?)$", "%1"):reverse()
+
---@field allowedLeagues? number[] # The leagues in which the item can be found. Found in all leagues if nil.
 +
---@field id integer # The item ID.
 +
---@field chance number # The base chance of the item being dropped.
 +
---@field minAmount integer # The minimum base amount of the item that can be dropped.
 +
---@field maxAmount integer # The maximum base amount of the item that can be dropped.
 +
 
 +
---Finds the first item in the loot table that matches the given item ID.
 +
---@param loot table # The loot table from Module:Loot/data.
 +
---@param id string|number # The ID of the item to find.
 +
---@return Item? # The item data, or nil if the item is not found.
 +
---@return string? # The monsterId where the item was found ("-1" for locations), or nil if the item was not found.
 +
---@return string? # The locationId where the item was found, or nil if the item was not found.
 +
local function getFirstLootMatch(loot, id)
 +
     id = tostring(id)
 +
  for locationId, location in pairs(loot) do
 +
    for sourceId, source in pairs(location) do
 +
      for _, item in ipairs(source) do
 +
        if item.id == id then
 +
          return item, sourceId, locationId
 +
        end
 +
      end
 +
    end
 +
  end
 +
  return
 
end
 
end
  
---decides the collase state based on the given string
+
---Finds the first source in the loot table that matches the given sourceId.
---@param collapse string
+
---@param loot table # The loot table from Module:Loot/data.
---@return string
+
---@param id string|number # The ID of the source to find.
local function decideCollapseState(collapse)
+
---@return Item[]? # The source data, or nil if the source was not found.
     if collapse == "collapsible" then
+
---@return string? # The locationId where the source was found, or nil if the source was not found.
        collapse = " mw-made-collapsible mw-collapsible"
+
local function getFirstSourceMatch(loot, id)
     elseif collapse == "collapsed" then
+
     id = tostring(id)
         collapse = " mw-made-collapsible mw-collapsible mw-collapsed"
+
  for locationId, location in pairs(loot) do
    else
+
     for sourceId, source in pairs(location) do
        collapse = ""
+
      if sourceId == id then
 +
         return source, locationId
 +
      end
 
     end
 
     end
    return collapse
+
  end
 +
  return
 
end
 
end
  
local function pairsByKeys(t, f)
+
--TODO: Add option to show percentages as fractions
     local a = {}
+
 
     local orgi_key_type
+
---Converts a floating-point number to a fraction.
     local orgi_key_numbered
+
---@param num number The floating-point number to convert.
     for n in pairs(t) do
+
---@param maxDenominator number The maximum denominator for the fraction.
         if tonumber(n) == nil then
+
---@return number The numerator of the fraction.
            table.insert(a, n)
+
---@return number The denominator of the fraction.
            orgi_key_type = "word"
+
local function floatToFraction(num, maxDenominator)
         elseif type(n) == "number" then
+
     local sign = num < 0 and -1 or 1
            table.insert(a, n)
+
    num = math.abs(num)
            orgi_key_type = "int"
+
 
         elseif type(n) == "string" and type(tonumber(n) == "number") then
+
     local bestNumerator, bestDenominator = 1, 1
             orgi_key_type = "number"
+
     local minDifference = math.huge
             table.insert(a, tonumber(n))
+
 
 +
     for d = 1, maxDenominator do
 +
         local n = math.floor(num*d + 0.5)
 +
        local approx = n / d
 +
         local difference = math.abs(num - approx)
 +
 
 +
         if difference < minDifference then
 +
             bestNumerator, bestDenominator = n, d
 +
             minDifference = difference
 
         end
 
         end
 
     end
 
     end
    table.sort(a, f)
+
 
    local key
+
     return sign * bestNumerator, bestDenominator
     local value
 
    local i = 0            -- iterator variable
 
    local iter = function() -- iterator function
 
        i = i + 1
 
        if a[i] == nil then
 
            return nil
 
        elseif orgi_key_type == "word" or orgi_key_type == "int" then
 
            key = a[i]
 
            value = t[a[i]]
 
        elseif orgi_key_type == "number" then
 
            key = tostring(a[i])
 
            value = t[tostring(a[i])]
 
        end
 
        return key, value
 
    end
 
    return iter
 
 
end
 
end
  
---fetches an item object from items/data module
+
---Converts a floating-point number to a fraction string.
---@param itemId integer|string or string
+
---@param num number The floating-point number to convert.
---@return table|string
+
---@param maxDenominator number The maximum denominator for the fraction.
local function getItem(itemId)
+
---@return string The fraction string.
     local item = itemsData[tostring(itemId)]
+
local function floatToFractionString(num, maxDenominator)
     if item then
+
     local numerator, denominator = floatToFraction(num, maxDenominator)
         return item
+
     if denominator == 1 then
    else
+
         return tostring(numerator)
        return "Module:Items/data out of date"
 
 
     end
 
     end
 +
    return string.format("%d/%d", numerator, denominator)
 
end
 
end
  
local function createImgCell(item)
+
---Formats a number up to a given number of decimal places, removing trailing zeros.
    local imgCell = mw.html.create('td')
+
---@param num number The number to format.
        :wikitext(img._img({ item.name, '30' }))
+
---@param digits number The number of decimal places to include (must be a non-negative integer).
        :done()
+
---@return string The formatted number as a string.
    return imgCell
+
local function toFixed(num, digits)
 +
  digits = math.max(0, math.floor(digits))
 +
  local formatted = string.format("%." .. digits .. "f", num)
 +
  formatted = formatted:gsub("%.?0+$", "")
 +
  return formatted
 
end
 
end
  
local function createNameCell(item)
+
function p.lootTable(frame)
    local nameCell = mw.html.create('td')
+
  local args = frame:getParent().args
        :wikitext("[[" .. item.name .. "]]")
+
  return p._lootTable(args)
        :done()
 
    return nameCell
 
 
end
 
end
  
local function createQuantityCell(drop)
+
function p._lootTable(_args)
     local s = ""
+
     local sourceName = _args["name"] or _args["title"] or mw.title.getCurrentTitle().text
     if drop.minAmount == drop.maxAmount then
+
    local sourceId, sourceTypeOrError = findSourceId(sourceName)
         s = drop.minAmount
+
     if not sourceId then
     else
+
         return sourceTypeOrError
         s = drop.minAmount .. "-" .. drop.maxAmount
+
    end
 +
 
 +
    local loot = getFirstSourceMatch(lootData, sourceId)
 +
     if not loot then
 +
         local div = mw.html.create("div")
 +
            :css("color", "red")
 +
            :wikitext("No loot found for monster or location named '" .. sourceName ..
 +
                "' The Module:Loot/data may be outdated.")
 +
        return tostring(div)
 
     end
 
     end
    local QuantityCell = mw.html.create('td'):css('text-align', 'center')
 
        :wikitext(s)
 
        :done()
 
    return QuantityCell
 
end
 
  
local function createRarityCell(drop)
+
    ---@param name any
    local RarityCell = mw.html.create('td'):css('text-align', 'center')
+
    ---@return mw.html.element
        :wikitext(round(drop.chance * 100) .. "%")
+
    ---@return string
        :done()
+
    local function makeTh(name)
    return RarityCell
+
        local th = mw.html.create("th")
end
+
            :addClass("headerSort")
 +
            :attr("tabindex", 0)
 +
            :attr("role", "columnheader button")
 +
            :attr("title", "Sort ascending")
 +
            :wikitext(name)
 +
        return th, tostring(th)
 +
    end
  
local function createPriceCell(item)
+
    ---@param names any[]
    local PriceCell = mw.html.create('td'):css('text-align', 'center')
+
    ---@return mw.html.element
         :wikitext(addSeparator(item.value or 1) .. " ")
+
    ---@return string
             :tag('img')
+
    local function makeTr(names)
                :attr('src', "https://www.play.idlescape.com/images/gold_coin.png")
+
        local tr = mw.html.create("tr")
                :attr('width', '15')
+
         for _, name in ipairs(names) do
                :attr('height', '15')
+
             tr:node(makeTh(name))
                :attr('alt', "Gold Coin")
+
        end
            :done()
+
        return tr, tostring(tr)
        :done()
+
     end
     return PriceCell
 
end
 
  
local function genTable(name, collapse)
 
  
     local mId = findId._findId({name, 'monster'})
+
     ---Creates a mw.html.element image & text container for loot tables.
     local t = mw.html.create('table')
+
    ---@param name string # The name of the item.
        :addClass("wikitable sortable jquery-tablesorter" .. collapse)
+
     ---@param href string # The URI of the item page. Should be "/p/" .. name
        :tag('tr')
+
    ---@param src string # The URI of the item image.
        :tag('th')
+
    ---@param width string # The width of the item image as css param. Default is "auto".
        :attr("colspan", "2")
+
    ---@param height string? # The height of the item image as css param. Default is "auto".
        :wikitext("Item")
+
    ---@return mw.html.element
        :done()
+
    local function makeImageContainer(name, alt, href, src, width, height)
        :tag('th')
+
         return mw.html.create("td")
         :wikitext("Quantity")
+
            :tag("span")
        :done()
+
            :css("white-space", "nowrap")
        :tag('th')
+
            :tag("a")
        :wikitext("Rarity")
+
            :addClass("mw-redirect")
        :done()
+
            :attr("href", href)
        :tag('th')
+
            :attr("title", name)
        :wikitext("Vendor Price")
+
            :tag("img")
        :done()
+
             :attr("src", src)
        :done()
+
             :attr("alt", alt)
    for _, drop in ipairs(monstersDropsData[tostring(mId)]) do
+
             :attr("width", width or "auto")
        local item = getItem(drop.id)
+
             :attr("height", height or "auto")
        t:tag('tr')
 
             :node(createImgCell(item))
 
             :node(createNameCell(item))
 
             :node(createQuantityCell(drop))
 
             :node(createRarityCell(drop))
 
            :node(createPriceCell(item))
 
 
             :done()
 
             :done()
 +
            :wikitext(name)
 +
            :allDone()
 
     end
 
     end
    return t
 
end
 
  
function p.monsterDrops(frame)
+
    local tableHtml = mw.html.create("table")
     return p._monsterDrops(frame:getParent().args)
+
        :addClass("wikitable sortable jquery-tablesorter")
end
+
        :tag("thead")
 +
        :node(makeTr({ "Item", "Quantity", "Rarity", "Price", "League" }))
 +
        :done()
 +
        :tag("tbody")
 +
 
 +
     for _, item in ipairs(loot) do
 +
        local item2 = itemData[tostring(item.id)]
 +
        local imageHtml = makeImageContainer(
 +
            item2.name,
 +
            item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""),
 +
            "/p/" .. item2.name,
 +
            "https://www.play.idlescape.com" .. item2.itemImage,
 +
            "45px"
 +
        )
 +
        local quantityHtml = mw.html.create("td")
 +
            :wikitext(item.minAmount .. " - " .. item.maxAmount)
 +
        local chanceHtml = mw.html.create("td")
 +
            :wikitext(toFixed(item.chance * 100, 3) .. "%")
 +
        local priceHtml = mw.html.create("td")
 +
            :wikitext(itemData[tostring(item.id)].value or "-")
 +
        local tradeableHtml = mw.html.create("td")
 +
            :wikitext(itemData[tostring(item.id)].tradeable or false)
 +
        local leaguesHtml = mw.html.create("td")
 +
        --TODO: Add indicator if league is active, the field is present in Module:Leagues/data but is incorrect
 +
        if item.allowedLeagues then
 +
            for _, leagueId in ipairs(item.allowedLeagues) do
 +
                local league = leagueData[tostring(leagueId)]
 +
                if league then
 +
                    local leagueHtml = makeImageContainer(
 +
                        league.name,
 +
                        league.name,
 +
                        "/p/" .. league.name,
 +
                        "https://www.play.idlescape.com" .. league.icon,
 +
                        "45px"
 +
                    )
 +
                    leaguesHtml:node(leagueHtml)
 +
                else
 +
                    leaguesHtml:wikitext("?")
 +
                end
 +
            end
 +
        else
 +
            leaguesHtml:wikitext("All")
 +
        end
 +
        local itemHtml = mw.html.create("tr")
 +
            :node(imageHtml)
 +
            :node(quantityHtml)
 +
            :node(chanceHtml)
 +
            :node(priceHtml)
 +
            :node(tradeableHtml)
 +
            :node(leaguesHtml)
 +
            :allDone()
  
function p._monsterDrops(_args)
+
        tableHtml:node(itemHtml)
    local name = ""
 
    if _args[1] then
 
        name = _args[1]
 
    else
 
        name = pageName
 
 
     end
 
     end
     local t = genTable(name, decideCollapseState(_args["collapse"]))
+
 
    return t
+
     return tostring(tableHtml)
 
end
 
end
  
 
return p
 
return p

Revision as of 20:11, 1 April 2025


local p = {}
local findId = require("Module:FindId")
local lootData = mw.loadData("Module:Loot/data")
local itemData = mw.loadData("Module:Items/data")
local leagueData = mw.loadData("Module:Leagues/data")


---Finds the source ID for a given name.
---@param name string The name of the source.
---@return number|nil The source ID (monsterID or locationID), or nil if the source is not found.
---@return string The source type ("monster" or "location"), or error message if the source is not found.
local function findSourceId(name)
  for _, source in ipairs({ "monster", "location" }) do
    local id = findId._findId({ name, source })
    if id ~= "id not found" then
      return id, source
    end
  end
  local div = mw.html.create("div")
    :css("color", "red")
    :wikitext("No monster or location named '" .. name .. "' The Module:Loot/data may be outdated.")
  return nil, tostring(div)
end

---@class Item
---@field allowedLeagues? number[] # The leagues in which the item can be found. Found in all leagues if nil.
---@field id integer # The item ID.
---@field chance number # The base chance of the item being dropped.
---@field minAmount integer # The minimum base amount of the item that can be dropped.
---@field maxAmount integer # The maximum base amount of the item that can be dropped.

---Finds the first item in the loot table that matches the given item ID.
---@param loot table # The loot table from Module:Loot/data.
---@param id string|number # The ID of the item to find.
---@return Item? # The item data, or nil if the item is not found.
---@return string? # The monsterId where the item was found ("-1" for locations), or nil if the item was not found.
---@return string? # The locationId where the item was found, or nil if the item was not found.
local function getFirstLootMatch(loot, id)
    id = tostring(id)
  for locationId, location in pairs(loot) do
    for sourceId, source in pairs(location) do
      for _, item in ipairs(source) do
        if item.id == id then
          return item, sourceId, locationId
        end
      end
    end
  end
  return
end

---Finds the first source in the loot table that matches the given sourceId.
---@param loot table # The loot table from Module:Loot/data.
---@param id string|number # The ID of the source to find.
---@return Item[]? # The source data, or nil if the source was not found.
---@return string? # The locationId where the source was found, or nil if the source was not found.
local function getFirstSourceMatch(loot, id)
    id = tostring(id)
  for locationId, location in pairs(loot) do
    for sourceId, source in pairs(location) do
      if sourceId == id then
        return source, locationId
      end
    end
  end
  return
end

--TODO: Add option to show percentages as fractions

---Converts a floating-point number to a fraction.
---@param num number The floating-point number to convert.
---@param maxDenominator number The maximum denominator for the fraction.
---@return number The numerator of the fraction.
---@return number The denominator of the fraction.
local function floatToFraction(num, maxDenominator)
    local sign = num < 0 and -1 or 1
    num = math.abs(num)

    local bestNumerator, bestDenominator = 1, 1
    local minDifference = math.huge

    for d = 1, maxDenominator do
        local n = math.floor(num*d + 0.5)
        local approx = n / d
        local difference = math.abs(num - approx)

        if difference < minDifference then
            bestNumerator, bestDenominator = n, d
            minDifference = difference
        end
    end

    return sign * bestNumerator, bestDenominator
end

---Converts a floating-point number to a fraction string. 
---@param num number The floating-point number to convert.
---@param maxDenominator number The maximum denominator for the fraction.
---@return string The fraction string.
local function floatToFractionString(num, maxDenominator)
    local numerator, denominator = floatToFraction(num, maxDenominator)
    if denominator == 1 then
        return tostring(numerator)
    end
    return string.format("%d/%d", numerator, denominator)
end

---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)
  formatted = formatted:gsub("%.?0+$", "")
  return formatted
end

function p.lootTable(frame)
  local args = frame:getParent().args
  return p._lootTable(args)
end

function p._lootTable(_args)
    local sourceName = _args["name"] or _args["title"] or mw.title.getCurrentTitle().text
    local sourceId, sourceTypeOrError = findSourceId(sourceName)
    if not sourceId then
        return sourceTypeOrError
    end

    local loot = getFirstSourceMatch(lootData, sourceId)
    if not loot then
        local div = mw.html.create("div")
            :css("color", "red")
            :wikitext("No loot found for monster or location named '" .. sourceName ..
                "' The Module:Loot/data may be outdated.")
        return tostring(div)
    end

    ---@param name any
    ---@return mw.html.element
    ---@return string
    local function makeTh(name)
        local th = mw.html.create("th")
            :addClass("headerSort")
            :attr("tabindex", 0)
            :attr("role", "columnheader button")
            :attr("title", "Sort ascending")
            :wikitext(name)
        return th, tostring(th)
    end

    ---@param names any[]
    ---@return mw.html.element
    ---@return string
    local function makeTr(names)
        local tr = mw.html.create("tr")
        for _, name in ipairs(names) do
            tr:node(makeTh(name))
        end
        return tr, tostring(tr)
    end


    ---Creates a mw.html.element image & text container for loot tables.
    ---@param name string # The name of the item.
    ---@param href string # The URI of the item page. Should be "/p/" .. name
    ---@param src string # The URI of the item image.
    ---@param width string # The width of the item image as css param. Default is "auto".
    ---@param height string? # The height of the item image as css param. Default is "auto".
    ---@return mw.html.element
    local function makeImageContainer(name, alt, href, src, width, height)
        return mw.html.create("td")
            :tag("span")
            :css("white-space", "nowrap")
            :tag("a")
            :addClass("mw-redirect")
            :attr("href", href)
            :attr("title", name)
            :tag("img")
            :attr("src", src)
            :attr("alt", alt)
            :attr("width", width or "auto")
            :attr("height", height or "auto")
            :done()
            :wikitext(name)
            :allDone()
    end

    local tableHtml = mw.html.create("table")
        :addClass("wikitable sortable jquery-tablesorter")
        :tag("thead")
        :node(makeTr({ "Item", "Quantity", "Rarity", "Price", "League" }))
        :done()
        :tag("tbody")

    for _, item in ipairs(loot) do
        local item2 = itemData[tostring(item.id)]
        local imageHtml = makeImageContainer(
            item2.name,
            item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""),
            "/p/" .. item2.name,
            "https://www.play.idlescape.com" .. item2.itemImage,
            "45px"
        )
        local quantityHtml = mw.html.create("td")
            :wikitext(item.minAmount .. " - " .. item.maxAmount)
        local chanceHtml = mw.html.create("td")
            :wikitext(toFixed(item.chance * 100, 3) .. "%")
        local priceHtml = mw.html.create("td")
            :wikitext(itemData[tostring(item.id)].value or "-")
        local tradeableHtml = mw.html.create("td")
            :wikitext(itemData[tostring(item.id)].tradeable or false)
        local leaguesHtml = mw.html.create("td")
        --TODO: Add indicator if league is active, the field is present in Module:Leagues/data but is incorrect
        if item.allowedLeagues then
            for _, leagueId in ipairs(item.allowedLeagues) do
                local league = leagueData[tostring(leagueId)]
                if league then
                    local leagueHtml = makeImageContainer(
                        league.name,
                        league.name,
                        "/p/" .. league.name,
                        "https://www.play.idlescape.com" .. league.icon,
                        "45px"
                    )
                    leaguesHtml:node(leagueHtml)
                else
                    leaguesHtml:wikitext("?")
                end
            end
        else
            leaguesHtml:wikitext("All")
        end
        local itemHtml = mw.html.create("tr")
            :node(imageHtml)
            :node(quantityHtml)
            :node(chanceHtml)
            :node(priceHtml)
            :node(tradeableHtml)
            :node(leaguesHtml)
            :allDone()

        tableHtml:node(itemHtml)
    end

    return tostring(tableHtml)
end

return p