Editing Module:Sandbox/Vergir

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.
-- Provides generic access to game data files
 
local Lists    = require("Module:AbilityTable/Lists")
local Notes    = require("Module:AbilityTable/Notes")
local Renderers = require("Module:AbilityTable/ComplexRenderers")
local GameData  = require("Module:GameData")


local p = {}
local p = {}


-- ============================================================
-- Dataset descriptors. Values are the data file paths passed to mw.loadJsonData.
-- TEST COPY of Module:AbilityTable with the proposed
-- Callers should use these constants rather than raw strings.
-- Module:AbilityTable/ComplexRenderers.RenderSpiritScaling inlined below,
p.Dataset = {
-- so the whole change can be previewed from one sandbox module.
    ITEMS    = "Data:ItemData.json",
-- ============================================================
    ABILITIES = "Data:AbilityData.json",
    HEROES    = "Data:HeroData.json",
    CONVARS  = "Data:Convars.json",
}


-- ============================================================
-- Street Brawl ships a partial overlay of ability properties rather than a full
-- Spirit scaling helpers
-- dataset, so it is deliberately kept out of p.Dataset: its top-level keys are
-- ============================================================
-- sections ("ability-changes", "item-buckets"), not entity records, and it is
-- not usable with get_entities. Only get_prop consults it.
local STREET_BRAWL = "Data:StreetBrawlData.json"


-- Returns the Spirit scaling entry of a prop value, or nil. A prop's Scale is
-- Returns true if a record represents an active, usable entity.
-- either a single { Value, Type } table or an array of them (a duration that
-- Applies consistently across all datasets:
-- scales with both Spirit and ability duration, for example).
--   - IsDisabled must not be true
local function spirit_scale(v)
--  - Name must not be nil
    if type(v) ~= "table" then return nil end
--  - IsSelectable, if present, must not be false
    local scale = v["Scale"]
    if type(scale) ~= "table" then return nil end
    if scale[1] == nil then
        if scale["Type"] == "spirit" then return scale end
        return nil
    end
    for _, entry in ipairs(scale) do
        if type(entry) == "table" and entry["Type"] == "spirit" then return entry end
    end
    return nil
end


-- Collects the non-zero Spirit scalings of one record layer (the base record
-- Specifically for hero abilities, some of them are split into two entities, eg. Vexing Bolt has a sub-ability: Redirect Bolt.
-- or one upgrade tier) as { key, value, multiply }, ordered the way the
-- The secondary sub-abilities most often have nil name and get filtered out by this function
-- ability card lists its props; props the card does not list come last, in
local function is_active(record)
-- key order. `labels` is the view's cardprops map (see Module:AbilityTable).
     if record["IsDisabled"] == true then return false end
local function spirit_scalings(layer, labels)
    if record["Name"] == nil then return false end
     local out = {}
    if record["IsSelectable"] ~= nil and record["IsSelectable"] == false then return false end
    for k, v in pairs(layer) do
     return true
        if k ~= "Upgrades" and tostring(k):sub(1, 1) ~= "_" then
            local s = spirit_scale(v)
            if s and type(s["Value"]) == "number" and s["Value"] ~= 0 then
                table.insert(out, {
                    key      = k,
                    value    = s["Value"],
                    multiply = s["Multiply"] == true,
                })
            end
        end
    end
    table.sort(out, function(a, b)
        local oa = labels[a.key] and labels[a.key].order or math.huge
        local ob = labels[b.key] and labels[b.key].order or math.huge
        if oa ~= ob then return oa < ob end
        return a.key < b.key
    end)
     return out
end
end


-- Formats a scaling value to 3 significant figures, as the ability cards do.
-- Recursively searches a record for a match against prop, which may be:
local function format_spirit(v)
--   - a key name: matches if the key exists with a non-zero/non-empty value
    return string.format("%.3g", v)
--   - a plain value: matches if the string appears as a value anywhere in the record
end
-- Returns true if a match is found, false otherwise.
 
local function record_matches_prop(record, prop)
-- Formats one Spirit scaling for the table cell through the compact {{Ss}}
     for k, v in pairs(record) do
-- form, the same way RenderBarrier does. The template is expanded via the
         -- Key match: property name exists with a meaningful value
-- frame the surrounding #invoke runs under, since module return values are
         if k == prop then
-- not re-expanded; the plain "×N" fallback covers a nil frame (module
             if type(v) == "table" then
-- console).
                 if v["Value"] ~= nil then
local function format_spirit_badge(v)
                    v = v["Value"]
    local text  = format_spirit(v)
                else
    local frame = mw.getCurrentFrame()
                    for _ in pairs(v) do return true end
    if frame then
                    return false
        return frame:expandTemplate{
                end
            title = "Ss",
            args  = { text, compact = "1", show_value = "1" },
        }
    end
    return "×" .. text
end
 
-- "A", "A and B", "A, B and C".
local function join_names(names)
    if #names <= 1 then return names[1] or "" end
    return table.concat(names, ", ", 1, #names - 1) .. " and " .. names[#names]
end
 
-- Renders Spirit Power scaling.
-- Lists every prop that scales with Spirit at base, one per line as an {{Ss}}
-- badge labelled with the prop's name from the ability card:
-- "x0.55 (Base Damage)". Two
-- props sharing a name are told apart by the card's Title
-- ("Damage: Cost of Stay"); props the card does not list show their raw key.
-- Upgrades are reported in Notes per prop, with the running total: additive
-- ones as "adds +×0.5 ... (total ×1.05)", ones that unlock scaling on a prop
-- as "grants ×1.3 ...", and Multiply upgrades as "multiplies ... by 1.15",
-- which is how the ability-upgrade gadget applies them.
function p.RenderSpiritScaling(ability, cardprops)
     local labels = cardprops or {}
 
    local base  = spirit_scalings(ability, labels)
    local tiers = {}  -- { tier = i, list = ... } in tier order
    local upgrades = ability["Upgrades"]
    if type(upgrades) == "table" then
         -- ipairs rather than #: mw.loadJsonData tables have no length.
         for i, tier in ipairs(upgrades) do
             if type(tier) == "table" then
                 table.insert(tiers, { tier = i, list = spirit_scalings(tier, labels) })
             end
             end
        end
            if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
    end
                 return true
 
    -- Names used by more than one scaling prop of this ability get their Title
    -- appended so the rows can be told apart.
    local keys_by_name = {}
    local function note_keys(list)
        for _, prop in ipairs(list) do
            local info = labels[prop.key]
            if info then
                 keys_by_name[info.name] = keys_by_name[info.name] or {}
                keys_by_name[info.name][prop.key] = true
             end
             end
         end
         end
    end
        if type(v) == "table" and k ~= "DisabledStateMask" then
    note_keys(base)
            if record_matches_prop(v, prop) then return true end
    for _, t in ipairs(tiers) do note_keys(t.list) end
         elseif type(v) == "string" then
 
            -- Value match: prop string appears as a value (e.g. Scale.Type = "melee")
    local function label_of(key)
             if v == prop then return true end
        local info = labels[key]
        if not info then return key end
         local n = 0
        for _ in pairs(keys_by_name[info.name] or {}) do n = n + 1 end
        if n > 1 and info.title then return info.name .. ": " .. info.title end
        return info.name
    end
 
    local cells  = {}
    local running = {}  -- prop key -> current total scaling
 
    if #base > 0 then
        local lines = {}
        for _, prop in ipairs(base) do
            table.insert(lines, format_spirit_badge(prop.value) .. " (" .. label_of(prop.key) .. ")")
             running[prop.key] = prop.value
         end
         end
        cells["Spirit Scaling"] = table.concat(lines, "<br>")
     end
     end
    return false
end


    local notes = {}
-- Returns true if a record matches at least one of the given properties,
    for _, t in ipairs(tiers) do
-- using the same key-name-or-string-value semantics as get_entities.
        local i, list = t.tier, t.list
-- Public so other modules (e.g. Module:AbilityTable membership) can reuse it.
        local groups, group_order = {}, {}  -- Multiply upgrades, grouped by multiplier
function p.entity_matches(record, properties)
        for _, up in ipairs(list) do
    for _, prop in ipairs(properties) do
            local label  = label_of(up.key)
        if record_matches_prop(record, prop) then
            local current = running[up.key]
            return true
            if up.multiply then
                local group = groups[up.value]
                if not group then
                    group = { labels = {}, totals = {} }
                    groups[up.value] = group
                    table.insert(group_order, up.value)
                end
                table.insert(group.labels, label)
                if current then
                    running[up.key] = current * up.value
                    table.insert(group.totals, "×" .. format_spirit(running[up.key]))
                else
                    table.insert(group.totals, "?")
                end
            elseif current then
                running[up.key] = current + up.value
                table.insert(notes, "T" .. i .. " upgrade adds +×" .. format_spirit(up.value)
                    .. " " .. label .. " scaling (total ×" .. format_spirit(running[up.key]) .. ")")
            else
                running[up.key] = up.value
                table.insert(notes, "T" .. i .. " upgrade grants ×" .. format_spirit(up.value)
                    .. " " .. label .. " scaling")
            end
        end
        for _, m in ipairs(group_order) do
            local group = groups[m]
            table.insert(notes, "T" .. i .. " upgrade multiplies " .. join_names(group.labels)
                .. " scaling by " .. format_spirit(m)
                .. " (total " .. table.concat(group.totals, ", ") .. ")")
         end
         end
     end
     end
 
     return false
    if #notes > 0 then cells["Notes"] = table.concat(notes, ". ") end
     return cells
end
end


-- get_cells(ability, cardprops) -> { [column header] = cell text, Notes = ... }
-- Returns all active entities from a dataset that match at least one of the
-- `cardprops` maps the ability's prop keys to their display labels; most
-- given properties. A property may be matched either as a key name (the
-- renderers ignore it.
-- property exists with a non-zero value) or as a plain string value anywhere
local extra_columns = {
-- in the record (e.g. "melee" matching Scale.Type = "melee").
    ["barrier"] = {
-- @param   dataset     string  one of the GameData.Dataset constants
        headers  = { "Barrier" },
-- @param   properties  table   array of property strings
        get_cells = Renderers.RenderBarrier,
-- @return              table   array of matching entity records
    },
function p.get_entities(dataset, properties)
    ["bossdmgscale"] = {
     local data = mw.loadJsonData(dataset)
        headers  = { "Damage to Objectives" },
     local results = {}
        get_cells = Renderers.RenderBossDamageScale,
    },
    ["charges"] = {
        headers   = { "Charges", "Time Between Charges", "Notes" },
        get_cells = Renderers.RenderCharges,
     },
    ["melee"] = {
        headers  = { "Base Damage", "Light Melee Scaling", "Notes" },
        get_cells = Renderers.RenderMelee,
    },
    ["heavymelee"] = {
        headers   = { "Base Damage", "Heavy Melee Scaling", "Notes" },
        get_cells = Renderers.RenderHeavyMelee,
    },
    ["scalingspirit"] = {
        headers   = { "Spirit Scaling", "Notes" },
        get_cells = p.RenderSpiritScaling,  -- TEST: inlined copy above
    },
    ["healreduce"] = {
        headers   = { "Heal Reduction", "Notes" },
        get_cells = Renderers.RenderHealReduce,
     },
    ["moveslow"] = {
        headers  = { "Move Slow", "Notes" },
        get_cells = Renderers.RenderMoveSlow,
     },
    ["dashslow"] = {
        headers  = { "Dash Slow", "Notes" },
        get_cells = Renderers.RenderDashSlow,
    },
    ["extradebuffresist"] = {
        headers  = { "Debuff Resist" },
        get_cells = Renderers.RenderExtraDebuffResist,
    },
}


-- ============================================================
    for _, record in pairs(data) do
-- Note enrichment
        if is_active(record) and p.entity_matches(record, properties) then
-- ============================================================
            table.insert(results, record)
 
         end
-- Merges auto-generated and human notes for full-table mode.
-- Capitalizes human note, joins with ". ", ensures trailing period.
local function enrich_notes_full(extra, human_note)
    if human_note then
        human_note = human_note:sub(1,1):upper() .. human_note:sub(2)
        extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human_note) or human_note
    end
    if extra["Notes"] and extra["Notes"]:sub(-1) ~= "." then
         extra["Notes"] = extra["Notes"] .. "."
     end
     end
end


-- Merges auto-generated and human notes for compact mode.
     return results
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
-- joins with "; ", strips trailing period.
local function enrich_notes_compact(extra, human_note)
     if human_note then
        local first = human_note:sub(1,1)
        if first:match("%u") and not human_note:sub(2,2):match("%d") then
            human_note = first:lower() .. human_note:sub(2)
        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
end


-- ============================================================
--------------------------------------------------------------------------------
-- Ability views
-- Unified property lookup: get_prop
--
-- Uses ResourceLookup to route directly to the correct dataset.
-- In-game Ability may containe 1 or more records in AbilityData.json
-- Returns raw values for use in templates and expressions.
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
--------------------------------------------------------------------------------
 
-- "Ability View" is a main-record + any relevant sub-ability records.
-- Each view entry also carries "cardprops", the display labels of the
-- ability's props taken from its card (see card_prop_labels). They are kept
-- beside the record rather than inside it so that membership matching, which
-- walks the whole record, never sees them.
-- ============================================================
 
-- Display labels for a card's props: prop key -> { name, title, order }, read
-- from the Info sections of the card in AbilityCards.json. The flat
-- AbilityData records carry no display names, and a key such as "Damage" is
-- reused across abilities under different labels, so the map is built per
-- card rather than wiki-wide. `order` is the position of the prop on the
-- card, so renderers can list props the way the in-game card does.
local function card_prop_labels(card)
    local labels, n = {}, 0


    local function walk(node)
-- Find an entity by display name or internal key.
        if type(node) ~= "table" then return end
-- Uses ResourceLookup type field to go directly to the right dataset.
         local key, name = node["Key"], node["Name"]
-- Returns (entity_record, type_string, internal_key) or (nil, nil, nil).
         if type(key) == "string" and type(name) == "string" and not labels[key] then
local function find_entity(identifier)
             n = n + 1
    local resource = mw.loadJsonData("Data:ResourceLookup.json")[identifier:lower()]
            local title = node["Title"]
    if resource then
            if type(title) == "string" then
         local data
                title = title:gsub(":%s*$", "")
         if resource.type == "ability" then
            else
            data = mw.loadJsonData(p.Dataset.ABILITIES)
                title = nil
        elseif resource.type == "hero" then
            end
             data = mw.loadJsonData(p.Dataset.HEROES)
            labels[key] = { name = name, title = title, order = n }
        elseif resource.type == "item" then
            data = mw.loadJsonData(p.Dataset.ITEMS)
         end
         end
         if node[1] ~= nil then
         if data and data[resource.key] then
            for _, child in ipairs(node) do walk(child) end
             return data[resource.key], resource.type, resource.key
        else
            -- Sections such as Range or Move key each prop by its name:
            -- { AttackDashRange = { Name = "...", Value = ... } }.
            local keyed = {}
            for k, v in pairs(node) do
                if type(k) == "string" and type(v) == "table" and v["Key"] == nil
                    and type(v["Name"]) == "string" and not labels[k]
                then
                    table.insert(keyed, k)
                end
            end
            table.sort(keyed)
            for _, k in ipairs(keyed) do
                n = n + 1
                labels[k] = { name = node[k]["Name"], order = n }
            end
            -- Main props before Alt props, then anything else in key order.
            if node["Main"] ~= nil then walk(node["Main"]) end
             if node["Alt"] ~= nil then walk(node["Alt"])  end
            local rest = {}
            for k, v in pairs(node) do
                if k ~= "Main" and k ~= "Alt" and k ~= "Upgrades" and type(v) == "table" then
                    table.insert(rest, k)
                end
            end
            table.sort(rest, function(a, b) return tostring(a) < tostring(b) end)
            for _, k in ipairs(rest) do walk(node[k]) end
        end
    end
 
    for _, section in ipairs({ "Info1", "Info2", "Info3" }) do
        walk(card[section])
    end
    local rest = {}
    for k, v in pairs(card) do
        if type(v) == "table" and k ~= "Upgrades"
            and k ~= "Info1" and k ~= "Info2" and k ~= "Info3"
        then
            table.insert(rest, k)
         end
         end
     end
     end
    table.sort(rest, function(a, b) return tostring(a) < tostring(b) end)
    for _, k in ipairs(rest) do walk(card[k]) end


     -- Header attributes are stored on the card without a Name.
     -- Fallback: try as direct internal key
     local header_attrs = {
     local datasets = {
         { "AbilityCastRange",           "Cast Range" },
         { p.Dataset.ABILITIES, "ability" },
         { "Radius",                     "Radius" },
         { p.Dataset.HEROES,   "hero" },
         { "AbilityDuration",            "Duration" },
         { p.Dataset.ITEMS,     "item" },
        { "AbilityChannelTime",          "Channel Time" },
        { "AbilityCooldown",            "Cooldown" },
        { "AbilityCharges",              "Charges" },
        { "AbilityCooldownBetweenCharge", "Charge Cooldown" },
     }
     }
     for _, attr in ipairs(header_attrs) do
     for _, ds in ipairs(datasets) do
         if not labels[attr[1]] then
         local data = mw.loadJsonData(ds[1])
            n = n + 1
        if data[identifier] then return data[identifier], ds[2], identifier end
            labels[attr[1]] = { name = attr[2], order = n }
        end
     end
     end


     return labels
     return nil, nil, nil
end
end


local _views = nil  -- card key -> { view = record, heroName = ..., abilityNum = ..., cardprops = ... }
-- Traverse a table using dot notation, unwrap tables with a .Value field.
local _order = nil  -- array of card keys, sorted by hero then slot
-- Supports numeric indices for arrays (e.g. "Upgrades.1.WeaponDamageBonus").
 
local function resolve_prop(tbl, prop)
local function build_views()
     if not prop or prop == "" then return nil end
     if _views then return _views, _order end
     local element = tbl
     _views, _order = {}, {}
     for segment in string.gmatch(prop, "[^%.]+") do
 
         if type(element) ~= "table" then return nil end
     local hero_data = mw.loadJsonData("Data:HeroData.json")
        local next = element[segment]
    local active_heroes = {}
        if next == nil then
    for _, hero in pairs(hero_data) do
             local num = tonumber(segment)
         if type(hero) == "table"
             if num then next = element[num] end
            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
        element = next
        if element == nil then return nil end
     end
     end
 
     if type(element) == "table" then
    local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
         return element.Value or ""
     local cards        = mw.loadJsonData("Data:AbilityCards.json")
 
    local card_of = {}
    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)
                    local key = type(ability) == "table" and ability["Key"] or nil
                    if slot_num and key and ability_data[key] then
                        card_of[key] = { heroName = hero_name, abilityNum = slot_num, card = ability }
                    end
                end
            end
         end
    end
 
    -- Attach each non-card record to the longest card key it prefixes.
    local subs = {}
    for key, record in pairs(ability_data) do
        if not card_of[key] and record["IsDisabled"] ~= true then
            local parent = nil
            for card_key in pairs(card_of) do
                if key:sub(1, #card_key + 1) == card_key .. "_"
                    and (parent == nil or #card_key > #parent)
                then
                    parent = card_key
                end
            end
            if parent then
                subs[parent] = subs[parent] or {}
                table.insert(subs[parent], record)
            end
        end
     end
     end
    return element
end


    for key, info in pairs(card_of) do
-- Looks up a console variable in Data:Convars.json. A convar value is stored
        local view = {}
-- either as a plain scalar (e.g. "adsp_alley_min": 122) or as a table holding
        for k, v in pairs(ability_data[key]) do view[k] = v end
-- the value plus a description (e.g. { value = 75, description = "..." }), in
        view["_subabilities"] = subs[key]
-- which case only the value is returned. The exact key is tried first, then a
        _views[key] = {
-- lower-case form. Returns (value, found).
            view      = view,
local function lookup_convar(key)
            heroName  = info.heroName,
    local data = mw.loadJsonData(p.Dataset.CONVARS)
            abilityNum = info.abilityNum,
    local entry = data[key]
            cardprops  = card_prop_labels(info.card),
    if entry == nil then
        }
         entry = data[mw.ustring.lower(key)]
         table.insert(_order, key)
     end
     end
 
     if entry == nil then
     table.sort(_order, function(a, b)
         return nil, false
        local ia, ib = _views[a], _views[b]
        if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end
         return ia.abilityNum < ib.abilityNum
    end)
 
    return _views, _order
end
 
-- ============================================================
-- Membership
-- 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).
local _excluded = {}
local function excluded_set(stat)
    if _excluded[stat] then return _excluded[stat] end
    local set = {}
    for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do
        set[name] = true
     end
     end
     _excluded[stat] = set
     if type(entry) == "table" then
    return set
        return entry.value, true
end
 
-- Per-list rules that can't be expressed as prop lists or exclusions.
-- Returns true to keep the ability, false to drop it.
local function special_ok(stat, record, ability_num)
    if stat == "dispelmagic" and ability_num == 4 then return false end
    return true
end
 
-- Public: does ability `ability_key` belong to list `stat`?
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 entry = views[ability_key]
    local view  = entry and entry.view
 
    if not view then
        view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]
        if not view then return false end
     end
     end
 
     return entry, true
     local name = view["Name"]
    if name and excluded_set(stat)[name] then return false end
 
    ability_num = ability_num or (entry and entry.abilityNum)
    if not special_ok(stat, view, ability_num) then return false end
 
    return GameData.entity_matches(view, props)
end
end


-- ============================================================
-- {{#invoke:GameData|get_prop|...}}
-- Entry point
-- Two modes:
-- ============================================================
--   Entity:  {{#invoke:GameData|get_prop|ENTITY_NAME|PROPERTY|VARIANT}}
 
--            Finds entity via ResourceLookup, returns the raw value (no
function p.render(frame)
--            formatting). Supports dot notation for nested properties
    local stat = mw.text.trim(frame.args[1] or ""):lower()
--            (e.g. "Scale.Value"); tables with a .Value field are unwrapped.
 
--            The optional VARIANT selects a game mode variant. "Street Brawl"
    local internal_keys = Lists.lists[stat]
--            (case and spacing insensitive) returns the value changed for
    if not internal_keys then
--            Street Brawl, falling back to the base value when Street Brawl
        return '<span class="error">AbilityTable: unknown list "' .. stat .. '".</span>'
--            does not change that property.
    end
--  Convar:  {{#invoke:GameData|get_prop|Convar|CONVAR_NAME}}
 
--            Looks up the convar in Data:Convars.json and returns its value,
    local hide_set = {}
--            unwrapping the { value, description } form when present.
    local hide_param = mw.text.trim(frame.args["hide"] or "")
function p.get_prop(frame)
    if hide_param ~= "" then
     local arg1 = frame.args[1]
        for col in hide_param:gmatch("[^,]+") do hide_set[mw.text.trim(col)] = true end
     local arg2 = frame.args[2]
    end
     local arg3 = frame.args[3]
 
     if not arg1 then return "" end
     local compact = mw.text.trim(frame.args["compact"] or "") ~= ""
 
     local collapsed = mw.text.trim(frame.args["collapsed"] or ""):lower()
 
     local views, order = build_views()
    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]


     local filtered = {}
     -- Convar mode: first argument is the literal keyword "Convar".
     for _, key in ipairs(order) do
     if mw.ustring.lower(arg1) == "convar" then
         if p.is_member(stat, key) then
         if not arg2 or arg2 == "" then return "" end
             table.insert(filtered, key)
        local value, found = lookup_convar(arg2)
        if not found then
             return '<span style="color:red;">Convar not found: ' .. arg2 .. '</span>'
         end
         end
        if value == nil then return "" end
        return value
     end
     end


     if #filtered == 0 then
     -- Entity mode: search abilities, heroes, and items.
        return "No abilities found for list: " .. stat
     local name = arg1
    end
     local prop = arg2
 
     if not prop then return "" end
     local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
     local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" })
     local disclaimer = frame:preprocess(
        '<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 entity, etype, ekey = find_entity(name)
        local hero_order    = {}
    if not entity then
        local hero_abilities = {}
         return '<span style="color:red;">Entity not found: ' .. name .. '</span>'
        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
            table.insert(hero_abilities[hero], views[key])
         end
 
        local items = {}
        for _, hero in ipairs(hero_order) do
            local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
            local ability_parts = {}
            for _, entry in ipairs(hero_abilities[hero]) do
                local ability = entry.view
                local name = ability["Name"]
                local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } }
 
                local extra = get_cells(ability, entry.cardprops)
                enrich_notes_compact(extra, stat_notes and stat_notes[name])
 
                local note = extra["Notes"]
                if note then
                    table.insert(ability_parts, ability_cell .. " (" .. note .. ")")
                else
                    table.insert(ability_parts, ability_cell)
                end
            end
            table.insert(items, "<li>" .. hero_cell .. ": " .. table.concat(ability_parts, ", ") .. "</li>")
        end
 
        return disclaimer .. "<ul>" .. table.concat(items) .. "</ul>"
    end
 
    local has_notes_header = false
    local final_headers = {}
    for _, h in ipairs(headers) do
        if h == "Notes" then
            has_notes_header = true
        elseif not hide_set[h] then
            table.insert(final_headers, h)
        end
     end
     end
    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


     local table_class = 'wikitable sortable ability-table'
     -- Street Brawl changes a handful of ability properties. The overlay lists
     if collapsed ~= "" then
    -- only what changed, so a miss here just means "unchanged" and the base
         table_class = table_class .. ' mw-collapsible'
    -- record answers instead. That includes the empty string resolve_prop
         if collapsed == "yes" or collapsed == "true" or collapsed == "1" then
    -- returns for a table with no Value field: the overlay records are partial,
             table_class = table_class .. ' mw-collapsed'
    -- so an overridden table often holds nothing but a nested Scale.
     if arg3 and arg3 ~= "" then
         local mode = mw.ustring.lower(mw.text.trim(arg3))
         mode = mode:gsub("%s+", "")
        if mode ~= "streetbrawl" then
             return '<span style="color:red;">Unknown variant: ' .. arg3 .. '</span>'
         end
         end
    end
        if etype == "ability" then
 
             local changed = mw.loadJsonData(STREET_BRAWL)["ability-changes"][ekey]
    local out = {}
            if changed then
    table.insert(out, disclaimer)
                local value = resolve_prop(changed, prop)
    table.insert(out, '{| class="' .. table_class .. '"')
                 if value ~= nil and value ~= "" then return value end
 
    local header_row = '! class="ability-table-col-hero" | Hero'
                    .. ' !! class="ability-table-col-ability" | Ability'
    for _, h in ipairs(final_headers) do
        header_row = header_row .. ' !! class="ability-table-col-'
             .. h:lower():gsub("%s+", "-") .. '" | ' .. 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
 
    local hero_seen = {}
 
    for _, key in ipairs(filtered) do
        local entry  = views[key]
        local ability = entry.view
        local hero    = entry.heroName
 
        local ability_cell = frame:expandTemplate{
            title = "AbilityIcon",
            args = { ability["Name"] }
        }
 
        local extra = get_cells(ability, entry.cardprops)
        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",
                 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
             end
            hero_seen[hero] = true
        else
            row = '| class="ability-table-cell-ability" | ' .. ability_cell
        end
        for _, header in ipairs(final_headers) do
            local value = extra[header]
            local cls  = "ability-table-cell-" .. header:lower():gsub("%s+", "-")
            local content = value ~= nil and tostring(value) or "—"
            row = row .. ' || class="' .. cls .. '" | ' .. content
         end
         end
        table.insert(out, "|-")
        table.insert(out, row)
     end
     end


     table.insert(out, "|}")
     local result = resolve_prop(entity, prop)
     return table.concat(out, "\n")
     if result ~= nil then return result end
end


-- TEST ONLY: plain-text dump of a list for review, one line per ability.
     return '<span style="color:red;">Prop not found: ' .. name .. '/' .. prop .. '</span>'
function p.dump(frame)
     local stat = mw.text.trim(frame.args[1] or ""):lower()
    local views, order = build_views()
    local spec = extra_columns[stat]
    local get_cells = spec and spec.get_cells or function() return {} end
    local out = {}
    for _, key in ipairs(order) do
        if p.is_member(stat, key) then
            local entry = views[key]
            local extra = get_cells(entry.view, entry.cardprops)
            local cell = extra["Spirit Scaling"] or "—"
            cell = mw.text.killMarkers(cell):gsub("<br>", " / "):gsub("<[^>]+>", "")
            table.insert(out, entry.heroName .. " | " .. entry.view["Name"] .. " | " .. cell .. " | " .. (extra["Notes"] or "—"))
        end
    end
    return "<pre>" .. mw.text.nowiki(table.concat(out, "\n")) .. "</pre>"
end
end


return p
return p
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: