Module:Loot table
Revision as of 03:14, 10 April 2025 by Demcookies (talk | contribs) (Fix correct handling of monsters that share an id with other monsters; needs a manual parameter "zone" with the name of the location the monster is found at as a value; needs to be included in the table "overrides" key points in the monster's Module:Monsters_stats/data table.)
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|nil The source type ("monster" or "location"), or nil 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 end ---@class Item ---@field allowedLeagues number[]|nil 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|nil The item data, or nil if the item is not found. ---@return string|nil The monsterId where the item was found ("-1" for locations), or nil if the item is not found. ---@return string|nil 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. ---@param sourceType string String representing the type of the source ("monster" or "location"). ---@param locationId string|number|nil Optional ID for the location to find the source from. Use `id` and `sourceType` for location loot. ---@return Item[]|nil The source data, or nil if the source is not found. ---@return string|nil The locationId where the source was found (`id` if `sourceType` is "location"), or nil if the source is not found. local function getFirstSourceMatch(loot, id, sourceType, locationId) id = tostring(id) locationId = locationId and tostring(locationId) or nil if sourceType == "location" then local location = loot[id] if not location then return end local source = location["-1"] if not source then return end return source, id end --Needed for 'override' monsters that share ID with other monsters -- otherwise the loot would be from first matching ID and could be wrong if locationId then local location = loot[locationId] for sourceId, source in pairs(location) do if sourceId == id then return source, locationId end end return end 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):gsub("%.?0+$", "") return formatted end ---Finds the ID of a monster that is overridden by another monster with the given name. ---@param name string The name of the monster that overrides another monster. ---@return string|nil The ID of the monster that is overridden, or nil if not found. local function findIdByOverrideName(name) local monsterStatsData = mw.loadData("Module:Monsters_stats/data") for id, monster in pairs(monsterStatsData) do for overrideName, overrideMonster in pairs(monster.overrides or {}) do if overrideName == name then return id end end end 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 --Some monsters use same id as other monsters so we need to specify location for them local locationId = args["zone"] and findSourceId(args["zone"]) local sourceId local sourceType if locationId then sourceId = findIdByOverrideName(sourceName) sourceType = "monster" else sourceId, sourceType = findSourceId(sourceName) end if not sourceId then return locationId and createErrorMessage( "Found no monsters overridden by '" .. sourceName .. "'. The Modules: Monsters_stats/data or Loot/data may be outdated." ) or createErrorMessage( "No monster or location named '" .. sourceName .. "'. The Module:Loot/data may be outdated. <br>Try adding parameter 'location' to the template, e.g.:" .. "<br><code>{{Loot_table|location=Palace of Flame}}</code>." ) end local loot = getFirstSourceMatch(lootData, sourceId, sourceType, locationId) if not loot then return createErrorMessage( "No loot found for monster or location named '" .. sourceName .. "' The Module:Loot/data may be outdated." ) end local lang = mw.language.getContentLanguage() ---@param name string Wikitext. ---@return mw.html 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 mw.html 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="' .. height .. '"' or "" ) 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 image = mw.html.create("img"):attr(imageAttributes) return string.format('[[%s|%s]]', title, tostring(image)) 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, has no value in the data 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 --TODO: Add option to show percentages as fractions (open options from the gear icon) -- Saving preferences would require special permissions? MAybe with mw.user or javascript to localStorage? local tableHtml = mw.html.create("table") :addClass("wikitable sortable jquery-tablesorter") --:node(createThs({ "⚙️", "Item", "Quantity", "Rarity", "Value", "Tradeable", "Leagues" })) :node(createThs({ " ", "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(createWikitextImage( item2.name, { src="https://www.play.idlescape.com" .. src, alt=item2.name .. (item2.extraTooltip and ". " .. item2.extraTooltip or ""), width=args.width or 30, heigth=args.height or nil } )) local itemHtml = mw.html.create("td") :wikitext('[[' .. item2.name .. ']]') local quantityHtml = mw.html.create("td") :css("text-align", "center") :wikitext(createQuantityText(item)) local chanceHtml = mw.html.create("td") :css("text-align", "center") :wikitext(toFixed(item.chance * 100, 3) .. "%") local priceHtml = mw.html.create("td") :css("text-align", "right") :wikitext(createValueText(item2)) local tradeableHtml = mw.html.create("td") :css("text-align", "center") :wikitext((item2.id == 1 or item2.tradeable) and "Yes" or "No") --Gold is tradeable 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 item.allowedLeagues then for _, leagueId in ipairs(item.allowedLeagues) do local league = leagueData[tostring(leagueId)] if league then leagueHtml:wikitext(createWikitextImage( league.name, { src="https://www.play.idlescape.com" .. league.icon, alt=league.name, width=args.width or 30, heigth=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