Editing Module:AbilityTable

Warning: You are not logged in. Once you make an edit, a temporary account will be created for you. Learn more. Log in or create an account to continue receiving notifications after this account expires, and to access other features.
The edit can be undone. Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
Latest revision Your text
Line 1: Line 1:
-- Renders sortable wikitables of abilities filtered by a named stat.
-- Renders sortable wikitables of abilities filtered by a named stat.


local Lists     = require("Module:AbilityTable/Lists")
-- Maps friendly stat names to internal property keys or values.
local Notes     = require("Module:AbilityTable/Notes")
local stat_to_props = {
local Renderers = require("Module:AbilityTable/ComplexRenderers")
    ["charges"]    = { "AbilityCharges" },
local GameData = require("Module:GameData")
     ["melee"]      = { "melee", "heavy_melee" },
 
    ["wpndmg"]    = { "weapon_damage_increase" },
local p = {}
     ["healreduce"] = { "HealAmpReceivePenaltyPercent", "HealAmpRegenPenaltyPercent", "DisableHealing" },
    ["debuffresist"] = {
        -- Do not insert: AbilityDuration, TickRate, SlowDuration
        -- they do more harm than good and include a lot of excessive abilities to the list
        "StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", "PetrifyDuration", "HexDuration",
        "ImmobilizeDuration", "LiftDuration", "DebuffDuration", "HoldInPlaceDuration", "ExplodeDebuffDuration", "SlowPercent",
        "DebuffAccuracy", "TimeSlowDuration", "LateCheckoutStun", "BulletDamageAmpDuration", "BulletResistReduction", "BurnDuration",
        "EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "StealDuration", "MaxSlowDuration", "EffectDuration",
        "BleedDuration", "VenomDuration", "TossDuration",
        -- can also just list abilities if they don't have a reusable property
        "Life Drain", "Combo", "Light Eater", "Spirit Lasso", "Card Trick"
    },
    ["moveslow"] = {
    "SlowPercent", "EnemySlowPct", "MoveSlowPercent", "MaxSlowPercent", "MoveSpeedSlowPct", "MovementSpeedSlow",
    "MovementSlowPct", "ClubSlowPercent", "MoveSpeedPenaltyPerStack", "MovementSlow"
    },
    ["dashslow"] = { "GroundDashReductionPercent", "MaxGroundDashReductionPercent", "MoveSpeedAndDashSlowPct", "EnemyDashSlowPercent" },
    ["reactivebarrier"] = {
        -- WIP, do not use yet.
        "StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", "PetrifyDuration", "HexDuration",
        "ImmobilizeDuration", "LiftDuration", "HoldInPlaceDuration", "RestrictionDuration", "PullDuration", "PullDownDuration",
        -- engine-side CCs with no matching data prop
        "Itani Lo Sahn", "Dazzling Trick", "Ground Strike", "Hotel Guest",
    },
    ["dispelmagic"] = {
        "SilenceDuration", "SilenceDebuff", "ImmobilizeDuration", "LiftDuration", "HoldInPlaceDuration",
        "RestrictionDuration",
        "SlowPercent", "EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "SlowDuration",
        "DebuffDuration", "ExplodeDebuffDuration", "BulletDamageAmpDuration", "BulletResistReduction",
        "StealDuration", "EffectDuration", "DebuffAccuracy", "TimeSlowDuration",
        "BurnDuration", "BleedDuration", "VenomDuration", "MaxSlowDuration",
        -- raw abilities
        "Life Drain", "Card Trick", "Sticky Bomb", "Light Eater", "Luggage Cart", "Essence Theft"
    },
    ["heal"] = {
        "HealingPerSecond", "HealAmount", "HealFixedHealth", "FlatHealthHealing", "HealingFactor", "MaxHealthRegen",
        "ExternalBonusHealthRegen", "BonusHealthRegen", "LifeStealPercentOnHit", "LifestealPercentHero", "MissingHealthPercentHeal", "MissingHealthBurstPct",
        "MissingHPHeal", "TechLifestealPercent", "AbilityLifestealPercentHero", "HealthStealPctHero", "BulletLifestealPercent", "TotalHealthRegen",
        "DamageHealMult", "HealPctVsHeroes", "InfestHeal", "HealOnKillPct", "HealMaxHealth", "HealMaxHealthPercent",
        "RegenIncomingDamagePercent",
        -- raw abilities
        "Rake", "Card Trick",
    },
}


