Difference between revisions of "Module:Loot table"

From Idlescape Wiki
Jump to navigation Jump to search
(Fix p.lootTable returns mw.html.element instead of string)
(Fix item image and name to their own cells. Remove manual thead and tbody tags)
Line 164: Line 164:
  
  
     ---Creates a mw.html.element image & text container for loot tables.
+
     ---Creates a mw.html.element image for loot tables.
 
     ---@param name string # The name of the item.
 
     ---@param name string # The name of the item.
 
     ---@param href string # The URI of the item page. Should be "/p/" .. name
 
     ---@param href string # The URI of the item page. Should be "/p/" .. name
Line 171: Line 171:
 
     ---@param height string? # The height 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
 
     ---@return mw.html.element
     local function makeImageContainer(name, alt, href, src, width, height)
+
    ---@return string
         return mw.html.create("td")
+
     local function makeImg(name, alt, href, src, width, height)
            :tag("span")
+
         local image = mw.html.create("td")
            :css("white-space", "nowrap")
 
 
             :tag("a")
 
             :tag("a")
 
             :addClass("mw-redirect")
 
             :addClass("mw-redirect")
Line 184: Line 183:
 
             :attr("width", width or "auto")
 
             :attr("width", width or "auto")
 
             :attr("height", height or "auto")
 
             :attr("height", height or "auto")
            :done()
 
            :wikitext(name)
 
 
             :allDone()
 
             :allDone()
 +
        return image, tostring(image)
 
     end
 
     end
  
 
     local tableHtml = mw.html.create("table")
 
     local tableHtml = mw.html.create("table")
 
         :addClass("wikitable sortable jquery-tablesorter")
 
         :addClass("wikitable sortable jquery-tablesorter")
        :tag("thead")
+
         :node(makeTr({ "Image", "Item", "Quantity", "Rarity", "Price", "Tradeable", "Leagues" }))
         :node(makeTr({ "Item", "Quantity", "Rarity", "Price", "Tradeable", "Leagues" }))
 
 
         :done()
 
         :done()
        :tag("tbody")
 
  
 
     for _, item in ipairs(loot) do
 
     for _, item in ipairs(loot) do
 
         local item2 = itemData[tostring(item.id)]
 
         local item2 = itemData[tostring(item.id)]
         local imageHtml = makeImageContainer(
+
         local imageHtml = makeImg(
 
             item2.name,
 
             item2.name,
 
             item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""),
 
             item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""),
Line 205: Line 201:
 
             "45px"
 
             "45px"
 
         )
 
         )
 +
        local itemHtml = mw.html.create("td")
 +
            :tag("a")
 +
            :attr("href", "/p/" .. item2.name)
 +
            :attr("title", item2.name)
 +
            :wikitext(item2.name)
 +
            :allDone()
 
         local quantityHtml = mw.html.create("td")
 
         local quantityHtml = mw.html.create("td")
 
             :wikitext(item.minAmount .. " - " .. item.maxAmount)
 
             :wikitext(item.minAmount .. " - " .. item.maxAmount)
Line 210: Line 212:
 
             :wikitext(toFixed(item.chance * 100, 3) .. "%")
 
             :wikitext(toFixed(item.chance * 100, 3) .. "%")
 
         local priceHtml = mw.html.create("td")
 
         local priceHtml = mw.html.create("td")
             :wikitext(itemData[tostring(item.id)].value or "-")
+
             :wikitext(item2.value or "-")
 
         local tradeableHtml = mw.html.create("td")
 
         local tradeableHtml = mw.html.create("td")
             :wikitext(itemData[tostring(item.id)].tradeable and "Yes" or "No")
+
             :wikitext(item2.tradeable and "Yes" or "No")
 
         local leaguesHtml = mw.html.create("td")
 
         local leaguesHtml = mw.html.create("td")
 
         --TODO: Add indicator if league is active, the field is present in Module:Leagues/data but is incorrect
 
         --TODO: Add indicator if league is active, the field is present in Module:Leagues/data but is incorrect
Line 219: Line 221:
 
                 local league = leagueData[tostring(leagueId)]
 
                 local league = leagueData[tostring(leagueId)]
 
                 if league then
 
                 if league then
                     local leagueHtml = makeImageContainer(
+
                     local leagueHtml = makeImg(
 
                         league.name,
 
                         league.name,
 
                         league.name,
 
                         league.name,
Line 234: Line 236:
 
             leaguesHtml:wikitext("All")
 
             leaguesHtml:wikitext("All")
 
         end
 
         end
         local itemHtml = mw.html.create("tr")
+
         local rowHtml = mw.html.create("tr")
 
             :node(imageHtml)
 
             :node(imageHtml)
 +
            :node(itemHtml)
 
             :node(quantityHtml)
 
             :node(quantityHtml)
 
             :node(chanceHtml)
 
             :node(chanceHtml)
Line 243: Line 246:
 
             :allDone()
 
             :allDone()
  
         tableHtml:node(itemHtml)
+
         tableHtml:node(rowHtml)
 
     end
 
     end
  

Revision as of 20:44, 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  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
    ---@return string
    local function makeImg(name, alt, href, src, width, height)
        local image = mw.html.create("td")
            :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")
            :allDone()
        return image, tostring(image)
    end

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

    for _, item in ipairs(loot) do
        local item2 = itemData[tostring(item.id)]
        local imageHtml = makeImg(
            item2.name,
            item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""),
            "/p/" .. item2.name,
            "https://www.play.idlescape.com" .. item2.itemImage,
            "45px"
        )
        local itemHtml = mw.html.create("td")
            :tag("a")
            :attr("href", "/p/" .. item2.name)
            :attr("title", item2.name)
            :wikitext(item2.name)
            :allDone()
        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(item2.value or "-")
        local tradeableHtml = mw.html.create("td")
            :wikitext(item2.tradeable and "Yes" or "No")
        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 = makeImg(
                        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 rowHtml = mw.html.create("tr")
            :node(imageHtml)
            :node(itemHtml)
            :node(quantityHtml)
            :node(chanceHtml)
            :node(priceHtml)
            :node(tradeableHtml)
            :node(leaguesHtml)
            :allDone()

        tableHtml:node(rowHtml)
    end

    tableHtml = tableHtml:allDone()
    return tableHtml
end

return p