Editing Module:AbilityTable

Jump to navigation Jump to search
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 lowercase friendly stat names to internal AbilityData 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"]    = { "melee", "heavy_melee", "weapon_damage_increase" },
local p = {}
    ["healreduce"] = { "HealAmpReceivePenaltyPercent", "HealAmpRegenPenaltyPercent", "DisableHealing" },
    ["debuff"]    = {
        "StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff",
        "PetrifyDuration", "HexDuration", "ImmobilizeDuration", "LiftDuration",
        "SlowPercent", "DebuffDuration", "HoldInPlaceDuration",
    },
}


local extra_columns = {
-- Editor-authored notes appended to the Notes column.
    ["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,
     },
     },
    ["heavymelee"] = {
}
        headers  = { "Base Damage", "Heavy Melee Scaling", "Notes" },
 
        get_cells = Renderers.RenderHeavyMelee,
-- Manually specified entries for abilities that have no matching props,
    },
-- or whose effects cannot be inferred from data alone.
    ["scalingspirit"] = {
-- Each entry: { effects = { "EffectName", ... }, duration = <number or nil> }
        headers  = { "Spirit Scaling", "Notes" },
-- effects list may include "(TN)" suffix for upgrade-only effects: "Stun (T3)"
        get_cells = Renderers.RenderSpiritScaling,
-- duration nil renders as "".
    },
local manual_entries = {
     ["healreduce"] = {
     ["debuff"] = {
         headers  = { "Heal Reduction", "Notes" },
         ["Combo"] = {
        get_cells = Renderers.RenderHealReduce,
            effects  = { "Stun" },
    },
            duration = 2.4,
    ["moveslow"] = {
        },
        headers  = { "Move Slow", "Notes" },
        ["Sand Blast"] = {
        get_cells = Renderers.RenderMoveSlow,
            effects  = { "Disarm" },
    },
            duration = 2.5,
    ["dashslow"] = {
        },
        headers  = { "Dash Slow", "Notes" },
        ["Spirit Lasso"] = {
        get_cells = Renderers.RenderDashSlow,
            effects  = { "Stun" },
    },
            duration = 2.25,
    ["extradebuffresist"] = {
        },
        headers  = { "Debuff Resist" },
        get_cells = Renderers.RenderExtraDebuffResist,
     },
     },
}
}
-- Unit strings appended to cell values by column name.
-- Values starting with "(" are treated as already-complete strings and
-- the suffix is NOT appended (e.g. "(See notes)" stays as-is).
local column_suffix = {
    ["Time Between Charges"] = "s",
    ["Duration"]            = "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
 
-- ============================================================
-- Per-stat cell renderers
-- Each function receives a guaranteed-matching ability record
-- and returns { ["Column Name"] = value }.
-- Human notes are appended by the framework, not here.
-- ============================================================
 
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
    else
        local cd = find_in_upgrades(ability, "AbilityCooldownBetweenCharge")
        if cd and cd.value and cd.value > 0 then upgrade_cooldown = cd.value end
    end
 
    local cells = {}
 
    if has_base then
         cells["Charges"] = tostring(base_charges)
        if cooldown then
            cells["Time Between Charges"] = tostring(cooldown)
        end
    end
 
    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
     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
end


-- Merges auto-generated and human notes for compact mode.
-- Finds the base light melee scale value: the first top-level property
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
-- whose Scale.Type is "melee" and whose scale value is > 0.
-- joins with "; ", strips trailing period.
local function find_base_melee_scale(ability)
local function enrich_notes_compact(extra, human_note)
    for _, v in pairs(ability) do
    if human_note then
        if type(v) == "table" then
        local first = human_note:sub(1,1)
            local scale = v["Scale"]
        if first:match("%u") and not human_note:sub(2,2):match("%d") then
            if type(scale) == "table"
             human_note = first:lower() .. human_note:sub(2)
                and scale["Type"] == "melee"
                and type(scale["Value"]) == "number"
                and scale["Value"] > 0
            then
                return scale["Value"]
             end
         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
    return nil
end
end


-- ============================================================
-- Scans all upgrade tiers for melee-scale changes. Upgrade objects are
-- Ability views
-- additive in the engine, so the final scale = base + upgrade delta.
--
-- Returns a list of note strings, one per relevant upgrade tier found.
-- In-game Ability may containe 1 or more records in AbilityData.json
local function build_melee_upgrade_notes(ability, base_melee_scale)
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
    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 = base_melee_scale + delta
                            final = math.floor(final * 1000 + 0.5) / 1000
                            table.insert(notes, "T" .. i .. " upgrade adds ×"
                                .. delta .. " scaling (total ×" .. final .. ").")
                        end
                    end
                end
            end
        end
    end
 
    return notes
end
 
local function melee_cells(ability)
    local cells = {}


-- "Ability View" is a main-record + any relevant sub-ability records.
    local base_scale = find_base_melee_scale(ability)
-- ============================================================
    if base_scale then
        cells["Light Melee Scaling"] = "×" .. base_scale
    end


local _views = nil  -- card key -> { view = record, heroName = ..., abilityNum = ... }
    local upgrade_notes = build_melee_upgrade_notes(ability, base_scale)
local _order = nil  -- array of card keys, sorted by hero then slot
    if #upgrade_notes > 0 then
        cells["Notes"] = table.concat(upgrade_notes, " ")
    end


local function build_views()
     return cells
     if _views then return _views, _order end
end
    _views, _order = {}, {}


    local hero_data = mw.loadJsonData("Data:HeroData.json")
-- Returns the base duration value using priority: VenomDuration > DebuffDuration > AbilityDuration > BurnDuration.
    local active_heroes = {}
-- Also returns the field name that was found (for upgrade scanning).
     for _, hero in pairs(hero_data) do
local function find_base_duration(ability)
        if type(hero) == "table"
     for _, field in ipairs({ "VenomDuration", "DebuffDuration", "AbilityDuration", "BurnDuration" }) do
            and hero["IsDisabled"] == false
        local v = num_val(ability[field])
            and type(hero["Name"]) == "string"
         if v and v > 0 then
            and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false)
             return v, field
         then
             active_heroes[hero["Name"]] = true
         end
         end
     end
     end
    return nil, nil
end
local function healreduce_cells(ability)
    local cells = {}
    local notes = {}
    local base_dur, dur_field = find_base_duration(ability)


     local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
     local first_pct        = nil
     local cards        = mw.loadJsonData("Data:AbilityCards.json")
    local first_tier      = nil
     local first_is_disable = false


     local card_of = {}
     local base_receive = num_val(ability["HealAmpReceivePenaltyPercent"])
     for _, hero_entry in pairs(cards) do
     local base_regen  = num_val(ability["HealAmpRegenPenaltyPercent"])
         if type(hero_entry) == "table" then
    local base_pct_raw = base_receive or base_regen
            local hero_name = hero_entry["Name"]
    if base_pct_raw and base_pct_raw ~= 0 then
            if hero_name and active_heroes[hero_name] then
         first_pct  = math.abs(base_pct_raw)
                for slot_str, ability in pairs(hero_entry) do
        first_tier = nil
                     local slot_num = tonumber(slot_str)
    elseif ability["DisableHealing"] and ability["DisableHealing"] ~= 0 then
                     local key = type(ability) == "table" and ability["Key"] or nil
        first_is_disable = true
                     if slot_num and key and ability_data[key] then
        first_tier = nil
                         card_of[key] = { heroName = hero_name, abilityNum = slot_num }
    else
        local upgrades = ability["Upgrades"]
        if type(upgrades) == "table" then
            for i, tier in ipairs(upgrades) do
                if type(tier) == "table" then
                     local up_receive = num_val(tier["HealAmpReceivePenaltyPercent"])
                     local up_regen  = num_val(tier["HealAmpRegenPenaltyPercent"])
                    local up_raw    = up_receive or up_regen
                     if up_raw and up_raw ~= 0 then
                        first_pct  = math.abs(up_raw)
                        first_tier = i
                        break
                    elseif tier["DisableHealing"] and tier["DisableHealing"] ~= 0 then
                         first_is_disable = true
                        first_tier = i
                        break
                     end
                     end
                 end
                 end
Line 124: Line 263:
     end
     end


     -- Attach each non-card record to the longest card key it prefixes.
     if first_is_disable then
     local subs = {}
        local val = "100%"
    for key, record in pairs(ability_data) do
        if first_tier then val = val .. " (T" .. first_tier .. ")" end
         if not card_of[key] and record["IsDisabled"] ~= true then
        cells["Heal Reduction"] = val
            local parent = nil
     elseif first_pct then
            for card_key in pairs(card_of) do
        local val = tostring(first_pct) .. "%"
                 if key:sub(1, #card_key + 1) == card_key .. "_"
        if first_tier then val = val .. " (T" .. first_tier .. ")" end
                     and (parent == nil or #card_key > #parent)
         cells["Heal Reduction"] = val
                 then
    end
                     parent = card_key
 
    if base_dur then
        cells["Duration"] = tostring(base_dur)
    end
 
    local upgrades = ability["Upgrades"]
    if type(upgrades) == "table" then
        local running_pct    = first_pct
        local running_dur    = base_dur
        local first_pct_seen = (first_tier == nil)
 
        for i, tier in ipairs(upgrades) do
            if type(tier) == "table" then
                local tier_notes = {}
 
                local up_receive    = num_val(tier["HealAmpReceivePenaltyPercent"])
                local up_regen      = num_val(tier["HealAmpRegenPenaltyPercent"])
                local pct_delta    = up_receive or up_regen
                local is_this_first = (first_tier == i)
 
                 if is_this_first then
                    first_pct_seen = true
                    if first_pct then running_pct = first_pct end
                elseif first_pct_seen and pct_delta and pct_delta ~= 0 then
                    local delta_abs = math.abs(pct_delta)
                    local total    = (running_pct or 0) + delta_abs
                    table.insert(tier_notes, "T" .. i .. " upgrade increases heal reduction by "
                        .. delta_abs .. "% (total " .. total .. "%).")
                    running_pct = total
                end
 
                if dur_field then
                     local dur_delta = num_val(tier[dur_field])
                    if dur_delta and dur_delta ~= 0 and running_dur then
                        local total_dur = running_dur + dur_delta
                        table.insert(tier_notes,
                            "T" .. i .. " upgrade "
                            .. (dur_delta > 0 and "increases" or "decreases")
                            .. " duration by " .. math.abs(dur_delta)
                            .. "s (total " .. total_dur .. "s).")
                        running_dur = total_dur
                    end
                end
 
                 for _, n in ipairs(tier_notes) do
                     table.insert(notes, n)
                 end
                 end
             end
             end
            if parent then
        end
                subs[parent] = subs[parent] or {}
    end
                table.insert(subs[parent], record)
 
    if #notes > 0 then
        cells["Notes"] = table.concat(notes, " ")
    end
 
    return cells
end
 
-- Maps AbilityData property names to human-readable effect names for the debuff table.
-- Properties listed here are the canonical CC props; DebuffDuration is intentionally
-- absent because it renders as "(See notes)" rather than a named effect.
local debuff_prop_to_effect = {
    StunDuration        = "Stun",
    SleepDuration      = "Sleep",
    SilenceDuration    = "Silence",
    SilenceDebuff      = "Silence",
    PetrifyDuration    = "Petrify",
    HexDuration        = "Hex",
    ImmobilizeDuration  = "Root",
    LiftDuration        = "Lift",
    SlowPercent        = "Slow",
    HoldInPlaceDuration = "Stop",
}
 
-- Duration fields corresponding to each CC prop (for reading the numeric value).
-- SlowPercent and SilenceDebuff/HoldInPlaceDuration have separate duration fields.
local debuff_prop_duration_field = {
    StunDuration        = "StunDuration",
    SleepDuration      = "SleepDuration",
    SilenceDuration    = "SilenceDuration",
    SilenceDebuff      = "SilenceDuration",  -- boolean flag; duration in SilenceDuration
    PetrifyDuration    = "PetrifyDuration",
    HexDuration        = "HexDuration",
    ImmobilizeDuration  = "ImmobilizeDuration",
    LiftDuration        = "LiftDuration",
    SlowPercent        = "SlowDuration",
    HoldInPlaceDuration = "HoldInPlaceDuration",
}
 
local function debuff_cells(ability, stat_manual)
    local cells = {}
 
    -- If a manual entry exists for this ability, use it directly.
    local manual = stat_manual and stat_manual[ability["Name"]]
    if manual then
        if manual.effects and #manual.effects > 0 then
            cells["Effects"] = table.concat(manual.effects, ", ")
        end
        if manual.duration then
            cells["Duration"] = tostring(manual.duration)
        end
        return cells
    end
 
    -- Collect effects from base props and upgrades.
    -- effects_seen: effect_name -> { tier = N or nil, dur_field, value }
    --  tier nil = present at base; tier N = first appears at upgrade N
    local effects_seen = {}  -- ordered list of { name, tier }
    local effects_set  = {}   -- name -> true, for dedup
 
    local CC_PROPS = {
        "StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff",
        "PetrifyDuration", "HexDuration", "ImmobilizeDuration", "LiftDuration",
        "SlowPercent", "HoldInPlaceDuration",
    }
 
    -- Check base fields
    for _, prop in ipairs(CC_PROPS) do
        local val = ability[prop]
        local effect = debuff_prop_to_effect[prop]
        if val and val ~= 0 and effect and not effects_set[effect] then
            table.insert(effects_seen, { name = effect, tier = nil })
            effects_set[effect] = true
        end
    end
 
    -- Check upgrades
    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(CC_PROPS) do
                    local val = tier[prop]
                    local effect = debuff_prop_to_effect[prop]
                    if val and val ~= 0 and effect and not effects_set[effect] then
                        table.insert(effects_seen, { name = effect, tier = i })
                        effects_set[effect] = true
                    end
                end
             end
             end
         end
         end
     end
     end


     for key, info in pairs(card_of) do
     -- Check DebuffDuration separately (adds "(See notes)" sentinel if present)
        local view = {}
    local has_debuff_dur = (ability["DebuffDuration"] and ability["DebuffDuration"] ~= 0)
         for k, v in pairs(ability_data[key]) do view[k] = v end
    if not has_debuff_dur and type(upgrades) == "table" then
        view["_subabilities"] = subs[key]
         for _, tier in ipairs(upgrades) do
         _views[key] = { view = view, heroName = info.heroName, abilityNum = info.abilityNum }
            if type(tier) == "table" and tier["DebuffDuration"] and tier["DebuffDuration"] ~= 0 then
         table.insert(_order, key)
                has_debuff_dur = true
                break
            end
         end
    end
 
    -- Build Effects cell string
    local effect_parts = {}
    for _, e in ipairs(effects_seen) do
        if e.tier then
            table.insert(effect_parts, e.name .. " (T" .. e.tier .. ")")
         else
            table.insert(effect_parts, e.name)
        end
    end
    if #effect_parts > 0 then
        cells["Effects"] = table.concat(effect_parts, ", ")
     end
     end


     table.sort(_order, function(a, b)
     -- Build Duration cell.
         local ia, ib = _views[a], _views[b]
    -- Rule: find all non-DebuffDuration duration fields present (base or upgrade).
         if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end
    -- If exactly one distinct field found -> use its base or first-appearing value.
         return ia.abilityNum < ib.abilityNum
    -- If DebuffDuration present and it's the only duration -> "(See notes)".
     end)
    -- If multiple distinct fields -> "(See notes)".
    -- If none -> nil (renders as "—").
    local dur_fields_found = {}  -- field_name -> value
    local DUR_FIELDS = {
         "StunDuration", "SleepDuration", "SilenceDuration", "PetrifyDuration",
         "HexDuration", "ImmobilizeDuration", "LiftDuration", "SlowDuration",
         "HoldInPlaceDuration",
     }


     return _views, _order
     for _, f in ipairs(DUR_FIELDS) do
end
        local val = num_val(ability[f])
        if val and val > 0 then
            dur_fields_found[f] = val
        end
    end
    -- Also check upgrades for any duration field not present at base
    if type(upgrades) == "table" then
        for _, tier in ipairs(upgrades) do
            if type(tier) == "table" then
                for _, f in ipairs(DUR_FIELDS) do
                    if not dur_fields_found[f] then
                        local val = num_val(tier[f])
                        if val and val > 0 then
                            dur_fields_found[f] = val
                        end
                    end
                end
            end
        end
    end


-- ============================================================
    local distinct_dur_count = 0
-- Membership
    local sole_dur_val      = nil
-- Pieces of code that help in telling  "is ability X a member of list Y".
    for _, val in pairs(dur_fields_found) do
-- main function is: p.is_member()
        distinct_dur_count = distinct_dur_count + 1
-- ============================================================
        sole_dur_val = val
    end


-- Memoized name-exclusion set per list (from Lists.exclude_abilities).
     if distinct_dur_count == 1 then
local _excluded = {}
        cells["Duration"] = tostring(sole_dur_val)
local function excluded_set(stat)
     elseif distinct_dur_count > 1 or has_debuff_dur then
     if _excluded[stat] then return _excluded[stat] end
         cells["Duration"] = "(See notes)"
    local set = {}
     for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do
         set[name] = true
     end
     end
     _excluded[stat] = set
     -- distinct_dur_count == 0 and no debuff_dur -> Duration stays nil -> "—"
     return set
 
     return cells
end
end


-- Per-list rules that can't be expressed as prop lists or exclusions.
local extra_columns = {
-- Returns true to keep the ability, false to drop it.
    ["charges"] = {
local function special_ok(stat, record, ability_num)
        headers  = { "Charges", "Time Between Charges", "Notes" },
     if stat == "dispelmagic" and ability_num == 4 then return false end
        get_cells = charges_cells,
     return true
    },
end
     ["melee"] = {
        headers  = { "Light Melee Scaling", "Notes" },
        get_cells = melee_cells,
    },
    ["healreduce"] = {
        headers  = { "Heal Reduction", "Duration", "Notes" },
        get_cells = healreduce_cells,
    },
     ["debuff"] = {
        headers  = { "Effects", "Duration" },
        get_cells = debuff_cells,
    },
}
 
-- ============================================================
-- Hero lookup (ability key -> { heroName, abilityNum })
-- ============================================================


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


    local views = build_views()
local function build_hero_lookup()
     local entry = views[ability_key]
     if _hero_lookup then return _hero_lookup end
     local view  = entry and entry.view
     _hero_lookup = {}


     if not view then
     local hero_data = mw.loadJsonData("Data:HeroData.json")
        view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]
    local active_heroes = {}
         if not view then return false end
    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
     end


     local name = view["Name"]
     local cards = mw.loadJsonData("Data:AbilityCards.json")
    if name and excluded_set(stat)[name] then return false end
    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" and ability["Key"] then
                        _hero_lookup[ability["Key"]] = {
                            heroName  = hero_name,
                            abilityNum = slot_num,
                        }
                    end
                end
            end
        end
    end


     ability_num = ability_num or (entry and entry.abilityNum)
     return _hero_lookup
    if not special_ok(stat, view, ability_num) then return false end
 
    return GameData.entity_matches(view, props)
end
end


Line 215: Line 560:
     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


    -- Build set of columns to hide from the "hide" parameter
     local hide_set = {}
     local hide_set = {}
     local hide_param = mw.text.trim(frame.args["hide"] or "")
     local hide_param = mw.text.trim(frame.args["hide"] or "")
     if hide_param ~= "" then
     if hide_param ~= "" then
         for col in hide_param:gmatch("[^,]+") do hide_set[mw.text.trim(col)] = true end
         for col in hide_param:gmatch("[^,]+") do
            hide_set[mw.text.trim(col)] = true
        end
     end
     end


     local compact = mw.text.trim(frame.args["compact"] or "") ~= ""
     local lookup     = build_hero_lookup()
 
    local collapsed = mw.text.trim(frame.args["collapsed"] or ""):lower()
 
     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 stat_notes = human_notes[stat]
     local stat_notes = Notes[stat]
     local stat_manual = manual_entries[stat]


     local filtered = {}
    -- Wrap the renderer to inject manual_entries for stats that need it
     for _, key in ipairs(order) do
     local get_cells
         if p.is_member(stat, key) then
     if spec then
             table.insert(filtered, key)
         if stat == "debuff" then
             get_cells = function(ability) return debuff_cells(ability, stat_manual) end
        else
            get_cells = spec.get_cells
         end
         end
    else
        get_cells = function() return {} end
     end
     end


     if #filtered == 0 then
     local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)
         return "No abilities found for list: " .. stat
 
    -- Keep only abilities belonging to active, released heroes
    local filtered = {}
    for _, ability in ipairs(abilities) do
         local key = ability["Key"]
        if key and lookup[key] then
            table.insert(filtered, ability)
        end
     end
     end


     local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
     -- Add manual entries whose ability key is not already in filtered
    local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" })
     if stat_manual then
    local disclaimer = frame:preprocess(
         local filtered_names = {}
        '<div class="ability-table-disclaimer">'
         for _, ability in ipairs(filtered) do
        .. 'This is a dynamic list '
             filtered_names[ability["Name"]] = true
        .. '<span class="ability-table-list-name">"' .. stat .. '"</span>. '
        end
        .. '[' .. tostring(lists_url) .. ' Edit contents]'
        for name, _ in pairs(stat_manual) do
        .. ' · '
             if not filtered_names[name] then
        .. '[' .. tostring(notes_url) .. ' Edit notes].'
                -- Find the ability record by name
        .. '</div>'
                -- We search all abilities returned by get_entities with an empty key list
    )
                -- instead: search the GameData for this name via a synthetic record
 
                -- Since we can't query by name, we inject a minimal synthetic record
     if compact then
                -- that debuff_cells will resolve via the manual entry
         local hero_order    = {}
                 table.insert(filtered, { Name = name, Key = "__manual__" .. name })
        local hero_abilities = {}
         for _, key in ipairs(filtered) do
             local hero = views[key].heroName
             if not hero_abilities[hero] then
                 table.insert(hero_order, hero)
                hero_abilities[hero] = {}
             end
             end
            table.insert(hero_abilities[hero], views[key].view)
         end
         end
    end


        local items = {}
    if #filtered == 0 then
         for _, hero in ipairs(hero_order) do
         return "No abilities found for stat: " .. stat
            local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
    end
            local ability_parts = {}
            for _, ability in ipairs(hero_abilities[hero]) do
                local name = ability["Name"]
                local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } }


                local extra = get_cells(ability)
    -- Sort by hero name, then ability slot number.
                enrich_notes_compact(extra, stat_notes and stat_notes[name])
    -- Manual entries (key starts with __manual__) go at the end if not in lookup.
 
    table.sort(filtered, function(a, b)
                local note = extra["Notes"]
        local ia = lookup[a["Key"]]
                if note then
        local ib = lookup[b["Key"]]
                    table.insert(ability_parts, ability_cell .. " (" .. note .. ")")
        if not ia and not ib then return (a["Name"] or "") < (b["Name"] or "") end
                else
        if not ia then return false end
                    table.insert(ability_parts, ability_cell)
        if not ib then return true end
                end
        if ia.heroName ~= ib.heroName then
            end
             return ia.heroName < ib.heroName
             table.insert(items, "<li>" .. hero_cell .. ": " .. table.concat(ability_parts, ", ") .. "</li>")
         end
         end
        return ia.abilityNum < ib.abilityNum
    end)


        return disclaimer .. "<ul>" .. table.concat(items) .. "</ul>"
    -- Determine final header list, respecting hide_set.
    end
 
     local has_notes_header = false
     local has_notes_header = false
     local final_headers = {}
     local final_headers = {}
Line 305: Line 650:
     end
     end
     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'
     -- Disclaimer
     if collapsed ~= "" then
     local edit_url = mw.uri.fullUrl("Module:AbilityTable", { action = "edit" })
         table_class = table_class .. ' mw-collapsible'
    local disclaimer = frame:preprocess(
         if collapsed == "yes" or collapsed == "true" or collapsed == "1" then
         '<div class="ability-table-disclaimer">'
            table_class = table_class .. ' mw-collapsed'
         .. "This is an automatic list. "
         end
        .. "[" .. tostring(edit_url) .. " Click here]"
     end
        .. " to add notes or correct the contents."
         .. "</div>"
     )


    -- Render wikitable
     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_cells = '! class="ability-table-col-hero" | Hero'
                    .. ' !! class="ability-table-col-ability" | Ability'
                      .. ' !! class="ability-table-col-ability" | Ability'
     for _, h in ipairs(final_headers) do
     for _, h in ipairs(final_headers) do
         header_row = header_row .. ' !! class="ability-table-col-'
         local cls = "ability-table-col-" .. h:lower():gsub("%s+", "-")
            .. h:lower():gsub("%s+", "-") .. '" | ' .. h
        header_cells = header_cells .. ' !! class="' .. cls .. '" | ' .. h
    end
    table.insert(out, header_row)
 
        local hero_counts = {}
    for _, key in ipairs(filtered) do
        local hero = views[key].heroName
        hero_counts[hero] = (hero_counts[hero] or 0) + 1
     end
     end
    table.insert(out, header_cells)


     local hero_seen = {}
     for _, ability in ipairs(filtered) do
        local info = lookup[ability["Key"]]


    for _, key in ipairs(filtered) do
         local hero_cell, ability_cell
        local entry  = views[key]
         if info then
        local ability = entry.view
             hero_cell = frame:expandTemplate{
        local hero    = entry.heroName
 
         local ability_cell = frame:expandTemplate{
            title = "AbilityIcon",
            args = { ability["Name"] }
        }
 
        local extra = get_cells(ability)
        enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]])
 
        local row
 
         if not hero_seen[hero] then
             local hero_cell = frame:expandTemplate{
                 title = "HeroIcon",
                 title = "HeroIcon",
                 args = { hero }
                 args = { info.heroName }
            }
            ability_cell = frame:expandTemplate{
                title = "AbilityIcon",
                args  = { ability["Name"] }
            }
        else
            -- Manual entry with no hero lookup: render name only
            hero_cell    = "—"
            ability_cell = frame:expandTemplate{
                title = "AbilityIcon",
                args  = { ability["Name"] }
             }
             }
        end


            if hero_counts[hero] > 1 then
        local extra = get_cells(ability)
                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
        -- Append human note to any renderer-generated note
        else
        if stat_notes and stat_notes[ability["Name"]] then
             row = '| class="ability-table-cell-ability" | ' .. ability_cell
             local human = stat_notes[ability["Name"]]
             extra["Notes"] = extra["Notes"] and (extra["Notes"] .. " " .. human) or human
         end
         end


        local row_cells = '| 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
             row = row .. ' || class="' .. cls .. '" | ' .. content
            if value ~= nil then
                local suffix = column_suffix[header] or ""
                -- Don't append suffix to already-complete strings like "(See notes)"
                if type(value) == "string" and value:sub(1, 1) == "(" then
                    content = value
                else
                    content = value .. suffix
                end
            else
                content = "—"
             end
            row_cells = row_cells .. ' || class="' .. cls .. '" | ' .. content
         end
         end


         table.insert(out, "|-")
         table.insert(out, "|-")
         table.insert(out, row)
         table.insert(out, row_cells)
     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: