Module:Infobox ability
Revision as of 22:24, 11 May 2025 by Demcookies (talk | contribs) (Init Infobox module for abilities.)
Documentation for this module may be created at Module:Infobox ability/doc
--#region Variables and Imports local p = {} local monstersData = mw.loadData("Module:Monsters/data") ---@type table<string, Ability> local abilitiesData = mw.loadData("Module:Abilities/data") ---@type table<string, Item> local itemsData = mw.loadData("Module:Items/data") ---@type table<string, Enchantment> local enchantmentsData = require("Module:Enchantment/data") local infobox = require("Module:Infobox") local headerCount = 1 local labelCount = 1 local dataCount = 1 local targetLabelMap = { ["lowestHP"] = "Lowest HP", ["lowestDef"] = "Lowest Defence", ["lowestHPPercent"] = "Lowest HP %", } --#endregion --#region Elements local function h() local s = "header" .. headerCount headerCount = headerCount + 1 labelCount = headerCount dataCount = headerCount return s end local function sbreak() local s = "sbreak" .. headerCount headerCount = headerCount + 1 labelCount = headerCount dataCount = headerCount return s end local function l() local s = "label" .. labelCount dataCount = labelCount labelCount = labelCount + 1 headerCount = labelCount return s end local function d() local s = "data" .. dataCount dataCount = dataCount + 1 headerCount = dataCount labelCount = dataCount return s end local function sl() local s = "s" .. l() return s end local function sd() local s = "s" .. d() return s end --#endregion --#region Utility Functions ---Returns the length of a table. ---@param tbl table # The table to count the length of. ---@return number # The length of the table. local function tableLength(tbl) local count = 0 for _ in pairs(tbl) do count = count + 1 end return count end ---Rounds a number to the specified number of decimal places. ---@param num number # The number to round. ---@param digits number # The number of decimal places to round to. ---@param keepTrailing boolean # Whether to keep trailing zeros and dot. ---@return string # The rounded number as a string. local function toFixed(num, digits, keepTrailing) keepTrailing = keepTrailing or false local mult = 10^digits local rounded = num >= 0 and math.floor(num * mult + 0.5) or math.ceil(num * mult - 0.5) local formatted = string.format("%." .. digits .. "f", rounded / mult) if not keepTrailing and digits > 0 then formatted = formatted:gsub("%.?0+$", "") end return formatted end ---Finds the first table entry in the given table with the given key and value. ---@param tbl table # The table to search. ---@param key any # The key to search for. ---@param val any # The value to search for. ---@return table|nil # The first table entry with the given key and value, or nil if not found. local function findByKeyVal(tbl, key, val) for k, v in pairs(tbl) do if v[key] == val then return v end end end ---Converts a CSV string into a table. ---@param s string # The CSV string to convert. ---@return table local function fromCSV(s) if string.sub(s, -1) ~= ',' then s = s .. ',' -- ending comma end s = string.gsub(s, "[%[%]]", ""):gsub(",%s", ",") local t = {} -- table to collect fields local fieldstart = 1 repeat local nexti = string.find(s, ',', fieldstart) table.insert(t, string.sub(s, fieldstart, nexti - 1)) fieldstart = nexti + 1 until fieldstart > string.len(s) return t end --#endregion --#region Generic Module Functions ---Formats the given URL to full play.idlescape.com URL. local function fullUrl(url) local newUrl = url if url:sub(1, 5) == "https" then return newUrl end if url:sub(1, 1) ~= "/" then newUrl = "/" .. newUrl end return "https://www.play.idlescape.com" .. newUrl end ---Creates an image link for the given name and URL. ---@param name string # The name of the link and image. ---@param url string # The URL of the image. ---@param word boolean # Whether to include the name in the link, if falsy it will be an image only. ---@param size number|string # The size of the image. ---@return string local function image(name, url, word, size) size = size or 20 url = fullUrl(url) return string.format( '[[%s|<img src="%s" alt="%s" width="%d" height="%d">%s]]', name, url, name, size, size, word and name or "" ) end --#endregion --#region Infobox Elements ---Creates a string containing formatted list of the given runes and their costs. ---@param runeCosts table # The rune costs from ability.runeCost. ---@return string local function createRuneList(runeCosts) if runeCosts == nil then return "" end local runes = {} for _, rune in ipairs(runeCosts) do local item = itemsData[tostring(rune.itemID)] if item then local e = mw.html.create("div") :css("width", "max-content") :wikitext(image(item.name, item.itemImage, true, 20) .. " x " .. rune.amount) table.insert(runes, tostring(e)) end end return table.concat(runes, " ") end ---Creates a string for the given damage over time effect. ---@param dot table # The damage over time effect from ability.healthChangeEvent. ---@return string local function createDOT(dot) if dot == nil then return "" end return string.format( "%d HP / %ds x %d%s", dot.healthChange, toFixed(dot.healthChangeDelay / 1000, 2), dot.dotCount, dot.targetsSelf and " to self" or "" ) end --- Adds the given buff to the infobox. ---@param args table ---@param buff table # The buff data to add. local function createBuff(args, buff) if buff == nil then return end local enchant = enchantmentsData[tostring(buff.enchantmentApply)] if enchant == nil then return end args[l()] = "Buff" args[d()] = image(enchant.name, enchant.buffIcon, true, 20) args[l()] = "Strength" args[d()] = tostring(buff.enchantmentStrength) args[l()] = "Amount" args[d()] = tostring(buff.enchantmentAmount) args[l()] = "Refresh to Amount" args[d()] = buff.refreshToAmount and tostring(buff.refreshToAmount) or "" args[l()] = "Condition to Apply" args[d()] = buff.onlyOnHit and "On Hit" or "Always" args[l()] = "Chance to Apply" args[d()] = buff.enchantmentChanceToApply and toFixed(buff.enchantmentChanceToApply * 100, 0) .. "%" or "" args[l()] = "Tooltip" args[d()] = enchant.getTooltip(buff.enchantmentStrength, enchant.strengthPerLevel) end ---Adds info for the given summoned monster into the infobox. ---@param args table # The infobox args. ---@param summon table # The summon data to add. local function createSummon(args, summon) if summon == nil then return end local monster = monstersData[tostring(summon.id)] if monster == nil then return end args[l()] = "Name" args[d()] = image(monster.name, monster.image, true, 20) args[l()] = "Count" args[d()] = summon.count and tostring(summon.count) or "" end ---Adds the given messages into the infobox. ---@param args table # The infobox args. ---@param messages table # The messages to add from ability data. local function createMessages(args, messages) if messages == nil then return end local length = tableLength(messages) local count = 1 for trigger, v in pairs(messages) do args[l()] = "Trigger" args[d()] = trigger == "onCast" and "On Cast" or "On Kill" args[l()] = "Chat Channel" args[d()] = v.type:gsub("^%l", string.upper) --args[sl()] = "Message" local text = '<p style="margin:auto;font-style:italic;font-size:1.2em">' .. v.message .. "</p>" if length > 1 and count < length then count = count + 1 text = text .. "<hr>" end args[sd()] = text end end ---Creates an infobox for the given ability. ---@param ability Ability ---@param sources string[]|nil ---@return string local function createInfobox(ability, sources) local args = {} args.autoheaders = "y" args.subbox = "no" args.bodystyle = " " args.title = ability.abilityName args.image = image(ability.abilityName, ability.abilityImage, false, 150) args[l()] = "Sources" args[d()] = sources and "[[" .. table.concat(sources, "]], [[") .. "]]" or "" args[l()] = "Cost" if ability.useRangedAmmo then args[d()] = tostring( mw.html.create("div") :css("width", "max-content") :wikitext("Uses ranged ammo") ) else args[d()] = createRuneList(ability.runeCost) end args[l()] = "Attack Type" args[d()] = ability.damageType args[l()] = "Attack Speed" args[d()] = tostring( mw.html.create("div") :css("width", "max-content") :wikitext(ability.baseSpeedCoeff .. "x Weapon speed") ) args[l()] = "Cooldown" args[d()] = toFixed(ability.cooldown and ability.cooldown / 1000 or 0, 2) .. "s" --args[l()] = '<span class="rt-commentedText tooltip tooltip-dotted" title="???">Skip Reactives</span>' --args[d()] = ability.skipReactives and "No" or "Yes" args[l()] = "Effect Over Time" args[d()] = ability.healthChangeEvent and createDOT(ability.healthChangeEvent) args[h()] = "Summons" createSummon(args, ability.summonFriendly) args[h()] = "Target" args[l()] = "Max Targets" args[d()] = tostring(ability.maxTargets) args[l()] = "Targets Self" args[d()] = ability.canTargetSelf and "Yes" or "No" args[l()] = "Targets Friendly" args[d()] = ability.targetFriendly and "Yes" or "No" args[l()] = "Targeting Override" args[d()] = ability.target and targetLabelMap[ability.target] or "" args[h()] = "Damage Stats" args[l()] = "Deals No Damage" args[d()] = ability.dealsNoDamage and "Yes" or "" args[l()] = "Crit Damage Multiplier" args[d()] = ability.critical and toFixed(ability.critical * 100, 0) .. "%" or "" args[l()] = '<span class="rt-commentedText tooltip tooltip-dotted" title="Damage Loss Per Subsequent Targets">AOE Damage Multiplier</span>' args[d()] = ability.diminishingAOEDamageMult and toFixed(ability.diminishingAOEDamageMult * 100, 0) .. "%" or "" args[l()] = "Min Damage Multiplier" args[d()] = ability.baseMinimumDamageCoeff and toFixed(ability.baseMinimumDamageCoeff * 100, 0) .. "%" or "" args[l()] = "Max Damage Multiplier" args[d()] = ability.baseMaximumDamageCoeff and toFixed(ability.baseMaximumDamageCoeff * 100, 0) .. "%" or "" for _, stat in ipairs(ability.damageScaling or {}) do args[l()] = stat.affinity args[d()] = stat.scaling .. "x" end args[h()] = "Accuracy Stats" args[l()] = "Accuracy Multiplier" args[d()] = ability.baseAccuracyCoeff and toFixed(ability.baseAccuracyCoeff * 100, 0) .. "%" or "" for _, stat in ipairs(ability.accuracyScaling or {}) do args[l()] = stat.affinity args[d()] = stat.scaling .. "x" end args[h()] = "Self Buff" createBuff(args, ability.selfBuff) args[h()] = "Target Buff" createBuff(args, ability.targetBuff) args[h()] = "Description" args[d()] = '<p style="margin:auto;font-style:italic;font-size:1.2em">' .. ability.description .. "</p>" args[h()] = "Chat Messages" createMessages(args, ability.messages) return infobox.infobox(args) end --#endregion --#region Module Functions function p.ability(frame) local args = frame:getParent().args return p._ability(args) end ---Creates an infobox for the given ability. ---@param args table ---@return string function p._ability(args) local name = args.name or args.title or args[1] or mw.title.getCurrentTitle().text local ability = findByKeyVal(abilitiesData, "abilityName", name) if not ability then return "<div style=\"color:red\"> No ability named '" .. name .. "'. The Module:Abilities/data may be outdated.</div>" end local sources = args.sources and fromCSV(args.sources) return createInfobox(ability, sources) end --#endregion return p