local extra_columns = {
-- Editor-authored notes go here.
    ["barrier"] = {
local human_notes = {
        headers  = { "Barrier" },
        get_cells = Renderers.RenderBarrier,
    },
     ["charges"] = {
     ["charges"] = {
         headers  = { "Charges", "Time Between Charges", "Notes" },
         ["Jar of Dead"]  = "Charges by being near when anything dies.",
        get_cells = Renderers.RenderCharges,
     },
     },
     ["melee"] = {
     ["melee"] = {
         headers   = { "Base Damage", "Light Melee Scaling", "Notes" },
         ["Puddle Punch"] = "Can be parried.",
         get_cells = Renderers.RenderMelee,
    },
    ["debuffresist"] = {
        ["Aura of Suffering"]   = "T1 slow, T3 extra damage. The effect is negligible.",
        ["Lightning Ball"]      = "T2 Slow. The effect is negligible.",
        ["Static Charge"]      = "Stun duration. Delay before stun is not affected.",
        ["Enchanter's Satchel"] = "T3 Slow.",
        ["Scorn"]              = "T3 extra damage from Mo & Krill.",
        ["Hotel Guest"]        = "Only T2 late check-out stun can be reduced.",
        ["Jar of Dead"]        = "T2 slow.",
        ["Grapple Arm"]        = "T1 extra damage from Bebop.",
        ["Binding Word"]        = "Immobilize Duration. Escape time is not affected.",
        ["Power Slash"]        = "T1 slow.",
        ["Kinetic Pulse"]      = "T2 bullet resist reduction and slow.",
        ["Pain Battery"]        = "T1 slow.",
        ["Power Surge"]        = "T3 Spirit Resist reduction.",
        ["Rain of Arrows"]      = "T2 slow.",
        ["Soul Exchange"]      = "T2 silence.",
        ["Ground Strike"]      = "T2 slow.",
        ["Flog"]                = "T2 Fire rate slow.",
    },
    ["moveslow"] = {
         ["Grapple Arm"]  = "Applies a short strong slow when the victim reaches Bebop.",
        ["Heavy Barrage"]  = "Slows McGinnis by unknown amount.",
     },
     },
     ["heavymelee"] = {
     ["reactivebarrier"] = {
         headers  = { "Base Damage", "Heavy Melee Scaling", "Notes" },
         ["Rend"]              = "Triggers on T3 upgrade only.",
         get_cells = Renderers.RenderHeavyMelee,
        ["Soul Exchange"]    = "Triggers on T2 upgrade only.",
        ["Air Drop"]          = "Triggers on T3 upgrade only.",
        ["Entangling Thorns"] = "Triggers on T3 upgrade only.",
        ["Ground Strike"]    = "Triggers only when slamming a mid-air enemy into the ground.",
        ["Djinn's Mark"]      = "Triggers on T3 upgrade only.",
         ["Time Wall"]        = "Triggers on T2 upgrade only.",
     },
     },
     ["scalingspirit"] = {
     ["heal"] = {
         headers   = { "Spirit Scaling", "Notes" },
    ["Siphon Life"]          = "Lifesteals off damage dealt while active.",
         get_cells = Renderers.RenderSpiritScaling,
        ["Infernal Resilience"]  = "Passively restores health for a percentage of all incoming damage.",
        ["Flawless Advance"]      = "Heals a flat amount per hit. Unlocked by T1 upgrade.",
        ["Exploding Uppercut"]    = "Restores a portion of missing health on use. Unlocked by T3 upgrade.",
        ["Leaping Slash"]        = "Heals a flat amount on hero hit. Unlocked by T1 upgrade.",
        ["Return to Shadows"]    = "Heals a flat amount on use. Unlocked by T3 upgrade.",
        ["Light Eater"]          = "Grants a buff that gives spirit lifesteal while active.",
        ["Bloodscent"]            = "Restores a portion of missing health on kill.",
        ["Rain of Arrows"]        = "Grants bullet and spirit lifesteal while active. Unlocked by T3 upgrade.",
        ["Napalm"]                = "Lifesteals off all damage dealt to napalmed targets. Unlocked by T2 upgrade.",
         ["Concussive Combustion"] = "Lifesteals off damage dealt. Unlocked by T2 upgrade.",
        ["Kudzu Connection"]      = "Grants bullet lifesteal. Also replicates a portion of healing.",
        ["Rejuvenating Aurora"]   = "Heals per second while channeling. T3 upgrade also adds passive max health regen.",
        ["Stone Form"]            = "Heals a percentage of max HP on cast.",
        ["Frost Grenade"]        = "Heals Kelvin and nearby allies for a flat amount.",
        ["Frozen Shelter"]        = "Regenerates health for all allies inside the dome.",
        ["Flog"]                  = "Heals Lash for a percentage of damage dealt to heroes.",
        ["Medicinal Specter"]    = "Heals nearby allies per second.",
        ["Rake"]                  = "Heals a flat amount on kill.",
        ["Scorn"]                = "Heals for a percentage of damage dealt.",
        ["Combo"]                = "Lifesteals off damage dealt. Unlocked by T1 upgrade.",
        ["Rallying Charge"]      = "Heal affected allies and Paige for a flat amount.",
        ["Tag Along"]            = "Restores a portion of target's missing health on attachment, then heals per second for a short duration.",
        ["Lil Helpers"]          = "Regenerates health for any trooper the helpers are attached to.",
        ["Lycan Curse"]          = "Restores a percentage of missing health on activation.",
        ["Go For The Throat"]    = "Lifesteals off melee damage dealt.",
        ["Pain Battery"]          = "Restores a portion of missing health on hero hit. Unlocked by T3 upgrade.",
        ["Jumpstart"]            = "Grants a flat health regen buff on use.",
        ["The Cube"]              = "Regenerates health for the target.",
        ["Last Stand"]            = "Lifesteals off all damage dealt while active.",
        ["Card Trick"]            = "Heals Wraith for a flat amount when a Heart or Joker card hits.",
        ["Crimson Slash"]        = "Heals a flat amount on hero hit.",
         ["Shadow Transformation"] = "Restores a percentage of max health per hero kill.",
     },
     },
     ["healreduce"] = {
}
         headers  = { "Heal Reduction", "Notes" },
 
         get_cells = Renderers.RenderHealReduce,
-- Type here to exclude ability from a list. If adding please include a comment.
local exclude_list = {
     ["debuffresist"] = {
         "Bookwyrm", -- has DebuffDuration for some reason
        "Itani Lo Sahn", -- has DebuffDuration but does not get reduced
        "Slam Fire", -- has DebuffDuration for some reason
         "Splatter", -- has SlowPercent but no duration to shave off.
     },
     },
     ["moveslow"] = {
     ["moveslow"] = {
         headers  = { "Move Slow", "Notes" },
         "Riposte", -- has a 40% 4s slow somehow? (SlowPercent + SlowDuration)
        get_cells = Renderers.RenderMoveSlow,
        "Grasping Hands", -- has SlowPercent
     },
     },
     ["dashslow"] = {
     ["dashslow"] = {
         headers  = { "Dash Slow", "Notes" },
         "Itani Lo Sahn", -- has GroundDashReductionPercent but also disables stamina.
         get_cells = Renderers.RenderDashSlow,
    },
    ["heal"] = {
         "Riposte", -- has AbilityLifestealPercentHero, but doesn't deal damage to heal off of.
     },
     },
     ["extradebuffresist"] = {
     ["dispelmagic"] = {
         headers  = { "Debuff Resist" },
         "Bookwyrm", -- has DebuffDuration for some reason
         get_cells = Renderers.RenderExtraDebuffResist,
         "Slam Fire", -- has DebuffDuration for some reason
     },
     },
}
}
-- Unit strings appended to cell values by column name.
local column_suffix = {
    ["Time Between Charges"] = "s",
}
-- Actual code below
local p = {}
local GameData = require("Module:GameData")


-- ============================================================
-- ============================================================
-- Note enrichment
-- Shared helpers
-- ============================================================
-- ============================================================


-- Merges auto-generated and human notes for full-table mode.
local function num_val(v)
-- Capitalizes human note, joins with ". ", ensures trailing period.
    if type(v) == "number" then return v end
local function enrich_notes_full(extra, human_note)
    if type(v) == "table"  then return v["Value"] end
     if human_note then
    return nil
        human_note = human_note:sub(1,1):upper() .. human_note:sub(2)
end
        extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human_note) or human_note
 
-- Searches Upgrades[] for a field and returns { tier = N, value = V } at its
-- first appearance, or nil if the field is absent from all upgrade tiers.
local function find_in_upgrades(ability, field)
    local upgrades = ability["Upgrades"]
     if type(upgrades) ~= "table" then return nil end
    for i, tier in ipairs(upgrades) do
        if type(tier) == "table" and tier[field] ~= nil then
            return { tier = i, value = num_val(tier[field]) }
        end
     end
     end
     if extra["Notes"] and extra["Notes"]:sub(-1) ~= "." then
     return nil
         extra["Notes"] = extra["Notes"] .. "."
end
 
-- Finds the first non-zero value for any of the given fields, checking base
-- then upgrades in order. Returns field, tier (nil=base, N=upgrade), abs value.
local function find_first_value(ability, fields)
    for _, field in ipairs(fields) do
        local v = num_val(ability[field])
        if v and v ~= 0 then return field, nil, math.abs(v) end
    end
    local upgrades = ability["Upgrades"]
    if type(upgrades) == "table" then
         for i, tier in ipairs(upgrades) do
            if type(tier) == "table" then
                for _, field in ipairs(fields) do
                    local v = num_val(tier[field])
                    if v and v ~= 0 then return field, i, math.abs(v) end
                end
            end
        end
     end
     end
    return nil, nil, nil
end
end


-- Merges auto-generated and human notes for compact mode.
-- ============================================================
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
-- Filtering
-- joins with "; ", strips trailing period.
-- ============================================================
local function enrich_notes_compact(extra, human_note)
 
     if human_note then
-- Filters a list of prop-matched abilities down to those that should appear
         local first = human_note:sub(1,1)
-- in the table for the given stat. Handles:
        if first:match("%u") and not human_note:sub(2,2):match("%d") then
--  1. key_lookup membership (ability belongs to an active released hero)
             human_note = first:lower() .. human_note:sub(2)
--   2. exclude_list (manual false-positive suppressions)
--   3. stat-specific logic (e.g. dispelmagic strips slot-4 ultimates)
local function filter_abilities(stat, abilities, key_lookup)
    local excluded = {}
     if exclude_list[stat] then
         for _, name in ipairs(exclude_list[stat]) do
             excluded[name] = true
         end
         end
        if human_note:sub(-1) == "." then human_note = human_note:sub(1, -2) end
        extra["Notes"] = extra["Notes"] and (extra["Notes"] .. "; " .. human_note) or human_note
     end
     end
    local result = {}
    for _, ability in ipairs(abilities) do
        local key  = ability["Key"]
        local info = key and key_lookup[key]
        if info and not excluded[ability["Name"]] then
            local skip = false
            -- dispelmagic: skip ultimates (slot 4)
            if stat == "dispelmagic" and info.abilityNum == 4 then
                skip = true
            end
            if not skip then
                table.insert(result, ability)
            end
        end
    end
    return result
end
end


-- ============================================================
-- ============================================================
-- Ability views
-- Per-stat cell renderers
--
-- Each function receives a guaranteed-matching ability record
-- In-game Ability may containe 1 or more records in AbilityData.json
-- and returns { ["Column Name"] = value }.
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
-- Human notes are appended by the framework, not here.
 
-- "Ability View" is a main-record + any relevant sub-ability records.
-- ============================================================
-- ============================================================


local _views = nil  -- card key -> { view = record, heroName = ..., abilityNum = ... }
-- Short display names for debuff properties.
local _order = nil  -- array of card keys, sorted by hero then slot
local debuff_short_name = {
    ["StunDuration"]              = "stun",
    ["SleepDuration"]              = "sleep",
    ["SilenceDuration"]            = "silence",
    ["SilenceDebuff"]              = "silence",
    ["PetrifyDuration"]            = "petrify",
    ["HexDuration"]                = "hex",
    ["ImmobilizeDuration"]        = "immobilize",
    ["LiftDuration"]              = "lift",
    ["HoldInPlaceDuration"]        = "immobilize",
    ["RestrictionDuration"]        = "immobilize",
    ["DebuffDuration"]            = "debuff",
    ["ExplodeDebuffDuration"]      = "debuff",
    ["SlowPercent"]                = "slow",
    ["EnemySlowPct"]              = "slow",
    ["MoveSlowPercent"]            = "slow",
    ["GroundDashReductionPercent"] = "dash slow",
    ["SlowDuration"]              = "slow",
    ["MaxSlowDuration"]            = "slow",
    ["BulletDamageAmpDuration"]    = "bullet amp",
    ["BulletResistReduction"]      = "bullet resist reduction",
    ["StealDuration"]              = "debuff",
    ["EffectDuration"]            = "debuff",
    ["DebuffAccuracy"]            = "debuff",
    ["TimeSlowDuration"]          = "time slow",
    ["LateCheckoutStun"]          = "stun",
    ["BurnDuration"]              = "burn",
    ["BleedDuration"]              = "bleed",
    ["VenomDuration"]              = "venom",
    ["TossDuration"]              = "Knockback",
}


local function build_views()
-- Returns true if prop is found with a non-zero value anywhere in record,
     if _views then return _views, _order end
-- excluding the Upgrades key. Mirrors GameData.record_matches_prop logic.
     _views, _order = {}, {}
local function prop_in_base(record, prop)
     for k, v in pairs(record) do
        if k == "Upgrades" then
            -- skip
        elseif k == prop then
            if type(v) == "table" then v = v["Value"] end
            if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
                return true
            end
        elseif type(v) == "table" then
            if prop_in_base(v, prop) then return true end
        elseif type(v) == "string" then
            if v == prop then return true end
        end
     end
    return false
end


    local hero_data = mw.loadJsonData("Data:HeroData.json")
-- Renderer shared by debuffresist and dispelmagic.
     local active_heroes = {}
-- Outputs a Notes cell describing which debuffs are added only by upgrades.
     for _, hero in pairs(hero_data) do
-- If any prop from the stat's list is found in the base ability, outputs nothing.
         if type(hero) == "table"
-- Otherwise lists per-tier debuff names: "T2 slow", "T1 disarm, T2 slow", etc.
            and hero["IsDisabled"] == false
local function debuff_cells(ability, stat)
            and type(hero["Name"]) == "string"
     local props = stat_to_props[stat]
            and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false)
    local cells = {}
        then
 
             active_heroes[hero["Name"]] = true
    -- Check if any prop is present in base (outside Upgrades).
    -- If so, this is a full-ability debuff — no note needed.
     for _, prop in ipairs(props) do
         if prop_in_base(ability, prop) then
             return cells
         end
         end
     end
     end


     local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
     -- Scan upgrades: collect props found per tier.
     local cards        = mw.loadJsonData("Data:AbilityCards.json")
     local upgrades = ability["Upgrades"]
    if type(upgrades) ~= "table" then return cells end


     local card_of = {}
    -- tier_debuffs[i] = list of short names found in tier i
     for _, hero_entry in pairs(cards) do
     local tier_debuffs = {}
         if type(hero_entry) == "table" then
    local seen_names  = {}  -- deduplicate short names across tiers
             local hero_name = hero_entry["Name"]
 
             if hero_name and active_heroes[hero_name] then
     for i, tier in ipairs(upgrades) do
                for slot_str, ability in pairs(hero_entry) do
         if type(tier) == "table" then
                    local slot_num = tonumber(slot_str)
             local names_this_tier = {}
                     local key = type(ability) == "table" and ability["Key"] or nil
             local seen_this_tier  = {}
                     if slot_num and key and ability_data[key] then
            for _, prop in ipairs(props) do
                        card_of[key] = { heroName = hero_name, abilityNum = slot_num }
                local v = tier[prop]
                if v ~= nil then
                     if type(v) == "table" then v = v["Value"] end
                     if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
                        local short = debuff_short_name[prop] or "debuff"
                        if not seen_this_tier[short] then
                            seen_this_tier[short] = true
                            if not seen_names[short] then
                                seen_names[short] = true
                                table.insert(names_this_tier, short)
                            end
                        end
                     end
                     end
                 end
                 end
             end
             end
            if #names_this_tier > 0 then
                tier_debuffs[i] = names_this_tier
            end
        end
    end
    -- Build note: "T1 slow, T2 silence + burn"
    local parts = {}
    for i = 1, #upgrades do
        if tier_debuffs[i] then
            table.insert(parts, "T" .. i .. " " .. table.concat(tier_debuffs[i], " + "))
        end
    end
    if #parts > 0 then
        cells["Notes"] = table.concat(parts, ", ")
    end
    return cells
