Module:AbilityTable/ComplexRenderers: Difference between revisions

Vergir (talk | contribs)
Fix is_base_property false positives from modifier metadata strings: remove table recursion since debuff props are always top-level keys (with help from vergir-bot LLM)
add scalingspirit
 
(11 intermediate revisions by 3 users not shown)
Line 47: Line 47:
end
end


-- Returns true if prop exists with a non-zero value at the top level of the
-- Sums the non-zero values of `fields` within a single record layer (the base
-- record, excluding the Upgrades key. Does not recurse into sub-tables to
-- ability record, or one upgrade tier). Returns nil when none are present.
-- avoid false positives from modifier metadata (e.g. prop names appearing as
local function sum_fields(layer, fields)
-- string values inside AutoRegisterModifierValueFromAbilityPropertyName).
    local total = nil
-- Value-objects like { Value = N } are unwrapped via the k == prop branch.
     for _, field in ipairs(fields) do
-- Name-based list entries (e.g. "Card Trick") match via the string branch
         local v = unwrap_value(layer[field])
-- against the top-level Name field.
        if v and v ~= 0 then total = (total or 0) + math.abs(v) end
local function is_base_property(record, prop)
     for k, v in pairs(record) do
         if k == "Upgrades" then
            -- skip
        elseif k == prop then
            if type(v) == "table" then v = v["Value"] end
            if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
                return true
            end
        elseif type(v) == "string" then
            if v == prop then return true end
        end
     end
     end
     return false
     return total
end
end


-- Finds the base-level melee scale value by scanning for Scale.Type == "melee".
-- Sums the barrier value and Spirit scaling contributed by a single record
local function find_base_melee_scale(ability)
-- layer (the base ability record, or one upgrade tier). Returns nil when the
     for _, v in pairs(ability) do
-- layer grants no barrier at all; a tier that only raises the scaling still
         if type(v) == "table" then
-- counts, so "+0 value, +1.0 scaling" upgrades are not lost.
             local scale = v["Scale"]
local function sum_barrier(layer, fields)
             if type(scale) == "table"
    local value, scale, found = 0, 0, false
                and scale["Type"] == "melee"
     for _, field in ipairs(fields) do
                and type(scale["Value"]) == "number"
        local entry = layer[field]
                 and scale["Value"] > 0
         if entry ~= nil then
            then
            local v = unwrap_value(entry) or 0
                return scale["Value"]
             local s = 0
             if type(entry) == "table" and type(entry["Scale"]) == "table" then
                 s = entry["Scale"]["Value"] or 0
             end
             end
            if v ~= 0 or s ~= 0 then found = true end
            value = value + v
            scale = scale + s
         end
         end
     end
     end
     return nil
     if not found then return nil end
    return value, scale
end
end


-- ============================================================
-- Pads a whole-number scaling out to one decimal, so a running total of 2
-- Debuff short names (used by RenderDebuff)
-- reads "2.0" next to its "1.5" neighbours instead of "2". Values that already
-- ============================================================
-- carry decimals are left alone, which keeps precision on things like 0.75.
local function format_scale(scale)
    local text = tostring(scale)
    if not text:find(".", 1, true) then text = text .. ".0" end
    return text
end


local debuff_short_name = {
-- Formats one barrier step. Spirit scaling goes through the compact {{Ss}}
    ["StunDuration"]              = "stun",
-- form, expanded via the frame the surrounding #invoke is already running
    ["SleepDuration"]              = "sleep",
-- under: module return values are not re-expanded, so a literal "{{Ss|0.8}}"
    ["SilenceDuration"]            = "silence",
-- would reach the page as text. The plain fallback covers a nil frame
    ["SilenceDebuff"]              = "silence",
-- (module console).
    ["PetrifyDuration"]            = "petrify",
local function format_barrier(value, scale)
    ["HexDuration"]                = "hex",
     local out = tostring(value)
     ["ImmobilizeDuration"]        = "immobilize",
     if scale and scale ~= 0 then
     ["LiftDuration"]              = "lift",
        local scale_text = format_scale(scale)
    ["HoldInPlaceDuration"]        = "immobilize",
        local frame = mw.getCurrentFrame()
    ["RestrictionDuration"]        = "immobilize",
        if frame then
    ["SlowPercent"]                = "slow",
            out = out .. " " .. frame:expandTemplate{
    ["EnemySlowPct"]              = "slow",
                title = "Ss",
    ["MoveSlowPercent"]            = "slow",
                args  = { scale_text, compact = "1", show_value = "1" },
    ["GroundDashReductionPercent"] = "dash slow",
            }
    ["SlowDuration"]              = "slow",
        else
    ["MaxSlowDuration"]            = "slow",
            out = out .. " ×" .. scale_text
    ["BulletDamageAmpDuration"]    = "bullet amp",
        end
    ["BulletResistReduction"]      = "bullet resist reduction",
     end
    ["TimeSlowDuration"]          = "time slow",
     return out
    ["LateCheckoutStun"]          = "stun",
end
     ["BurnDuration"]              = "burn",
     ["BleedDuration"]              = "bleed",
    ["VenomDuration"]              = "venom",
    ["TossDuration"]              = "knockback",
}


-- ============================================================
-- ============================================================
Line 174: Line 168:


local p = {}
local p = {}
-- Renders the barrier an ability grants, with its Spirit Power scaling.
-- Both the value and the scaling accumulate across upgrades, so every step
-- after the first shows the running total: "100 x0.8 → 180 x1.5 (T2)".
-- A tier that only improves the scaling still gets its own segment.
function p.RenderBarrier(ability)
    local fields  = Lists.lists["barrier"]
    local segments = {}
    local value, scale = sum_barrier(ability, fields)
    if value then
        table.insert(segments, format_barrier(value, scale))
    else
        value, scale = 0, 0
    end
    local upgrades = ability["Upgrades"]
    if type(upgrades) == "table" then
        for i, tier in ipairs(upgrades) do
            if type(tier) == "table" then
                local delta_value, delta_scale = sum_barrier(tier, fields)
                if delta_value then
                    value, scale = value + delta_value, scale + delta_scale
                    table.insert(segments,
                        format_barrier(value, scale) .. " (T" .. i .. ")")
                end
            end
        end
    end
    if #segments == 0 then return {} end
    return { ["Barrier"] = table.concat(segments, " → ") }
end


-- Renders charge count and time between charges.
-- Renders charge count and time between charges.
Line 179: Line 206:
function p.RenderCharges(ability)
function p.RenderCharges(ability)
     local base_charges  = unwrap_value(ability["AbilityCharges"])
     local base_charges  = unwrap_value(ability["AbilityCharges"])
     local base_cooldown = ability["AbilityCooldownBetweenCharge"]
     local base_cooldown = unwrap_value(ability["AbilityCooldownBetweenCharge"])
     local has_base      = base_charges ~= nil and base_charges > 0
     local has_base      = base_charges ~= nil and base_charges > 0
     local upgrade_info  = find_in_upgrades(ability, "AbilityCharges")
     local upgrade_info  = find_in_upgrades(ability, "AbilityCharges")
Line 377: Line 404:
end
end


-- Renders heal reduction percentage.
-- Renders Spirit Power scaling.
-- Special case for DisableHealing, shown as 100%.
-- Shows the Scale.Value for abilities whose Scale.Type is "spirit".
function p.RenderHealReduce(ability)
function p.RenderSpiritScaling(ability)
    -- DisableHealing is always 100%, handle it separately
     local base_scale = nil
     local v = unwrap_value(ability["DisableHealing"])
     local base_prop = nil
    if v and v ~= 0 then
        return { ["Heal Reduction"] = "100%" }
    end
     local upgrade = find_in_upgrades(ability, "DisableHealing")
    if upgrade then
        return { ["Heal Reduction"] = "100% (T" .. upgrade.tier .. ")" }
    end


     -- Everything else goes through the generic renderer
     -- Find base Spirit scaling
    local filtered = {}
     for k, v in pairs(ability) do
     for _, prop in ipairs(Lists.lists["healreduce"]) do
         if k ~= "Upgrades" and type(v) == "table" then
         if prop ~= "DisableHealing" then table.insert(filtered, prop) end
            local scale = v["Scale"]
            if type(scale) == "table"
              and scale["Type"] == "spirit"
              and type(scale["Value"]) == "number"
              and scale["Value"] ~= 0 then
                base_scale = scale["Value"]
                base_prop = k
                break
            end
        end
     end
     end
    return render_upgradable_stat(ability, "Heal Reduction", filtered, "%")
end


