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",
        "DebuffAccuracy", "TimeSlowDuration", "LateCheckoutStun",
    },
}


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 whose effects cannot be inferred from
    },
-- data alone (e.g. engine-side CC with no matching data fields).
    ["scalingspirit"] = {
-- The hero is resolved automatically from AbilityCards.json via the ability name.
        headers   = { "Spirit Scaling", "Notes" },
-- Each entry requires:
        get_cells = Renderers.RenderSpiritScaling,
--   effects  = { "EffectName", ... } (supports "Effect (TN)" for upgrade-only)
    },
--   duration = <number or nil>        (nil renders as "")
     ["healreduce"] = {
local manual_entries = {
         headers  = { "Heal Reduction", "Notes" },
     ["debuff"] = {
        get_cells = Renderers.RenderHealReduce,
         ["Combo"] = {
    },
            effects  = { "Stun" },
    ["moveslow"] = {
            duration = 2.4,
        headers  = { "Move Slow", "Notes" },
        },
        get_cells = Renderers.RenderMoveSlow,
        ["Sand Blast"] = {
    },
            effects  = { "Disarm" },
    ["dashslow"] = {
            duration = 2.5,
        headers  = { "Dash Slow", "Notes" },
        },
        get_cells = Renderers.RenderDashSlow,
        ["Spirit Lasso"] = {
    },
            effects  = { "Stun" },
    ["extradebuffresist"] = {
            duration = 2.25,
        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")
-- ============================================================
-- Shared helpers
-- ============================================================
local function num_val(v)
    if type(v) == "number" then return v end
    if type(v) == "table"  then return v["Value"] end
    return nil
end
-- 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
    return nil
end


-- ============================================================
-- ============================================================
-- Note enrichment
-- 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.
-- ============================================================
-- ============================================================


-- Merges auto-generated and human notes for full-table mode.
local function charges_cells(ability)
-- Capitalizes human note, joins with ". ", ensures trailing period.
    local base_charges  = num_val(ability["AbilityCharges"])
local function enrich_notes_full(extra, human_note)
    local base_cooldown = ability["AbilityCooldownBetweenCharge"]
     if human_note then
    local has_base      = base_charges ~= nil and base_charges > 0
         human_note = human_note:sub(1,1):upper() .. human_note:sub(2)
    local upgrade_info  = find_in_upgrades(ability, "AbilityCharges")
         extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human_note) or human_note
 
    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
     end
     if extra["Notes"] and extra["Notes"]:sub(-1) ~= "." then
 
        extra["Notes"] = extra["Notes"] .. "."
    local cells = {}
 
     if has_base then
        cells["Charges"] = tostring(base_charges)
        if cooldown then
            cells["Time Between Charges"] = tostring(cooldown)
        end
     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
    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.
local function find_base_melee_scale(ability)
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
    for _, v in pairs(ability) do
-- joins with "; ", strips trailing period.
        if type(v) == "table" then
local function enrich_notes_compact(extra, human_note)
            local scale = v["Scale"]
    if human_note then
            if type(scale) == "table"
        local first = human_note:sub(1,1)
                and scale["Type"] == "melee"
        if first:match("%u") and not human_note:sub(2,2):match("%d") then
                and type(scale["Value"]) == "number"
             human_note = first:lower() .. human_note:sub(2)
                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


-- ============================================================
local function build_melee_upgrade_notes(ability, base_melee_scale)
-- Ability views
    local upgrades = ability["Upgrades"]
--
    if type(upgrades) ~= "table" then return {} end
-- In-game Ability may containe 1 or more records in AbilityData.json
    local notes = {}
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
    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
    return notes
end


-- "Ability View" is a main-record + any relevant sub-ability records.
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 _views = nil  -- card key -> { view = record, heroName = ..., abilityNum = ... }
local function find_base_duration(ability)
local _order = nil   -- array of card keys, sorted by hero then slot
    for _, field in ipairs({ "VenomDuration", "DebuffDuration", "AbilityDuration", "BurnDuration" }) do
        local v = num_val(ability[field])
        if v and v > 0 then return v, field end
    end
    return nil, nil
end


local function build_views()
local function healreduce_cells(ability)
     if _views then return _views, _order end
     local cells = {}
     _views, _order = {}, {}
     local notes = {}
    local base_dur, dur_field = find_base_duration(ability)


     local hero_data = mw.loadJsonData("Data:HeroData.json")
     local first_pct, first_tier, first_is_disable = nil, nil, false
     local active_heroes = {}
    local base_receive = num_val(ability["HealAmpReceivePenaltyPercent"])
     for _, hero in pairs(hero_data) do
    local base_regen  = num_val(ability["HealAmpRegenPenaltyPercent"])
         if type(hero) == "table"
     local base_pct_raw = base_receive or base_regen
            and hero["IsDisabled"] == false
     if base_pct_raw and base_pct_raw ~= 0 then
            and type(hero["Name"]) == "string"
         first_pct = math.abs(base_pct_raw)
             and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false)
    elseif ability["DisableHealing"] and ability["DisableHealing"] ~= 0 then
        then
        first_is_disable = true
            active_heroes[hero["Name"]] = true
    else
        local upgrades = ability["Upgrades"]
        if type(upgrades) == "table" then
             for i, tier in ipairs(upgrades) do
                if type(tier) == "table" then
                    local up_raw = num_val(tier["HealAmpReceivePenaltyPercent"])
                                or num_val(tier["HealAmpRegenPenaltyPercent"])
                    if up_raw and up_raw ~= 0 then
                        first_pct, first_tier = math.abs(up_raw), i
                        break
                    elseif tier["DisableHealing"] and tier["DisableHealing"] ~= 0 then
                        first_is_disable, first_tier = true, i
                        break
                    end
                end
            end
         end
         end
     end
     end


     local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
     if first_is_disable then
     local cards        = mw.loadJsonData("Data:AbilityCards.json")
        cells["Heal Reduction"] = "100%" .. (first_tier and " (T" .. first_tier .. ")" or "")
     elseif first_pct then
        cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "")
    end
    if base_dur then cells["Duration"] = tostring(base_dur) end


     local card_of = {}
     local upgrades = ability["Upgrades"]
     for _, hero_entry in pairs(cards) do
     if type(upgrades) == "table" then
        if type(hero_entry) == "table" then
        local running_pct, running_dur = first_pct, base_dur
            local hero_name = hero_entry["Name"]
        local first_pct_seen = (first_tier == nil)
            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 tier_notes = {}
                     local key = type(ability) == "table" and ability["Key"] or nil
                local pct_delta = num_val(tier["HealAmpReceivePenaltyPercent"])
                     if slot_num and key and ability_data[key] then
                              or num_val(tier["HealAmpRegenPenaltyPercent"])
                         card_of[key] = { heroName = hero_name, abilityNum = slot_num }
                if first_tier == i 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
                 end
                 end
                for _, n in ipairs(tier_notes) do table.insert(notes, n) end
             end
             end
         end
         end
     end
     end


     -- Attach each non-card record to the longest card key it prefixes.
     if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
    local subs = {}
    return cells
     for key, record in pairs(ability_data) do
end
         if not card_of[key] and record["IsDisabled"] ~= true then
 
             local parent = nil
-- Maps AbilityData property names to human-readable effect names for the debuff table.
            for card_key in pairs(card_of) do
-- Props not listed here fall back to using the prop name itself as the effect label.
                 if key:sub(1, #card_key + 1) == card_key .. "_"
-- DebuffDuration is absent: it signals "(See notes)" on Duration rather than a named effect.
                     and (parent == nil or #card_key > #parent)
local debuff_prop_to_effect = {
                then
     StunDuration        = "Stun",
                     parent = card_key
    SleepDuration      = "Sleep",
    SilenceDuration    = "Silence",
    SilenceDebuff      = "Silence",
    PetrifyDuration    = "Petrify",
    HexDuration        = "Hex",
    ImmobilizeDuration  = "Root",
    LiftDuration        = "Lift",
    SlowPercent        = "Slow",
    HoldInPlaceDuration = "Stop",
    DebuffAccuracy      = "Inaccuracy",
    TimeSlowDuration    = "Time Slow",
    LateCheckoutStun    = "Stun",
}
 
local function debuff_cells(ability, stat_manual)
    local cells = {}
 
    -- Manual entry takes full precedence.
    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
 
    local effects_seen = {}  -- ordered list of { name, tier }
    local effects_set  = {}  -- effect name -> true, for dedup
 
    -- Iterate stat_to_props["debuff"], skipping DebuffDuration (handled separately).
    -- Use debuff_prop_to_effect for the display name; fall back to the prop name itself.
    local function collect_effects(obj, tier)
        for _, prop in ipairs(stat_to_props["debuff"]) do
            if prop ~= "DebuffDuration" then
                local val = obj[prop]
                 if val and val ~= 0 then
                    local effect = debuff_prop_to_effect[prop] or prop
                     if not effects_set[effect] then
                        table.insert(effects_seen, { name = effect, tier = tier })
                        effects_set[effect] = true
                     end
                 end
                 end
             end
             end
            if parent then
        end
                subs[parent] = subs[parent] or {}
    end
                 table.insert(subs[parent], record)
 
    collect_effects(ability, nil)
    local upgrades = ability["Upgrades"]
    if type(upgrades) == "table" then
        for i, tier in ipairs(upgrades) do
            if type(tier) == "table" then
                 collect_effects(tier, i)
             end
             end
         end
         end
     end
     end


     for key, info in pairs(card_of) do
     -- DebuffDuration: present anywhere → Duration shows "(See notes)"
        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
     end


     table.sort(_order, function(a, b)
     -- Effects cell
         local ia, ib = _views[a], _views[b]
    local effect_parts = {}
        if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end
    for _, e in ipairs(effects_seen) do
         return ia.abilityNum < ib.abilityNum
        table.insert(effect_parts, e.tier and (e.name .. " (T" .. e.tier .. ")") or e.name)
     end)
    end
    if #effect_parts > 0 then
        cells["Effects"] = table.concat(effect_parts, ", ")
    end
 
    -- Duration cell: collect distinct non-DebuffDuration duration fields (base or upgrade).
    -- If exactly one found → use its value. If multiple, or DebuffDuration present → "(See notes)".
    local DUR_FIELDS = {
        "StunDuration", "SleepDuration", "SilenceDuration", "PetrifyDuration",
        "HexDuration", "ImmobilizeDuration", "LiftDuration", "SlowDuration",
         "HoldInPlaceDuration", "AbilityDuration", "TimeSlowDuration", "LateCheckoutStun",
    }
    local dur_found = {}
    local function collect_durations(obj)
        for _, f in ipairs(DUR_FIELDS) do
            if not dur_found[f] then
                local val = num_val(obj[f])
                if val and val > 0 then dur_found[f] = val end
            end
         end
     end


     return _views, _order
     collect_durations(ability)
end
    if type(upgrades) == "table" then
        for _, tier in ipairs(upgrades) do
            if type(tier) == "table" then collect_durations(tier) end
        end
    end


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


-- Memoized name-exclusion set per list (from Lists.exclude_abilities).
     if count == 1 then
local _excluded = {}
        cells["Duration"] = tostring(sole_val)
local function excluded_set(stat)
     elseif 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
 
     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,
     },
}
 
-- ============================================================
-- Name lookup: ability name -> { heroName, abilityNum }
-- Built from HeroData (active hero filter) + AbilityCards.
-- Used for both prop-matched and manual abilities.
-- ============================================================


-- Public: does ability `ability_key` belong to list `stat`?
local _name_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_name_lookup()
     local entry = views[ability_key]
     if _name_lookup then return _name_lookup end
     local view  = entry and entry.view
     _name_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["Name"] then
                        _name_lookup[ability["Name"]] = {
                            heroName  = hero_name,
                            abilityNum = slot_num,
                        }
                    end
                end
            end
        end
    end


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


Line 215: Line 472:
     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 226: Line 483:
     end
     end


     local compact = mw.text.trim(frame.args["compact"] or "") ~= ""
     local name_lookup = build_name_lookup()
    local spec        = extra_columns[stat]
    local headers    = spec and spec.headers or {}
    local stat_notes  = human_notes[stat]
    local stat_manual = manual_entries[stat]


     local collapsed = mw.text.trim(frame.args["collapsed"] or ""):lower()
     local get_cells
    if spec then
        if stat == "debuff" then
            get_cells = function(ability) return debuff_cells(ability, stat_manual) end
        else
            get_cells = spec.get_cells
        end
    else
        get_cells = function() return {} end
    end


     local views, order = build_views()
     local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)
    local spec      = extra_columns[stat]
    local headers    = spec and spec.headers or {}
    local get_cells  = spec and spec.get_cells or function() return {} end
    local stat_notes = Notes[stat]


    -- Keep only abilities belonging to active, released heroes
     local filtered = {}
     local filtered = {}
     for _, key in ipairs(order) do
    local filtered_names = {}
         if p.is_member(stat, key) then
     for _, ability in ipairs(abilities) do
             table.insert(filtered, key)
         if ability["Name"] and name_lookup[ability["Name"]] then
             table.insert(filtered, ability)
            filtered_names[ability["Name"]] = true
         end
         end
     end
     end


     if #filtered == 0 then
     -- Inject manual entries not already covered by prop-matching.
        return "No abilities found for list: " .. stat
     -- Hero and slot are resolved via name_lookup; if the ability isn't in AbilityCards
    end
    -- the row shows "" in the Hero column and sorts to the end.
 
     if stat_manual then
    local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
         for name, _ in pairs(stat_manual) do
    local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" })
             if not filtered_names[name] then
     local disclaimer = frame:preprocess(
                 table.insert(filtered, { Name = name })
        '<div class="ability-table-disclaimer">'
        .. 'This is a dynamic list '
        .. '<span class="ability-table-list-name">"' .. stat .. '"</span>. '
        .. '[' .. tostring(lists_url) .. ' Edit contents]'
        .. ' · '
        .. '[' .. tostring(notes_url) .. ' Edit notes].'
        .. '</div>'
    )
 
     if compact then
        local hero_order    = {}
        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])
    -- Abilities not in name_lookup sort to the end alphabetically.
 
    table.sort(filtered, function(a, b)
                local note = extra["Notes"]
        local ia = name_lookup[a["Name"]]
                if note then
        local ib = name_lookup[b["Name"]]
                    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 return ia.heroName < ib.heroName end
            end
        return ia.abilityNum < ib.abilityNum
            table.insert(items, "<li>" .. hero_cell .. ": " .. table.concat(ability_parts, ", ") .. "</li>")
     end)
         end
 
         return disclaimer .. "<ul>" .. table.concat(items) .. "</ul>"
     end


    -- Determine final headers
     local has_notes_header = false
     local has_notes_header = false
     local final_headers = {}
     local final_headers = {}
Line 307: Line 552:
     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>"
     )


     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 574:
     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 = name_lookup[ability["Name"]]
         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 not hero_seen[hero] then
         if stat_notes and stat_notes[ability["Name"]] then
             local hero_cell = frame:expandTemplate{
             local human = stat_notes[ability["Name"]]
                title = "HeroIcon",
             extra["Notes"] = extra["Notes"] and (extra["Notes"] .. " " .. human) or human
                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: