<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://deadlock.wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=~2026-DurationAmmoMelee1155</id>
	<title>The Deadlock Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://deadlock.wiki/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=~2026-DurationAmmoMelee1155"/>
	<link rel="alternate" type="text/html" href="https://deadlock.wiki/Special:Contributions/~2026-DurationAmmoMelee1155"/>
	<updated>2026-09-14T02:58:59Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>https://deadlock.wiki/Module:AbilityTable?diff=157562</id>
		<title>Module:AbilityTable</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Module:AbilityTable?diff=157562"/>
		<updated>2026-09-14T00:34:15Z</updated>

		<summary type="html">&lt;p&gt;~2026-DurationAmmoMelee1155: scalingspirit: Upgrades column instead of Notes (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Renders sortable wikitables of abilities filtered by a named stat.&lt;br /&gt;
&lt;br /&gt;
local Lists     = require(&amp;quot;Module:AbilityTable/Lists&amp;quot;)&lt;br /&gt;
local Notes     = require(&amp;quot;Module:AbilityTable/Notes&amp;quot;)&lt;br /&gt;
local Renderers = require(&amp;quot;Module:AbilityTable/ComplexRenderers&amp;quot;)&lt;br /&gt;
local GameData  = require(&amp;quot;Module:GameData&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
-- get_cells(ability, cardprops) -&amp;gt; { [column header] = cell text, Notes = ... }; most renderers ignore cardprops&lt;br /&gt;
local extra_columns = {&lt;br /&gt;
    [&amp;quot;barrier&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Barrier&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderBarrier,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;bossdmgscale&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Damage to Objectives&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderBossDamageScale,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;charges&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Charges&amp;quot;, &amp;quot;Time Between Charges&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderCharges,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;melee&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Base Damage&amp;quot;, &amp;quot;Light Melee Scaling&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderMelee,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;heavymelee&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Base Damage&amp;quot;, &amp;quot;Heavy Melee Scaling&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderHeavyMelee,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;scalingspirit&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Spirit Scaling&amp;quot;, &amp;quot;Upgrades&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderSpiritScaling,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;healreduce&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Heal Reduction&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderHealReduce,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;moveslow&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Move Slow&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderMoveSlow,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;dashslow&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Dash Slow&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderDashSlow,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;extradebuffresist&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Debuff Resist&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderExtraDebuffResist,&lt;br /&gt;
    },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Note enrichment&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Merges auto-generated and human notes for full-table mode.&lt;br /&gt;
-- Capitalizes human note, joins with &amp;quot;. &amp;quot;, ensures trailing period.&lt;br /&gt;
local function enrich_notes_full(extra, human_note)&lt;br /&gt;
    if human_note then&lt;br /&gt;
        human_note = human_note:sub(1,1):upper() .. human_note:sub(2)&lt;br /&gt;
        extra[&amp;quot;Notes&amp;quot;] = extra[&amp;quot;Notes&amp;quot;] and (extra[&amp;quot;Notes&amp;quot;] .. &amp;quot;. &amp;quot; .. human_note) or human_note&lt;br /&gt;
    end&lt;br /&gt;
    if extra[&amp;quot;Notes&amp;quot;] and extra[&amp;quot;Notes&amp;quot;]:sub(-1) ~= &amp;quot;.&amp;quot; then&lt;br /&gt;
        extra[&amp;quot;Notes&amp;quot;] = extra[&amp;quot;Notes&amp;quot;] .. &amp;quot;.&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Merges auto-generated and human notes for compact mode.&lt;br /&gt;
-- Decapitalizes human note (skips T{N} prefixes like &amp;quot;T1 slow&amp;quot;),&lt;br /&gt;
-- joins with &amp;quot;; &amp;quot;, strips trailing period.&lt;br /&gt;
local function enrich_notes_compact(extra, human_note)&lt;br /&gt;
    if human_note then&lt;br /&gt;
        local first = human_note:sub(1,1)&lt;br /&gt;
        if first:match(&amp;quot;%u&amp;quot;) and not human_note:sub(2,2):match(&amp;quot;%d&amp;quot;) then&lt;br /&gt;
            human_note = first:lower() .. human_note:sub(2)&lt;br /&gt;
        end&lt;br /&gt;
        if human_note:sub(-1) == &amp;quot;.&amp;quot; then human_note = human_note:sub(1, -2) end&lt;br /&gt;
        extra[&amp;quot;Notes&amp;quot;] = extra[&amp;quot;Notes&amp;quot;] and (extra[&amp;quot;Notes&amp;quot;] .. &amp;quot;; &amp;quot; .. human_note) or human_note&lt;br /&gt;
    end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Ability views&lt;br /&gt;
--&lt;br /&gt;
-- In-game Ability may containe 1 or more records in AbilityData.json&lt;br /&gt;
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).&lt;br /&gt;
&lt;br /&gt;
-- &amp;quot;Ability View&amp;quot; is a main-record + any relevant sub-ability records.&lt;br /&gt;
-- Views also carry &amp;quot;cardprops&amp;quot; (see card_prop_labels), kept outside the record so membership matching never sees them.&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Card prop labels: key -&amp;gt; { name, title, order }, read from the card in AbilityCards.json.&lt;br /&gt;
-- Built per card, since a key like &amp;quot;Damage&amp;quot; is labelled differently on different cards.&lt;br /&gt;
local function card_prop_labels(card)&lt;br /&gt;
    local labels, n = {}, 0&lt;br /&gt;
&lt;br /&gt;
    local function walk(node)&lt;br /&gt;
        if type(node) ~= &amp;quot;table&amp;quot; then return end&lt;br /&gt;
        local key, name = node[&amp;quot;Key&amp;quot;], node[&amp;quot;Name&amp;quot;]&lt;br /&gt;
        if type(key) == &amp;quot;string&amp;quot; and type(name) == &amp;quot;string&amp;quot; and not labels[key] then&lt;br /&gt;
            n = n + 1&lt;br /&gt;
            local title = node[&amp;quot;Title&amp;quot;]&lt;br /&gt;
            if type(title) == &amp;quot;string&amp;quot; then&lt;br /&gt;
                title = title:gsub(&amp;quot;:%s*$&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
            else&lt;br /&gt;
                title = nil&lt;br /&gt;
            end&lt;br /&gt;
            labels[key] = { name = name, title = title, order = n }&lt;br /&gt;
        end&lt;br /&gt;
        if node[1] ~= nil then&lt;br /&gt;
            for _, child in ipairs(node) do walk(child) end&lt;br /&gt;
        else&lt;br /&gt;
            -- Keyed sections (Range, Move, ...): { PropKey = { Name = ..., Value = ... } }&lt;br /&gt;
            local keyed = {}&lt;br /&gt;
            for k, v in pairs(node) do&lt;br /&gt;
                if type(k) == &amp;quot;string&amp;quot; and type(v) == &amp;quot;table&amp;quot; and v[&amp;quot;Key&amp;quot;] == nil&lt;br /&gt;
                    and type(v[&amp;quot;Name&amp;quot;]) == &amp;quot;string&amp;quot; and not labels[k]&lt;br /&gt;
                then&lt;br /&gt;
                    table.insert(keyed, k)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            table.sort(keyed)&lt;br /&gt;
            for _, k in ipairs(keyed) do&lt;br /&gt;
                n = n + 1&lt;br /&gt;
                labels[k] = { name = node[k][&amp;quot;Name&amp;quot;], order = n }&lt;br /&gt;
            end&lt;br /&gt;
            -- Main props before Alt props, then anything else in key order.&lt;br /&gt;
            if node[&amp;quot;Main&amp;quot;] ~= nil then walk(node[&amp;quot;Main&amp;quot;]) end&lt;br /&gt;
            if node[&amp;quot;Alt&amp;quot;]  ~= nil then walk(node[&amp;quot;Alt&amp;quot;])  end&lt;br /&gt;
            local rest = {}&lt;br /&gt;
            for k, v in pairs(node) do&lt;br /&gt;
                if k ~= &amp;quot;Main&amp;quot; and k ~= &amp;quot;Alt&amp;quot; and k ~= &amp;quot;Upgrades&amp;quot; and type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                    table.insert(rest, k)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            table.sort(rest, function(a, b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
            for _, k in ipairs(rest) do walk(node[k]) end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    for _, section in ipairs({ &amp;quot;Info1&amp;quot;, &amp;quot;Info2&amp;quot;, &amp;quot;Info3&amp;quot; }) do&lt;br /&gt;
        walk(card[section])&lt;br /&gt;
    end&lt;br /&gt;
    local rest = {}&lt;br /&gt;
    for k, v in pairs(card) do&lt;br /&gt;
        if type(v) == &amp;quot;table&amp;quot; and k ~= &amp;quot;Upgrades&amp;quot;&lt;br /&gt;
            and k ~= &amp;quot;Info1&amp;quot; and k ~= &amp;quot;Info2&amp;quot; and k ~= &amp;quot;Info3&amp;quot;&lt;br /&gt;
        then&lt;br /&gt;
            table.insert(rest, k)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    table.sort(rest, function(a, b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
    for _, k in ipairs(rest) do walk(card[k]) end&lt;br /&gt;
&lt;br /&gt;
    -- Header attributes are stored on the card without a Name.&lt;br /&gt;
    local header_attrs = {&lt;br /&gt;
        { &amp;quot;AbilityCastRange&amp;quot;,            &amp;quot;Cast Range&amp;quot; },&lt;br /&gt;
        { &amp;quot;Radius&amp;quot;,                      &amp;quot;Radius&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityDuration&amp;quot;,             &amp;quot;Duration&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityChannelTime&amp;quot;,          &amp;quot;Channel Time&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityCooldown&amp;quot;,             &amp;quot;Cooldown&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityCharges&amp;quot;,              &amp;quot;Charges&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityCooldownBetweenCharge&amp;quot;, &amp;quot;Charge Cooldown&amp;quot; },&lt;br /&gt;
    }&lt;br /&gt;
    for _, attr in ipairs(header_attrs) do&lt;br /&gt;
        if not labels[attr[1]] then&lt;br /&gt;
            n = n + 1&lt;br /&gt;
            labels[attr[1]] = { name = attr[2], order = n }&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return labels&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local _views = nil   -- card key -&amp;gt; { view = record, heroName = ..., abilityNum = ..., cardprops = ... }&lt;br /&gt;
local _order = nil   -- array of card keys, sorted by hero then slot&lt;br /&gt;
&lt;br /&gt;
local function build_views()&lt;br /&gt;
    if _views then return _views, _order end&lt;br /&gt;
    _views, _order = {}, {}&lt;br /&gt;
&lt;br /&gt;
    local hero_data = mw.loadJsonData(&amp;quot;Data:HeroData.json&amp;quot;)&lt;br /&gt;
    local active_heroes = {}&lt;br /&gt;
    for _, hero in pairs(hero_data) do&lt;br /&gt;
        if type(hero) == &amp;quot;table&amp;quot;&lt;br /&gt;
            and hero[&amp;quot;IsDisabled&amp;quot;] == false&lt;br /&gt;
            and type(hero[&amp;quot;Name&amp;quot;]) == &amp;quot;string&amp;quot;&lt;br /&gt;
            and (hero[&amp;quot;IsSelectable&amp;quot;] == nil or hero[&amp;quot;IsSelectable&amp;quot;] ~= false)&lt;br /&gt;
        then&lt;br /&gt;
            active_heroes[hero[&amp;quot;Name&amp;quot;]] = true&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)&lt;br /&gt;
    local cards        = mw.loadJsonData(&amp;quot;Data:AbilityCards.json&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    local card_of = {}&lt;br /&gt;
    for _, hero_entry in pairs(cards) do&lt;br /&gt;
        if type(hero_entry) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            local hero_name = hero_entry[&amp;quot;Name&amp;quot;]&lt;br /&gt;
            if hero_name and active_heroes[hero_name] then&lt;br /&gt;
                for slot_str, ability in pairs(hero_entry) do&lt;br /&gt;
                    local slot_num = tonumber(slot_str)&lt;br /&gt;
                    local key = type(ability) == &amp;quot;table&amp;quot; and ability[&amp;quot;Key&amp;quot;] or nil&lt;br /&gt;
                    if slot_num and key and ability_data[key] then&lt;br /&gt;
                        card_of[key] = { heroName = hero_name, abilityNum = slot_num, card = ability }&lt;br /&gt;
                    end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Attach each non-card record to the longest card key it prefixes.&lt;br /&gt;
    local subs = {}&lt;br /&gt;
    for key, record in pairs(ability_data) do&lt;br /&gt;
        if not card_of[key] and record[&amp;quot;IsDisabled&amp;quot;] ~= true then&lt;br /&gt;
            local parent = nil&lt;br /&gt;
            for card_key in pairs(card_of) do&lt;br /&gt;
                if key:sub(1, #card_key + 1) == card_key .. &amp;quot;_&amp;quot;&lt;br /&gt;
                    and (parent == nil or #card_key &amp;gt; #parent)&lt;br /&gt;
                then&lt;br /&gt;
                    parent = card_key&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            if parent then&lt;br /&gt;
                subs[parent] = subs[parent] or {}&lt;br /&gt;
                table.insert(subs[parent], record)&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    for key, info in pairs(card_of) do&lt;br /&gt;
        local view = {}&lt;br /&gt;
        for k, v in pairs(ability_data[key]) do view[k] = v end&lt;br /&gt;
        view[&amp;quot;_subabilities&amp;quot;] = subs[key]&lt;br /&gt;
        _views[key] = {&lt;br /&gt;
            view       = view,&lt;br /&gt;
            heroName   = info.heroName,&lt;br /&gt;
            abilityNum = info.abilityNum,&lt;br /&gt;
            cardprops  = card_prop_labels(info.card),&lt;br /&gt;
        }&lt;br /&gt;
        table.insert(_order, key)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    table.sort(_order, function(a, b)&lt;br /&gt;
        local ia, ib = _views[a], _views[b]&lt;br /&gt;
        if ia.heroName ~= ib.heroName then return ia.heroName &amp;lt; ib.heroName end&lt;br /&gt;
        return ia.abilityNum &amp;lt; ib.abilityNum&lt;br /&gt;
    end)&lt;br /&gt;
&lt;br /&gt;
    return _views, _order&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Membership&lt;br /&gt;
-- Pieces of code that help in telling  &amp;quot;is ability X a member of list Y&amp;quot;.&lt;br /&gt;
-- main function is: p.is_member()&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Memoized name-exclusion set per list (from Lists.exclude_abilities).&lt;br /&gt;
local _excluded = {}&lt;br /&gt;
local function excluded_set(stat)&lt;br /&gt;
    if _excluded[stat] then return _excluded[stat] end&lt;br /&gt;
    local set = {}&lt;br /&gt;
    for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do&lt;br /&gt;
        set[name] = true&lt;br /&gt;
    end&lt;br /&gt;
    _excluded[stat] = set&lt;br /&gt;
    return set&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Per-list rules that can&#039;t be expressed as prop lists or exclusions.&lt;br /&gt;
-- Returns true to keep the ability, false to drop it.&lt;br /&gt;
local function special_ok(stat, record, ability_num)&lt;br /&gt;
    if stat == &amp;quot;dispelmagic&amp;quot; and ability_num == 4 then return false end&lt;br /&gt;
    return true&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Public: does ability `ability_key` belong to list `stat`?&lt;br /&gt;
function p.is_member(stat, ability_key, ability_num)&lt;br /&gt;
    local props = Lists.lists[stat]&lt;br /&gt;
    if not props then return false end&lt;br /&gt;
&lt;br /&gt;
    local views = build_views()&lt;br /&gt;
    local entry = views[ability_key]&lt;br /&gt;
    local view  = entry and entry.view&lt;br /&gt;
&lt;br /&gt;
    if not view then&lt;br /&gt;
        view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]&lt;br /&gt;
        if not view then return false end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local name = view[&amp;quot;Name&amp;quot;]&lt;br /&gt;
    if name and excluded_set(stat)[name] then return false end&lt;br /&gt;
&lt;br /&gt;
    ability_num = ability_num or (entry and entry.abilityNum)&lt;br /&gt;
    if not special_ok(stat, view, ability_num) then return false end&lt;br /&gt;
&lt;br /&gt;
    return GameData.entity_matches(view, props)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Entry point&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
function p.render(frame)&lt;br /&gt;
    local stat = mw.text.trim(frame.args[1] or &amp;quot;&amp;quot;):lower()&lt;br /&gt;
&lt;br /&gt;
    local internal_keys = Lists.lists[stat]&lt;br /&gt;
    if not internal_keys then&lt;br /&gt;
        return &#039;&amp;lt;span class=&amp;quot;error&amp;quot;&amp;gt;AbilityTable: unknown list &amp;quot;&#039; .. stat .. &#039;&amp;quot;.&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local hide_set = {}&lt;br /&gt;
    local hide_param = mw.text.trim(frame.args[&amp;quot;hide&amp;quot;] or &amp;quot;&amp;quot;)&lt;br /&gt;
    if hide_param ~= &amp;quot;&amp;quot; then&lt;br /&gt;
        for col in hide_param:gmatch(&amp;quot;[^,]+&amp;quot;) do hide_set[mw.text.trim(col)] = true end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local compact = mw.text.trim(frame.args[&amp;quot;compact&amp;quot;] or &amp;quot;&amp;quot;) ~= &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    local collapsed = mw.text.trim(frame.args[&amp;quot;collapsed&amp;quot;] or &amp;quot;&amp;quot;):lower()&lt;br /&gt;
&lt;br /&gt;
    local views, order = build_views()&lt;br /&gt;
    local spec       = extra_columns[stat]&lt;br /&gt;
    local headers    = spec and spec.headers or {}&lt;br /&gt;
    local get_cells  = spec and spec.get_cells or function() return {} end&lt;br /&gt;
    local stat_notes = Notes[stat]&lt;br /&gt;
&lt;br /&gt;
    local filtered = {}&lt;br /&gt;
    for _, key in ipairs(order) do&lt;br /&gt;
        if p.is_member(stat, key) then&lt;br /&gt;
            table.insert(filtered, key)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #filtered == 0 then&lt;br /&gt;
        return &amp;quot;No abilities found for list: &amp;quot; .. stat&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local lists_url = mw.uri.fullUrl(&amp;quot;Module:AbilityTable/Lists&amp;quot;)&lt;br /&gt;
    local notes_url = mw.uri.fullUrl(&amp;quot;Module:AbilityTable/Notes&amp;quot;, { action = &amp;quot;edit&amp;quot; })&lt;br /&gt;
    local disclaimer = frame:preprocess(&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ability-table-disclaimer&amp;quot;&amp;gt;&#039;&lt;br /&gt;
        .. &#039;This is a dynamic list &#039;&lt;br /&gt;
        .. &#039;&amp;lt;span class=&amp;quot;ability-table-list-name&amp;quot;&amp;gt;&amp;quot;&#039; .. stat .. &#039;&amp;quot;&amp;lt;/span&amp;gt;. &#039;&lt;br /&gt;
        .. &#039;[&#039; .. tostring(lists_url) .. &#039; Edit contents]&#039;&lt;br /&gt;
        .. &#039; · &#039;&lt;br /&gt;
        .. &#039;[&#039; .. tostring(notes_url) .. &#039; Edit notes].&#039;&lt;br /&gt;
        .. &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    if compact then&lt;br /&gt;
        local hero_order     = {}&lt;br /&gt;
        local hero_abilities = {}&lt;br /&gt;
        for _, key in ipairs(filtered) do&lt;br /&gt;
            local hero = views[key].heroName&lt;br /&gt;
            if not hero_abilities[hero] then&lt;br /&gt;
                table.insert(hero_order, hero)&lt;br /&gt;
                hero_abilities[hero] = {}&lt;br /&gt;
            end&lt;br /&gt;
            table.insert(hero_abilities[hero], views[key])&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        local items = {}&lt;br /&gt;
        for _, hero in ipairs(hero_order) do&lt;br /&gt;
            local hero_cell = frame:expandTemplate{ title = &amp;quot;HeroIcon&amp;quot;, args = { hero } }&lt;br /&gt;
            local ability_parts = {}&lt;br /&gt;
            for _, entry in ipairs(hero_abilities[hero]) do&lt;br /&gt;
                local ability = entry.view&lt;br /&gt;
                local name = ability[&amp;quot;Name&amp;quot;]&lt;br /&gt;
                local ability_cell = frame:expandTemplate{ title = &amp;quot;AbilityIcon&amp;quot;, args = { name } }&lt;br /&gt;
&lt;br /&gt;
                local extra = get_cells(ability, entry.cardprops)&lt;br /&gt;
                enrich_notes_compact(extra, stat_notes and stat_notes[name])&lt;br /&gt;
&lt;br /&gt;
                local note = extra[&amp;quot;Notes&amp;quot;]&lt;br /&gt;
                if note then&lt;br /&gt;
                    table.insert(ability_parts, ability_cell .. &amp;quot; (&amp;quot; .. note .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                else&lt;br /&gt;
                    table.insert(ability_parts, ability_cell)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            table.insert(items, &amp;quot;&amp;lt;li&amp;gt;&amp;quot; .. hero_cell .. &amp;quot;: &amp;quot; .. table.concat(ability_parts, &amp;quot;, &amp;quot;) .. &amp;quot;&amp;lt;/li&amp;gt;&amp;quot;)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        return disclaimer .. &amp;quot;&amp;lt;ul&amp;gt;&amp;quot; .. table.concat(items) .. &amp;quot;&amp;lt;/ul&amp;gt;&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local has_notes_header = false&lt;br /&gt;
    local final_headers = {}&lt;br /&gt;
    for _, h in ipairs(headers) do&lt;br /&gt;
        if h == &amp;quot;Notes&amp;quot; then&lt;br /&gt;
            has_notes_header = true&lt;br /&gt;
        elseif not hide_set[h] then&lt;br /&gt;
            table.insert(final_headers, h)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set[&amp;quot;Notes&amp;quot;]&lt;br /&gt;
    if needs_notes then table.insert(final_headers, &amp;quot;Notes&amp;quot;) end&lt;br /&gt;
&lt;br /&gt;
    local table_class = &#039;wikitable sortable ability-table&#039;&lt;br /&gt;
    if collapsed ~= &amp;quot;&amp;quot; then&lt;br /&gt;
        table_class = table_class .. &#039; mw-collapsible&#039;&lt;br /&gt;
        if collapsed == &amp;quot;yes&amp;quot; or collapsed == &amp;quot;true&amp;quot; or collapsed == &amp;quot;1&amp;quot; then&lt;br /&gt;
            table_class = table_class .. &#039; mw-collapsed&#039;&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local out = {}&lt;br /&gt;
    table.insert(out, disclaimer)&lt;br /&gt;
    table.insert(out, &#039;{| class=&amp;quot;&#039; .. table_class .. &#039;&amp;quot;&#039;)&lt;br /&gt;
&lt;br /&gt;
    local header_row = &#039;! class=&amp;quot;ability-table-col-hero&amp;quot; | Hero&#039;&lt;br /&gt;
                    .. &#039; !! class=&amp;quot;ability-table-col-ability&amp;quot; | Ability&#039;&lt;br /&gt;
    for _, h in ipairs(final_headers) do&lt;br /&gt;
        header_row = header_row .. &#039; !! class=&amp;quot;ability-table-col-&#039;&lt;br /&gt;
            .. h:lower():gsub(&amp;quot;%s+&amp;quot;, &amp;quot;-&amp;quot;) .. &#039;&amp;quot; | &#039; .. h&lt;br /&gt;
    end&lt;br /&gt;
    table.insert(out, header_row)&lt;br /&gt;
&lt;br /&gt;
        local hero_counts = {}&lt;br /&gt;
    for _, key in ipairs(filtered) do&lt;br /&gt;
        local hero = views[key].heroName&lt;br /&gt;
        hero_counts[hero] = (hero_counts[hero] or 0) + 1&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local hero_seen = {}&lt;br /&gt;
&lt;br /&gt;
    for _, key in ipairs(filtered) do&lt;br /&gt;
        local entry   = views[key]&lt;br /&gt;
        local ability = entry.view&lt;br /&gt;
        local hero    = entry.heroName&lt;br /&gt;
&lt;br /&gt;
        local ability_cell = frame:expandTemplate{&lt;br /&gt;
            title = &amp;quot;AbilityIcon&amp;quot;,&lt;br /&gt;
            args = { ability[&amp;quot;Name&amp;quot;] }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        local extra = get_cells(ability, entry.cardprops)&lt;br /&gt;
        enrich_notes_full(extra, stat_notes and stat_notes[ability[&amp;quot;Name&amp;quot;]])&lt;br /&gt;
&lt;br /&gt;
        local row&lt;br /&gt;
&lt;br /&gt;
        if not hero_seen[hero] then&lt;br /&gt;
            local hero_cell = frame:expandTemplate{&lt;br /&gt;
                title = &amp;quot;HeroIcon&amp;quot;,&lt;br /&gt;
                args = { hero }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            if hero_counts[hero] &amp;gt; 1 then&lt;br /&gt;
                row = &#039;| rowspan=&amp;quot;&#039; .. hero_counts[hero] .. &#039;&amp;quot; class=&amp;quot;ability-table-cell-hero&amp;quot; | &#039;&lt;br /&gt;
                    .. hero_cell&lt;br /&gt;
                    .. &#039; || class=&amp;quot;ability-table-cell-ability&amp;quot; | &#039; .. ability_cell&lt;br /&gt;
            else&lt;br /&gt;
                row = &#039;| class=&amp;quot;ability-table-cell-hero&amp;quot; | &#039;&lt;br /&gt;
                    .. hero_cell&lt;br /&gt;
                    .. &#039; || class=&amp;quot;ability-table-cell-ability&amp;quot; | &#039; .. ability_cell&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            hero_seen[hero] = true&lt;br /&gt;
        else&lt;br /&gt;
            row = &#039;| class=&amp;quot;ability-table-cell-ability&amp;quot; | &#039; .. ability_cell&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        for _, header in ipairs(final_headers) do&lt;br /&gt;
            local value = extra[header]&lt;br /&gt;
            local cls   = &amp;quot;ability-table-cell-&amp;quot; .. header:lower():gsub(&amp;quot;%s+&amp;quot;, &amp;quot;-&amp;quot;)&lt;br /&gt;
            local content = value ~= nil and tostring(value) or &amp;quot;—&amp;quot;&lt;br /&gt;
            row = row .. &#039; || class=&amp;quot;&#039; .. cls .. &#039;&amp;quot; | &#039; .. content&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        table.insert(out, &amp;quot;|-&amp;quot;)&lt;br /&gt;
        table.insert(out, row)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    table.insert(out, &amp;quot;|}&amp;quot;)&lt;br /&gt;
    return table.concat(out, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>~2026-DurationAmmoMelee1155</name></author>
	</entry>
	<entry>
		<id>https://deadlock.wiki/Module:AbilityTable/ComplexRenderers?diff=157561</id>
		<title>Module:AbilityTable/ComplexRenderers</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Module:AbilityTable/ComplexRenderers?diff=157561"/>
		<updated>2026-09-14T00:33:16Z</updated>

		<summary type="html">&lt;p&gt;~2026-DurationAmmoMelee1155: RenderSpiritScaling: upgrades go to an Upgrades cell with {{Ss}} totals; drop &amp;quot;+&amp;quot; on additive deltas (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Complex cell renderers for AbilityTable.&lt;br /&gt;
local Lists = require(&amp;quot;Module:AbilityTable/Lists&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Shared helpers&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Unwraps a value that may be a raw number or a { Value = N } table.&lt;br /&gt;
local function unwrap_value(v)&lt;br /&gt;
    if type(v) == &amp;quot;number&amp;quot; then return v end&lt;br /&gt;
    if type(v) == &amp;quot;table&amp;quot;  then return v[&amp;quot;Value&amp;quot;] end&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Searches Upgrades[] for a field and returns { tier = N, value = V } at its&lt;br /&gt;
-- first appearance, or nil if the field is absent from all upgrade tiers.&lt;br /&gt;
local function find_in_upgrades(ability, field)&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    for i, tier in ipairs(upgrades) do&lt;br /&gt;
        if type(tier) == &amp;quot;table&amp;quot; and tier[field] ~= nil then&lt;br /&gt;
            return { tier = i, value = unwrap_value(tier[field]) }&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Finds the first non-zero value for any of the given fields, checking base&lt;br /&gt;
-- then upgrades in order. Returns field, tier (nil = base), absolute value.&lt;br /&gt;
local function find_first_occurrence(ability, fields)&lt;br /&gt;
    for _, field in ipairs(fields) do&lt;br /&gt;
        local v = unwrap_value(ability[field])&lt;br /&gt;
        if v and v ~= 0 then return field, nil, math.abs(v) end&lt;br /&gt;
    end&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                for _, field in ipairs(fields) do&lt;br /&gt;
                    local v = unwrap_value(tier[field])&lt;br /&gt;
                    if v and v ~= 0 then return field, i, math.abs(v) end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return nil, nil, nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Sums the non-zero values of `fields` within a single record layer (the base&lt;br /&gt;
-- ability record, or one upgrade tier). Returns nil when none are present.&lt;br /&gt;
local function sum_fields(layer, fields)&lt;br /&gt;
    local total = nil&lt;br /&gt;
    for _, field in ipairs(fields) do&lt;br /&gt;
        local v = unwrap_value(layer[field])&lt;br /&gt;
        if v and v ~= 0 then total = (total or 0) + math.abs(v) end&lt;br /&gt;
    end&lt;br /&gt;
    return total&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Sums the barrier value and Spirit scaling contributed by a single record&lt;br /&gt;
-- layer (the base ability record, or one upgrade tier). Returns nil when the&lt;br /&gt;
-- layer grants no barrier at all; a tier that only raises the scaling still&lt;br /&gt;
-- counts, so &amp;quot;+0 value, +1.0 scaling&amp;quot; upgrades are not lost.&lt;br /&gt;
local function sum_barrier(layer, fields)&lt;br /&gt;
    local value, scale, found = 0, 0, false&lt;br /&gt;
    for _, field in ipairs(fields) do&lt;br /&gt;
        local entry = layer[field]&lt;br /&gt;
        if entry ~= nil then&lt;br /&gt;
            local v = unwrap_value(entry) or 0&lt;br /&gt;
            local s = 0&lt;br /&gt;
            if type(entry) == &amp;quot;table&amp;quot; and type(entry[&amp;quot;Scale&amp;quot;]) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                s = entry[&amp;quot;Scale&amp;quot;][&amp;quot;Value&amp;quot;] or 0&lt;br /&gt;
            end&lt;br /&gt;
            if v ~= 0 or s ~= 0 then found = true end&lt;br /&gt;
            value = value + v&lt;br /&gt;
            scale = scale + s&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    if not found then return nil end&lt;br /&gt;
    return value, scale&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Pads a whole-number scaling out to one decimal, so a running total of 2&lt;br /&gt;
-- reads &amp;quot;2.0&amp;quot; next to its &amp;quot;1.5&amp;quot; neighbours instead of &amp;quot;2&amp;quot;. Values that already&lt;br /&gt;
-- carry decimals are left alone, which keeps precision on things like 0.75.&lt;br /&gt;
local function format_scale(scale)&lt;br /&gt;
    local text = tostring(scale)&lt;br /&gt;
    if not text:find(&amp;quot;.&amp;quot;, 1, true) then text = text .. &amp;quot;.0&amp;quot; end&lt;br /&gt;
    return text&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Formats one barrier step. Spirit scaling goes through the compact {{Ss}}&lt;br /&gt;
-- form, expanded via the frame the surrounding #invoke is already running&lt;br /&gt;
-- under: module return values are not re-expanded, so a literal &amp;quot;{{Ss|0.8}}&amp;quot;&lt;br /&gt;
-- would reach the page as text. The plain fallback covers a nil frame&lt;br /&gt;
-- (module console).&lt;br /&gt;
local function format_barrier(value, scale)&lt;br /&gt;
    local out = tostring(value)&lt;br /&gt;
    if scale and scale ~= 0 then&lt;br /&gt;
        local scale_text = format_scale(scale)&lt;br /&gt;
        local frame = mw.getCurrentFrame()&lt;br /&gt;
        if frame then&lt;br /&gt;
            out = out .. &amp;quot; &amp;quot; .. frame:expandTemplate{&lt;br /&gt;
                title = &amp;quot;Ss&amp;quot;,&lt;br /&gt;
                args  = { scale_text, compact = &amp;quot;1&amp;quot;, show_value = &amp;quot;1&amp;quot; },&lt;br /&gt;
            }&lt;br /&gt;
        else&lt;br /&gt;
            out = out .. &amp;quot; ×&amp;quot; .. scale_text&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Formats a 0–1 multiplier as a percentage: 0.2 → &amp;quot;20%&amp;quot;, 0.125 → &amp;quot;12.5%&amp;quot;.&lt;br /&gt;
-- %.10g absorbs floating-point noise from the ×100.&lt;br /&gt;
local function format_percent(v)&lt;br /&gt;
    return string.format(&amp;quot;%.10g&amp;quot;, v * 100) .. &amp;quot;%&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Generic renderer for a single numeric stat that can increase&lt;br /&gt;
-- additively through upgrades. Shows value in a named column&lt;br /&gt;
-- and generates upgrade notes with running totals.&lt;br /&gt;
-- Note: auto-generated notes omit trailing periods; the main&lt;br /&gt;
-- module&#039;s ensure_period handles that.&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
local function render_upgradable_stat(ability, col_name, fields, suffix)&lt;br /&gt;
    suffix = suffix or &amp;quot;&amp;quot;&lt;br /&gt;
    local cells = {}&lt;br /&gt;
    local notes = {}&lt;br /&gt;
&lt;br /&gt;
    local first_field, first_tier, first_val = find_first_occurrence(ability, fields)&lt;br /&gt;
    if not first_field then return cells end&lt;br /&gt;
&lt;br /&gt;
    local cell_str = tostring(first_val) .. suffix&lt;br /&gt;
    if first_tier then cell_str = cell_str .. &amp;quot; (T&amp;quot; .. first_tier .. &amp;quot;)&amp;quot; end&lt;br /&gt;
    cells[col_name] = cell_str&lt;br /&gt;
&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        local running    = first_val&lt;br /&gt;
        local seen_first = (first_tier == nil)&lt;br /&gt;
&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                if first_tier == i then&lt;br /&gt;
                    seen_first = true&lt;br /&gt;
                elseif seen_first then&lt;br /&gt;
                    local delta = nil&lt;br /&gt;
                    for _, field in ipairs(fields) do&lt;br /&gt;
                        local v = unwrap_value(tier[field])&lt;br /&gt;
                        if v and v ~= 0 then delta = math.abs(v); break end&lt;br /&gt;
                    end&lt;br /&gt;
                    if delta then&lt;br /&gt;
                        local total = running + delta&lt;br /&gt;
                        table.insert(notes, &amp;quot;T&amp;quot; .. i .. &amp;quot; upgrade increases &amp;quot;&lt;br /&gt;
                            .. col_name:lower() .. &amp;quot; by &amp;quot; .. delta .. suffix&lt;br /&gt;
                            .. &amp;quot; (total &amp;quot; .. total .. suffix .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                        running = total&lt;br /&gt;
                    end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #notes &amp;gt; 0 then cells[&amp;quot;Notes&amp;quot;] = table.concat(notes, &amp;quot; &amp;quot;) end&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Exported renderers&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
-- Renders the barrier an ability grants, with its Spirit Power scaling.&lt;br /&gt;
-- Both the value and the scaling accumulate across upgrades, so every step&lt;br /&gt;
-- after the first shows the running total: &amp;quot;100 x0.8 → 180 x1.5 (T2)&amp;quot;.&lt;br /&gt;
-- A tier that only improves the scaling still gets its own segment.&lt;br /&gt;
function p.RenderBarrier(ability)&lt;br /&gt;
    local fields   = Lists.lists[&amp;quot;barrier&amp;quot;]&lt;br /&gt;
    local segments = {}&lt;br /&gt;
&lt;br /&gt;
    local value, scale = sum_barrier(ability, fields)&lt;br /&gt;
    if value then&lt;br /&gt;
        table.insert(segments, format_barrier(value, scale))&lt;br /&gt;
    else&lt;br /&gt;
        value, scale = 0, 0&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                local delta_value, delta_scale = sum_barrier(tier, fields)&lt;br /&gt;
                if delta_value then&lt;br /&gt;
                    value, scale = value + delta_value, scale + delta_scale&lt;br /&gt;
                    table.insert(segments,&lt;br /&gt;
                        format_barrier(value, scale) .. &amp;quot; (T&amp;quot; .. i .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #segments == 0 then return {} end&lt;br /&gt;
    return { [&amp;quot;Barrier&amp;quot;] = table.concat(segments, &amp;quot; → &amp;quot;) }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders charge count and time between charges.&lt;br /&gt;
-- Special case for abilities that gain charges only through upgrades.&lt;br /&gt;
function p.RenderCharges(ability)&lt;br /&gt;
    local base_charges  = unwrap_value(ability[&amp;quot;AbilityCharges&amp;quot;])&lt;br /&gt;
    local base_cooldown = unwrap_value(ability[&amp;quot;AbilityCooldownBetweenCharge&amp;quot;])&lt;br /&gt;
    local has_base      = base_charges ~= nil and base_charges &amp;gt; 0&lt;br /&gt;
    local upgrade_info  = find_in_upgrades(ability, &amp;quot;AbilityCharges&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    local cooldown, upgrade_cooldown&lt;br /&gt;
    if has_base then&lt;br /&gt;
        if type(base_cooldown) == &amp;quot;number&amp;quot; and base_cooldown &amp;gt; 0 then&lt;br /&gt;
            cooldown = base_cooldown&lt;br /&gt;
        else&lt;br /&gt;
            local cd = find_in_upgrades(ability, &amp;quot;AbilityCooldownBetweenCharge&amp;quot;)&lt;br /&gt;
            if cd and cd.value and cd.value &amp;gt; 0 then cooldown = cd.value end&lt;br /&gt;
        end&lt;br /&gt;
    else&lt;br /&gt;
        local cd = find_in_upgrades(ability, &amp;quot;AbilityCooldownBetweenCharge&amp;quot;)&lt;br /&gt;
        if cd and cd.value and cd.value &amp;gt; 0 then upgrade_cooldown = cd.value end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells = {}&lt;br /&gt;
&lt;br /&gt;
    if has_base then&lt;br /&gt;
        cells[&amp;quot;Charges&amp;quot;] = tostring(base_charges)&lt;br /&gt;
        if cooldown then&lt;br /&gt;
            cells[&amp;quot;Time Between Charges&amp;quot;] = tostring(cooldown) .. &amp;quot;s&amp;quot;&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local note_parts = {}&lt;br /&gt;
    if upgrade_info then&lt;br /&gt;
        local n = upgrade_info.value or &amp;quot;?&amp;quot;&lt;br /&gt;
        local charge_str = n .. &amp;quot; charge&amp;quot; .. (n ~= 1 and &amp;quot;s&amp;quot; or &amp;quot;&amp;quot;)&lt;br /&gt;
        if has_base then&lt;br /&gt;
            table.insert(note_parts, &amp;quot;+&amp;quot; .. charge_str .. &amp;quot; on T&amp;quot; .. upgrade_info.tier .. &amp;quot; upgrade&amp;quot;)&lt;br /&gt;
        else&lt;br /&gt;
            local cd_str = upgrade_cooldown and (upgrade_cooldown .. &amp;quot;s&amp;quot;) or &amp;quot;unknown&amp;quot;&lt;br /&gt;
            table.insert(note_parts, &amp;quot;Becomes charged on T&amp;quot; .. upgrade_info.tier&lt;br /&gt;
                .. &amp;quot; upgrade with &amp;quot; .. charge_str&lt;br /&gt;
                .. &amp;quot; and &amp;quot; .. cd_str .. &amp;quot; time between charges&amp;quot;)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    if ability[&amp;quot;AbilityChargesConditionally&amp;quot;] ~= nil then&lt;br /&gt;
        table.insert(note_parts, &amp;quot;Has a conditional charge&amp;quot;)&lt;br /&gt;
    end&lt;br /&gt;
    if #note_parts &amp;gt; 0 then&lt;br /&gt;
        cells[&amp;quot;Notes&amp;quot;] = table.concat(note_parts, &amp;quot; &amp;quot;)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders base damage and melee scaling.&lt;br /&gt;
-- Special case for upgrades that switch from Light to Heavy Melee scaling.&lt;br /&gt;
function p.RenderMelee(ability)&lt;br /&gt;
    local melee_types = {}&lt;br /&gt;
    for _, t in ipairs(Lists.lists[&amp;quot;melee&amp;quot;]) do melee_types[t] = true end&lt;br /&gt;
&lt;br /&gt;
    -- Find the base property with active melee/heavy_melee scaling (Scale.Value &amp;gt; 0)&lt;br /&gt;
    local base_prop, base_damage, base_scale, base_type&lt;br /&gt;
    for k, v in pairs(ability) do&lt;br /&gt;
        if k ~= &amp;quot;Upgrades&amp;quot; and type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
            if type(scale) == &amp;quot;table&amp;quot; and melee_types[scale[&amp;quot;Type&amp;quot;]]&lt;br /&gt;
               and type(scale[&amp;quot;Value&amp;quot;]) == &amp;quot;number&amp;quot; and scale[&amp;quot;Value&amp;quot;] &amp;gt; 0 then&lt;br /&gt;
                base_prop = k&lt;br /&gt;
                base_damage = v[&amp;quot;Value&amp;quot;] or 0&lt;br /&gt;
                base_scale = scale[&amp;quot;Value&amp;quot;]&lt;br /&gt;
                base_type = scale[&amp;quot;Type&amp;quot;]&lt;br /&gt;
                break&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells = {}&lt;br /&gt;
    cells[&amp;quot;Base Damage&amp;quot;] = tostring(base_damage or 0)&lt;br /&gt;
    if base_scale then&lt;br /&gt;
        cells[&amp;quot;Light Melee Scaling&amp;quot;] = &amp;quot;×&amp;quot; .. base_scale&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) ~= &amp;quot;table&amp;quot; then return cells end&lt;br /&gt;
&lt;br /&gt;
    local notes = {}&lt;br /&gt;
    local running_damage = base_damage or 0&lt;br /&gt;
    local running_scale = base_scale or 0&lt;br /&gt;
    local running_type = base_type&lt;br /&gt;
&lt;br /&gt;
    for i, tier in ipairs(upgrades) do&lt;br /&gt;
        if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            local damage_delta = 0&lt;br /&gt;
            local new_scale = nil&lt;br /&gt;
            local new_type = nil&lt;br /&gt;
&lt;br /&gt;
            for k, v in pairs(tier) do&lt;br /&gt;
                if type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                    local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
                    if type(scale) == &amp;quot;table&amp;quot; and melee_types[scale[&amp;quot;Type&amp;quot;]] then&lt;br /&gt;
                        damage_delta = damage_delta + (v[&amp;quot;Value&amp;quot;] or 0)&lt;br /&gt;
                        if scale[&amp;quot;Value&amp;quot;] &amp;gt; 0 then&lt;br /&gt;
                            new_scale = scale[&amp;quot;Value&amp;quot;]&lt;br /&gt;
                            new_type = scale[&amp;quot;Type&amp;quot;]&lt;br /&gt;
                        end&lt;br /&gt;
                    end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            local flat_delta = 0&lt;br /&gt;
            if base_prop and type(tier[base_prop]) == &amp;quot;number&amp;quot; then&lt;br /&gt;
                flat_delta = tier[base_prop]&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            local tier_parts = {}&lt;br /&gt;
&lt;br /&gt;
            if new_type and new_type ~= running_type then&lt;br /&gt;
                running_damage = running_damage + damage_delta&lt;br /&gt;
                running_scale = new_scale&lt;br /&gt;
                running_type = new_type&lt;br /&gt;
                local type_label = new_type == &amp;quot;heavy_melee&amp;quot; and &amp;quot;&#039;&#039;&#039;Heavy Melee&#039;&#039;&#039;&amp;quot; or &amp;quot;Light Melee&amp;quot;&lt;br /&gt;
                local parts = {}&lt;br /&gt;
                if running_damage &amp;gt; 0 then&lt;br /&gt;
                    table.insert(parts, running_damage .. &amp;quot; base damage&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
                table.insert(parts, &amp;quot;×&amp;quot; .. new_scale .. &amp;quot; &amp;quot; .. type_label .. &amp;quot; scaling&amp;quot;)&lt;br /&gt;
                table.insert(tier_parts, &amp;quot;on T&amp;quot; .. i .. &amp;quot; becomes &amp;quot; .. table.concat(parts, &amp;quot; with &amp;quot;))&lt;br /&gt;
            elseif new_scale then&lt;br /&gt;
                running_damage = running_damage + damage_delta&lt;br /&gt;
                running_scale = running_scale + new_scale&lt;br /&gt;
                local parts = {}&lt;br /&gt;
                if damage_delta ~= 0 then&lt;br /&gt;
                    table.insert(parts, &amp;quot;+&amp;quot; .. damage_delta .. &amp;quot; base damage&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
                table.insert(parts, &amp;quot;+×&amp;quot; .. new_scale .. &amp;quot; scaling (total ×&amp;quot; .. running_scale .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                table.insert(tier_parts, &amp;quot;on T&amp;quot; .. i .. &amp;quot; gets &amp;quot; .. table.concat(parts, &amp;quot;, &amp;quot;))&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            if flat_delta ~= 0 and not new_scale then&lt;br /&gt;
                running_damage = running_damage + flat_delta&lt;br /&gt;
                table.insert(tier_parts, &amp;quot;on T&amp;quot; .. i .. &amp;quot; gets +&amp;quot; .. flat_delta .. &amp;quot; base damage (total &amp;quot; .. running_damage .. &amp;quot;)&amp;quot;)&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            if #tier_parts &amp;gt; 0 then&lt;br /&gt;
                table.insert(notes, table.concat(tier_parts, &amp;quot;, &amp;quot;))&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #notes &amp;gt; 0 then cells[&amp;quot;Notes&amp;quot;] = table.concat(notes, &amp;quot;. &amp;quot;) end&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders base damage and heavy melee scaling.&lt;br /&gt;
-- Notes when heavy melee is only available via upgrade.&lt;br /&gt;
function p.RenderHeavyMelee(ability)&lt;br /&gt;
    -- Find base heavy_melee property&lt;br /&gt;
    local base_damage, base_scale&lt;br /&gt;
    for k, v in pairs(ability) do&lt;br /&gt;
        if k ~= &amp;quot;Upgrades&amp;quot; and type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
            if type(scale) == &amp;quot;table&amp;quot; and scale[&amp;quot;Type&amp;quot;] == &amp;quot;heavy_melee&amp;quot;&lt;br /&gt;
               and type(scale[&amp;quot;Value&amp;quot;]) == &amp;quot;number&amp;quot; then&lt;br /&gt;
                base_damage = v[&amp;quot;Value&amp;quot;] or 0&lt;br /&gt;
                base_scale = scale[&amp;quot;Value&amp;quot;]&lt;br /&gt;
                break&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells = {}&lt;br /&gt;
&lt;br /&gt;
    -- Active at base&lt;br /&gt;
    if base_scale and base_scale &amp;gt; 0 then&lt;br /&gt;
        cells[&amp;quot;Base Damage&amp;quot;] = tostring(base_damage)&lt;br /&gt;
        cells[&amp;quot;Heavy Melee Scaling&amp;quot;] = &amp;quot;×&amp;quot; .. base_scale&lt;br /&gt;
        return cells&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Not at base — find in upgrades&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) ~= &amp;quot;table&amp;quot; then return cells end&lt;br /&gt;
&lt;br /&gt;
    for i, tier in ipairs(upgrades) do&lt;br /&gt;
        if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            for k, v in pairs(tier) do&lt;br /&gt;
                if type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                    local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
                    if type(scale) == &amp;quot;table&amp;quot; and scale[&amp;quot;Type&amp;quot;] == &amp;quot;heavy_melee&amp;quot;&lt;br /&gt;
                       and type(scale[&amp;quot;Value&amp;quot;]) == &amp;quot;number&amp;quot; and scale[&amp;quot;Value&amp;quot;] &amp;gt; 0 then&lt;br /&gt;
                        cells[&amp;quot;Base Damage&amp;quot;] = tostring(v[&amp;quot;Value&amp;quot;] or 0)&lt;br /&gt;
                        cells[&amp;quot;Heavy Melee Scaling&amp;quot;] = &amp;quot;×&amp;quot; .. scale[&amp;quot;Value&amp;quot;]&lt;br /&gt;
                        cells[&amp;quot;Notes&amp;quot;] = &amp;quot;Only becomes Heavy Melee on &#039;&#039;&#039;T&amp;quot; .. i .. &amp;quot;&#039;&#039;&#039; upgrade&amp;quot;&lt;br /&gt;
                        return cells&lt;br /&gt;
                    end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Spirit scaling helpers&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Spirit scaling entry of a prop, or nil. Scale is a single { Value, Type }&lt;br /&gt;
-- or an array of them (e.g. a duration scaling with both Spirit and duration).&lt;br /&gt;
local function spirit_scale(v)&lt;br /&gt;
    if type(v) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
    if type(scale) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    if scale[1] == nil then&lt;br /&gt;
        if scale[&amp;quot;Type&amp;quot;] == &amp;quot;spirit&amp;quot; then return scale end&lt;br /&gt;
        return nil&lt;br /&gt;
    end&lt;br /&gt;
    for _, entry in ipairs(scale) do&lt;br /&gt;
        if type(entry) == &amp;quot;table&amp;quot; and entry[&amp;quot;Type&amp;quot;] == &amp;quot;spirit&amp;quot; then return entry end&lt;br /&gt;
    end&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Non-zero Spirit scalings of one layer (base record or upgrade tier) as&lt;br /&gt;
-- { key, value, multiply }, in card order; props not on the card last, by key.&lt;br /&gt;
local function spirit_scalings(layer, labels)&lt;br /&gt;
    local out = {}&lt;br /&gt;
    for k, v in pairs(layer) do&lt;br /&gt;
        if k ~= &amp;quot;Upgrades&amp;quot; and tostring(k):sub(1, 1) ~= &amp;quot;_&amp;quot; then&lt;br /&gt;
            local s = spirit_scale(v)&lt;br /&gt;
            if s and type(s[&amp;quot;Value&amp;quot;]) == &amp;quot;number&amp;quot; and s[&amp;quot;Value&amp;quot;] ~= 0 then&lt;br /&gt;
                table.insert(out, {&lt;br /&gt;
                    key      = k,&lt;br /&gt;
                    value    = s[&amp;quot;Value&amp;quot;],&lt;br /&gt;
                    multiply = s[&amp;quot;Multiply&amp;quot;] == true,&lt;br /&gt;
                })&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    table.sort(out, function(a, b)&lt;br /&gt;
        local oa = labels[a.key] and labels[a.key].order or math.huge&lt;br /&gt;
        local ob = labels[b.key] and labels[b.key].order or math.huge&lt;br /&gt;
        if oa ~= ob then return oa &amp;lt; ob end&lt;br /&gt;
        return a.key &amp;lt; b.key&lt;br /&gt;
    end)&lt;br /&gt;
    return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Formats a scaling value to 3 significant figures, as the ability cards do.&lt;br /&gt;
local function format_spirit(v)&lt;br /&gt;
    return string.format(&amp;quot;%.3g&amp;quot;, v)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Compact {{Ss}} badge for the cell, expanded via the current frame like&lt;br /&gt;
-- format_barrier; plain &amp;quot;×N&amp;quot; when there is no frame (module console).&lt;br /&gt;
local function format_spirit_badge(v)&lt;br /&gt;
    local text  = format_spirit(v)&lt;br /&gt;
    local frame = mw.getCurrentFrame()&lt;br /&gt;
    if frame then&lt;br /&gt;
        return frame:expandTemplate{&lt;br /&gt;
            title = &amp;quot;Ss&amp;quot;,&lt;br /&gt;
            args  = { text, compact = &amp;quot;1&amp;quot;, show_value = &amp;quot;1&amp;quot; },&lt;br /&gt;
        }&lt;br /&gt;
    end&lt;br /&gt;
    return &amp;quot;×&amp;quot; .. text&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- &amp;quot;A&amp;quot;, &amp;quot;A and B&amp;quot;, &amp;quot;A, B and C&amp;quot;.&lt;br /&gt;
local function join_names(names)&lt;br /&gt;
    if #names &amp;lt;= 1 then return names[1] or &amp;quot;&amp;quot; end&lt;br /&gt;
    return table.concat(names, &amp;quot;, &amp;quot;, 1, #names - 1) .. &amp;quot; and &amp;quot; .. names[#names]&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders Spirit Power scaling: one {{Ss}} badge per scaling prop, labelled from&lt;br /&gt;
-- the card (&amp;quot;x0.55 (Base Damage)&amp;quot;; &amp;quot;Damage: Cost of Stay&amp;quot; when names collide;&lt;br /&gt;
-- raw key when the card lacks it). The Upgrades cell lists upgrades per prop&lt;br /&gt;
-- with running totals; Multiply upgrades multiply the current scaling, as the&lt;br /&gt;
-- ability-upgrade gadget does.&lt;br /&gt;
function p.RenderSpiritScaling(ability, cardprops)&lt;br /&gt;
    local labels = cardprops or {}&lt;br /&gt;
&lt;br /&gt;
    local base  = spirit_scalings(ability, labels)&lt;br /&gt;
    local tiers = {}   -- { tier = i, list = ... } in tier order&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        -- ipairs rather than #: mw.loadJsonData tables have no length.&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                table.insert(tiers, { tier = i, list = spirit_scalings(tier, labels) })&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Props sharing a Name get their Title appended so they can be told apart.&lt;br /&gt;
    local keys_by_name = {}&lt;br /&gt;
    local function note_keys(list)&lt;br /&gt;
        for _, prop in ipairs(list) do&lt;br /&gt;
            local info = labels[prop.key]&lt;br /&gt;
            if info then&lt;br /&gt;
                keys_by_name[info.name] = keys_by_name[info.name] or {}&lt;br /&gt;
                keys_by_name[info.name][prop.key] = true&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    note_keys(base)&lt;br /&gt;
    for _, t in ipairs(tiers) do note_keys(t.list) end&lt;br /&gt;
&lt;br /&gt;
    local function label_of(key)&lt;br /&gt;
        local info = labels[key]&lt;br /&gt;
        if not info then return key end&lt;br /&gt;
        local n = 0&lt;br /&gt;
        for _ in pairs(keys_by_name[info.name] or {}) do n = n + 1 end&lt;br /&gt;
        if n &amp;gt; 1 and info.title then return info.name .. &amp;quot;: &amp;quot; .. info.title end&lt;br /&gt;
        return info.name&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells   = {}&lt;br /&gt;
    local running = {}   -- prop key -&amp;gt; current total scaling&lt;br /&gt;
&lt;br /&gt;
    if #base &amp;gt; 0 then&lt;br /&gt;
        local lines = {}&lt;br /&gt;
        for _, prop in ipairs(base) do&lt;br /&gt;
            table.insert(lines, format_spirit_badge(prop.value) .. &amp;quot; (&amp;quot; .. label_of(prop.key) .. &amp;quot;)&amp;quot;)&lt;br /&gt;
            running[prop.key] = prop.value&lt;br /&gt;
        end&lt;br /&gt;
        cells[&amp;quot;Spirit Scaling&amp;quot;] = table.concat(lines, &amp;quot;&amp;lt;br&amp;gt;&amp;quot;)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local lines = {}&lt;br /&gt;
    for _, t in ipairs(tiers) do&lt;br /&gt;
        local i, list = t.tier, t.list&lt;br /&gt;
        local groups, group_order = {}, {}   -- Multiply upgrades, grouped by multiplier&lt;br /&gt;
        for _, up in ipairs(list) do&lt;br /&gt;
            local label   = label_of(up.key)&lt;br /&gt;
            local current = running[up.key]&lt;br /&gt;
            if up.multiply then&lt;br /&gt;
                local group = groups[up.value]&lt;br /&gt;
                if not group then&lt;br /&gt;
                    group = { labels = {}, totals = {} }&lt;br /&gt;
                    groups[up.value] = group&lt;br /&gt;
                    table.insert(group_order, up.value)&lt;br /&gt;
                end&lt;br /&gt;
                table.insert(group.labels, label)&lt;br /&gt;
                if current then&lt;br /&gt;
                    running[up.key] = current * up.value&lt;br /&gt;
                    table.insert(group.totals, format_spirit_badge(running[up.key]))&lt;br /&gt;
                else&lt;br /&gt;
                    table.insert(group.totals, &amp;quot;?&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
            elseif current then&lt;br /&gt;
                running[up.key] = current + up.value&lt;br /&gt;
                table.insert(lines, &amp;quot;T&amp;quot; .. i .. &amp;quot; adds ×&amp;quot; .. format_spirit(up.value) .. &amp;quot; &amp;quot; .. label&lt;br /&gt;
                    .. &amp;quot; scaling (total &amp;quot; .. format_spirit_badge(running[up.key]) .. &amp;quot;)&amp;quot;)&lt;br /&gt;
            else&lt;br /&gt;
                running[up.key] = up.value&lt;br /&gt;
                table.insert(lines, &amp;quot;T&amp;quot; .. i .. &amp;quot; grants &amp;quot; .. format_spirit_badge(up.value) .. &amp;quot; &amp;quot; .. label .. &amp;quot; scaling&amp;quot;)&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
        for _, m in ipairs(group_order) do&lt;br /&gt;
            local group = groups[m]&lt;br /&gt;
            table.insert(lines, &amp;quot;T&amp;quot; .. i .. &amp;quot; multiplies &amp;quot; .. join_names(group.labels)&lt;br /&gt;
                .. &amp;quot; scaling by &amp;quot; .. format_spirit(m)&lt;br /&gt;
                .. &amp;quot; (total &amp;quot; .. table.concat(group.totals, &amp;quot;, &amp;quot;) .. &amp;quot;)&amp;quot;)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #lines &amp;gt; 0 then cells[&amp;quot;Upgrades&amp;quot;] = table.concat(lines, &amp;quot;&amp;lt;br&amp;gt;&amp;quot;) end&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders heal reduction percentage.&lt;br /&gt;
-- Special case for DisableHealing, shown as 100%.&lt;br /&gt;
function p.RenderHealReduce(ability)&lt;br /&gt;
    -- DisableHealing is always 100%, handle it separately&lt;br /&gt;
    local v = unwrap_value(ability[&amp;quot;DisableHealing&amp;quot;])&lt;br /&gt;
    if v and v ~= 0 then&lt;br /&gt;
        return { [&amp;quot;Heal Reduction&amp;quot;] = &amp;quot;100%&amp;quot; }&lt;br /&gt;
    end&lt;br /&gt;
    local upgrade = find_in_upgrades(ability, &amp;quot;DisableHealing&amp;quot;)&lt;br /&gt;
    if upgrade then&lt;br /&gt;
        return { [&amp;quot;Heal Reduction&amp;quot;] = &amp;quot;100% (T&amp;quot; .. upgrade.tier .. &amp;quot;)&amp;quot; }&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Everything else goes through the generic renderer&lt;br /&gt;
    local filtered = {}&lt;br /&gt;
    for _, prop in ipairs(Lists.lists[&amp;quot;healreduce&amp;quot;]) do&lt;br /&gt;
        if prop ~= &amp;quot;DisableHealing&amp;quot; then table.insert(filtered, prop) end&lt;br /&gt;
    end&lt;br /&gt;
    return render_upgradable_stat(ability, &amp;quot;Heal Reduction&amp;quot;, filtered, &amp;quot;%&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders move slow percentage.&lt;br /&gt;
-- Appends a note if the ability also self-slows the caster.&lt;br /&gt;
function p.RenderMoveSlow(ability)&lt;br /&gt;
    local cells = render_upgradable_stat(ability, &amp;quot;Move Slow&amp;quot;, Lists.lists[&amp;quot;moveslow&amp;quot;], &amp;quot;%&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    local channel_slow = unwrap_value(ability[&amp;quot;ChannelSlowPercent&amp;quot;])&lt;br /&gt;
    if channel_slow and channel_slow ~= 0 then&lt;br /&gt;
        local note = &amp;quot;Also slows the caster for &amp;quot; .. math.abs(channel_slow) .. &amp;quot;%&amp;quot;&lt;br /&gt;
        cells[&amp;quot;Notes&amp;quot;] = cells[&amp;quot;Notes&amp;quot;] and (cells[&amp;quot;Notes&amp;quot;] .. &amp;quot;; &amp;quot; .. note) or note&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders dash slow percentage.&lt;br /&gt;
-- Special case for GroundDashReductionPercent coexisting with another dashslow&lt;br /&gt;
-- prop, which means it&#039;s a caster self-slow and is noted separately.&lt;br /&gt;
function p.RenderDashSlow(ability)&lt;br /&gt;
    local stat_props = Lists.lists[&amp;quot;dashslow&amp;quot;]&lt;br /&gt;
    local props = {}&lt;br /&gt;
    for _, pr in ipairs(stat_props) do table.insert(props, pr) end&lt;br /&gt;
&lt;br /&gt;
    local caster_dash_slow = nil&lt;br /&gt;
    local base_ground = unwrap_value(ability[&amp;quot;GroundDashReductionPercent&amp;quot;])&lt;br /&gt;
    if base_ground and base_ground ~= 0 then&lt;br /&gt;
        local has_enemy_prop = false&lt;br /&gt;
        for _, prop in ipairs(stat_props) do&lt;br /&gt;
            if prop ~= &amp;quot;GroundDashReductionPercent&amp;quot; then&lt;br /&gt;
                local v = unwrap_value(ability[prop])&lt;br /&gt;
                if v and v ~= 0 then has_enemy_prop = true; break end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
        if not has_enemy_prop then&lt;br /&gt;
            local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
            if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                for _, tier in ipairs(upgrades) do&lt;br /&gt;
                    if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                        for _, prop in ipairs(stat_props) do&lt;br /&gt;
                            if prop ~= &amp;quot;GroundDashReductionPercent&amp;quot; then&lt;br /&gt;
                                local v = unwrap_value(tier[prop])&lt;br /&gt;
                                if v and v ~= 0 then has_enemy_prop = true; break end&lt;br /&gt;
                            end&lt;br /&gt;
                        end&lt;br /&gt;
                    end&lt;br /&gt;
                    if has_enemy_prop then break end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
        if has_enemy_prop then&lt;br /&gt;
            caster_dash_slow = math.abs(base_ground)&lt;br /&gt;
            local filtered = {}&lt;br /&gt;
            for _, prop in ipairs(props) do&lt;br /&gt;
                if prop ~= &amp;quot;GroundDashReductionPercent&amp;quot; then&lt;br /&gt;
                    table.insert(filtered, prop)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            props = filtered&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells = render_upgradable_stat(ability, &amp;quot;Dash Slow&amp;quot;, props, &amp;quot;%&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    if caster_dash_slow then&lt;br /&gt;
        local note = &amp;quot;Gives &amp;quot; .. caster_dash_slow .. &amp;quot;% dash slow for the caster&amp;quot;&lt;br /&gt;
        cells[&amp;quot;Notes&amp;quot;] = cells[&amp;quot;Notes&amp;quot;] and (cells[&amp;quot;Notes&amp;quot;] .. &amp;quot;; &amp;quot; .. note) or note&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders the amount of Debuff Resist an ability grants.&lt;br /&gt;
-- Upgrades stack additively onto the base value, so every step after the first&lt;br /&gt;
-- shows the running total: &amp;quot;20% (T3)&amp;quot; when granted only by an upgrade,&lt;br /&gt;
-- &amp;quot;10% → 25% (T2)&amp;quot; when a base value is later increased. Chains for any number&lt;br /&gt;
-- of steps. No ability currently has both a base and an upgrade value.&lt;br /&gt;
function p.RenderExtraDebuffResist(ability)&lt;br /&gt;
    local fields   = Lists.lists[&amp;quot;extradebuffresist&amp;quot;]&lt;br /&gt;
    local segments = {}&lt;br /&gt;
&lt;br /&gt;
    local running = sum_fields(ability, fields)&lt;br /&gt;
    if running then&lt;br /&gt;
        table.insert(segments, running .. &amp;quot;%&amp;quot;)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                local delta = sum_fields(tier, fields)&lt;br /&gt;
                if delta then&lt;br /&gt;
                    running = (running or 0) + delta&lt;br /&gt;
                    table.insert(segments, running .. &amp;quot;% (T&amp;quot; .. i .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #segments == 0 then return {} end&lt;br /&gt;
    return { [&amp;quot;Debuff Resist&amp;quot;] = table.concat(segments, &amp;quot; → &amp;quot;) }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders how much of an ability&#039;s damage applies to objectives (Guardians,&lt;br /&gt;
-- Walkers, Patron, Mid-Boss). BossDamageScale is a 0–1 multiplier shown as a&lt;br /&gt;
-- percentage: 0.2 → &amp;quot;20%&amp;quot;. The field never appears in upgrade tiers, so only&lt;br /&gt;
-- the base value is read; sub-ability records are checked as a fallback when&lt;br /&gt;
-- the main record lacks it.&lt;br /&gt;
function p.RenderBossDamageScale(ability)&lt;br /&gt;
    local v = unwrap_value(ability[&amp;quot;BossDamageScale&amp;quot;])&lt;br /&gt;
    if (v == nil or v == 0) and type(ability[&amp;quot;_subabilities&amp;quot;]) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        for _, sub in ipairs(ability[&amp;quot;_subabilities&amp;quot;]) do&lt;br /&gt;
            local sv = unwrap_value(sub[&amp;quot;BossDamageScale&amp;quot;])&lt;br /&gt;
            if sv and sv ~= 0 then v = sv; break end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    if v == nil or v == 0 then return {} end&lt;br /&gt;
    return { [&amp;quot;Damage to Objectives&amp;quot;] = format_percent(v) }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>~2026-DurationAmmoMelee1155</name></author>
	</entry>
	<entry>
		<id>https://deadlock.wiki/Module:AbilityTable/Lists?diff=157556</id>
		<title>Module:AbilityTable/Lists</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Module:AbilityTable/Lists?diff=157556"/>
		<updated>2026-09-14T00:19:25Z</updated>

		<summary type="html">&lt;p&gt;~2026-DurationAmmoMelee1155: Exclude zero-scale Bloodscent and Eternal Night from scalingspirit (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;local lists = {&lt;br /&gt;
	-- Abilities that give barrier&lt;br /&gt;
	[&amp;quot;barrier&amp;quot;] = {&lt;br /&gt;
		&amp;quot;AirDropBulletShield&amp;quot;,&lt;br /&gt;
		&amp;quot;CombatBarrier&amp;quot;,&lt;br /&gt;
	},&lt;br /&gt;
&lt;br /&gt;
    -- Abilities whose damage is scaled against objectives/bosses (Guardians, Walkers, Patron, Mid-Boss)&lt;br /&gt;
    [&amp;quot;bossdmgscale&amp;quot;] = {&lt;br /&gt;
        &amp;quot;BossDamageScale&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
	&lt;br /&gt;
    -- Abilities that have charges&lt;br /&gt;
    [&amp;quot;charges&amp;quot;] = {&lt;br /&gt;
        &amp;quot;AbilityCharges&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that get cancelled by Cursed Relic&lt;br /&gt;
    -- BonusMoveSpeed does not get cleansed by cursed relic and must not be here.&lt;br /&gt;
    [&amp;quot;cursedrelic&amp;quot;] = {&lt;br /&gt;
        &amp;quot;AirControlPercent&amp;quot;, &amp;quot;AllowDashWhenChanneling&amp;quot;, &amp;quot;BehaviorCannotCancelDuringChannel&amp;quot;, &amp;quot;BehaviorChannelled&amp;quot;, &amp;quot;BehaviorExclusiveUse&amp;quot;, &amp;quot;BehaviorStartCooldownOnToggleOff&amp;quot;,&lt;br /&gt;
        &amp;quot;BonusFireRate&amp;quot;, &amp;quot;BuffDuration&amp;quot;, &amp;quot;BulletDamageAmp&amp;quot;, &amp;quot;CombatBarrier&amp;quot;, &amp;quot;DashModifier&amp;quot;, &amp;quot;DashSpeed&amp;quot;,&lt;br /&gt;
        &amp;quot;HealingPerSecond&amp;quot;, &amp;quot;MovementSpeedBonus&amp;quot;, &amp;quot;MoveSpeedBonus&amp;quot;, &amp;quot;SpiritResist&amp;quot;, &amp;quot;TechArmorGain&amp;quot;, &amp;quot;WeaponDamageBonus&amp;quot;,&lt;br /&gt;
        &amp;quot;Dust Devil&amp;quot;, &amp;quot;Life Drain&amp;quot;, &amp;quot;Kinetic Carbine&amp;quot;, &amp;quot;Power Slash&amp;quot;, &amp;quot;Power Surge&amp;quot;, &amp;quot;Siphon Life&amp;quot;,&lt;br /&gt;
        &amp;quot;Smoke Bomb&amp;quot;, &amp;quot;The Cube&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that apply a dash slow to their targets&lt;br /&gt;
    [&amp;quot;dashslow&amp;quot;] = {&lt;br /&gt;
        &amp;quot;EnemyDashSlowPercent&amp;quot;, &amp;quot;GroundDashReductionPercent&amp;quot;, &amp;quot;MaxGroundDashReductionPercent&amp;quot;, &amp;quot;MoveSpeedAndDashSlowPct&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities whose duration is reduced by Debuff Resist (Debuff Reducer etc.)&lt;br /&gt;
    [&amp;quot;debuffresist&amp;quot;] = {&lt;br /&gt;
        &amp;quot;BuildUpPerShot&amp;quot;, &amp;quot;BulletDamageAmpDuration&amp;quot;, &amp;quot;BulletResistReduction&amp;quot;, &amp;quot;BurnDuration&amp;quot;, &amp;quot;CitadelSilenced&amp;quot;, &amp;quot;DamageAmpPercentage&amp;quot;,&lt;br /&gt;
        &amp;quot;DebuffAccuracy&amp;quot;, &amp;quot;DebuffDuration&amp;quot;, &amp;quot;EnemySlowPct&amp;quot;, &amp;quot;FadingSlowPercent&amp;quot;, &amp;quot;GroundDashReductionPercent&amp;quot;, &amp;quot;HealAmpReceivePenaltyPercent&amp;quot;,&lt;br /&gt;
        &amp;quot;HoldInPlaceDuration&amp;quot;, &amp;quot;ImmobilizeDuration&amp;quot;, &amp;quot;MaxSlowDuration&amp;quot;, &amp;quot;MeleeResistReduction&amp;quot;, &amp;quot;MovementSlow&amp;quot;, &amp;quot;MovementSlowPct&amp;quot;, &amp;quot;MovementSpeedSlow&amp;quot;,&lt;br /&gt;
        &amp;quot;MoveSlowPercent&amp;quot;, &amp;quot;PetrifyDuration&amp;quot;, &amp;quot;ProcDamage&amp;quot;, &amp;quot;Silenced&amp;quot;, &amp;quot;SilenceDuration&amp;quot;, &amp;quot;SleepDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;SlowPercent&amp;quot;, &amp;quot;StealDuration&amp;quot;, &amp;quot;StunDuration&amp;quot;, &amp;quot;TimeSlowDuration&amp;quot;, &amp;quot;TossDuration&amp;quot;, &amp;quot;VenomDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;VictimStackDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;Card Trick&amp;quot;, &amp;quot;Combo&amp;quot;, &amp;quot;Light Eater&amp;quot;, &amp;quot;Spirit Lasso&amp;quot;, &amp;quot;Stalker&#039;s Mark&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that give debuff resist.&lt;br /&gt;
    [&amp;quot;extradebuffresist&amp;quot;] = {&lt;br /&gt;
    	&amp;quot;StatusResistancePercent&amp;quot;	&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities whose effects can be cleansed by Dispel Magic&lt;br /&gt;
    [&amp;quot;dispelmagic&amp;quot;] = {&lt;br /&gt;
        &amp;quot;BuildUpDecayDelay&amp;quot;, &amp;quot;BuildUpPerShot&amp;quot;, &amp;quot;BulletDamageAmpDuration&amp;quot;, &amp;quot;BulletResistReduction&amp;quot;, &amp;quot;BurnDuration&amp;quot;, &amp;quot;CitadelSilenced&amp;quot;,&lt;br /&gt;
        &amp;quot;DebuffAccuracy&amp;quot;, &amp;quot;DebuffDuration&amp;quot;, &amp;quot;DotHealthPercent&amp;quot;, &amp;quot;Drag&amp;quot;, &amp;quot;EnemySlowPct&amp;quot;, &amp;quot;FadingSlowPercent&amp;quot;,&lt;br /&gt;
        &amp;quot;FuseTime&amp;quot;, &amp;quot;GroundAuraModifier&amp;quot;, &amp;quot;GroundDashReductionPercent&amp;quot;, &amp;quot;HealAmpReceivePenaltyPercent&amp;quot;, &amp;quot;HoldInPlaceDuration&amp;quot;, &amp;quot;ImmobilizeDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;MaxSlowDuration&amp;quot;, &amp;quot;MeleeResistReduction&amp;quot;, &amp;quot;MovementSlow&amp;quot;, &amp;quot;MovementSlowPct&amp;quot;, &amp;quot;MovementSpeedSlow&amp;quot;, &amp;quot;MoveSlowPercent&amp;quot;, &amp;quot;ProcDamage&amp;quot;,&lt;br /&gt;
        &amp;quot;Silenced&amp;quot;, &amp;quot;SlowDuration&amp;quot;, &amp;quot;SlowPercent&amp;quot;, &amp;quot;StealDuration&amp;quot;, &amp;quot;TimeSlowDuration&amp;quot;, &amp;quot;TossDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;VenomDuration&amp;quot;, &amp;quot;VictimStackDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;Card Trick&amp;quot;, &amp;quot;Light Eater&amp;quot;, &amp;quot;Stalker&#039;s Mark&amp;quot;, &amp;quot;Static Charge&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities whose debuffs can be cleansed by Divine Barrier (combination of debuffresist and dispelmagic)&lt;br /&gt;
    [&amp;quot;divinebarrier&amp;quot;] = {&lt;br /&gt;
        &amp;quot;BuildUpDecayDelay&amp;quot;, &amp;quot;BuildUpPerShot&amp;quot;, &amp;quot;BulletDamageAmpDuration&amp;quot;, &amp;quot;BulletResistReduction&amp;quot;, &amp;quot;BurnDuration&amp;quot;, &amp;quot;CitadelSilenced&amp;quot;,&lt;br /&gt;
        &amp;quot;DamageAmpPercentage&amp;quot;, &amp;quot;DebuffAccuracy&amp;quot;, &amp;quot;DebuffDuration&amp;quot;, &amp;quot;DotHealthPercent&amp;quot;, &amp;quot;EnemySlowPct&amp;quot;, &amp;quot;FadingSlowPercent&amp;quot;,&lt;br /&gt;
        &amp;quot;FuseTime&amp;quot;, &amp;quot;GroundAuraModifier&amp;quot;, &amp;quot;GroundDashReductionPercent&amp;quot;, &amp;quot;HealAmpReceivePenaltyPercent&amp;quot;, &amp;quot;HoldInPlaceDuration&amp;quot;, &amp;quot;ImmobilizeDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;MaxSlowDuration&amp;quot;, &amp;quot;MeleeResistReduction&amp;quot;, &amp;quot;MovementSlow&amp;quot;, &amp;quot;MovementSlowPct&amp;quot;, &amp;quot;MovementSpeedSlow&amp;quot;, &amp;quot;MoveSlowPercent&amp;quot;, &amp;quot;PetrifyDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;ProcDamage&amp;quot;, &amp;quot;Silenced&amp;quot;, &amp;quot;SilenceDuration&amp;quot;, &amp;quot;SleepDuration&amp;quot;, &amp;quot;SlowDuration&amp;quot;, &amp;quot;SlowPercent&amp;quot;,&lt;br /&gt;
        &amp;quot;StealDuration&amp;quot;, &amp;quot;StunDuration&amp;quot;, &amp;quot;TimeSlowDuration&amp;quot;, &amp;quot;TossDuration&amp;quot;, &amp;quot;VenomDuration&amp;quot;, &amp;quot;VictimStackDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;Card Trick&amp;quot;, &amp;quot;Combo&amp;quot;, &amp;quot;Light Eater&amp;quot;, &amp;quot;Spirit Lasso&amp;quot;, &amp;quot;Stalker&#039;s Mark&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that don&#039;t break stealth on cast or recast.&lt;br /&gt;
	[&amp;quot;dontbreakstealth&amp;quot;] = {&lt;br /&gt;
		&amp;quot;BehaviorDontBreakInvisibility&amp;quot;, &amp;quot;DoesNotBreakInvis&amp;quot;,&lt;br /&gt;
		&amp;quot;Spectral Wall&amp;quot;&lt;br /&gt;
	},&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that restore health, buffed by Heal Amp items&lt;br /&gt;
    [&amp;quot;heal&amp;quot;] = {&lt;br /&gt;
        &amp;quot;AbilityLifestealPercentHero&amp;quot;, &amp;quot;BonusHealthRegen&amp;quot;, &amp;quot;BulletLifestealPercent&amp;quot;, &amp;quot;DamageHealMult&amp;quot;, &amp;quot;ExternalBonusHealthRegen&amp;quot;, &amp;quot;FlatHealthHealing&amp;quot;,&lt;br /&gt;
        &amp;quot;HealAmount&amp;quot;, &amp;quot;HealFixedHealth&amp;quot;, &amp;quot;HealingFactor&amp;quot;, &amp;quot;HealingPerSecond&amp;quot;, &amp;quot;HealMaxHealth&amp;quot;, &amp;quot;HealMaxHealthPercent&amp;quot;,&lt;br /&gt;
        &amp;quot;HealOnKillPct&amp;quot;, &amp;quot;HealPctVsHeroes&amp;quot;, &amp;quot;HealthStealPctHero&amp;quot;, &amp;quot;InfestHeal&amp;quot;, &amp;quot;LifestealPercentHero&amp;quot;, &amp;quot;LifeStealPercentOnHit&amp;quot;,  &amp;quot;LifeDrainPerSecond&amp;quot;,&lt;br /&gt;
        &amp;quot;MaxHealthRegen&amp;quot;, &amp;quot;MissingHealthBurstPct&amp;quot;, &amp;quot;MissingHealthPercentHeal&amp;quot;, &amp;quot;MissingHPHeal&amp;quot;, &amp;quot;RegenIncomingDamagePercent&amp;quot;, &amp;quot;TechLifestealPercent&amp;quot;,&lt;br /&gt;
        &amp;quot;TotalHealthRegen&amp;quot;,&lt;br /&gt;
        &amp;quot;Card Trick&amp;quot;, &amp;quot;Rake&amp;quot;, &amp;quot;Soul Exchange&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that trigger Healing Tempo Passive&lt;br /&gt;
    [&amp;quot;healingtempo&amp;quot;] = {&lt;br /&gt;
        &amp;quot;BonusHealthRegen&amp;quot;, &amp;quot;DamageHealMult&amp;quot;, &amp;quot;ExternalBonusHealthRegen&amp;quot;, &amp;quot;FlatHealthHealing&amp;quot;, &amp;quot;HealAmount&amp;quot;, &amp;quot;HealFixedHealth&amp;quot;,&lt;br /&gt;
        &amp;quot;HealingFactor&amp;quot;, &amp;quot;HealingPerSecond&amp;quot;, &amp;quot;HealMaxHealth&amp;quot;, &amp;quot;HealMaxHealthPercent&amp;quot;, &amp;quot;HealOnKillPct&amp;quot;, &amp;quot;HealPctVsHeroes&amp;quot;,&lt;br /&gt;
        &amp;quot;HealthStealPctHero&amp;quot;, &amp;quot;InfestHeal&amp;quot;, &amp;quot;LifeDrainPerSecond&amp;quot;, &amp;quot;MaxHealthRegen&amp;quot;, &amp;quot;MeleeLifesteal&amp;quot;, &amp;quot;MissingHealthBurstPct&amp;quot;,&lt;br /&gt;
        &amp;quot;MissingHealthPercentHeal&amp;quot;, &amp;quot;MissingHPHeal&amp;quot;, &amp;quot;TetherSharedHealPct&amp;quot;, &amp;quot;TotalHealthRegen&amp;quot;,&lt;br /&gt;
        &amp;quot;Card Trick&amp;quot;, &amp;quot;Fire Scarabs&amp;quot;, &amp;quot;Rake&amp;quot;, &amp;quot;Soul Exchange&amp;quot;, &lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that reduce healing on their targets&lt;br /&gt;
    [&amp;quot;healreduce&amp;quot;] = {&lt;br /&gt;
        &amp;quot;DisableHealing&amp;quot;, &amp;quot;HealAmpReceivePenaltyPercent&amp;quot;, &amp;quot;HealAmpRegenPenaltyPercent&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that deal melee damage but scale with heavy melee&lt;br /&gt;
    [&amp;quot;heavymelee&amp;quot;] = {&lt;br /&gt;
        &amp;quot;heavy_melee&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that trigger Indomitable. Should be mostly same as reactivebarrier.&lt;br /&gt;
    [&amp;quot;indomitable&amp;quot;] = {&lt;br /&gt;
        &amp;quot;ChainLength&amp;quot;, &amp;quot;ChargeDragEnemy&amp;quot;, &amp;quot;CitadelRoot&amp;quot;, &amp;quot;CitadelSilenced&amp;quot;, &amp;quot;HexDuration&amp;quot;, &amp;quot;HoldInPlaceDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;Immobilize&amp;quot;, &amp;quot;ImmobilizeDuration&amp;quot;, &amp;quot;LiftDuration&amp;quot;, &amp;quot;PetrifyDuration&amp;quot;, &amp;quot;RestrictionDuration&amp;quot;, &amp;quot;Silenced&amp;quot;,&lt;br /&gt;
        &amp;quot;SilenceDuration&amp;quot;, &amp;quot;SleepDuration&amp;quot;, &amp;quot;StunDuration&amp;quot;, &amp;quot;Tether&amp;quot;,&lt;br /&gt;
        &amp;quot;Combo&amp;quot;, &amp;quot;Hotel Guest&amp;quot;, &amp;quot;Paradoxical Swap&amp;quot;, &amp;quot;Spirit Lasso&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that deal melee damage&lt;br /&gt;
    [&amp;quot;melee&amp;quot;] = {&lt;br /&gt;
        &amp;quot;heavy_melee&amp;quot;, &amp;quot;melee&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that are considered &amp;quot;movement-based&amp;quot;. They get disabled by Slowing Hex and some other things.&lt;br /&gt;
    [&amp;quot;movementbased&amp;quot;] = {&lt;br /&gt;
    	&amp;quot;BehaviorMovement&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that apply a move slow to their targets&lt;br /&gt;
    [&amp;quot;moveslow&amp;quot;] = {&lt;br /&gt;
        &amp;quot;EnemySlowPct&amp;quot;, &amp;quot;MaxSlowPercent&amp;quot;, &amp;quot;MovementSlow&amp;quot;, &amp;quot;MovementSlowPct&amp;quot;, &amp;quot;MovementSpeedSlow&amp;quot;, &amp;quot;MoveSlowPercent&amp;quot;,&lt;br /&gt;
        &amp;quot;MoveSpeedPenaltyPerStack&amp;quot;, &amp;quot;MoveSpeedSlowPct&amp;quot;, &amp;quot;SlowPercent&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that apply some effect on a bullet hit that can be mitigated with Plated Armor&lt;br /&gt;
    [&amp;quot;platedarmor&amp;quot;] = {&lt;br /&gt;
        &amp;quot;BonusDamagePerBullet&amp;quot;, &amp;quot;BuildUpDecayDelay&amp;quot;, &amp;quot;BuildUpDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;Blasted&amp;quot;, &amp;quot;Card Trick&amp;quot;, &amp;quot;Crackshot&amp;quot;, &amp;quot;Fixation&amp;quot;, &amp;quot;Ira Domini&amp;quot;, &amp;quot;Light Eater&amp;quot;,&lt;br /&gt;
        &amp;quot;Power Surge&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that trigger Reactive Barrier&lt;br /&gt;
    [&amp;quot;reactivebarrier&amp;quot;] = {&lt;br /&gt;
        &amp;quot;ChainLength&amp;quot;, &amp;quot;ChargeDragEnemy&amp;quot;, &amp;quot;CitadelRoot&amp;quot;, &amp;quot;CitadelSilenced&amp;quot;, &amp;quot;HexDuration&amp;quot;, &amp;quot;HoldInPlaceDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;Immobilize&amp;quot;, &amp;quot;ImmobilizeDuration&amp;quot;, &amp;quot;LiftDuration&amp;quot;, &amp;quot;PetrifyDuration&amp;quot;, &amp;quot;RestrictionDuration&amp;quot;, &amp;quot;Silenced&amp;quot;,&lt;br /&gt;
        &amp;quot;SilenceDuration&amp;quot;, &amp;quot;SleepDuration&amp;quot;, &amp;quot;StunDuration&amp;quot;, &amp;quot;Tether&amp;quot;,&lt;br /&gt;
        &amp;quot;Combo&amp;quot;, &amp;quot;Hotel Guest&amp;quot;, &amp;quot;Paradoxical Swap&amp;quot;, &amp;quot;Singularity&amp;quot;, &amp;quot;Spirit Lasso&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that scale with Boons&lt;br /&gt;
	[&amp;quot;scalingboon&amp;quot;] = {&lt;br /&gt;
	    &amp;quot;Scale.Type:power_increase&amp;quot;,&lt;br /&gt;
	},&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that scale with ability duration&lt;br /&gt;
    [&amp;quot;scalingduration&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Scale.Type:duration&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that scale with ability range&lt;br /&gt;
    [&amp;quot;scalingrange&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Scale.Type:range&amp;quot;, &amp;quot;Scale.Type:radius&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that scale with ability cooldown&lt;br /&gt;
    [&amp;quot;scalingcooldown&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Scale.Type:cooldown&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that scale with spirit&lt;br /&gt;
    [&amp;quot;scalingspirit&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Scale.Type:spirit&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- for testing purposes. Feel free to change however you wish&lt;br /&gt;
    [&amp;quot;test&amp;quot;] = {&lt;br /&gt;
        &amp;quot;BehaviorChannelled&amp;quot;&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities whose negative effects are prevented by Unstoppable&lt;br /&gt;
    [&amp;quot;unstoppable&amp;quot;] = {&lt;br /&gt;
        &amp;quot;ChainLength&amp;quot;, &amp;quot;ChargeDragEnemy&amp;quot;, &amp;quot;CitadelRoot&amp;quot;, &amp;quot;CitadelSilenced&amp;quot;, &amp;quot;DisarmDuration&amp;quot;, &amp;quot;Drag&amp;quot;,&lt;br /&gt;
        &amp;quot;DragModifier&amp;quot;, &amp;quot;EnemySlowPct&amp;quot;, &amp;quot;FadingSlowPercent&amp;quot;, &amp;quot;GroundDashReductionPercent&amp;quot;, &amp;quot;HexDuration&amp;quot;, &amp;quot;HoldInPlaceDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;Immobilize&amp;quot;, &amp;quot;ImmobilizeDuration&amp;quot;, &amp;quot;LiftDuration&amp;quot;, &amp;quot;MaxSlowDuration&amp;quot;, &amp;quot;MovementSlow&amp;quot;, &amp;quot;MovementSlowPct&amp;quot;,&lt;br /&gt;
        &amp;quot;MovementSpeedSlow&amp;quot;, &amp;quot;MoveSlowPercent&amp;quot;, &amp;quot;OutgoingTechDamagePercent&amp;quot;, &amp;quot;PetrifyDuration&amp;quot;, &amp;quot;RestrictionDuration&amp;quot;, &amp;quot;Silenced&amp;quot;,&lt;br /&gt;
        &amp;quot;SilenceDuration&amp;quot;, &amp;quot;SleepDuration&amp;quot;, &amp;quot;SlowDuration&amp;quot;, &amp;quot;SlowPercent&amp;quot;, &amp;quot;StunDuration&amp;quot;, &amp;quot;Tether&amp;quot;,&lt;br /&gt;
        &amp;quot;TimeSlowDuration&amp;quot;, &amp;quot;TossDuration&amp;quot;,&lt;br /&gt;
        &amp;quot;Card Trick&amp;quot;, &amp;quot;Combo&amp;quot;, &amp;quot;Hotel Guest&amp;quot;, &amp;quot;Paradoxical Swap&amp;quot;, &amp;quot;Singularity&amp;quot;, &amp;quot;Spirit Lasso&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
&lt;br /&gt;
    -- Abilities that deal weapon damage&lt;br /&gt;
    [&amp;quot;weapondamage&amp;quot;] = {&lt;br /&gt;
        &amp;quot;weapon_damage_increase&amp;quot;,&lt;br /&gt;
        &amp;quot;Crackshot&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that scale with weapon damage&lt;br /&gt;
    [&amp;quot;weaponscaling&amp;quot;] = {&lt;br /&gt;
        &amp;quot;weapon_damage_increase&amp;quot;, &amp;quot;weapon_power&amp;quot;&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
    -- Abilities that summons NPCs&lt;br /&gt;
    [&amp;quot;summons&amp;quot;] = {&lt;br /&gt;
        &amp;quot;TurretLifeTime&amp;quot;, &amp;quot;SkullLifetime&amp;quot;, &amp;quot;SummonCount&amp;quot;, &amp;quot;SummonHealth&amp;quot;, &amp;quot;TurretLifetime&amp;quot;, &amp;quot;HelperCount&amp;quot;&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
        -- Copy of summons list for Heroic Aura&lt;br /&gt;
    [&amp;quot;heroicaura&amp;quot;] = {&lt;br /&gt;
        &amp;quot;TurretLifeTime&amp;quot;, &amp;quot;SkullLifetime&amp;quot;, &amp;quot;SummonCount&amp;quot;, &amp;quot;SummonHealth&amp;quot;, &amp;quot;TurretLifetime&amp;quot;, &amp;quot;HelperCount&amp;quot;&lt;br /&gt;
    },&lt;br /&gt;
    &lt;br /&gt;
            -- Lifesteal abilities&lt;br /&gt;
    [&amp;quot;lifesteal&amp;quot;] = {&lt;br /&gt;
        &amp;quot;AbilityLifestealPercentHero&amp;quot;, &amp;quot;LifeStealPercentOnHit&amp;quot;, &amp;quot;BulletLifesteal&amp;quot;, &amp;quot;BulletLifestealPercent&amp;quot;, &amp;quot;TechLifestealPercent&amp;quot;, &amp;quot;TargetLifesteal&amp;quot;,&lt;br /&gt;
        &amp;quot;TargetLifestealNonHero&amp;quot;, &amp;quot;LifestealPercentHero&amp;quot;, &amp;quot;LifeStealPercent&amp;quot;, &amp;quot;MeleeLifesteal&amp;quot;, &amp;quot;BeamLifesteal&amp;quot;&lt;br /&gt;
    },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- Abilities that should be excluded from automatic lists&lt;br /&gt;
-- If adding here please add a comment with a reason&lt;br /&gt;
local exclude_abilities = {&lt;br /&gt;
	[&amp;quot;cursedrelic&amp;quot;] = {&lt;br /&gt;
		&amp;quot;Concussive Combustion&amp;quot;, -- has BuffModifier that does nothing&lt;br /&gt;
	    &amp;quot;Enchanter&#039;s Satchel&amp;quot;, -- has BehaviorChannelled but Pocket can&#039;t be targeted in it.&lt;br /&gt;
	    &amp;quot;Grasping Hands&amp;quot;, -- has BuffModifier that does nothing&lt;br /&gt;
	    &amp;quot;Ira Domini&amp;quot;, -- has BehaviorCannotCancelDuringChannel but does not dispelled&lt;br /&gt;
	    &amp;quot;Lycan Curse&amp;quot;, -- has BonusFireRate that does not get dispelled&lt;br /&gt;
	    &amp;quot;Return to Shadows&amp;quot;, -- has BehaviorChannelled but is untargetable during the channel&lt;br /&gt;
	    &amp;quot;Serrated Knives&amp;quot;, -- has BehaviorChannelled for some reason&lt;br /&gt;
	    &amp;quot;Shadow Transformation&amp;quot;, -- has BehaviorCannotCancelDuringChannel but it gives invulnerability and then unstoppable.&lt;br /&gt;
	    &amp;quot;Shocking Reanimation&amp;quot;, -- Has BehaviorChannelled, BonusFireRate, BuffModifier but none get any effective treatment&lt;br /&gt;
	    &amp;quot;Vexing Bolt&amp;quot;, -- has BehaviorChannelled but does not get cancelled&lt;br /&gt;
	},&lt;br /&gt;
    [&amp;quot;dashslow&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Itani Lo Sahn&amp;quot;, -- has GroundDashReductionPercent but also disables stamina&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;debuffresist&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Bookwyrm&amp;quot;,       -- has DebuffDuration for some reason&lt;br /&gt;
        &amp;quot;Frozen Shelter&amp;quot;, -- has SlowPercent but no duration to shave off&lt;br /&gt;
        &amp;quot;Itani Lo Sahn&amp;quot;,  -- has lots of debuffs but none are affected by debuffresist&lt;br /&gt;
        &amp;quot;Slam Fire&amp;quot;,      -- has DebuffDuration for some reason&lt;br /&gt;
        &amp;quot;Splatter&amp;quot;,       -- has SlowPercent but no duration to shave off&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;dispelmagic&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Slam Fire&amp;quot;, -- has DebuffDuration for some reason&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;divinebarrier&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Bookwyrm&amp;quot;,       -- has DebuffDuration for some reason&lt;br /&gt;
        &amp;quot;Frozen Shelter&amp;quot;, -- has SlowPercent but no duration to shave off&lt;br /&gt;
        &amp;quot;Slam Fire&amp;quot;,      -- has DebuffDuration for some reason&lt;br /&gt;
        &amp;quot;Splatter&amp;quot;,       -- has SlowPercent but no duration to shave off&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;dontbreakstealth&amp;quot;] = {&lt;br /&gt;
    	--&amp;quot;Riposte&amp;quot;,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;heal&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Riposte&amp;quot;, -- has AbilityLifestealPercentHero but doesn&#039;t deal damage to heal off of&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;healingtempo&amp;quot;] = {&lt;br /&gt;
    	&amp;quot;Riposte&amp;quot;, -- has AbilityLifestealPercentHero but doesn&#039;t deal damage to heal off of&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;indomitable&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Entangling Bola&amp;quot;, -- has CitadelRoot that never gets applied,&lt;br /&gt;
        &amp;quot;Itani Lo Sahn&amp;quot;,   -- has silence, just doesn&#039;t trigger Indomitable (bug?),&lt;br /&gt;
        &amp;quot;Life Drain&amp;quot;,      -- has CitadelSilenced but doesn&#039;t apply any silence&lt;br /&gt;
        &amp;quot;Singularity&amp;quot;,     -- has stun, just doesn&#039;t trigger Indomitable (bug?),&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;movementbased&amp;quot;] = {&lt;br /&gt;
    	&amp;quot;Slither&amp;quot;, -- has BehaviorMovement but doesn&#039;t get disabled (because is passive?)&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;moveslow&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Grasping Hands&amp;quot;, -- has SlowPercent&lt;br /&gt;
        &amp;quot;Riposte&amp;quot;,        -- has a 40% 4s slow somehow? (SlowPercent + SlowDuration)&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;platedarmor&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Arctic Beam&amp;quot;, -- has BuildUpDuration but doesn&#039;t get applied with bullets.&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;reactivebarrier&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Entangling Bola&amp;quot;, -- has CitadelRoot that never gets applied,&lt;br /&gt;
        &amp;quot;Life Drain&amp;quot;,      -- has CitadelSilenced but doesn&#039;t apply any silence&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;scalingspirit&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Bloodscent&amp;quot;,    -- has Spirit scale entries but all with Value 0&lt;br /&gt;
        &amp;quot;Eternal Night&amp;quot;, -- has a Spirit scale entry with Value 0&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;unstoppable&amp;quot;] = {&lt;br /&gt;
        &amp;quot;Life Drain&amp;quot;, -- has CitadelSilenced but doesn&#039;t apply any silence&lt;br /&gt;
    },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
return {&lt;br /&gt;
    lists             = lists,&lt;br /&gt;
    exclude_abilities = exclude_abilities,&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>~2026-DurationAmmoMelee1155</name></author>
	</entry>
	<entry>
		<id>https://deadlock.wiki/Module:AbilityTable?diff=157555</id>
		<title>Module:AbilityTable</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Module:AbilityTable?diff=157555"/>
		<updated>2026-09-14T00:18:25Z</updated>

		<summary type="html">&lt;p&gt;~2026-DurationAmmoMelee1155: Per-prop Spirit scaling: pass card prop labels to renderers (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Renders sortable wikitables of abilities filtered by a named stat.&lt;br /&gt;
&lt;br /&gt;
local Lists     = require(&amp;quot;Module:AbilityTable/Lists&amp;quot;)&lt;br /&gt;
local Notes     = require(&amp;quot;Module:AbilityTable/Notes&amp;quot;)&lt;br /&gt;
local Renderers = require(&amp;quot;Module:AbilityTable/ComplexRenderers&amp;quot;)&lt;br /&gt;
local GameData  = require(&amp;quot;Module:GameData&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
-- get_cells(ability, cardprops) -&amp;gt; { [column header] = cell text, Notes = ... }; most renderers ignore cardprops&lt;br /&gt;
local extra_columns = {&lt;br /&gt;
    [&amp;quot;barrier&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Barrier&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderBarrier,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;bossdmgscale&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Damage to Objectives&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderBossDamageScale,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;charges&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Charges&amp;quot;, &amp;quot;Time Between Charges&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderCharges,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;melee&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Base Damage&amp;quot;, &amp;quot;Light Melee Scaling&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderMelee,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;heavymelee&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Base Damage&amp;quot;, &amp;quot;Heavy Melee Scaling&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderHeavyMelee,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;scalingspirit&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Spirit Scaling&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderSpiritScaling,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;healreduce&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Heal Reduction&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderHealReduce,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;moveslow&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Move Slow&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderMoveSlow,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;dashslow&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Dash Slow&amp;quot;, &amp;quot;Notes&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderDashSlow,&lt;br /&gt;
    },&lt;br /&gt;
    [&amp;quot;extradebuffresist&amp;quot;] = {&lt;br /&gt;
        headers   = { &amp;quot;Debuff Resist&amp;quot; },&lt;br /&gt;
        get_cells = Renderers.RenderExtraDebuffResist,&lt;br /&gt;
    },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Note enrichment&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Merges auto-generated and human notes for full-table mode.&lt;br /&gt;
-- Capitalizes human note, joins with &amp;quot;. &amp;quot;, ensures trailing period.&lt;br /&gt;
local function enrich_notes_full(extra, human_note)&lt;br /&gt;
    if human_note then&lt;br /&gt;
        human_note = human_note:sub(1,1):upper() .. human_note:sub(2)&lt;br /&gt;
        extra[&amp;quot;Notes&amp;quot;] = extra[&amp;quot;Notes&amp;quot;] and (extra[&amp;quot;Notes&amp;quot;] .. &amp;quot;. &amp;quot; .. human_note) or human_note&lt;br /&gt;
    end&lt;br /&gt;
    if extra[&amp;quot;Notes&amp;quot;] and extra[&amp;quot;Notes&amp;quot;]:sub(-1) ~= &amp;quot;.&amp;quot; then&lt;br /&gt;
        extra[&amp;quot;Notes&amp;quot;] = extra[&amp;quot;Notes&amp;quot;] .. &amp;quot;.&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Merges auto-generated and human notes for compact mode.&lt;br /&gt;
-- Decapitalizes human note (skips T{N} prefixes like &amp;quot;T1 slow&amp;quot;),&lt;br /&gt;
-- joins with &amp;quot;; &amp;quot;, strips trailing period.&lt;br /&gt;
local function enrich_notes_compact(extra, human_note)&lt;br /&gt;
    if human_note then&lt;br /&gt;
        local first = human_note:sub(1,1)&lt;br /&gt;
        if first:match(&amp;quot;%u&amp;quot;) and not human_note:sub(2,2):match(&amp;quot;%d&amp;quot;) then&lt;br /&gt;
            human_note = first:lower() .. human_note:sub(2)&lt;br /&gt;
        end&lt;br /&gt;
        if human_note:sub(-1) == &amp;quot;.&amp;quot; then human_note = human_note:sub(1, -2) end&lt;br /&gt;
        extra[&amp;quot;Notes&amp;quot;] = extra[&amp;quot;Notes&amp;quot;] and (extra[&amp;quot;Notes&amp;quot;] .. &amp;quot;; &amp;quot; .. human_note) or human_note&lt;br /&gt;
    end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Ability views&lt;br /&gt;
--&lt;br /&gt;
-- In-game Ability may containe 1 or more records in AbilityData.json&lt;br /&gt;
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).&lt;br /&gt;
&lt;br /&gt;
-- &amp;quot;Ability View&amp;quot; is a main-record + any relevant sub-ability records.&lt;br /&gt;
-- Views also carry &amp;quot;cardprops&amp;quot; (see card_prop_labels), kept outside the record so membership matching never sees them.&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Card prop labels: key -&amp;gt; { name, title, order }, read from the card in AbilityCards.json.&lt;br /&gt;
-- Built per card, since a key like &amp;quot;Damage&amp;quot; is labelled differently on different cards.&lt;br /&gt;
local function card_prop_labels(card)&lt;br /&gt;
    local labels, n = {}, 0&lt;br /&gt;
&lt;br /&gt;
    local function walk(node)&lt;br /&gt;
        if type(node) ~= &amp;quot;table&amp;quot; then return end&lt;br /&gt;
        local key, name = node[&amp;quot;Key&amp;quot;], node[&amp;quot;Name&amp;quot;]&lt;br /&gt;
        if type(key) == &amp;quot;string&amp;quot; and type(name) == &amp;quot;string&amp;quot; and not labels[key] then&lt;br /&gt;
            n = n + 1&lt;br /&gt;
            local title = node[&amp;quot;Title&amp;quot;]&lt;br /&gt;
            if type(title) == &amp;quot;string&amp;quot; then&lt;br /&gt;
                title = title:gsub(&amp;quot;:%s*$&amp;quot;, &amp;quot;&amp;quot;)&lt;br /&gt;
            else&lt;br /&gt;
                title = nil&lt;br /&gt;
            end&lt;br /&gt;
            labels[key] = { name = name, title = title, order = n }&lt;br /&gt;
        end&lt;br /&gt;
        if node[1] ~= nil then&lt;br /&gt;
            for _, child in ipairs(node) do walk(child) end&lt;br /&gt;
        else&lt;br /&gt;
            -- Keyed sections (Range, Move, ...): { PropKey = { Name = ..., Value = ... } }&lt;br /&gt;
            local keyed = {}&lt;br /&gt;
            for k, v in pairs(node) do&lt;br /&gt;
                if type(k) == &amp;quot;string&amp;quot; and type(v) == &amp;quot;table&amp;quot; and v[&amp;quot;Key&amp;quot;] == nil&lt;br /&gt;
                    and type(v[&amp;quot;Name&amp;quot;]) == &amp;quot;string&amp;quot; and not labels[k]&lt;br /&gt;
                then&lt;br /&gt;
                    table.insert(keyed, k)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            table.sort(keyed)&lt;br /&gt;
            for _, k in ipairs(keyed) do&lt;br /&gt;
                n = n + 1&lt;br /&gt;
                labels[k] = { name = node[k][&amp;quot;Name&amp;quot;], order = n }&lt;br /&gt;
            end&lt;br /&gt;
            -- Main props before Alt props, then anything else in key order.&lt;br /&gt;
            if node[&amp;quot;Main&amp;quot;] ~= nil then walk(node[&amp;quot;Main&amp;quot;]) end&lt;br /&gt;
            if node[&amp;quot;Alt&amp;quot;]  ~= nil then walk(node[&amp;quot;Alt&amp;quot;])  end&lt;br /&gt;
            local rest = {}&lt;br /&gt;
            for k, v in pairs(node) do&lt;br /&gt;
                if k ~= &amp;quot;Main&amp;quot; and k ~= &amp;quot;Alt&amp;quot; and k ~= &amp;quot;Upgrades&amp;quot; and type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                    table.insert(rest, k)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            table.sort(rest, function(a, b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
            for _, k in ipairs(rest) do walk(node[k]) end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    for _, section in ipairs({ &amp;quot;Info1&amp;quot;, &amp;quot;Info2&amp;quot;, &amp;quot;Info3&amp;quot; }) do&lt;br /&gt;
        walk(card[section])&lt;br /&gt;
    end&lt;br /&gt;
    local rest = {}&lt;br /&gt;
    for k, v in pairs(card) do&lt;br /&gt;
        if type(v) == &amp;quot;table&amp;quot; and k ~= &amp;quot;Upgrades&amp;quot;&lt;br /&gt;
            and k ~= &amp;quot;Info1&amp;quot; and k ~= &amp;quot;Info2&amp;quot; and k ~= &amp;quot;Info3&amp;quot;&lt;br /&gt;
        then&lt;br /&gt;
            table.insert(rest, k)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    table.sort(rest, function(a, b) return tostring(a) &amp;lt; tostring(b) end)&lt;br /&gt;
    for _, k in ipairs(rest) do walk(card[k]) end&lt;br /&gt;
&lt;br /&gt;
    -- Header attributes are stored on the card without a Name.&lt;br /&gt;
    local header_attrs = {&lt;br /&gt;
        { &amp;quot;AbilityCastRange&amp;quot;,            &amp;quot;Cast Range&amp;quot; },&lt;br /&gt;
        { &amp;quot;Radius&amp;quot;,                      &amp;quot;Radius&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityDuration&amp;quot;,             &amp;quot;Duration&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityChannelTime&amp;quot;,          &amp;quot;Channel Time&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityCooldown&amp;quot;,             &amp;quot;Cooldown&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityCharges&amp;quot;,              &amp;quot;Charges&amp;quot; },&lt;br /&gt;
        { &amp;quot;AbilityCooldownBetweenCharge&amp;quot;, &amp;quot;Charge Cooldown&amp;quot; },&lt;br /&gt;
    }&lt;br /&gt;
    for _, attr in ipairs(header_attrs) do&lt;br /&gt;
        if not labels[attr[1]] then&lt;br /&gt;
            n = n + 1&lt;br /&gt;
            labels[attr[1]] = { name = attr[2], order = n }&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return labels&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local _views = nil   -- card key -&amp;gt; { view = record, heroName = ..., abilityNum = ..., cardprops = ... }&lt;br /&gt;
local _order = nil   -- array of card keys, sorted by hero then slot&lt;br /&gt;
&lt;br /&gt;
local function build_views()&lt;br /&gt;
    if _views then return _views, _order end&lt;br /&gt;
    _views, _order = {}, {}&lt;br /&gt;
&lt;br /&gt;
    local hero_data = mw.loadJsonData(&amp;quot;Data:HeroData.json&amp;quot;)&lt;br /&gt;
    local active_heroes = {}&lt;br /&gt;
    for _, hero in pairs(hero_data) do&lt;br /&gt;
        if type(hero) == &amp;quot;table&amp;quot;&lt;br /&gt;
            and hero[&amp;quot;IsDisabled&amp;quot;] == false&lt;br /&gt;
            and type(hero[&amp;quot;Name&amp;quot;]) == &amp;quot;string&amp;quot;&lt;br /&gt;
            and (hero[&amp;quot;IsSelectable&amp;quot;] == nil or hero[&amp;quot;IsSelectable&amp;quot;] ~= false)&lt;br /&gt;
        then&lt;br /&gt;
            active_heroes[hero[&amp;quot;Name&amp;quot;]] = true&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)&lt;br /&gt;
    local cards        = mw.loadJsonData(&amp;quot;Data:AbilityCards.json&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    local card_of = {}&lt;br /&gt;
    for _, hero_entry in pairs(cards) do&lt;br /&gt;
        if type(hero_entry) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            local hero_name = hero_entry[&amp;quot;Name&amp;quot;]&lt;br /&gt;
            if hero_name and active_heroes[hero_name] then&lt;br /&gt;
                for slot_str, ability in pairs(hero_entry) do&lt;br /&gt;
                    local slot_num = tonumber(slot_str)&lt;br /&gt;
                    local key = type(ability) == &amp;quot;table&amp;quot; and ability[&amp;quot;Key&amp;quot;] or nil&lt;br /&gt;
                    if slot_num and key and ability_data[key] then&lt;br /&gt;
                        card_of[key] = { heroName = hero_name, abilityNum = slot_num, card = ability }&lt;br /&gt;
                    end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Attach each non-card record to the longest card key it prefixes.&lt;br /&gt;
    local subs = {}&lt;br /&gt;
    for key, record in pairs(ability_data) do&lt;br /&gt;
        if not card_of[key] and record[&amp;quot;IsDisabled&amp;quot;] ~= true then&lt;br /&gt;
            local parent = nil&lt;br /&gt;
            for card_key in pairs(card_of) do&lt;br /&gt;
                if key:sub(1, #card_key + 1) == card_key .. &amp;quot;_&amp;quot;&lt;br /&gt;
                    and (parent == nil or #card_key &amp;gt; #parent)&lt;br /&gt;
                then&lt;br /&gt;
                    parent = card_key&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            if parent then&lt;br /&gt;
                subs[parent] = subs[parent] or {}&lt;br /&gt;
                table.insert(subs[parent], record)&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    for key, info in pairs(card_of) do&lt;br /&gt;
        local view = {}&lt;br /&gt;
        for k, v in pairs(ability_data[key]) do view[k] = v end&lt;br /&gt;
        view[&amp;quot;_subabilities&amp;quot;] = subs[key]&lt;br /&gt;
        _views[key] = {&lt;br /&gt;
            view       = view,&lt;br /&gt;
            heroName   = info.heroName,&lt;br /&gt;
            abilityNum = info.abilityNum,&lt;br /&gt;
            cardprops  = card_prop_labels(info.card),&lt;br /&gt;
        }&lt;br /&gt;
        table.insert(_order, key)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    table.sort(_order, function(a, b)&lt;br /&gt;
        local ia, ib = _views[a], _views[b]&lt;br /&gt;
        if ia.heroName ~= ib.heroName then return ia.heroName &amp;lt; ib.heroName end&lt;br /&gt;
        return ia.abilityNum &amp;lt; ib.abilityNum&lt;br /&gt;
    end)&lt;br /&gt;
&lt;br /&gt;
    return _views, _order&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Membership&lt;br /&gt;
-- Pieces of code that help in telling  &amp;quot;is ability X a member of list Y&amp;quot;.&lt;br /&gt;
-- main function is: p.is_member()&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Memoized name-exclusion set per list (from Lists.exclude_abilities).&lt;br /&gt;
local _excluded = {}&lt;br /&gt;
local function excluded_set(stat)&lt;br /&gt;
    if _excluded[stat] then return _excluded[stat] end&lt;br /&gt;
    local set = {}&lt;br /&gt;
    for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do&lt;br /&gt;
        set[name] = true&lt;br /&gt;
    end&lt;br /&gt;
    _excluded[stat] = set&lt;br /&gt;
    return set&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Per-list rules that can&#039;t be expressed as prop lists or exclusions.&lt;br /&gt;
-- Returns true to keep the ability, false to drop it.&lt;br /&gt;
local function special_ok(stat, record, ability_num)&lt;br /&gt;
    if stat == &amp;quot;dispelmagic&amp;quot; and ability_num == 4 then return false end&lt;br /&gt;
    return true&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Public: does ability `ability_key` belong to list `stat`?&lt;br /&gt;
function p.is_member(stat, ability_key, ability_num)&lt;br /&gt;
    local props = Lists.lists[stat]&lt;br /&gt;
    if not props then return false end&lt;br /&gt;
&lt;br /&gt;
    local views = build_views()&lt;br /&gt;
    local entry = views[ability_key]&lt;br /&gt;
    local view  = entry and entry.view&lt;br /&gt;
&lt;br /&gt;
    if not view then&lt;br /&gt;
        view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]&lt;br /&gt;
        if not view then return false end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local name = view[&amp;quot;Name&amp;quot;]&lt;br /&gt;
    if name and excluded_set(stat)[name] then return false end&lt;br /&gt;
&lt;br /&gt;
    ability_num = ability_num or (entry and entry.abilityNum)&lt;br /&gt;
    if not special_ok(stat, view, ability_num) then return false end&lt;br /&gt;
&lt;br /&gt;
    return GameData.entity_matches(view, props)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Entry point&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
function p.render(frame)&lt;br /&gt;
    local stat = mw.text.trim(frame.args[1] or &amp;quot;&amp;quot;):lower()&lt;br /&gt;
&lt;br /&gt;
    local internal_keys = Lists.lists[stat]&lt;br /&gt;
    if not internal_keys then&lt;br /&gt;
        return &#039;&amp;lt;span class=&amp;quot;error&amp;quot;&amp;gt;AbilityTable: unknown list &amp;quot;&#039; .. stat .. &#039;&amp;quot;.&amp;lt;/span&amp;gt;&#039;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local hide_set = {}&lt;br /&gt;
    local hide_param = mw.text.trim(frame.args[&amp;quot;hide&amp;quot;] or &amp;quot;&amp;quot;)&lt;br /&gt;
    if hide_param ~= &amp;quot;&amp;quot; then&lt;br /&gt;
        for col in hide_param:gmatch(&amp;quot;[^,]+&amp;quot;) do hide_set[mw.text.trim(col)] = true end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local compact = mw.text.trim(frame.args[&amp;quot;compact&amp;quot;] or &amp;quot;&amp;quot;) ~= &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
    local collapsed = mw.text.trim(frame.args[&amp;quot;collapsed&amp;quot;] or &amp;quot;&amp;quot;):lower()&lt;br /&gt;
&lt;br /&gt;
    local views, order = build_views()&lt;br /&gt;
    local spec       = extra_columns[stat]&lt;br /&gt;
    local headers    = spec and spec.headers or {}&lt;br /&gt;
    local get_cells  = spec and spec.get_cells or function() return {} end&lt;br /&gt;
    local stat_notes = Notes[stat]&lt;br /&gt;
&lt;br /&gt;
    local filtered = {}&lt;br /&gt;
    for _, key in ipairs(order) do&lt;br /&gt;
        if p.is_member(stat, key) then&lt;br /&gt;
            table.insert(filtered, key)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #filtered == 0 then&lt;br /&gt;
        return &amp;quot;No abilities found for list: &amp;quot; .. stat&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local lists_url = mw.uri.fullUrl(&amp;quot;Module:AbilityTable/Lists&amp;quot;)&lt;br /&gt;
    local notes_url = mw.uri.fullUrl(&amp;quot;Module:AbilityTable/Notes&amp;quot;, { action = &amp;quot;edit&amp;quot; })&lt;br /&gt;
    local disclaimer = frame:preprocess(&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ability-table-disclaimer&amp;quot;&amp;gt;&#039;&lt;br /&gt;
        .. &#039;This is a dynamic list &#039;&lt;br /&gt;
        .. &#039;&amp;lt;span class=&amp;quot;ability-table-list-name&amp;quot;&amp;gt;&amp;quot;&#039; .. stat .. &#039;&amp;quot;&amp;lt;/span&amp;gt;. &#039;&lt;br /&gt;
        .. &#039;[&#039; .. tostring(lists_url) .. &#039; Edit contents]&#039;&lt;br /&gt;
        .. &#039; · &#039;&lt;br /&gt;
        .. &#039;[&#039; .. tostring(notes_url) .. &#039; Edit notes].&#039;&lt;br /&gt;
        .. &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    )&lt;br /&gt;
&lt;br /&gt;
    if compact then&lt;br /&gt;
        local hero_order     = {}&lt;br /&gt;
        local hero_abilities = {}&lt;br /&gt;
        for _, key in ipairs(filtered) do&lt;br /&gt;
            local hero = views[key].heroName&lt;br /&gt;
            if not hero_abilities[hero] then&lt;br /&gt;
                table.insert(hero_order, hero)&lt;br /&gt;
                hero_abilities[hero] = {}&lt;br /&gt;
            end&lt;br /&gt;
            table.insert(hero_abilities[hero], views[key])&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        local items = {}&lt;br /&gt;
        for _, hero in ipairs(hero_order) do&lt;br /&gt;
            local hero_cell = frame:expandTemplate{ title = &amp;quot;HeroIcon&amp;quot;, args = { hero } }&lt;br /&gt;
            local ability_parts = {}&lt;br /&gt;
            for _, entry in ipairs(hero_abilities[hero]) do&lt;br /&gt;
                local ability = entry.view&lt;br /&gt;
                local name = ability[&amp;quot;Name&amp;quot;]&lt;br /&gt;
                local ability_cell = frame:expandTemplate{ title = &amp;quot;AbilityIcon&amp;quot;, args = { name } }&lt;br /&gt;
&lt;br /&gt;
                local extra = get_cells(ability, entry.cardprops)&lt;br /&gt;
                enrich_notes_compact(extra, stat_notes and stat_notes[name])&lt;br /&gt;
&lt;br /&gt;
                local note = extra[&amp;quot;Notes&amp;quot;]&lt;br /&gt;
                if note then&lt;br /&gt;
                    table.insert(ability_parts, ability_cell .. &amp;quot; (&amp;quot; .. note .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                else&lt;br /&gt;
                    table.insert(ability_parts, ability_cell)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            table.insert(items, &amp;quot;&amp;lt;li&amp;gt;&amp;quot; .. hero_cell .. &amp;quot;: &amp;quot; .. table.concat(ability_parts, &amp;quot;, &amp;quot;) .. &amp;quot;&amp;lt;/li&amp;gt;&amp;quot;)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        return disclaimer .. &amp;quot;&amp;lt;ul&amp;gt;&amp;quot; .. table.concat(items) .. &amp;quot;&amp;lt;/ul&amp;gt;&amp;quot;&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local has_notes_header = false&lt;br /&gt;
    local final_headers = {}&lt;br /&gt;
    for _, h in ipairs(headers) do&lt;br /&gt;
        if h == &amp;quot;Notes&amp;quot; then&lt;br /&gt;
            has_notes_header = true&lt;br /&gt;
        elseif not hide_set[h] then&lt;br /&gt;
            table.insert(final_headers, h)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set[&amp;quot;Notes&amp;quot;]&lt;br /&gt;
    if needs_notes then table.insert(final_headers, &amp;quot;Notes&amp;quot;) end&lt;br /&gt;
&lt;br /&gt;
    local table_class = &#039;wikitable sortable ability-table&#039;&lt;br /&gt;
    if collapsed ~= &amp;quot;&amp;quot; then&lt;br /&gt;
        table_class = table_class .. &#039; mw-collapsible&#039;&lt;br /&gt;
        if collapsed == &amp;quot;yes&amp;quot; or collapsed == &amp;quot;true&amp;quot; or collapsed == &amp;quot;1&amp;quot; then&lt;br /&gt;
            table_class = table_class .. &#039; mw-collapsed&#039;&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local out = {}&lt;br /&gt;
    table.insert(out, disclaimer)&lt;br /&gt;
    table.insert(out, &#039;{| class=&amp;quot;&#039; .. table_class .. &#039;&amp;quot;&#039;)&lt;br /&gt;
&lt;br /&gt;
    local header_row = &#039;! class=&amp;quot;ability-table-col-hero&amp;quot; | Hero&#039;&lt;br /&gt;
                    .. &#039; !! class=&amp;quot;ability-table-col-ability&amp;quot; | Ability&#039;&lt;br /&gt;
    for _, h in ipairs(final_headers) do&lt;br /&gt;
        header_row = header_row .. &#039; !! class=&amp;quot;ability-table-col-&#039;&lt;br /&gt;
            .. h:lower():gsub(&amp;quot;%s+&amp;quot;, &amp;quot;-&amp;quot;) .. &#039;&amp;quot; | &#039; .. h&lt;br /&gt;
    end&lt;br /&gt;
    table.insert(out, header_row)&lt;br /&gt;
&lt;br /&gt;
        local hero_counts = {}&lt;br /&gt;
    for _, key in ipairs(filtered) do&lt;br /&gt;
        local hero = views[key].heroName&lt;br /&gt;
        hero_counts[hero] = (hero_counts[hero] or 0) + 1&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local hero_seen = {}&lt;br /&gt;
&lt;br /&gt;
    for _, key in ipairs(filtered) do&lt;br /&gt;
        local entry   = views[key]&lt;br /&gt;
        local ability = entry.view&lt;br /&gt;
        local hero    = entry.heroName&lt;br /&gt;
&lt;br /&gt;
        local ability_cell = frame:expandTemplate{&lt;br /&gt;
            title = &amp;quot;AbilityIcon&amp;quot;,&lt;br /&gt;
            args = { ability[&amp;quot;Name&amp;quot;] }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        local extra = get_cells(ability, entry.cardprops)&lt;br /&gt;
        enrich_notes_full(extra, stat_notes and stat_notes[ability[&amp;quot;Name&amp;quot;]])&lt;br /&gt;
&lt;br /&gt;
        local row&lt;br /&gt;
&lt;br /&gt;
        if not hero_seen[hero] then&lt;br /&gt;
            local hero_cell = frame:expandTemplate{&lt;br /&gt;
                title = &amp;quot;HeroIcon&amp;quot;,&lt;br /&gt;
                args = { hero }&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            if hero_counts[hero] &amp;gt; 1 then&lt;br /&gt;
                row = &#039;| rowspan=&amp;quot;&#039; .. hero_counts[hero] .. &#039;&amp;quot; class=&amp;quot;ability-table-cell-hero&amp;quot; | &#039;&lt;br /&gt;
                    .. hero_cell&lt;br /&gt;
                    .. &#039; || class=&amp;quot;ability-table-cell-ability&amp;quot; | &#039; .. ability_cell&lt;br /&gt;
            else&lt;br /&gt;
                row = &#039;| class=&amp;quot;ability-table-cell-hero&amp;quot; | &#039;&lt;br /&gt;
                    .. hero_cell&lt;br /&gt;
                    .. &#039; || class=&amp;quot;ability-table-cell-ability&amp;quot; | &#039; .. ability_cell&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            hero_seen[hero] = true&lt;br /&gt;
        else&lt;br /&gt;
            row = &#039;| class=&amp;quot;ability-table-cell-ability&amp;quot; | &#039; .. ability_cell&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        for _, header in ipairs(final_headers) do&lt;br /&gt;
            local value = extra[header]&lt;br /&gt;
            local cls   = &amp;quot;ability-table-cell-&amp;quot; .. header:lower():gsub(&amp;quot;%s+&amp;quot;, &amp;quot;-&amp;quot;)&lt;br /&gt;
            local content = value ~= nil and tostring(value) or &amp;quot;—&amp;quot;&lt;br /&gt;
            row = row .. &#039; || class=&amp;quot;&#039; .. cls .. &#039;&amp;quot; | &#039; .. content&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        table.insert(out, &amp;quot;|-&amp;quot;)&lt;br /&gt;
        table.insert(out, row)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    table.insert(out, &amp;quot;|}&amp;quot;)&lt;br /&gt;
    return table.concat(out, &amp;quot;\n&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>~2026-DurationAmmoMelee1155</name></author>
	</entry>
	<entry>
		<id>https://deadlock.wiki/Module:AbilityTable/ComplexRenderers?diff=157554</id>
		<title>Module:AbilityTable/ComplexRenderers</title>
		<link rel="alternate" type="text/html" href="https://deadlock.wiki/Module:AbilityTable/ComplexRenderers?diff=157554"/>
		<updated>2026-09-14T00:17:27Z</updated>

		<summary type="html">&lt;p&gt;~2026-DurationAmmoMelee1155: RenderSpiritScaling: list every scaling prop with card labels and {{Ss}}, per-prop upgrade notes (with help from vergir-bot LLM)&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-- Complex cell renderers for AbilityTable.&lt;br /&gt;
local Lists = require(&amp;quot;Module:AbilityTable/Lists&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Shared helpers&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Unwraps a value that may be a raw number or a { Value = N } table.&lt;br /&gt;
local function unwrap_value(v)&lt;br /&gt;
    if type(v) == &amp;quot;number&amp;quot; then return v end&lt;br /&gt;
    if type(v) == &amp;quot;table&amp;quot;  then return v[&amp;quot;Value&amp;quot;] end&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Searches Upgrades[] for a field and returns { tier = N, value = V } at its&lt;br /&gt;
-- first appearance, or nil if the field is absent from all upgrade tiers.&lt;br /&gt;
local function find_in_upgrades(ability, field)&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    for i, tier in ipairs(upgrades) do&lt;br /&gt;
        if type(tier) == &amp;quot;table&amp;quot; and tier[field] ~= nil then&lt;br /&gt;
            return { tier = i, value = unwrap_value(tier[field]) }&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Finds the first non-zero value for any of the given fields, checking base&lt;br /&gt;
-- then upgrades in order. Returns field, tier (nil = base), absolute value.&lt;br /&gt;
local function find_first_occurrence(ability, fields)&lt;br /&gt;
    for _, field in ipairs(fields) do&lt;br /&gt;
        local v = unwrap_value(ability[field])&lt;br /&gt;
        if v and v ~= 0 then return field, nil, math.abs(v) end&lt;br /&gt;
    end&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                for _, field in ipairs(fields) do&lt;br /&gt;
                    local v = unwrap_value(tier[field])&lt;br /&gt;
                    if v and v ~= 0 then return field, i, math.abs(v) end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return nil, nil, nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Sums the non-zero values of `fields` within a single record layer (the base&lt;br /&gt;
-- ability record, or one upgrade tier). Returns nil when none are present.&lt;br /&gt;
local function sum_fields(layer, fields)&lt;br /&gt;
    local total = nil&lt;br /&gt;
    for _, field in ipairs(fields) do&lt;br /&gt;
        local v = unwrap_value(layer[field])&lt;br /&gt;
        if v and v ~= 0 then total = (total or 0) + math.abs(v) end&lt;br /&gt;
    end&lt;br /&gt;
    return total&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Sums the barrier value and Spirit scaling contributed by a single record&lt;br /&gt;
-- layer (the base ability record, or one upgrade tier). Returns nil when the&lt;br /&gt;
-- layer grants no barrier at all; a tier that only raises the scaling still&lt;br /&gt;
-- counts, so &amp;quot;+0 value, +1.0 scaling&amp;quot; upgrades are not lost.&lt;br /&gt;
local function sum_barrier(layer, fields)&lt;br /&gt;
    local value, scale, found = 0, 0, false&lt;br /&gt;
    for _, field in ipairs(fields) do&lt;br /&gt;
        local entry = layer[field]&lt;br /&gt;
        if entry ~= nil then&lt;br /&gt;
            local v = unwrap_value(entry) or 0&lt;br /&gt;
            local s = 0&lt;br /&gt;
            if type(entry) == &amp;quot;table&amp;quot; and type(entry[&amp;quot;Scale&amp;quot;]) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                s = entry[&amp;quot;Scale&amp;quot;][&amp;quot;Value&amp;quot;] or 0&lt;br /&gt;
            end&lt;br /&gt;
            if v ~= 0 or s ~= 0 then found = true end&lt;br /&gt;
            value = value + v&lt;br /&gt;
            scale = scale + s&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    if not found then return nil end&lt;br /&gt;
    return value, scale&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Pads a whole-number scaling out to one decimal, so a running total of 2&lt;br /&gt;
-- reads &amp;quot;2.0&amp;quot; next to its &amp;quot;1.5&amp;quot; neighbours instead of &amp;quot;2&amp;quot;. Values that already&lt;br /&gt;
-- carry decimals are left alone, which keeps precision on things like 0.75.&lt;br /&gt;
local function format_scale(scale)&lt;br /&gt;
    local text = tostring(scale)&lt;br /&gt;
    if not text:find(&amp;quot;.&amp;quot;, 1, true) then text = text .. &amp;quot;.0&amp;quot; end&lt;br /&gt;
    return text&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Formats one barrier step. Spirit scaling goes through the compact {{Ss}}&lt;br /&gt;
-- form, expanded via the frame the surrounding #invoke is already running&lt;br /&gt;
-- under: module return values are not re-expanded, so a literal &amp;quot;{{Ss|0.8}}&amp;quot;&lt;br /&gt;
-- would reach the page as text. The plain fallback covers a nil frame&lt;br /&gt;
-- (module console).&lt;br /&gt;
local function format_barrier(value, scale)&lt;br /&gt;
    local out = tostring(value)&lt;br /&gt;
    if scale and scale ~= 0 then&lt;br /&gt;
        local scale_text = format_scale(scale)&lt;br /&gt;
        local frame = mw.getCurrentFrame()&lt;br /&gt;
        if frame then&lt;br /&gt;
            out = out .. &amp;quot; &amp;quot; .. frame:expandTemplate{&lt;br /&gt;
                title = &amp;quot;Ss&amp;quot;,&lt;br /&gt;
                args  = { scale_text, compact = &amp;quot;1&amp;quot;, show_value = &amp;quot;1&amp;quot; },&lt;br /&gt;
            }&lt;br /&gt;
        else&lt;br /&gt;
            out = out .. &amp;quot; ×&amp;quot; .. scale_text&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Formats a 0–1 multiplier as a percentage: 0.2 → &amp;quot;20%&amp;quot;, 0.125 → &amp;quot;12.5%&amp;quot;.&lt;br /&gt;
-- %.10g absorbs floating-point noise from the ×100.&lt;br /&gt;
local function format_percent(v)&lt;br /&gt;
    return string.format(&amp;quot;%.10g&amp;quot;, v * 100) .. &amp;quot;%&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Generic renderer for a single numeric stat that can increase&lt;br /&gt;
-- additively through upgrades. Shows value in a named column&lt;br /&gt;
-- and generates upgrade notes with running totals.&lt;br /&gt;
-- Note: auto-generated notes omit trailing periods; the main&lt;br /&gt;
-- module&#039;s ensure_period handles that.&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
local function render_upgradable_stat(ability, col_name, fields, suffix)&lt;br /&gt;
    suffix = suffix or &amp;quot;&amp;quot;&lt;br /&gt;
    local cells = {}&lt;br /&gt;
    local notes = {}&lt;br /&gt;
&lt;br /&gt;
    local first_field, first_tier, first_val = find_first_occurrence(ability, fields)&lt;br /&gt;
    if not first_field then return cells end&lt;br /&gt;
&lt;br /&gt;
    local cell_str = tostring(first_val) .. suffix&lt;br /&gt;
    if first_tier then cell_str = cell_str .. &amp;quot; (T&amp;quot; .. first_tier .. &amp;quot;)&amp;quot; end&lt;br /&gt;
    cells[col_name] = cell_str&lt;br /&gt;
&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        local running    = first_val&lt;br /&gt;
        local seen_first = (first_tier == nil)&lt;br /&gt;
&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                if first_tier == i then&lt;br /&gt;
                    seen_first = true&lt;br /&gt;
                elseif seen_first then&lt;br /&gt;
                    local delta = nil&lt;br /&gt;
                    for _, field in ipairs(fields) do&lt;br /&gt;
                        local v = unwrap_value(tier[field])&lt;br /&gt;
                        if v and v ~= 0 then delta = math.abs(v); break end&lt;br /&gt;
                    end&lt;br /&gt;
                    if delta then&lt;br /&gt;
                        local total = running + delta&lt;br /&gt;
                        table.insert(notes, &amp;quot;T&amp;quot; .. i .. &amp;quot; upgrade increases &amp;quot;&lt;br /&gt;
                            .. col_name:lower() .. &amp;quot; by &amp;quot; .. delta .. suffix&lt;br /&gt;
                            .. &amp;quot; (total &amp;quot; .. total .. suffix .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                        running = total&lt;br /&gt;
                    end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #notes &amp;gt; 0 then cells[&amp;quot;Notes&amp;quot;] = table.concat(notes, &amp;quot; &amp;quot;) end&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Exported renderers&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
-- Renders the barrier an ability grants, with its Spirit Power scaling.&lt;br /&gt;
-- Both the value and the scaling accumulate across upgrades, so every step&lt;br /&gt;
-- after the first shows the running total: &amp;quot;100 x0.8 → 180 x1.5 (T2)&amp;quot;.&lt;br /&gt;
-- A tier that only improves the scaling still gets its own segment.&lt;br /&gt;
function p.RenderBarrier(ability)&lt;br /&gt;
    local fields   = Lists.lists[&amp;quot;barrier&amp;quot;]&lt;br /&gt;
    local segments = {}&lt;br /&gt;
&lt;br /&gt;
    local value, scale = sum_barrier(ability, fields)&lt;br /&gt;
    if value then&lt;br /&gt;
        table.insert(segments, format_barrier(value, scale))&lt;br /&gt;
    else&lt;br /&gt;
        value, scale = 0, 0&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                local delta_value, delta_scale = sum_barrier(tier, fields)&lt;br /&gt;
                if delta_value then&lt;br /&gt;
                    value, scale = value + delta_value, scale + delta_scale&lt;br /&gt;
                    table.insert(segments,&lt;br /&gt;
                        format_barrier(value, scale) .. &amp;quot; (T&amp;quot; .. i .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #segments == 0 then return {} end&lt;br /&gt;
    return { [&amp;quot;Barrier&amp;quot;] = table.concat(segments, &amp;quot; → &amp;quot;) }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders charge count and time between charges.&lt;br /&gt;
-- Special case for abilities that gain charges only through upgrades.&lt;br /&gt;
function p.RenderCharges(ability)&lt;br /&gt;
    local base_charges  = unwrap_value(ability[&amp;quot;AbilityCharges&amp;quot;])&lt;br /&gt;
    local base_cooldown = unwrap_value(ability[&amp;quot;AbilityCooldownBetweenCharge&amp;quot;])&lt;br /&gt;
    local has_base      = base_charges ~= nil and base_charges &amp;gt; 0&lt;br /&gt;
    local upgrade_info  = find_in_upgrades(ability, &amp;quot;AbilityCharges&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    local cooldown, upgrade_cooldown&lt;br /&gt;
    if has_base then&lt;br /&gt;
        if type(base_cooldown) == &amp;quot;number&amp;quot; and base_cooldown &amp;gt; 0 then&lt;br /&gt;
            cooldown = base_cooldown&lt;br /&gt;
        else&lt;br /&gt;
            local cd = find_in_upgrades(ability, &amp;quot;AbilityCooldownBetweenCharge&amp;quot;)&lt;br /&gt;
            if cd and cd.value and cd.value &amp;gt; 0 then cooldown = cd.value end&lt;br /&gt;
        end&lt;br /&gt;
    else&lt;br /&gt;
        local cd = find_in_upgrades(ability, &amp;quot;AbilityCooldownBetweenCharge&amp;quot;)&lt;br /&gt;
        if cd and cd.value and cd.value &amp;gt; 0 then upgrade_cooldown = cd.value end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells = {}&lt;br /&gt;
&lt;br /&gt;
    if has_base then&lt;br /&gt;
        cells[&amp;quot;Charges&amp;quot;] = tostring(base_charges)&lt;br /&gt;
        if cooldown then&lt;br /&gt;
            cells[&amp;quot;Time Between Charges&amp;quot;] = tostring(cooldown) .. &amp;quot;s&amp;quot;&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local note_parts = {}&lt;br /&gt;
    if upgrade_info then&lt;br /&gt;
        local n = upgrade_info.value or &amp;quot;?&amp;quot;&lt;br /&gt;
        local charge_str = n .. &amp;quot; charge&amp;quot; .. (n ~= 1 and &amp;quot;s&amp;quot; or &amp;quot;&amp;quot;)&lt;br /&gt;
        if has_base then&lt;br /&gt;
            table.insert(note_parts, &amp;quot;+&amp;quot; .. charge_str .. &amp;quot; on T&amp;quot; .. upgrade_info.tier .. &amp;quot; upgrade&amp;quot;)&lt;br /&gt;
        else&lt;br /&gt;
            local cd_str = upgrade_cooldown and (upgrade_cooldown .. &amp;quot;s&amp;quot;) or &amp;quot;unknown&amp;quot;&lt;br /&gt;
            table.insert(note_parts, &amp;quot;Becomes charged on T&amp;quot; .. upgrade_info.tier&lt;br /&gt;
                .. &amp;quot; upgrade with &amp;quot; .. charge_str&lt;br /&gt;
                .. &amp;quot; and &amp;quot; .. cd_str .. &amp;quot; time between charges&amp;quot;)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    if ability[&amp;quot;AbilityChargesConditionally&amp;quot;] ~= nil then&lt;br /&gt;
        table.insert(note_parts, &amp;quot;Has a conditional charge&amp;quot;)&lt;br /&gt;
    end&lt;br /&gt;
    if #note_parts &amp;gt; 0 then&lt;br /&gt;
        cells[&amp;quot;Notes&amp;quot;] = table.concat(note_parts, &amp;quot; &amp;quot;)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders base damage and melee scaling.&lt;br /&gt;
-- Special case for upgrades that switch from Light to Heavy Melee scaling.&lt;br /&gt;
function p.RenderMelee(ability)&lt;br /&gt;
    local melee_types = {}&lt;br /&gt;
    for _, t in ipairs(Lists.lists[&amp;quot;melee&amp;quot;]) do melee_types[t] = true end&lt;br /&gt;
&lt;br /&gt;
    -- Find the base property with active melee/heavy_melee scaling (Scale.Value &amp;gt; 0)&lt;br /&gt;
    local base_prop, base_damage, base_scale, base_type&lt;br /&gt;
    for k, v in pairs(ability) do&lt;br /&gt;
        if k ~= &amp;quot;Upgrades&amp;quot; and type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
            if type(scale) == &amp;quot;table&amp;quot; and melee_types[scale[&amp;quot;Type&amp;quot;]]&lt;br /&gt;
               and type(scale[&amp;quot;Value&amp;quot;]) == &amp;quot;number&amp;quot; and scale[&amp;quot;Value&amp;quot;] &amp;gt; 0 then&lt;br /&gt;
                base_prop = k&lt;br /&gt;
                base_damage = v[&amp;quot;Value&amp;quot;] or 0&lt;br /&gt;
                base_scale = scale[&amp;quot;Value&amp;quot;]&lt;br /&gt;
                base_type = scale[&amp;quot;Type&amp;quot;]&lt;br /&gt;
                break&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells = {}&lt;br /&gt;
    cells[&amp;quot;Base Damage&amp;quot;] = tostring(base_damage or 0)&lt;br /&gt;
    if base_scale then&lt;br /&gt;
        cells[&amp;quot;Light Melee Scaling&amp;quot;] = &amp;quot;×&amp;quot; .. base_scale&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) ~= &amp;quot;table&amp;quot; then return cells end&lt;br /&gt;
&lt;br /&gt;
    local notes = {}&lt;br /&gt;
    local running_damage = base_damage or 0&lt;br /&gt;
    local running_scale = base_scale or 0&lt;br /&gt;
    local running_type = base_type&lt;br /&gt;
&lt;br /&gt;
    for i, tier in ipairs(upgrades) do&lt;br /&gt;
        if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            local damage_delta = 0&lt;br /&gt;
            local new_scale = nil&lt;br /&gt;
            local new_type = nil&lt;br /&gt;
&lt;br /&gt;
            for k, v in pairs(tier) do&lt;br /&gt;
                if type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                    local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
                    if type(scale) == &amp;quot;table&amp;quot; and melee_types[scale[&amp;quot;Type&amp;quot;]] then&lt;br /&gt;
                        damage_delta = damage_delta + (v[&amp;quot;Value&amp;quot;] or 0)&lt;br /&gt;
                        if scale[&amp;quot;Value&amp;quot;] &amp;gt; 0 then&lt;br /&gt;
                            new_scale = scale[&amp;quot;Value&amp;quot;]&lt;br /&gt;
                            new_type = scale[&amp;quot;Type&amp;quot;]&lt;br /&gt;
                        end&lt;br /&gt;
                    end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            local flat_delta = 0&lt;br /&gt;
            if base_prop and type(tier[base_prop]) == &amp;quot;number&amp;quot; then&lt;br /&gt;
                flat_delta = tier[base_prop]&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            local tier_parts = {}&lt;br /&gt;
&lt;br /&gt;
            if new_type and new_type ~= running_type then&lt;br /&gt;
                running_damage = running_damage + damage_delta&lt;br /&gt;
                running_scale = new_scale&lt;br /&gt;
                running_type = new_type&lt;br /&gt;
                local type_label = new_type == &amp;quot;heavy_melee&amp;quot; and &amp;quot;&#039;&#039;&#039;Heavy Melee&#039;&#039;&#039;&amp;quot; or &amp;quot;Light Melee&amp;quot;&lt;br /&gt;
                local parts = {}&lt;br /&gt;
                if running_damage &amp;gt; 0 then&lt;br /&gt;
                    table.insert(parts, running_damage .. &amp;quot; base damage&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
                table.insert(parts, &amp;quot;×&amp;quot; .. new_scale .. &amp;quot; &amp;quot; .. type_label .. &amp;quot; scaling&amp;quot;)&lt;br /&gt;
                table.insert(tier_parts, &amp;quot;on T&amp;quot; .. i .. &amp;quot; becomes &amp;quot; .. table.concat(parts, &amp;quot; with &amp;quot;))&lt;br /&gt;
            elseif new_scale then&lt;br /&gt;
                running_damage = running_damage + damage_delta&lt;br /&gt;
                running_scale = running_scale + new_scale&lt;br /&gt;
                local parts = {}&lt;br /&gt;
                if damage_delta ~= 0 then&lt;br /&gt;
                    table.insert(parts, &amp;quot;+&amp;quot; .. damage_delta .. &amp;quot; base damage&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
                table.insert(parts, &amp;quot;+×&amp;quot; .. new_scale .. &amp;quot; scaling (total ×&amp;quot; .. running_scale .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                table.insert(tier_parts, &amp;quot;on T&amp;quot; .. i .. &amp;quot; gets &amp;quot; .. table.concat(parts, &amp;quot;, &amp;quot;))&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            if flat_delta ~= 0 and not new_scale then&lt;br /&gt;
                running_damage = running_damage + flat_delta&lt;br /&gt;
                table.insert(tier_parts, &amp;quot;on T&amp;quot; .. i .. &amp;quot; gets +&amp;quot; .. flat_delta .. &amp;quot; base damage (total &amp;quot; .. running_damage .. &amp;quot;)&amp;quot;)&lt;br /&gt;
            end&lt;br /&gt;
&lt;br /&gt;
            if #tier_parts &amp;gt; 0 then&lt;br /&gt;
                table.insert(notes, table.concat(tier_parts, &amp;quot;, &amp;quot;))&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #notes &amp;gt; 0 then cells[&amp;quot;Notes&amp;quot;] = table.concat(notes, &amp;quot;. &amp;quot;) end&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders base damage and heavy melee scaling.&lt;br /&gt;
-- Notes when heavy melee is only available via upgrade.&lt;br /&gt;
function p.RenderHeavyMelee(ability)&lt;br /&gt;
    -- Find base heavy_melee property&lt;br /&gt;
    local base_damage, base_scale&lt;br /&gt;
    for k, v in pairs(ability) do&lt;br /&gt;
        if k ~= &amp;quot;Upgrades&amp;quot; and type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
            if type(scale) == &amp;quot;table&amp;quot; and scale[&amp;quot;Type&amp;quot;] == &amp;quot;heavy_melee&amp;quot;&lt;br /&gt;
               and type(scale[&amp;quot;Value&amp;quot;]) == &amp;quot;number&amp;quot; then&lt;br /&gt;
                base_damage = v[&amp;quot;Value&amp;quot;] or 0&lt;br /&gt;
                base_scale = scale[&amp;quot;Value&amp;quot;]&lt;br /&gt;
                break&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells = {}&lt;br /&gt;
&lt;br /&gt;
    -- Active at base&lt;br /&gt;
    if base_scale and base_scale &amp;gt; 0 then&lt;br /&gt;
        cells[&amp;quot;Base Damage&amp;quot;] = tostring(base_damage)&lt;br /&gt;
        cells[&amp;quot;Heavy Melee Scaling&amp;quot;] = &amp;quot;×&amp;quot; .. base_scale&lt;br /&gt;
        return cells&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Not at base — find in upgrades&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) ~= &amp;quot;table&amp;quot; then return cells end&lt;br /&gt;
&lt;br /&gt;
    for i, tier in ipairs(upgrades) do&lt;br /&gt;
        if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
            for k, v in pairs(tier) do&lt;br /&gt;
                if type(v) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                    local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
                    if type(scale) == &amp;quot;table&amp;quot; and scale[&amp;quot;Type&amp;quot;] == &amp;quot;heavy_melee&amp;quot;&lt;br /&gt;
                       and type(scale[&amp;quot;Value&amp;quot;]) == &amp;quot;number&amp;quot; and scale[&amp;quot;Value&amp;quot;] &amp;gt; 0 then&lt;br /&gt;
                        cells[&amp;quot;Base Damage&amp;quot;] = tostring(v[&amp;quot;Value&amp;quot;] or 0)&lt;br /&gt;
                        cells[&amp;quot;Heavy Melee Scaling&amp;quot;] = &amp;quot;×&amp;quot; .. scale[&amp;quot;Value&amp;quot;]&lt;br /&gt;
                        cells[&amp;quot;Notes&amp;quot;] = &amp;quot;Only becomes Heavy Melee on &#039;&#039;&#039;T&amp;quot; .. i .. &amp;quot;&#039;&#039;&#039; upgrade&amp;quot;&lt;br /&gt;
                        return cells&lt;br /&gt;
                    end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- ============================================================&lt;br /&gt;
-- Spirit scaling helpers&lt;br /&gt;
-- ============================================================&lt;br /&gt;
&lt;br /&gt;
-- Spirit scaling entry of a prop, or nil. Scale is a single { Value, Type }&lt;br /&gt;
-- or an array of them (e.g. a duration scaling with both Spirit and duration).&lt;br /&gt;
local function spirit_scale(v)&lt;br /&gt;
    if type(v) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    local scale = v[&amp;quot;Scale&amp;quot;]&lt;br /&gt;
    if type(scale) ~= &amp;quot;table&amp;quot; then return nil end&lt;br /&gt;
    if scale[1] == nil then&lt;br /&gt;
        if scale[&amp;quot;Type&amp;quot;] == &amp;quot;spirit&amp;quot; then return scale end&lt;br /&gt;
        return nil&lt;br /&gt;
    end&lt;br /&gt;
    for _, entry in ipairs(scale) do&lt;br /&gt;
        if type(entry) == &amp;quot;table&amp;quot; and entry[&amp;quot;Type&amp;quot;] == &amp;quot;spirit&amp;quot; then return entry end&lt;br /&gt;
    end&lt;br /&gt;
    return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Non-zero Spirit scalings of one layer (base record or upgrade tier) as&lt;br /&gt;
-- { key, value, multiply }, in card order; props not on the card last, by key.&lt;br /&gt;
local function spirit_scalings(layer, labels)&lt;br /&gt;
    local out = {}&lt;br /&gt;
    for k, v in pairs(layer) do&lt;br /&gt;
        if k ~= &amp;quot;Upgrades&amp;quot; and tostring(k):sub(1, 1) ~= &amp;quot;_&amp;quot; then&lt;br /&gt;
            local s = spirit_scale(v)&lt;br /&gt;
            if s and type(s[&amp;quot;Value&amp;quot;]) == &amp;quot;number&amp;quot; and s[&amp;quot;Value&amp;quot;] ~= 0 then&lt;br /&gt;
                table.insert(out, {&lt;br /&gt;
                    key      = k,&lt;br /&gt;
                    value    = s[&amp;quot;Value&amp;quot;],&lt;br /&gt;
                    multiply = s[&amp;quot;Multiply&amp;quot;] == true,&lt;br /&gt;
                })&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    table.sort(out, function(a, b)&lt;br /&gt;
        local oa = labels[a.key] and labels[a.key].order or math.huge&lt;br /&gt;
        local ob = labels[b.key] and labels[b.key].order or math.huge&lt;br /&gt;
        if oa ~= ob then return oa &amp;lt; ob end&lt;br /&gt;
        return a.key &amp;lt; b.key&lt;br /&gt;
    end)&lt;br /&gt;
    return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Formats a scaling value to 3 significant figures, as the ability cards do.&lt;br /&gt;
local function format_spirit(v)&lt;br /&gt;
    return string.format(&amp;quot;%.3g&amp;quot;, v)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Compact {{Ss}} badge for the cell, expanded via the current frame like&lt;br /&gt;
-- format_barrier; plain &amp;quot;×N&amp;quot; when there is no frame (module console).&lt;br /&gt;
local function format_spirit_badge(v)&lt;br /&gt;
    local text  = format_spirit(v)&lt;br /&gt;
    local frame = mw.getCurrentFrame()&lt;br /&gt;
    if frame then&lt;br /&gt;
        return frame:expandTemplate{&lt;br /&gt;
            title = &amp;quot;Ss&amp;quot;,&lt;br /&gt;
            args  = { text, compact = &amp;quot;1&amp;quot;, show_value = &amp;quot;1&amp;quot; },&lt;br /&gt;
        }&lt;br /&gt;
    end&lt;br /&gt;
    return &amp;quot;×&amp;quot; .. text&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- &amp;quot;A&amp;quot;, &amp;quot;A and B&amp;quot;, &amp;quot;A, B and C&amp;quot;.&lt;br /&gt;
local function join_names(names)&lt;br /&gt;
    if #names &amp;lt;= 1 then return names[1] or &amp;quot;&amp;quot; end&lt;br /&gt;
    return table.concat(names, &amp;quot;, &amp;quot;, 1, #names - 1) .. &amp;quot; and &amp;quot; .. names[#names]&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders Spirit Power scaling: one {{Ss}} badge per scaling prop, labelled from&lt;br /&gt;
-- the card (&amp;quot;x0.55 (Base Damage)&amp;quot;; &amp;quot;Damage: Cost of Stay&amp;quot; when names collide;&lt;br /&gt;
-- raw key when the card lacks it). Notes list upgrades per prop with running&lt;br /&gt;
-- totals; Multiply upgrades multiply the current scaling, as the gadget does.&lt;br /&gt;
function p.RenderSpiritScaling(ability, cardprops)&lt;br /&gt;
    local labels = cardprops or {}&lt;br /&gt;
&lt;br /&gt;
    local base  = spirit_scalings(ability, labels)&lt;br /&gt;
    local tiers = {}   -- { tier = i, list = ... } in tier order&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        -- ipairs rather than #: mw.loadJsonData tables have no length.&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                table.insert(tiers, { tier = i, list = spirit_scalings(tier, labels) })&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Props sharing a Name get their Title appended so they can be told apart.&lt;br /&gt;
    local keys_by_name = {}&lt;br /&gt;
    local function note_keys(list)&lt;br /&gt;
        for _, prop in ipairs(list) do&lt;br /&gt;
            local info = labels[prop.key]&lt;br /&gt;
            if info then&lt;br /&gt;
                keys_by_name[info.name] = keys_by_name[info.name] or {}&lt;br /&gt;
                keys_by_name[info.name][prop.key] = true&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    note_keys(base)&lt;br /&gt;
    for _, t in ipairs(tiers) do note_keys(t.list) end&lt;br /&gt;
&lt;br /&gt;
    local function label_of(key)&lt;br /&gt;
        local info = labels[key]&lt;br /&gt;
        if not info then return key end&lt;br /&gt;
        local n = 0&lt;br /&gt;
        for _ in pairs(keys_by_name[info.name] or {}) do n = n + 1 end&lt;br /&gt;
        if n &amp;gt; 1 and info.title then return info.name .. &amp;quot;: &amp;quot; .. info.title end&lt;br /&gt;
        return info.name&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells   = {}&lt;br /&gt;
    local running = {}   -- prop key -&amp;gt; current total scaling&lt;br /&gt;
&lt;br /&gt;
    if #base &amp;gt; 0 then&lt;br /&gt;
        local lines = {}&lt;br /&gt;
        for _, prop in ipairs(base) do&lt;br /&gt;
            table.insert(lines, format_spirit_badge(prop.value) .. &amp;quot; (&amp;quot; .. label_of(prop.key) .. &amp;quot;)&amp;quot;)&lt;br /&gt;
            running[prop.key] = prop.value&lt;br /&gt;
        end&lt;br /&gt;
        cells[&amp;quot;Spirit Scaling&amp;quot;] = table.concat(lines, &amp;quot;&amp;lt;br&amp;gt;&amp;quot;)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local notes = {}&lt;br /&gt;
    for _, t in ipairs(tiers) do&lt;br /&gt;
        local i, list = t.tier, t.list&lt;br /&gt;
        local groups, group_order = {}, {}   -- Multiply upgrades, grouped by multiplier&lt;br /&gt;
        for _, up in ipairs(list) do&lt;br /&gt;
            local label   = label_of(up.key)&lt;br /&gt;
            local current = running[up.key]&lt;br /&gt;
            if up.multiply then&lt;br /&gt;
                local group = groups[up.value]&lt;br /&gt;
                if not group then&lt;br /&gt;
                    group = { labels = {}, totals = {} }&lt;br /&gt;
                    groups[up.value] = group&lt;br /&gt;
                    table.insert(group_order, up.value)&lt;br /&gt;
                end&lt;br /&gt;
                table.insert(group.labels, label)&lt;br /&gt;
                if current then&lt;br /&gt;
                    running[up.key] = current * up.value&lt;br /&gt;
                    table.insert(group.totals, &amp;quot;×&amp;quot; .. format_spirit(running[up.key]))&lt;br /&gt;
                else&lt;br /&gt;
                    table.insert(group.totals, &amp;quot;?&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
            elseif current then&lt;br /&gt;
                running[up.key] = current + up.value&lt;br /&gt;
                table.insert(notes, &amp;quot;T&amp;quot; .. i .. &amp;quot; upgrade adds +×&amp;quot; .. format_spirit(up.value)&lt;br /&gt;
                    .. &amp;quot; &amp;quot; .. label .. &amp;quot; scaling (total ×&amp;quot; .. format_spirit(running[up.key]) .. &amp;quot;)&amp;quot;)&lt;br /&gt;
            else&lt;br /&gt;
                running[up.key] = up.value&lt;br /&gt;
                table.insert(notes, &amp;quot;T&amp;quot; .. i .. &amp;quot; upgrade grants ×&amp;quot; .. format_spirit(up.value)&lt;br /&gt;
                    .. &amp;quot; &amp;quot; .. label .. &amp;quot; scaling&amp;quot;)&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
        for _, m in ipairs(group_order) do&lt;br /&gt;
            local group = groups[m]&lt;br /&gt;
            table.insert(notes, &amp;quot;T&amp;quot; .. i .. &amp;quot; upgrade multiplies &amp;quot; .. join_names(group.labels)&lt;br /&gt;
                .. &amp;quot; scaling by &amp;quot; .. format_spirit(m)&lt;br /&gt;
                .. &amp;quot; (total &amp;quot; .. table.concat(group.totals, &amp;quot;, &amp;quot;) .. &amp;quot;)&amp;quot;)&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #notes &amp;gt; 0 then cells[&amp;quot;Notes&amp;quot;] = table.concat(notes, &amp;quot;. &amp;quot;) end&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders heal reduction percentage.&lt;br /&gt;
-- Special case for DisableHealing, shown as 100%.&lt;br /&gt;
function p.RenderHealReduce(ability)&lt;br /&gt;
    -- DisableHealing is always 100%, handle it separately&lt;br /&gt;
    local v = unwrap_value(ability[&amp;quot;DisableHealing&amp;quot;])&lt;br /&gt;
    if v and v ~= 0 then&lt;br /&gt;
        return { [&amp;quot;Heal Reduction&amp;quot;] = &amp;quot;100%&amp;quot; }&lt;br /&gt;
    end&lt;br /&gt;
    local upgrade = find_in_upgrades(ability, &amp;quot;DisableHealing&amp;quot;)&lt;br /&gt;
    if upgrade then&lt;br /&gt;
        return { [&amp;quot;Heal Reduction&amp;quot;] = &amp;quot;100% (T&amp;quot; .. upgrade.tier .. &amp;quot;)&amp;quot; }&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    -- Everything else goes through the generic renderer&lt;br /&gt;
    local filtered = {}&lt;br /&gt;
    for _, prop in ipairs(Lists.lists[&amp;quot;healreduce&amp;quot;]) do&lt;br /&gt;
        if prop ~= &amp;quot;DisableHealing&amp;quot; then table.insert(filtered, prop) end&lt;br /&gt;
    end&lt;br /&gt;
    return render_upgradable_stat(ability, &amp;quot;Heal Reduction&amp;quot;, filtered, &amp;quot;%&amp;quot;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders move slow percentage.&lt;br /&gt;
-- Appends a note if the ability also self-slows the caster.&lt;br /&gt;
function p.RenderMoveSlow(ability)&lt;br /&gt;
    local cells = render_upgradable_stat(ability, &amp;quot;Move Slow&amp;quot;, Lists.lists[&amp;quot;moveslow&amp;quot;], &amp;quot;%&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    local channel_slow = unwrap_value(ability[&amp;quot;ChannelSlowPercent&amp;quot;])&lt;br /&gt;
    if channel_slow and channel_slow ~= 0 then&lt;br /&gt;
        local note = &amp;quot;Also slows the caster for &amp;quot; .. math.abs(channel_slow) .. &amp;quot;%&amp;quot;&lt;br /&gt;
        cells[&amp;quot;Notes&amp;quot;] = cells[&amp;quot;Notes&amp;quot;] and (cells[&amp;quot;Notes&amp;quot;] .. &amp;quot;; &amp;quot; .. note) or note&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders dash slow percentage.&lt;br /&gt;
-- Special case for GroundDashReductionPercent coexisting with another dashslow&lt;br /&gt;
-- prop, which means it&#039;s a caster self-slow and is noted separately.&lt;br /&gt;
function p.RenderDashSlow(ability)&lt;br /&gt;
    local stat_props = Lists.lists[&amp;quot;dashslow&amp;quot;]&lt;br /&gt;
    local props = {}&lt;br /&gt;
    for _, pr in ipairs(stat_props) do table.insert(props, pr) end&lt;br /&gt;
&lt;br /&gt;
    local caster_dash_slow = nil&lt;br /&gt;
    local base_ground = unwrap_value(ability[&amp;quot;GroundDashReductionPercent&amp;quot;])&lt;br /&gt;
    if base_ground and base_ground ~= 0 then&lt;br /&gt;
        local has_enemy_prop = false&lt;br /&gt;
        for _, prop in ipairs(stat_props) do&lt;br /&gt;
            if prop ~= &amp;quot;GroundDashReductionPercent&amp;quot; then&lt;br /&gt;
                local v = unwrap_value(ability[prop])&lt;br /&gt;
                if v and v ~= 0 then has_enemy_prop = true; break end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
        if not has_enemy_prop then&lt;br /&gt;
            local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
            if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                for _, tier in ipairs(upgrades) do&lt;br /&gt;
                    if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                        for _, prop in ipairs(stat_props) do&lt;br /&gt;
                            if prop ~= &amp;quot;GroundDashReductionPercent&amp;quot; then&lt;br /&gt;
                                local v = unwrap_value(tier[prop])&lt;br /&gt;
                                if v and v ~= 0 then has_enemy_prop = true; break end&lt;br /&gt;
                            end&lt;br /&gt;
                        end&lt;br /&gt;
                    end&lt;br /&gt;
                    if has_enemy_prop then break end&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
        if has_enemy_prop then&lt;br /&gt;
            caster_dash_slow = math.abs(base_ground)&lt;br /&gt;
            local filtered = {}&lt;br /&gt;
            for _, prop in ipairs(props) do&lt;br /&gt;
                if prop ~= &amp;quot;GroundDashReductionPercent&amp;quot; then&lt;br /&gt;
                    table.insert(filtered, prop)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
            props = filtered&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local cells = render_upgradable_stat(ability, &amp;quot;Dash Slow&amp;quot;, props, &amp;quot;%&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    if caster_dash_slow then&lt;br /&gt;
        local note = &amp;quot;Gives &amp;quot; .. caster_dash_slow .. &amp;quot;% dash slow for the caster&amp;quot;&lt;br /&gt;
        cells[&amp;quot;Notes&amp;quot;] = cells[&amp;quot;Notes&amp;quot;] and (cells[&amp;quot;Notes&amp;quot;] .. &amp;quot;; &amp;quot; .. note) or note&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    return cells&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders the amount of Debuff Resist an ability grants.&lt;br /&gt;
-- Upgrades stack additively onto the base value, so every step after the first&lt;br /&gt;
-- shows the running total: &amp;quot;20% (T3)&amp;quot; when granted only by an upgrade,&lt;br /&gt;
-- &amp;quot;10% → 25% (T2)&amp;quot; when a base value is later increased. Chains for any number&lt;br /&gt;
-- of steps. No ability currently has both a base and an upgrade value.&lt;br /&gt;
function p.RenderExtraDebuffResist(ability)&lt;br /&gt;
    local fields   = Lists.lists[&amp;quot;extradebuffresist&amp;quot;]&lt;br /&gt;
    local segments = {}&lt;br /&gt;
&lt;br /&gt;
    local running = sum_fields(ability, fields)&lt;br /&gt;
    if running then&lt;br /&gt;
        table.insert(segments, running .. &amp;quot;%&amp;quot;)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    local upgrades = ability[&amp;quot;Upgrades&amp;quot;]&lt;br /&gt;
    if type(upgrades) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        for i, tier in ipairs(upgrades) do&lt;br /&gt;
            if type(tier) == &amp;quot;table&amp;quot; then&lt;br /&gt;
                local delta = sum_fields(tier, fields)&lt;br /&gt;
                if delta then&lt;br /&gt;
                    running = (running or 0) + delta&lt;br /&gt;
                    table.insert(segments, running .. &amp;quot;% (T&amp;quot; .. i .. &amp;quot;)&amp;quot;)&lt;br /&gt;
                end&lt;br /&gt;
            end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    if #segments == 0 then return {} end&lt;br /&gt;
    return { [&amp;quot;Debuff Resist&amp;quot;] = table.concat(segments, &amp;quot; → &amp;quot;) }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Renders how much of an ability&#039;s damage applies to objectives (Guardians,&lt;br /&gt;
-- Walkers, Patron, Mid-Boss). BossDamageScale is a 0–1 multiplier shown as a&lt;br /&gt;
-- percentage: 0.2 → &amp;quot;20%&amp;quot;. The field never appears in upgrade tiers, so only&lt;br /&gt;
-- the base value is read; sub-ability records are checked as a fallback when&lt;br /&gt;
-- the main record lacks it.&lt;br /&gt;
function p.RenderBossDamageScale(ability)&lt;br /&gt;
    local v = unwrap_value(ability[&amp;quot;BossDamageScale&amp;quot;])&lt;br /&gt;
    if (v == nil or v == 0) and type(ability[&amp;quot;_subabilities&amp;quot;]) == &amp;quot;table&amp;quot; then&lt;br /&gt;
        for _, sub in ipairs(ability[&amp;quot;_subabilities&amp;quot;]) do&lt;br /&gt;
            local sv = unwrap_value(sub[&amp;quot;BossDamageScale&amp;quot;])&lt;br /&gt;
            if sv and sv ~= 0 then v = sv; break end&lt;br /&gt;
        end&lt;br /&gt;
    end&lt;br /&gt;
    if v == nil or v == 0 then return {} end&lt;br /&gt;
    return { [&amp;quot;Damage to Objectives&amp;quot;] = format_percent(v) }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>~2026-DurationAmmoMelee1155</name></author>
	</entry>
</feed>