-- Shows a note if an ability acquires a debuff via upgrades.
-- Shared by debuffresist and dispelmagic.
function p.RenderDebuff(ability, stat)
    local stat_props = Lists.lists[stat]
     local cells = {}
     local cells = {}


     -- If any prop exists in base, this is a base-level debuff — no note needed.
     if base_scale then
    for _, prop in ipairs(stat_props) do
        cells["Spirit Scaling"] = "×" .. base_scale
        if is_base_property(ability, prop) then
            return cells
        end
     end
     end


Line 414: Line 434:
     if type(upgrades) ~= "table" then return cells end
     if type(upgrades) ~= "table" then return cells end


     local tier_debuffs = {}
     local notes = {}
    local seen_names  = {}


     for i, tier in ipairs(upgrades) do
     for i, tier in ipairs(upgrades) do
         if type(tier) == "table" then
         if type(tier) == "table" then
             local names_this_tier = {}
             local upgrade_scale = nil
            local seen_this_tier  = {}
 
             for _, prop in ipairs(stat_props) do
             for k, v in pairs(tier) do
                 local v = tier[prop]
                 if type(v) == "table" then
                if v ~= nil then
                    local scale = v["Scale"]
                    if type(v) == "table" then v = v["Value"] end
                     if type(scale) == "table"
                     if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
                      and scale["Type"] == "spirit"
                        local short = debuff_short_name[prop] or "debuff"
                      and type(scale["Value"]) == "number"
                        if not seen_this_tier[short] then
                      and scale["Value"] ~= 0 then
                            seen_this_tier[short] = true
                        upgrade_scale = scale["Value"]
                            if not seen_names[short] then
                         break
                                seen_names[short] = true
                                table.insert(names_this_tier, short)
                            end
                         end
                     end
                     end
                 end
                 end
             end
             end
             if #names_this_tier > 0 then
 
                 tier_debuffs[i] = names_this_tier
             if upgrade_scale then
                 table.insert(notes,
                    "on T" .. i .. " gets +×" .. upgrade_scale .. " Spirit scaling")
             end
             end
         end
         end
     end
     end


     local tier_indices = {}
     if #notes > 0 then
    for i in pairs(tier_debuffs) do table.insert(tier_indices, i) end
        cells["Notes"] = table.concat(notes, ". ")
     table.sort(tier_indices)
    end
 
     return cells
end


     local parts = {}
-- Renders heal reduction percentage.
     for _, i in ipairs(tier_indices) do
-- Special case for DisableHealing, shown as 100%.
         table.insert(parts, "T" .. i .. " " .. table.concat(tier_debuffs[i], " + "))
function p.RenderHealReduce(ability)
    -- DisableHealing is always 100%, handle it separately
     local v = unwrap_value(ability["DisableHealing"])
    if v and v ~= 0 then
        return { ["Heal Reduction"] = "100%" }
     end
    local upgrade = find_in_upgrades(ability, "DisableHealing")
    if upgrade then
         return { ["Heal Reduction"] = "100% (T" .. upgrade.tier .. ")" }
     end
     end


     if #parts > 0 then cells["Notes"] = table.concat(parts, ", ") end
     -- Everything else goes through the generic renderer
     return cells
    local filtered = {}
    for _, prop in ipairs(Lists.lists["healreduce"]) do
        if prop ~= "DisableHealing" then table.insert(filtered, prop) end
    end
     return render_upgradable_stat(ability, "Heal Reduction", filtered, "%")
end
end


Line 524: Line 556:


     return cells
     return cells
end
-- Renders the amount of Debuff Resist an ability grants.
-- Upgrades stack additively onto the base value, so every step after the first
-- shows the running total: "20% (T3)" when granted only by an upgrade,
-- "10% → 25% (T2)" when a base value is later increased. Chains for any number
-- of steps. No ability currently has both a base and an upgrade value.
function p.RenderExtraDebuffResist(ability)
    local fields  = Lists.lists["extradebuffresist"]
    local segments = {}
    local running = sum_fields(ability, fields)
    if running then
        table.insert(segments, running .. "%")
    end
    local upgrades = ability["Upgrades"]
    if type(upgrades) == "table" then
        for i, tier in ipairs(upgrades) do
            if type(tier) == "table" then
                local delta = sum_fields(tier, fields)
                if delta then
                    running = (running or 0) + delta
                    table.insert(segments, running .. "% (T" .. i .. ")")
                end
            end
        end
    end
    if #segments == 0 then return {} end
    return { ["Debuff Resist"] = table.concat(segments, " → ") }
end
end


return p
return p