Module:Infobox ability
Jump to navigation
Jump to search
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, Talent> local talentsData = mw.loadData("Module:Talents/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 clearInfoboxInstance() infobox.clear() headerCount = 1 labelCount = 1 dataCount = 1 end 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|nil # Whether to keep trailing zeros and dot. Default: false. ---@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. ---@generic T ---@param tbl table<any, T> # The table to search. ---@param key any # The key to search for. ---@param val any # The value to search for. ---@return T|nil # The first table entry with the given key and value, or nil if not found. local function findByKeyVal(tbl, key, val) if type(tbl) ~= "table" then return nil end for k, v in pairs(tbl) do if v[key] == val then return v end end end ---Checks if a table contains a specific value. ---@param t table ---@param value any ---@return boolean local function hasValue(t, value) if type(t) ~= "table" then return false end for _, v in pairs(t) do if value == v then return true end end return false 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, 4) == "http" or url:sub(1,2) == "[[" 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"><span style="position:relative;top:1px;">%s</span>]]', 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(rune.amount .. " " .. image(item.name, item.itemImage, true, 20)) 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 local lang = mw.language.getContentLanguage() return string.format( "%sHP / %ds x %d%s", lang:formatNum(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 local function createCoeff(coeff) if coeff == nil then return "" end --return toFixed(coeff * 100, 0) .. "%" return coeff .. "x" end local function createCritical(args, critical) if critical == nil then return "" end if critical.chanceAdditive or critical.chanceMult then args[l()] = "Critical Chance" if critical.chanceAdditive then args[d()] = "+" .. toFixed(critical.chanceAdditive * 100, 0) elseif critical.chanceMult then args[l()] = critical.chanceMult .. "x" end end if critical.damageAdditive or critical.damageMult then args[l()] = "Critical Damage" if critical.damageAdditive then args[d()] = "+" .. toFixed(critical.damageAdditive * 100, 0) elseif critical.damageMult then args[l()] = critical.damageMult .. "x" end end end ---Creates a wikitext links for the ability's sources ---@param id string|number|nil The ability ID to find sources for. ---@return string # A string containing the sources of the ability, formatted as wikitext links. local function getSources(id) id = tonumber(id) if not id then return "" end local sources = {} for _, item in pairs(itemsData) do if item.equipmentStats and item.equipmentStats.grantedAbility then for _, abilityID in ipairs(item.equipmentStats.grantedAbility) do if abilityID == id then sources[item.name] = { id = item.id, type = 2, image = image(item.name, item.itemIcon or item.itemImage, true, 20) } end end end if item.relatedAbility == id then sources[item.name] = { id = item.id, type = 1, image = image(item.name, item.itemIcon or item.itemImage, true, 20) } end end local talentSrc for _, talent in pairs(talentsData) do if hasValue(talent.abilities, id) then talentSrc = talentSrc or mw.getCurrentFrame() :callParserFunction("filepath:Total_level_mastery_icon_center.png") sources[talent.name] = { id = talent.id, type = 0, image = image(talent.name, talentSrc, true, 20) } end end local sourceArray = {} -- From set to array for _, source in pairs(sources) do table.insert(sourceArray, source) end -- Sort by type: talent < ability book < item, then by id table.sort(sourceArray, function(a, b) if a.type == b.type then return a.id < b.id end return a.type < b.type end) -- Remove type needed for sorting for i, source in ipairs(sourceArray) do sourceArray[i] = source.image end return table.concat(sourceArray, "<br>") 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) local sources_ = sources and "[[" .. table.concat(sources, "]]<br>[[") .. "]]" or "" sources_ = sources_ .. getSources(ability.id) args[l()] = "Sources" args[d()] = sources_ 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.dealsNoDamage and "Support / " or "") .. 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()] = "Damage Stats" args[l()] = "Deals No Damage" args[d()] = ability.dealsNoDamage and "Yes" or "" args[l()] = "Crit Damage Multiplier" args[d()] = createCritical(args, ability.critical) args[l()] = '<span class="rt-commentedText tooltip tooltip-dotted" title="Damage Loss Per Subsequent Targets">AOE Damage Multiplier</span>' args[d()] = createCoeff(ability.diminishingAOEDamageMult) args[l()] = "Min Damage Multiplier" args[d()] = createCoeff(ability.baseMinimumDamageCoeff) args[l()] = "Max Damage Multiplier" args[d()] = createCoeff(ability.baseMaximumDamageCoeff) 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()] = createCoeff(ability.baseAccuracyCoeff) for _, stat in ipairs(ability.accuracyScaling or {}) do args[l()] = stat.affinity args[d()] = stat.scaling .. "x" end 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()] = "Self Buff / Debuff" createBuff(args, ability.selfBuff) args[h()] = "Target Buff / Debuff" 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 or abilities found in a item. ---@param args {[1]: string|nil, name: string|nil, title: string|nil, item: string|nil, sources: string|nil} ---@return string function p._ability(args) local itemName = args.item local item = findByKeyVal(itemsData, "name", itemName) if item then local count = 0 local td = mw.html.create("td") local abilities = item.equipmentStats and item.equipmentStats.grantedAbility or {} for i= tableLength(abilities), 1, -1 do local ability = abilitiesData[tostring(abilities[i])] if ability then td:node(createInfobox(ability)) clearInfoboxInstance() count = count + 1 end end if count > 0 then local parent = mw.html.create("table") :addClass("mw-collapsible") :tag("caption") :css("text-align", "left") :wikitext(item.name .. " grants the following abilities when worn: ") :done() return parent:node(mw.html.create("tr"):node(td)) end return "<div style=\"color:red\">No abilities found for item '" .. itemName .. "'.</div>" else 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 end --#endregion return p