Module:Infobox ability
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