Difference between revisions of "Module:Infobox ability"
Jump to navigation
Jump to search
Demcookies (talk | contribs) (Fix change damage & accuracy multiplier from % to x. Fix critical damage and chance) |
Demcookies (talk | contribs) (Add option to create Infobox ability list for abilities granted by the item with argument 'item = item name'. Add formatting to heal over time value) |
||
| Line 23: | Line 23: | ||
--#region Elements | --#region Elements | ||
| + | local function clearInfoboxInstance() | ||
| + | infobox.clear() | ||
| + | headerCount = 1 | ||
| + | labelCount = 1 | ||
| + | dataCount = 1 | ||
| + | end | ||
| + | |||
local function h() | local function h() | ||
local s = "header" .. headerCount | local s = "header" .. headerCount | ||
| Line 181: | Line 188: | ||
local function createDOT(dot) | local function createDOT(dot) | ||
if dot == nil then return "" end | if dot == nil then return "" end | ||
| + | local lang = mw.language.getContentLanguage() | ||
return string.format( | return string.format( | ||
| − | "% | + | "%sHP / %ds x %d%s", |
| − | dot.healthChange, | + | lang.formatNum(dot.healthChange), |
toFixed(dot.healthChangeDelay / 1000, 2), | toFixed(dot.healthChangeDelay / 1000, 2), | ||
dot.dotCount, | dot.dotCount, | ||
| Line 300: | Line 308: | ||
args.title = ability.abilityName | args.title = ability.abilityName | ||
args.image = image(ability.abilityName, ability.abilityImage, false, 150) | args.image = image(ability.abilityName, ability.abilityImage, false, 150) | ||
| − | |||
args[l()] = "Sources" | args[l()] = "Sources" | ||
| Line 338: | Line 345: | ||
args[h()] = "Summons" | args[h()] = "Summons" | ||
createSummon(args, ability.summonFriendly) | createSummon(args, ability.summonFriendly) | ||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
| − | |||
args[h()] = "Damage Stats" | args[h()] = "Damage Stats" | ||
| − | |||
args[l()] = "Deals No Damage" | args[l()] = "Deals No Damage" | ||
args[d()] = ability.dealsNoDamage and "Yes" or "" | args[d()] = ability.dealsNoDamage and "Yes" or "" | ||
| Line 376: | Line 366: | ||
args[d()] = stat.scaling .. "x" | args[d()] = stat.scaling .. "x" | ||
end | end | ||
| − | |||
args[h()] = "Accuracy Stats" | args[h()] = "Accuracy Stats" | ||
| − | |||
args[l()] = "Accuracy Multiplier" | args[l()] = "Accuracy Multiplier" | ||
args[d()] = createCoeff(ability.baseAccuracyCoeff) | args[d()] = createCoeff(ability.baseAccuracyCoeff) | ||
| Line 388: | Line 376: | ||
end | 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" | args[h()] = "Self Buff" | ||
createBuff(args, ability.selfBuff) | createBuff(args, ability.selfBuff) | ||
| − | |||
args[h()] = "Target Buff" | args[h()] = "Target Buff" | ||
createBuff(args, ability.targetBuff) | createBuff(args, ability.targetBuff) | ||
| − | |||
args[h()] = "Description" | args[h()] = "Description" | ||
args[d()] = '<p style="margin:auto;font-style:italic;font-size:1.2em">' .. ability.description .. "</p>" | args[d()] = '<p style="margin:auto;font-style:italic;font-size:1.2em">' .. ability.description .. "</p>" | ||
| − | |||
args[h()] = "Chat Messages" | args[h()] = "Chat Messages" | ||
createMessages(args, ability.messages) | createMessages(args, ability.messages) | ||
| − | |||
return infobox.infobox(args) | return infobox.infobox(args) | ||
| Line 415: | Line 411: | ||
end | end | ||
| − | ---Creates an infobox for the given ability. | + | ---Creates an infobox for the given ability or abilities found in a item. |
| − | ---@param args | + | ---@param args {[1]: string|nil, name: string|nil, title: string|nil, item: string|nil, sources: string|nil} |
---@return string | ---@return string | ||
function p._ability(args) | function p._ability(args) | ||
| − | local name = args.name or args.title or args[1] or mw.title.getCurrentTitle().text | + | 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 | end | ||
--#endregion | --#endregion | ||
return p | return p | ||
Revision as of 22:13, 23 May 2025
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 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.
---@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
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 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()] = "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"
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 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