Editing Module:Sandbox/Vergir

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.
-- Test harness for the proposed Module:GameData prop-matching change.
 
-- Renders with {{#invoke:Sandbox/Vergir|test}}
local Lists    = require("Module:AbilityTable/Lists")
-- Diagnostics with {{#invoke:Sandbox/Vergir|diag|TARGET|TARGET|...}}
local Notes    = require("Module:AbilityTable/Notes")
--
local Renderers = require("Module:AbilityTable/ComplexRenderers")
-- Holds a copy of both the current and the proposed matching code, runs the
local GameData  = require("Module:GameData")
-- proposed one against Data:AbilityData.json, and checks that every existing
-- list in Module:AbilityTable/Lists still selects exactly the same abilities.


local p = {}
local p = {}


-- ============================================================
local Lists = require("Module:AbilityTable/Lists")
-- TEST COPY of Module:AbilityTable with the proposed
-- Module:AbilityTable/ComplexRenderers.RenderSpiritScaling inlined below,
-- so the whole change can be previewed from one sandbox module.
-- ============================================================


-- ============================================================
-- ============================================================
-- Spirit scaling helpers
-- Current implementation (unchanged, still used for plain targets)
-- ============================================================
-- ============================================================


-- Returns the Spirit scaling entry of a prop value, or nil. A prop's Scale is
local function record_matches_prop(record, prop)
-- either a single { Value, Type } table or an array of them (a duration that
     for k, v in pairs(record) do
-- scales with both Spirit and ability duration, for example).
         if k == prop then
local function spirit_scale(v)
             if type(v) == "table" then
    if type(v) ~= "table" then return nil end
                if v["Value"] ~= nil then
    local scale = v["Scale"]
                     v = v["Value"]
    if type(scale) ~= "table" then return nil end
                 else
    if scale[1] == nil then
                    for _ in pairs(v) do return true end
        if scale["Type"] == "spirit" then return scale end
                    return false
        return nil
                end
    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
-- or one upgrade tier) as { key, value, multiply }, ordered the way the
-- ability card lists its props; props the card does not list come last, in
-- key order. `labels` is the view's cardprops map (see Module:AbilityTable).
local function spirit_scalings(layer, labels)
    local out = {}
     for k, v in pairs(layer) do
         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
 
-- Formats a scaling value to 3 significant figures, as the ability cards do.
local function format_spirit(v)
    return string.format("%.3g", v)
end
 
-- Formats one Spirit scaling for the table cell through the compact {{Ss}}
-- form, the same way RenderBarrier does. The template is expanded via the
-- frame the surrounding #invoke runs under, since module return values are
-- not re-expanded; the plain "×N" fallback covers a nil frame (module
-- console).
local function format_spirit_badge(v)
    local text  = format_spirit(v)
    local frame = mw.getCurrentFrame()
    if frame then
        return frame:expandTemplate{
            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
 
             if v == prop then return true end
    local function label_of(key)
        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 = {}
local function entity_matches_old(record, properties)
     for _, t in ipairs(tiers) do
     for _, prop in ipairs(properties) do
         local i, list = t.tier, t.list
         if record_matches_prop(record, prop) then return true end
        local groups, group_order = {}, {}  -- Multiply upgrades, grouped by multiplier
        for _, up in ipairs(list) do
            local label  = label_of(up.key)
            local current = running[up.key]
            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
 
     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 = ... }
-- `cardprops` maps the ability's prop keys to their display labels; most
-- renderers ignore it.
local extra_columns = {
    ["barrier"] = {
        headers  = { "Barrier" },
        get_cells = Renderers.RenderBarrier,
    },
    ["bossdmgscale"] = {
        headers  = { "Damage to Objectives" },
        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,
    },
}


-- ============================================================
-- ============================================================
-- Note enrichment
-- Proposed additions
-- ============================================================
-- ============================================================


-- Merges auto-generated and human notes for full-table mode.
local function value_is_meaningful(v)
-- Capitalizes human note, joins with ". ", ensures trailing period.
     if type(v) == "table" then
local function enrich_notes_full(extra, human_note)
        if v["Value"] ~= nil then
     if human_note then
            v = v["Value"]
        human_note = human_note:sub(1,1):upper() .. human_note:sub(2)
        else
        extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human_note) or human_note
            for _ in pairs(v) do return true end
    end
            return false
    if extra["Notes"] and extra["Notes"]:sub(-1) ~= "." then
        end
        extra["Notes"] = extra["Notes"] .. "."
     end
     end
    return v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m"
end
end


-- Merges auto-generated and human notes for compact mode.
local function value_equals(v, expected)
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
     if type(v) == "table" then v = v["Value"] end
-- joins with "; ", strips trailing period.
    if v == nil or type(v) == "table" then return false end
local function enrich_notes_compact(extra, human_note)
    return mw.ustring.lower(tostring(v)) == expected
     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


-- ============================================================
local function follow_path(node, segments, index, value)
-- Ability views
    if index > #segments then
--
         if value ~= nil then return value_equals(node, value) end
-- In-game Ability may containe 1 or more records in AbilityData.json
        return value_is_meaningful(node)
-- 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)
        if type(node) ~= "table" then return end
        local key, name = node["Key"], node["Name"]
        if type(key) == "string" and type(name) == "string" and not labels[key] then
            n = n + 1
            local title = node["Title"]
            if type(title) == "string" then
                title = title:gsub(":%s*$", "")
            else
                title = nil
            end
            labels[key] = { name = name, title = title, order = n }
        end
        if node[1] ~= nil then
            for _, child in ipairs(node) do walk(child) end
         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
     end
    if type(node) ~= "table" then return false end


     for _, section in ipairs({ "Info1", "Info2", "Info3" }) do
     local segment = segments[index]
         walk(card[section])
    local next_node = node[segment]
    if next_node == nil then
        local num = tonumber(segment)
         if num then next_node = node[num] end
     end
     end
     local rest = {}
     if next_node ~= nil and follow_path(next_node, segments, index + 1, value) then
    for k, v in pairs(card) do
         return true
        if type(v) == "table" and k ~= "Upgrades"
            and k ~= "Info1" and k ~= "Info2" and k ~= "Info3"
        then
            table.insert(rest, k)
         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.
     for _, element in ipairs(node) do
    local header_attrs = {
         if type(element) == "table" and follow_path(element, segments, index, value) then
        { "AbilityCastRange",            "Cast Range" },
             return true
        { "Radius",                      "Radius" },
        { "AbilityDuration",            "Duration" },
        { "AbilityChannelTime",          "Channel Time" },
        { "AbilityCooldown",            "Cooldown" },
        { "AbilityCharges",              "Charges" },
        { "AbilityCooldownBetweenCharge", "Charge Cooldown" },
    }
     for _, attr in ipairs(header_attrs) do
         if not labels[attr[1]] then
             n = n + 1
            labels[attr[1]] = { name = attr[2], order = n }
         end
         end
     end
     end
 
     return false
     return labels
end
end


local _views = nil  -- card key -> { view = record, heroName = ..., abilityNum = ..., cardprops = ... }
local function record_matches_path(record, segments, value)
local _order = nil  -- array of card keys, sorted by hero then slot
     for k, v in pairs(record) do
 
         if k == segments[1] and follow_path(v, segments, 2, value) then
local function build_views()
             return true
    if _views then return _views, _order end
    _views, _order = {}, {}
 
    local hero_data = mw.loadJsonData("Data:HeroData.json")
    local active_heroes = {}
     for _, hero in pairs(hero_data) do
         if type(hero) == "table"
            and hero["IsDisabled"] == false
            and type(hero["Name"]) == "string"
            and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false)
        then
            active_heroes[hero["Name"]] = true
        end
    end
 
    local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
    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
    end
         if type(v) == "table" and k ~= "DisabledStateMask" then
 
             if record_matches_path(v, segments, value) then return true 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
     end
 
     return false
    for key, info in pairs(card_of) do
        local view = {}
        for k, v in pairs(ability_data[key]) do view[k] = v end
        view["_subabilities"] = subs[key]
        _views[key] = {
            view      = view,
            heroName  = info.heroName,
            abilityNum = info.abilityNum,
            cardprops  = card_prop_labels(info.card),
        }
        table.insert(_order, key)
    end
 
    table.sort(_order, function(a, b)
        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
end


-- ============================================================
local function parse_target(prop)
-- Membership
    if type(prop) ~= "string" then return nil 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).
    local keys, value = prop, nil
local _excluded = {}
    local colon = prop:find(":", 1, true)
local function excluded_set(stat)
     if colon then
     if _excluded[stat] then return _excluded[stat] end
        keys  = mw.text.trim(prop:sub(1, colon - 1))
    local set = {}
        value = mw.text.trim(prop:sub(colon + 1))
    for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do
        if keys == "" or value == "" then return nil end
         set[name] = true
         value = mw.ustring.lower(value)
     end
     end
    _excluded[stat] = set
    return set
end


-- Per-list rules that can't be expressed as prop lists or exclusions.
    local segments = {}
-- Returns true to keep the ability, false to drop it.
    for segment in keys:gmatch("[^%.]+") do table.insert(segments, segment) end
local function special_ok(stat, record, ability_num)
     if #segments == 0 then return nil end
     if stat == "dispelmagic" and ability_num == 4 then return false end
    if #segments == 1 and value == nil then return nil end
     return true
 
     return segments, value
end
end


-- Public: does ability `ability_key` belong to list `stat`?
local function entity_matches_new(record, properties)
function p.is_member(stat, ability_key, ability_num)
     for _, prop in ipairs(properties) do
     local props = Lists.lists[stat]
        local segments, value = parse_target(prop)
    if not props then return false end
        if segments and record_matches_path(record, segments, value) then
 
            return true
    local views = build_views()
         elseif record_matches_prop(record, prop) then
    local entry = views[ability_key]
            return true
    local view  = entry and entry.view
         end
 
    if not view then
         view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]
         if not view then return false end
     end
     end
 
     return false
     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


-- ============================================================
-- ============================================================
-- Entry point
-- Cases
-- ============================================================
-- ============================================================


function p.render(frame)
local cases = {
     local stat = mw.text.trim(frame.args[1] or ""):lower()
    { target = "Type:spirit",                        expected = 215, note = "key found at any depth" },
    { target = "Type:SPIRIT",                        expected = 215, note = "case-insensitive value" },
    { target = "Damage:spirit",                      expected = 0,  note = "nested value must not match" },
    { target = "Damage:90",                          expected = 6,  note = ".Value unwrapped" },
    { target = "AbilityCharges:2",                  expected = 19,  note = "number compared as string" },
    { target = "AbilityCooldown.Scale.Type",        expected = 300, note = "path, no value" },
    { target = "AbilityCooldown.Scale.Type:cooldown",expected = 299, note = "Parry scales on parry_cd" },
     { target = "AbilityCooldown.Scale.Type:spirit",  expected = 0,  note = "wrong value on a valid path" },
    { target = "Radius.Scale.Value:1",              expected = 67,  note = "explicit .Value segment" },
    { target = "Upgrades.Damage",                    expected = 63,  note = "steps through array entries" },
    { target = "Upgrades.1.AbilityCooldown",        expected = 79,  note = "numeric index segment" },
    { target = "38.1 50.8",                          expected = 1,  note = "dotted value falls back" },
}


     local internal_keys = Lists.lists[stat]
local function select_names(data, properties, matcher)
     if not internal_keys then
     local names = {}
         return '<span class="error">AbilityTable: unknown list "' .. stat .. '".</span>'
     for key, record in pairs(data) do
         if type(record) == "table" and matcher(record, properties) then
            table.insert(names, record["Name"] or key)
        end
     end
     end
    table.sort(names)
    return names
end


    local hide_set = {}
local function ok_cell(pass)
    local hide_param = mw.text.trim(frame.args["hide"] or "")
     if pass then
     if hide_param ~= "" then
         return 'style="background:#d5f5d5" | pass'
         for col in hide_param:gmatch("[^,]+") do hide_set[mw.text.trim(col)] = true end
     end
     end
    return 'style="background:#f5d5d5" | <b>FAIL</b>'
end
function p.test(frame)
    local data = mw.loadJsonData("Data:AbilityData.json")


     local compact = mw.text.trim(frame.args["compact"] or "") ~= ""
     local out = {}
    local failures = 0


     local collapsed = mw.text.trim(frame.args["collapsed"] or ""):lower()
     -- --- new target forms ---
    table.insert(out, "=== Proposed target forms ===")
    table.insert(out, '{| class="wikitable"')
    table.insert(out, "! Target !! Expected !! Actual !! Result !! Sample !! Note")


     local views, order = build_views()
     for _, case in ipairs(cases) do
    local spec      = extra_columns[stat]
        local names = select_names(data, { case.target }, entity_matches_new)
    local headers    = spec and spec.headers or {}
        local pass = (#names == case.expected)
    local get_cells = spec and spec.get_cells or function() return {} end
        if not pass then failures = failures + 1 end
    local stat_notes = Notes[stat]


    local filtered = {}
        local sample = {}
    for _, key in ipairs(order) do
        for i = 1, math.min(4, #names) do table.insert(sample, names[i]) end
        if p.is_member(stat, key) then
        local sample_text = #sample > 0 and table.concat(sample, ", ") or "—"
            table.insert(filtered, key)
         if #names > #sample then sample_text = sample_text .. ", …" end
         end
    end


    if #filtered == 0 then
        table.insert(out, "|-")
        return "No abilities found for list: " .. stat
        table.insert(out, "| <code>" .. mw.text.nowiki(case.target) .. "</code>"
            .. " || " .. case.expected
            .. " || " .. #names
            .. " || " .. ok_cell(pass)
            .. " || " .. sample_text
            .. " || " .. case.note)
     end
     end
    table.insert(out, "|}")


     local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
     -- --- regression across every existing list ---
     local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" })
     table.insert(out, "=== Existing lists: old vs new ===")
     local disclaimer = frame:preprocess(
     table.insert(out, '{| class="wikitable sortable"')
        '<div class="ability-table-disclaimer">'
     table.insert(out, "! List !! Old !! New !! Result")
        .. '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
            table.insert(hero_abilities[hero], views[key])
        end


        local items = {}
    local list_names = {}
        for _, hero in ipairs(hero_order) do
    for name in pairs(Lists.lists) do table.insert(list_names, name) end
            local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
    table.sort(list_names)
            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)
    for _, name in ipairs(list_names) do
                enrich_notes_compact(extra, stat_notes and stat_notes[name])
        local props = Lists.lists[name]
        local old_names = select_names(data, props, entity_matches_old)
        local new_names = select_names(data, props, entity_matches_new)


                local note = extra["Notes"]
        local same = (#old_names == #new_names)
                if note then
        if same then
                    table.insert(ability_parts, ability_cell .. " (" .. note .. ")")
            for i = 1, #old_names do
                else
                 if old_names[i] ~= new_names[i] then same = false break end
                    table.insert(ability_parts, ability_cell)
                 end
             end
             end
            table.insert(items, "<li>" .. hero_cell .. ": " .. table.concat(ability_parts, ", ") .. "</li>")
         end
         end
        if not same then failures = failures + 1 end


         return disclaimer .. "<ul>" .. table.concat(items) .. "</ul>"
         table.insert(out, "|-")
        table.insert(out, "| " .. name
            .. " || " .. #old_names
            .. " || " .. #new_names
            .. " || " .. ok_cell(same))
     end
     end
    table.insert(out, "|}")


     local has_notes_header = false
     local summary
     local final_headers = {}
     if failures == 0 then
    for _, h in ipairs(headers) do
         summary = "'''All checks passed.'''"
         if h == "Notes" then
    else
            has_notes_header = true
         summary = "'''" .. failures .. " check(s) failed.'''"
         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'
     return summary .. "\n" .. table.concat(out, "\n")
    if collapsed ~= "" then
end
        table_class = table_class .. ' mw-collapsible'
        if collapsed == "yes" or collapsed == "true" or collapsed == "1" then
            table_class = table_class .. ' mw-collapsed'
        end
    end
 
    local out = {}
    table.insert(out, disclaimer)
    table.insert(out, '{| class="' .. table_class .. '"')
 
    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{
-- Lists every ability matched by each given target, plus the dataset size.
            title = "AbilityIcon",
-- Used to tell a logic fault apart from the dataset having moved on.
            args = { ability["Name"] }
function p.diag(frame)
        }
    local data = mw.loadJsonData("Data:AbilityData.json")


        local extra = get_cells(ability, entry.cardprops)
    local total = 0
        enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]])
    for _ in pairs(data) do total = total + 1 end


        local row
    local out = { "Records in Data:AbilityData.json: '''" .. total .. "'''" }
 
    for i = 1, 5 do
        if not hero_seen[hero] then
        local target = frame.args[i]
            local hero_cell = frame:expandTemplate{
         if target and target ~= "" then
                title = "HeroIcon",
             local names = select_names(data, { target }, entity_matches_new)
                args = { hero }
             table.insert(out, "* <code>" .. mw.text.nowiki(target) .. "</code> ("
            }
                .. #names .. "): " .. table.concat(names, ", "))
 
            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
 
        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, "|}")
     return table.concat(out, "\n")
     return table.concat(out, "\n")
end
-- TEST ONLY: plain-text dump of a list for review, one line per ability.
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: