Difference between revisions of "Module:Infobox ability"

From Idlescape Wiki
Jump to navigation Jump to search
(Init Infobox module for abilities.)
 
(Fix change damage & accuracy multiplier from % to x. Fix critical damage and chance)
Line 81: Line 81:
 
---@param num number # The number to round.
 
---@param num number # The number to round.
 
---@param digits number # The number of decimal places to round to.
 
---@param digits number # The number of decimal places to round to.
---@param keepTrailing boolean # Whether to keep trailing zeros and dot.
+
---@param keepTrailing boolean|nil # Whether to keep trailing zeros and dot. Default: false.
 
---@return string # The rounded number as a string.
 
---@return string # The rounded number as a string.
 
local function toFixed(num, digits, keepTrailing)
 
local function toFixed(num, digits, keepTrailing)
Line 258: Line 258:
 
         end
 
         end
 
         args[sd()] = text
 
         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
 
end
 
end
Line 333: Line 361:
  
 
     args[l()] = "Crit Damage Multiplier"
 
     args[l()] = "Crit Damage Multiplier"
     args[d()] = ability.critical and toFixed(ability.critical * 100, 0) .. "%" or ""
+
     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[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[d()] = createCoeff(ability.diminishingAOEDamageMult)
  
 
     args[l()] = "Min Damage Multiplier"
 
     args[l()] = "Min Damage Multiplier"
     args[d()] = ability.baseMinimumDamageCoeff and toFixed(ability.baseMinimumDamageCoeff * 100, 0) .. "%" or ""
+
     args[d()] = createCoeff(ability.baseMinimumDamageCoeff)
  
 
     args[l()] = "Max Damage Multiplier"
 
     args[l()] = "Max Damage Multiplier"
     args[d()] = ability.baseMaximumDamageCoeff  and toFixed(ability.baseMaximumDamageCoeff * 100, 0) .. "%" or ""
+
     args[d()] = createCoeff(ability.baseMaximumDamageCoeff)
  
 
     for _, stat in ipairs(ability.damageScaling or {}) do
 
     for _, stat in ipairs(ability.damageScaling or {}) do
Line 353: Line 381:
  
 
     args[l()] = "Accuracy Multiplier"
 
     args[l()] = "Accuracy Multiplier"
     args[d()] = ability.baseAccuracyCoeff and toFixed(ability.baseAccuracyCoeff * 100, 0) .. "%" or ""
+
     args[d()] = createCoeff(ability.baseAccuracyCoeff)
  
 
     for _, stat in ipairs(ability.accuracyScaling or {}) do
 
     for _, stat in ipairs(ability.accuracyScaling or {}) do

Revision as of 23:11, 11 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 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
    return string.format(
        "%d&nbsp;HP&nbsp;/&nbsp;%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

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()] = "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()] = 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()] = "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