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