Difference between revisions of "Module:Loot table"

From Idlescape Wiki
Jump to navigation Jump to search
(Fix changes output from raw html to html with 'A' tags (images are still html but inside the wikitext template string) as wikitext, similar to Module:Img)
m (Demcookies moved page Module:LootTable/sandbox to Module:LootTable without leaving a redirect: Update module works correctly. The 'A' and 'IMG' tags are now displayed correctly so the module is ready for use.)
(No difference)

Revision as of 17:19, 3 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")


---Creates a mw.html 'DIV' element containing an "error" message.
---@param message string # Message as wikitext.
---@return mw.html # The message wrapped in a mw.html 'DIV' element as red text.
local function createErrorMessage(message)
	local e = mw.html.create("div")
		:css("color", "red")
		:wikitext(message)
	return e
end

---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|mw.html # The source type ("monster" or "location"), or mw.html element with 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 errorMessage = createErrorMessage("No monster or location named '" .. name .. "'. The Module:Loot/data may be outdated.")
	return nil, errorMessage
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 is not found.
---@return string? # The locationId where the item was found, or nil if the item is 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
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 is not found.
---@return string? # The locationId where the source was found, or nil if the source is 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
end

--TODO: Add option to show percentages as fractions

---Converts a floating-point number to a approximation of 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 approximation of fraction as string. 
---@param num number # The floating-point number to convert.
---@param maxDenominator number # The maximum denominator for the fraction.
---@return string # The fraction as string ('numerator/denominator').
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 tostring(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 div
    end

    local lang = mw.language.getContentLanguage()

    ---@param name string # Wikitext.
    ---@return mw.html # 'TH' element.
    local function createTh(name)
        local th = mw.html.create("th")
            :addClass("headerSort")
            :attr("tabindex", 0)
            :attr("title", "Sort ascending")
            :wikitext(name)
        return th
    end

    ---@param names string[] # Array of wikitext.
    ---@return 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(createTh(name))
        end
        return tr
    end

    ---Creates a 'A' element containing an image.
    ---@param name string # The name of the item.
    ---@param src string # The URI to the item image.
    ---@param width? string|integer # The width of the item image as integer.
    ---@param height? string|integer # The height of the item image as integer.
    ---@return mw.html # mw.html 'A' element containing an image.
    local function createImg(name, src, alt, width, height)
        local e = mw.html.create("a")
            :attr("href", tostring(mw.uri.localUrl(name)))
            :attr("title", name)
            :tag("img")
            :attr("src", src)
            :attr("alt", alt)
        if width then
            e:attr("width", width)
        end
        if height then
            e:attr("height", height)
        end
        return e:done()
    end
    
    local function createImgText(name, src, alt, width, height)
    	return string.format(
    		'[[%s|<img src="%s" alt="%s"%s%s>]]',
    		name,
    		src,
    		alt,
    		width and ' width="' .. width .. '"' or "",
    		height and ' heigth="' .. heigth .. '"' or ""
    	)
    end

    ---Formats the value of an item as string.
    ---@param item table # Item table from Module:Items/data
    ---@return string # The formatted value of the item as string, or '-' if no value.
    local function createValueText(item)
        if item.id == 1 then -- Gold
            return "1"
        end
    	return item.value and lang:formatNum(item.value) or "-"
    end
    
    ---Formats the Item drop quantities as string.
    ---@param item Item # Item table from Module:Loot/data
    ---@return string # The formatted quantity range as string.
    local function createQuantityText(item)
    	if item.minAmount == item.maxAmount then
    		return tostring(item.minAmount)
    	end
    	return lang:formatNum(item.minAmount) .. " - " .. lang:formatNum(item.maxAmount)
    end

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

    for _, item in ipairs(loot) do
        local item2 = itemData[tostring(item.id)]
        --IS itemList.ts has some incorrect URIs, e.g. for Feather
        local src = item2.itemImage
        src = src:sub(1, 1) ~= "/" and "/" .. src or src
        local imageHtml = mw.html.create("td")
			:wikitext(createImgText(
				item2.name,
				"https://www.play.idlescape.com" .. src,
				item2.name .. (item2.extraTooltip and "\n" .. item2.extraTooltip or ""),
				args.width or 45,
				args.height or nil
			))
        local itemHtml = mw.html.create("td")
        	:wikitext('[[' .. item2.name .. ']]')
            --[[:tag("a")
            :attr("href", tostring(mw.uri.localUrl(item2.name)))
            :attr("title", item2.name)
            :wikitext(item2.name)
            :done()]]
        local quantityHtml = mw.html.create("td")
            :wikitext(createQuantityText(item))
        local chanceHtml = mw.html.create("td")
            :wikitext(toFixed(item.chance * 100, 3) .. "%")
        local priceHtml = mw.html.create("td")
            :wikitext(createValueText(item2))
        local tradeableHtml = mw.html.create("td")
            :wikitext(item2.tradeable and "Yes" or "No")
        local leagueHtml = mw.html.create("td")
		--TODO: Add indicator if league is inactive, the field is present in
		--      leagueList.ts / Module:Leagues/data but some are incorrect
        if item.allowedLeagues then
            for _, leagueId in ipairs(item.allowedLeagues) do
                local league = leagueData[tostring(leagueId)]
                if league then
                	leagueHtml:wikitext(createImgText(
						league.name,
						"https://www.play.idlescape.com" .. league.icon,
						league.name,
						args.width or 45,
						args.height or nil
					))
                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(priceHtml)
            :node(tradeableHtml)
            :node(leagueHtml)

        tableHtml:node(rowHtml)
	end

	return tableHtml
end

return p