end
local function charges_cells(ability)
    local base_charges  = num_val(ability["AbilityCharges"])
    local base_cooldown = ability["AbilityCooldownBetweenCharge"]
    local has_base      = base_charges ~= nil and base_charges > 0
    local upgrade_info  = find_in_upgrades(ability, "AbilityCharges")
    local cooldown, upgrade_cooldown
    if has_base then
        if type(base_cooldown) == "number" and base_cooldown > 0 then
            cooldown = base_cooldown
        else
            local cd = find_in_upgrades(ability, "AbilityCooldownBetweenCharge")
            if cd and cd.value and cd.value > 0 then cooldown = cd.value end
         end
         end
    else
        local cd = find_in_upgrades(ability, "AbilityCooldownBetweenCharge")
        if cd and cd.value and cd.value > 0 then upgrade_cooldown = cd.value end
     end
     end


     -- Attach each non-card record to the longest card key it prefixes.
     local cells = {}
     local subs = {}
 
     for key, record in pairs(ability_data) do
    if has_base then
         if not card_of[key] and record["IsDisabled"] ~= true then
        cells["Charges"] = tostring(base_charges)
            local parent = nil
        if cooldown then
             for card_key in pairs(card_of) do
            cells["Time Between Charges"] = tostring(cooldown)
                 if key:sub(1, #card_key + 1) == card_key .. "_"
        end
                     and (parent == nil or #card_key > #parent)
    end
                then
 
                     parent = card_key
     local note_parts = {}
    if upgrade_info then
        local n = upgrade_info.value or "?"
        local charge_str = n .. " charge" .. (n ~= 1 and "s" or "")
        if has_base then
            table.insert(note_parts, "+" .. charge_str .. " on T" .. upgrade_info.tier .. " upgrade.")
        else
            local cd_str = upgrade_cooldown and (upgrade_cooldown .. "s") or "unknown"
            table.insert(note_parts, "Becomes charged on T" .. upgrade_info.tier
                .. " upgrade with " .. charge_str
                .. " and " .. cd_str .. " time between charges.")
        end
    end
    if ability["AbilityChargesConditionally"] ~= nil then
        table.insert(note_parts, "Has a conditional charge.")
    end
    if #note_parts > 0 then
        cells["Notes"] = table.concat(note_parts, " ")
    end
 
    return cells
end
 
local function find_base_melee_scale(ability)
     for _, v in pairs(ability) do
         if type(v) == "table" then
            local scale = v["Scale"]
            if type(scale) == "table"
                and scale["Type"] == "melee"
                and type(scale["Value"]) == "number"
                and scale["Value"] > 0
            then
                return scale["Value"]
            end
        end
    end
    return nil
end
 
local function build_melee_upgrade_notes(ability, base_melee_scale)
    local upgrades = ability["Upgrades"]
    if type(upgrades) ~= "table" then return {} end
    local notes = {}
    for i, tier in ipairs(upgrades) do
        if type(tier) == "table" then
             for _, v in pairs(tier) do
                 if type(v) == "table" then
                    local scale = v["Scale"]
                     if type(scale) == "table" and type(scale["Value"]) == "number" then
                        local stype = scale["Type"]
                        local delta = scale["Value"]
                        if stype == "heavy_melee" and delta > 0 then
                            table.insert(notes, "T" .. i .. " upgrade changes scaling to ×"
                                .. delta .. " of Heavy Melee damage.")
                        elseif stype == "melee" and delta ~= 0 and base_melee_scale then
                            local final = math.floor((base_melee_scale + delta) * 1000 + 0.5) / 1000
                            table.insert(notes, "T" .. i .. " upgrade adds ×"
                                .. delta .. " scaling (total ×" .. final .. ").")
                        end
                     end
                 end
                 end
             end
             end
            if parent then
        end
                subs[parent] = subs[parent] or {}
    end
                 table.insert(subs[parent], record)
    return notes
end
 
local function melee_cells(ability)
    local cells = {}
    local base_scale = find_base_melee_scale(ability)
    if base_scale then cells["Light Melee Scaling"] = "×" .. base_scale end
    local upgrade_notes = build_melee_upgrade_notes(ability, base_scale)
    if #upgrade_notes > 0 then cells["Notes"] = table.concat(upgrade_notes, " ") end
    return cells
end
 
local function healreduce_cells(ability)
    local cells = {}
    local notes = {}
    local props = stat_to_props["healreduce"]
 
    local first_pct, first_tier, first_is_disable = nil, nil, false
 
    for _, prop in ipairs(props) do
        local v = num_val(ability[prop])
        if v and v ~= 0 then
            if prop == "DisableHealing" then
                 first_is_disable = true
            else
                first_pct = math.abs(v)
            end
            break
        end
    end
    if not first_pct and not first_is_disable then
        local upgrades = ability["Upgrades"]
        if type(upgrades) == "table" then
            for i, tier in ipairs(upgrades) do
                if type(tier) == "table" then
                    for _, prop in ipairs(props) do
                        local v = num_val(tier[prop])
                        if v and v ~= 0 then
                            if prop == "DisableHealing" then
                                first_is_disable, first_tier = true, i
                            else
                                first_pct, first_tier = math.abs(v), i
                            end
                            break
                        end
                    end
                end
                if first_pct or first_is_disable then break end
             end
             end
         end
         end
     end
     end


     for key, info in pairs(card_of) do
     if first_is_disable then
         local view = {}
         cells["Heal Reduction"] = "100%" .. (first_tier and " (T" .. first_tier .. ")" or "")
        for k, v in pairs(ability_data[key]) do view[k] = v end
    elseif first_pct then
         view["_subabilities"] = subs[key]
         cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "")
        _views[key] = { view = view, heroName = info.heroName, abilityNum = info.abilityNum }
        table.insert(_order, key)
     end
     end


     table.sort(_order, function(a, b)
     local upgrades = ability["Upgrades"]
        local ia, ib = _views[a], _views[b]
    if type(upgrades) == "table" then
        if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end
        local running        = first_pct
         return ia.abilityNum < ib.abilityNum
        local first_pct_seen = (first_tier == nil)
     end)
 
        for i, tier in ipairs(upgrades) do
            if type(tier) == "table" then
                if first_tier == i then
                    first_pct_seen = true
                    if first_pct then running = first_pct end
                elseif first_pct_seen then
                    local delta = nil
                    for _, prop in ipairs(props) do
                        if prop ~= "DisableHealing" then
                            local v = num_val(tier[prop])
                            if v and v ~= 0 then delta = math.abs(v); break end
                        end
                    end
                    if delta then
                        local total = (running or 0) + delta
                        table.insert(notes, "T" .. i .. " upgrade increases heal reduction by "
                            .. delta .. "% (total " .. total .. "%).")
                        running = total
                    end
                end
            end
         end
     end


     return _views, _order
    if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
     return cells
end
end


-- ============================================================
-- Generic renderer for a numeric stat column.
-- Membership
-- col_name: the column header string
-- Pieces of code that help in telling  "is ability X a member of list Y".
-- fields:  ordered list of property names (first non-zero wins)
-- main function is: p.is_member()
-- suffix:  string appended to values in cell and notes (e.g. "%" or "")
-- ============================================================
-- Upgrade deltas are additive; notes track running totals.
local function val_stat_cells(ability, col_name, fields, suffix)
    suffix = suffix or ""
    local cells = {}
    local notes = {}
 
    local first_field, first_tier, first_val = find_first_value(ability, fields)
    if not first_field then return cells end
 
    local cell_str = tostring(first_val) .. suffix
    if first_tier then cell_str = cell_str .. " (T" .. first_tier .. ")" end
    cells[col_name] = cell_str
 
    local upgrades = ability["Upgrades"]
    if type(upgrades) == "table" then
        local running    = first_val
        local seen_first = (first_tier == nil)


-- Memoized name-exclusion set per list (from Lists.exclude_abilities).
        for i, tier in ipairs(upgrades) do
local _excluded = {}
            if type(tier) == "table" then
local function excluded_set(stat)
                if first_tier == i then
    if _excluded[stat] then return _excluded[stat] end
                    seen_first = true
    local set = {}
                elseif seen_first then
    for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do
                    local delta = nil
        set[name] = true
                    for _, field in ipairs(fields) do
                        local v = num_val(tier[field])
                        if v and v ~= 0 then delta = math.abs(v); break end
                    end
                    if delta then
                        local total = running + delta
                        table.insert(notes, "T" .. i .. " upgrade increases "
                            .. col_name:lower() .. " by " .. delta .. suffix
                            .. " (total " .. total .. suffix .. ").")
                        running = total
                    end
                end
            end
        end
     end
     end
     _excluded[stat] = set
 
     return set
     if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
     return cells
end
end


-- Per-list rules that can't be expressed as prop lists or exclusions.
local function moveslow_cells(ability)
-- Returns true to keep the ability, false to drop it.
    local cells = val_stat_cells(ability, "Move Slow", stat_to_props["moveslow"], "%")
local function special_ok(stat, record, ability_num)
 
     if stat == "dispelmagic" and ability_num == 4 then return false end
    local channel_slow = num_val(ability["ChannelSlowPercent"])
     return true
     if channel_slow and channel_slow ~= 0 then
        local note = "Also slows the caster for " .. math.abs(channel_slow) .. "%."
        cells["Notes"] = cells["Notes"] and (cells["Notes"] .. " " .. note) or note
    end
 
     return cells
end
end


-- Public: does ability `ability_key` belong to list `stat`?
local function dashslow_cells(ability)
function p.is_member(stat, ability_key, ability_num)
     local props = stat_to_props["dashslow"]
     local props = Lists.lists[stat]
    if not props then return false end


     local views = build_views()
     local caster_dash_slow = nil
     local entry = views[ability_key]
    local base_ground = num_val(ability["GroundDashReductionPercent"])
    local view  = entry and entry.view
     if base_ground and base_ground ~= 0 then
        local has_enemy_prop = false
        for _, p in ipairs(stat_to_props["dashslow"]) do
            if p ~= "GroundDashReductionPercent" then
                local v = num_val(ability[p])
                if v and v ~= 0 then has_enemy_prop = true; break end
            end
        end
        if not has_enemy_prop then
            local upgrades = ability["Upgrades"]
            if type(upgrades) == "table" then
                for _, tier in ipairs(upgrades) do
                    if type(tier) == "table" then
                        for _, p in ipairs(stat_to_props["dashslow"]) do
                            if p ~= "GroundDashReductionPercent" then
                                local v = num_val(tier[p])
                                if v and v ~= 0 then has_enemy_prop = true; break end
                            end
                        end
                    end
                    if has_enemy_prop then break end
                end
            end
        end
        if has_enemy_prop then
            caster_dash_slow = math.abs(base_ground)
            local filtered = {}
            for _, p in ipairs(props) do
                if p ~= "GroundDashReductionPercent" then
                    table.insert(filtered, p)
                end
            end
            props = filtered
        end
    end
 
    local cells = val_stat_cells(ability, "Dash Slow", props, "%")


     if not view then
     if caster_dash_slow then
         view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]
         local note = "Gives " .. caster_dash_slow .. "% dash slow for the caster."
        if not view then return false end
        cells["Notes"] = cells["Notes"] and (cells["Notes"] .. " " .. note) or note
     end
     end


     local name = view["Name"]
     return cells
    if name and excluded_set(stat)[name] then return false end
end


     ability_num = ability_num or (entry and entry.abilityNum)
local extra_columns = {
     if not special_ok(stat, view, ability_num) then return false end
    ["charges"] = {
        headers  = { "Charges", "Time Between Charges", "Notes" },
        get_cells = charges_cells,
    },
    ["melee"] = {
        headers  = { "Light Melee Scaling", "Notes" },
        get_cells = melee_cells,
    },
    ["healreduce"] = {
        headers  = { "Heal Reduction", "Notes" },
        get_cells = healreduce_cells,
    },
     ["debuffresist"] = {
        headers  = { "Notes" },
        get_cells = function(ability) return debuff_cells(ability, "debuffresist") end,
     },
    ["dispelmagic"] = {
        headers  = { "Notes" },
        get_cells = function(ability) return debuff_cells(ability, "dispelmagic") end,
    },
    ["moveslow"] = {
        headers  = { "Move Slow", "Notes" },
        get_cells = moveslow_cells,
    },
    ["dashslow"] = {
        headers  = { "Dash Slow", "Notes" },
        get_cells = dashslow_cells,
    },
}


     return GameData.entity_matches(view, props)
-- ============================================================
-- Hero lookup
--  _key_lookup  : ability key  -> { heroName, abilityNum }
--  _name_to_key : ability name -> ability key
-- Both built together from HeroData + AbilityCards.
-- ============================================================
 
local _key_lookup  = nil
local _name_to_key = nil
 
local function build_lookup()
     if _key_lookup then return _key_lookup, _name_to_key end
    _key_lookup  = {}
    _name_to_key = {}
 
    local hero_data = mw.loadJsonData("Data:HeroData.json")
    local active_heroes = {}
    for _, hero in pairs(hero_data) do
        if type(hero) == "table"
            and hero["IsDisabled"] == false
            and type(hero["Name"]) == "string"
            and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false)
        then
            active_heroes[hero["Name"]] = true
        end
    end
 
    local cards = mw.loadJsonData("Data:AbilityCards.json")
    for _, hero_entry in pairs(cards) do
        if type(hero_entry) == "table" then
            local hero_name = hero_entry["Name"]
            if hero_name and active_heroes[hero_name] then
                for slot_str, ability in pairs(hero_entry) do
                    local slot_num = tonumber(slot_str)
                    if slot_num and type(ability) == "table" then
                        local info = { heroName = hero_name, abilityNum = slot_num }
                        if ability["Key"] then
                            _key_lookup[ability["Key"]] = info
                        end
                        if ability["Name"] and ability["Key"] then
                            _name_to_key[ability["Name"]] = ability["Key"]
                        end
                    end
                end
            end
        end
    end
 
    return _key_lookup, _name_to_key
end
end


Line 215: Line 748:
     local stat = mw.text.trim(frame.args[1] or ""):lower()
     local stat = mw.text.trim(frame.args[1] or ""):lower()


     local internal_keys = Lists.lists[stat]
     local internal_keys = stat_to_props[stat]
     if not internal_keys then
     if not internal_keys then
         return '<span class="error">AbilityTable: unknown list "' .. stat .. '".</span>'
         return '<span class="error">AbilityTable: unknown stat "' .. stat .. '".</span>'
     end
     end


Line 228: Line 761:
     local compact = mw.text.trim(frame.args["compact"] or "") ~= ""
     local compact = mw.text.trim(frame.args["compact"] or "") ~= ""


     local collapsed = mw.text.trim(frame.args["collapsed"] or ""):lower()
     local key_lookup, _ = build_lookup()
 
    local views, order = build_views()
     local spec      = extra_columns[stat]
     local spec      = extra_columns[stat]
     local headers    = spec and spec.headers or {}
     local headers    = spec and spec.headers or {}
     local get_cells  = spec and spec.get_cells or function() return {} end
     local get_cells  = spec and spec.get_cells or function() return {} end
     local stat_notes = Notes[stat]
     local stat_notes = human_notes[stat]


     local filtered = {}
     local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)
    for _, key in ipairs(order) do
    local filtered  = filter_abilities(stat, abilities, key_lookup)
        if p.is_member(stat, key) then
            table.insert(filtered, key)
        end
    end


     if #filtered == 0 then
     if #filtered == 0 then
         return "No abilities found for list: " .. stat
         return "No abilities found for stat: " .. stat
     end
     end


     local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
     table.sort(filtered, function(a, b)
     local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" })
        local ia = key_lookup[a["Key"]]
        local ib = key_lookup[b["Key"]]
        if not ia and not ib then return (a["Name"] or "") < (b["Name"] or "") end
        if not ia then return false end
        if not ib then return true end
        if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end
        return ia.abilityNum < ib.abilityNum
    end)
 
     local edit_url = mw.uri.fullUrl("Module:AbilityTable", { action = "edit" })
     local disclaimer = frame:preprocess(
     local disclaimer = frame:preprocess(
         '<div class="ability-table-disclaimer">'
         '<div class="ability-table-disclaimer">'
         .. 'This is a dynamic list '
         .. "This is an automatic list. "
        .. '<span class="ability-table-list-name">"' .. stat .. '"</span>. '
         .. "[" .. tostring(edit_url) .. " Click here]"
         .. '[' .. tostring(lists_url) .. ' Edit contents]'
         .. " to add notes or correct the contents."
         .. ' · '
         .. "</div>"
        .. '[' .. tostring(notes_url) .. ' Edit notes].'
         .. '</div>'
     )
     )


Line 262: Line 796:
         local hero_order    = {}
         local hero_order    = {}
         local hero_abilities = {}
         local hero_abilities = {}
         for _, key in ipairs(filtered) do
         for _, ability in ipairs(filtered) do
             local hero = views[key].heroName
             local info = key_lookup[ability["Key"]]
            local hero = info and info.heroName or "—"
             if not hero_abilities[hero] then
             if not hero_abilities[hero] then
                 table.insert(hero_order, hero)
                 table.insert(hero_order, hero)
                 hero_abilities[hero] = {}
                 hero_abilities[hero] = {}
             end
             end
             table.insert(hero_abilities[hero], views[key].view)
             table.insert(hero_abilities[hero], ability["Name"])
         end
         end


Line 275: Line 810:
             local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
             local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
             local ability_parts = {}
             local ability_parts = {}
             for _, ability in ipairs(hero_abilities[hero]) do
             for _, name in ipairs(hero_abilities[hero]) do
                local name = ability["Name"]
                 local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } }
                 local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } }
 
                 local note = stat_notes and stat_notes[name]
                 local extra = get_cells(ability)
                enrich_notes_compact(extra, stat_notes and stat_notes[name])
 
                local note = extra["Notes"]
                 if note then
                 if note then
                     table.insert(ability_parts, ability_cell .. " (" .. note .. ")")
                     table.insert(ability_parts, ability_cell .. " (" .. note .. ")")
Line 306: Line 836:
     local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set["Notes"]
     local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set["Notes"]
     if needs_notes then table.insert(final_headers, "Notes") end
     if needs_notes then table.insert(final_headers, "Notes") end
    local table_class = 'wikitable sortable ability-table'
    if collapsed ~= "" then
        table_class = table_class .. ' mw-collapsible'
        if collapsed == "yes" or collapsed == "true" or collapsed == "1" then
            table_class = table_class .. ' mw-collapsed'
        end
    end


     local out = {}
     local out = {}
     table.insert(out, disclaimer)
     table.insert(out, disclaimer)
     table.insert(out, '{| class="' .. table_class .. '"')
     table.insert(out, '{| class="wikitable sortable ability-table"')


     local header_row = '! class="ability-table-col-hero" | Hero'
     local header_row = '! class="ability-table-col-hero" | Hero'
Line 327: Line 849:
     table.insert(out, header_row)
     table.insert(out, header_row)


        local hero_counts = {}
     for _, ability in ipairs(filtered) do
     for _, key in ipairs(filtered) do
         local info = key_lookup[ability["Key"]]
         local hero = views[key].heroName
        hero_counts[hero] = (hero_counts[hero] or 0) + 1
    end


    local hero_seen = {}
        local hero_cell
 
        if info then
    for _, key in ipairs(filtered) do
            hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { info.heroName } }
         local entry  = views[key]
         else
        local ability = entry.view
            hero_cell = "—"
         local hero    = entry.heroName
         end
 
         local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { ability["Name"] } }
         local ability_cell = frame:expandTemplate{
            title = "AbilityIcon",
            args = { ability["Name"] }
        }


         local extra = get_cells(ability)
         local extra = get_cells(ability)
        enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]])


        local row
         if stat_notes and stat_notes[ability["Name"]] then
 
             local human = stat_notes[ability["Name"]]
         if not hero_seen[hero] then
             extra["Notes"] = extra["Notes"] and (extra["Notes"] .. " " .. human) or human
             local hero_cell = frame:expandTemplate{
                title = "HeroIcon",
                args = { hero }
             }
 
            if hero_counts[hero] > 1 then
                row = '| rowspan="' .. hero_counts[hero] .. '" class="ability-table-cell-hero" | '
                    .. hero_cell
                    .. ' || class="ability-table-cell-ability" | ' .. ability_cell
            else
                row = '| class="ability-table-cell-hero" | '
                    .. hero_cell
                    .. ' || class="ability-table-cell-ability" | ' .. ability_cell
            end
 
            hero_seen[hero] = true
        else
            row = '| class="ability-table-cell-ability" | ' .. ability_cell
         end
         end


        local row = '| class="ability-table-cell-hero" | '    .. hero_cell
                .. ' || class="ability-table-cell-ability" | ' .. ability_cell
         for _, header in ipairs(final_headers) do
         for _, header in ipairs(final_headers) do
             local value = extra[header]
             local value = extra[header]
             local cls  = "ability-table-cell-" .. header:lower():gsub("%s+", "-")
             local cls  = "ability-table-cell-" .. header:lower():gsub("%s+", "-")
             local content = value ~= nil and tostring(value) or "—"
             local content
            if value ~= nil then
                local suffix = column_suffix[header] or ""
                content = (type(value) == "string" and value:sub(1,1) == "(")
                    and value or (value .. suffix)
            else
                content = "—"
            end
             row = row .. ' || class="' .. cls .. '" | ' .. content
             row = row .. ' || class="' .. cls .. '" | ' .. content
         end
         end
Please note that all contributions to The Deadlock Wiki are considered to be released under the Creative Commons Attribution-NonCommercial-ShareAlike (see Deadlock:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!
Cancel Editing help (opens in new window)
Preview page with this template

Page included on this page: