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.
local p = {};
local heroes_data = mw.loadJsonData("Data:HeroData.json")
local util_module = require('Module:Utilities')
local lang_module = require('Module:Lang')


local Lists    = require("Module:AbilityTable/Lists")
--------------------------------------------------------------------------------
local Notes    = require("Module:AbilityTable/Notes")
-- Internal helpers
local Renderers = require("Module:AbilityTable/ComplexRenderers")
--------------------------------------------------------------------------------
local GameData  = require("Module:GameData")


local p = {}
-- Localized string for a key, falling back to a readable version of `fallback`
-- plus a "missing translation" tooltip.
local function localize(key, fallback)
local result = lang_module.get_string(key)
if (result == "") or (result == nil) then
result = util_module.add_space_before_cap(fallback) .. mw.getCurrentFrame():expandTemplate{title="MissingValveTranslationTooltip"}
end
return result
end
 
-- Raw scaling value of a stat under LevelScaling / SpiritScaling, or nil when
-- the hero has no scaling of that type for that stat.
local function raw_scaling(hero_data, scaling_type, stat)
local scaling = hero_data and hero_data[scaling_type .. "Scaling"]
if (scaling == nil or stat == nil) then return nil end
return scaling[stat]
end


-- ============================================================
-- Mapping from hero name to internal tag name
-- TEST COPY of Module:AbilityTable with the proposed
local TAG_NAME_MAP = {
-- Module:AbilityTable/ComplexRenderers.RenderSpiritScaling inlined below,
["Abrams"] = "Abrams",
-- so the whole change can be previewed from one sandbox module.
["Bebop"] = "Bebop",
-- ============================================================
["Billy"] = "Punkgoat",
["Calico"] = "Nano",
["The Doorman"] = "Doorman",
["Drifter"] = "Drifter",
["Dynamo"] = "Dynamo",
["Apollo"] = "Fencer",
["Graves"] = "Necro",
["Grey Talon"] = "Orion",
["Haze"] = "Haze",
["Holliday"] = "Astro",
["Infernus"] = "Inferno",
["Ivy"] = "Tengu",
["Kelvin"] = "Kelvin",
["Lady Geist"] = "Geist",
["Lash"] = "Lash",
["McGinnis"] = "Engineer",
["Mina"] = "VampireBat",
["Mirage"] = "Mirage",
["Mo & Krill"] = "Digger",
["Paige"] = "Bookworm",
["Paradox"] = "Chrono",
["Celeste"] = "Unicorn",
["Pocket"] = "Synth",
["Rem"] = "Familiar",
["Seven"] = "Gigawatt",
["Shiv"] = "Shiv",
["Silver"] = "Werewolf",
["Silver (Transformed)"] = "Werewolf",
["Sinclair"] = "Magician",
["Venator"] = "Priest",
["Victor"] = "Frank",
["Vindicta"] = "Vindicta",
["Viscous"] = "Viscous",
["Vyper"] = "Viper",
["Warden"] = "Warden",
["Wraith"] = "Wraith",
["Yamato"] = "Yamato",
}


-- ============================================================
--------------------------------------------------------------------------------
-- Spirit scaling helpers
-- Data access, for use by other modules
-- ============================================================
--------------------------------------------------------------------------------


-- Returns the Spirit scaling entry of a prop value, or nil. A prop's Scale is
-- returns the table of a specific item, used by external modules
-- either a single { Value, Type } table or an array of them (a duration that
function p.get_json_item(name)
-- scales with both Spirit and ability duration, for example).
for i,v in pairs(heroes_data) do
local function spirit_scale(v)
if (v["Name"] == name) then
    if type(v) ~= "table" then return nil end
return v
    local scale = v["Scale"]
end
    if type(scale) ~= "table" then return nil end
end
    if scale[1] == nil then
return nil
        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
end


-- Collects the non-zero Spirit scalings of one record layer (the base record
-- Hero table for a hero_* key or an exact English name, or nil.
-- or one upgrade tier) as { key, value, multiply }, ordered the way the
local function hero_by_key_or_name(input)
-- ability card lists its props; props the card does not list come last, in
local hero = heroes_data[input]
-- key order. `labels` is the view's cardprops map (see Module:AbilityTable).
if (hero == nil) then hero = p.get_json_item(input) end
local function spirit_scalings(layer, labels)
return hero
    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
end


-- Formats a scaling value to 3 significant figures, as the ability cards do.
-- Value of a stat, checking the hero's top level, then Weapon, then AltFire.
local function format_spirit(v)
-- Returns nil when the hero has no such stat.
    return string.format("%.3g", v)
function p.get_stat(hero_data, stat)
if (hero_data == nil or stat == nil) then return nil end
local value = hero_data[stat]
if (value == nil and hero_data.Weapon) then
value = hero_data.Weapon[stat]
end
if (value == nil and hero_data.Weapon and hero_data.Weapon.AltFire) then
value = hero_data.Weapon.AltFire[stat]
end
return value
end
end


-- Formats one Spirit scaling for the table cell through the compact {{Ss}}
-- Ordered list of a hero's scalings for a stat, Level first then Spirit:
-- form, the same way RenderBarrier does. The template is expanded via the
--  { { type = "Level", value = 1.5 }, { type = "Spirit", value = 0.04 } }
-- frame the surrounding #invoke runs under, since module return values are
-- Absent and zero scalings are omitted, so an empty list means "no scaling".
-- not re-expanded; the plain "×N" fallback covers a nil frame (module
function p.get_scalings(hero_data, stat)
-- console).
local scalings = {}
local function format_spirit_badge(v)
for _, scaling_type in ipairs({"Level", "Spirit"}) do
    local text  = format_spirit(v)
local value = raw_scaling(hero_data, scaling_type, stat)
    local frame = mw.getCurrentFrame()
if (value ~= nil and value ~= 0) then
    if frame then
table.insert(scalings, { type = scaling_type, value = value })
        return frame:expandTemplate{
end
            title = "Ss",
end
            args  = { text, compact = "1", show_value = "1" },
return scalings
        }
    end
    return "×" .. text
end
end


-- "A", "A and B", "A, B and C".
-- DEPRECATED, kept for [[Module:HeroComparisonTable]]. Use p.get_scalings.
local function join_names(names)
-- Keyed by scaling value, so it silently drops an entry when Level and Spirit
    if #names <= 1 then return names[1] or "" end
-- scale by the same amount, and it keeps zero scalings.
    return table.concat(names, ", ", 1, #names - 1) .. " and " .. names[#names]
function p.get_hero_scaling_data(hero_data, hero_stat_key)
local scaling_data_returned = {}
for _, scaling_type in ipairs({"Spirit", "Level"}) do
local scaling_value = raw_scaling(hero_data, scaling_type, hero_stat_key)
if (scaling_value ~= nil) then
scaling_data_returned[scaling_value] = scaling_type
end
end
return scaling_data_returned
end
end


-- Renders Spirit Power scaling.
-- Whether a hero has a stat at all: a non-zero base value, or any scaling.
-- Lists every prop that scales with Spirit at base, one per line as an {{Ss}}
function p.has_stat(hero_data, stat)
-- badge labelled with the prop's name from the ability card:
local base = p.get_stat(hero_data, stat)
-- "x0.55 (Base Damage)". Two
if (base ~= nil and base ~= 0) then return true end
-- props sharing a name are told apart by the card's Title
return #p.get_scalings(hero_data, stat) > 0
-- ("Damage: Cost of Stay"); props the card does not list show their raw key.
end
-- 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)
-- Resolves a hero_* key or an English hero name to a hero_* key.
    local tiers = {}  -- { tier = i, list = ... } in tier order
-- Returns nil when the input matches no hero. Name matching is case-insensitive.
    local upgrades = ability["Upgrades"]
function p.find_hero_key(input)
    if type(upgrades) == "table" then
if (input == nil or input == '') then return nil end
        -- ipairs rather than #: mw.loadJsonData tables have no length.
if (heroes_data[input] ~= nil) then return input end
        for i, tier in ipairs(upgrades) do
local wanted = mw.ustring.lower(input)
            if type(tier) == "table" then
for hero_key, hero_data in pairs(heroes_data) do
                table.insert(tiers, { tier = i, list = spirit_scalings(tier, labels) })
if (hero_data["Name"] and mw.ustring.lower(hero_data["Name"]) == wanted) then
            end
return hero_key
        end
end
    end
end
return nil
end


    -- Names used by more than one scaling prop of this ability get their Title
-- A hero's Nth roster tag (1-3) by English name, or '' when there is no such tag.
    -- appended so the rows can be told apart.
function p.hero_tag(hero_en, index)
    local keys_by_name = {}
local tagname = hero_en and TAG_NAME_MAP[hero_en]
    local function note_keys(list)
if (tagname == nil) then return '' end
        for _, prop in ipairs(list) do
if (index ~= 1 and index ~= 2 and index ~= 3) then return '' end
            local info = labels[prop.key]
return lang_module.get_string(string.format("Citadel_%s_HeroTag_%d", tagname, index)) or ''
            if info then
end
                keys_by_name[info.name] = keys_by_name[info.name] or {}
                keys_by_name[info.name][prop.key] = true
            end
        end
    end
    note_keys(base)
    for _, t in ipairs(tiers) do note_keys(t.list) end


    local function label_of(key)
--------------------------------------------------------------------------------
        local info = labels[key]
-- Wikitext entry points
        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  = {}
-- returns the key of the specified hero's english name
    local running = {}  -- prop key -> current total scaling
function p.get_hero_key(frame)
 
local hero_name_input = frame.args[1]
    if #base > 0 then
if not hero_name_input then
        local lines = {}
return "Error: Hero name not specified."
        for _, prop in ipairs(base) do
end
            table.insert(lines, format_spirit_badge(prop.value) .. " (" .. label_of(prop.key) .. ")")
return p.find_hero_key(hero_name_input) or "Hero not found."
            running[prop.key] = prop.value
end
        end
        cells["Spirit Scaling"] = table.concat(lines, "<br>")
    end


    local notes = {}
--{{#invoke:HeroData|resolve_key|HERO_KEY_OR_NAME}}--
    for _, t in ipairs(tiers) do
-- The matching hero_* key, or '' when the input matches no hero.
        local i, list = t.tier, t.list
function p.resolve_key(frame)
        local groups, group_order = {}, {}   -- Multiply upgrades, grouped by multiplier
return p.find_hero_key(frame.args[1]) or ''
        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
 
    if #notes > 0 then cells["Notes"] = table.concat(notes, ". ") end
    return cells
end
end


-- get_cells(ability, cardprops) -> { [column header] = cell text, Notes = ... }
--{{#invoke:HeroData|get_hero_var|HERO_NAME|STAT_NAME|sig_figs_or_localize}}--
-- `cardprops` maps the ability's prop keys to their display labels; most
--sig_figs optional for rounding floats
-- renderers ignore it.
p.get_hero_var = function(frame)
local extra_columns = {
     local hero_name = frame.args[1]
     ["barrier"] = {
    local hero_stat_key = frame.args[2]
        headers  = { "Barrier" },
     local sig_figs_or_localize = frame.args[3]
        get_cells = Renderers.RenderBarrier,
      
    },
     local hero = hero_by_key_or_name(hero_name)
    ["bossdmgscale"] = {
     if(hero == nil) then return "Hero Not Found" end
        headers  = { "Damage to Objectives" },
      
        get_cells = Renderers.RenderBossDamageScale,
     local var_value = p.get_stat(hero, hero_stat_key)
    },
     if(var_value == nil) then return 0 end
    ["charges"] = {
      
        headers  = { "Charges", "Time Between Charges", "Notes" },
     --round
        get_cells = Renderers.RenderCharges,
    if (sig_figs_or_localize ~= nil and tonumber(sig_figs_or_localize) ~= nil) then
     },
         var_value = util_module.round_to_sig_fig(var_value, sig_figs_or_localize)
    ["melee"] = {
         if (var_value == nil) then return "get_hero_var() error with rounding" end
        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
-- ============================================================
 
-- 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
     end
     if extra["Notes"] and extra["Notes"]:sub(-1) ~= "." then
   
         extra["Notes"] = extra["Notes"] .. "."
    --localize
     if (sig_figs_or_localize == "true") then
         return lang_module.get_string(var_value)
     end
     end
end


-- Merges auto-generated and human notes for compact mode.
     return var_value
-- 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


-- ============================================================
--{{#invoke:HeroData|get_alt_fire_var|HERO_NAME|STAT_NAME|sig_figs_or_localize}}--
-- Ability views
p.get_alt_fire_var = function(frame)
--
     local hero_name = frame.args[1]
-- In-game Ability may containe 1 or more records in AbilityData.json
     local stat_key = frame.args[2]
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
    local sig_figs_or_localize = frame.args[3]
 
   
-- "Ability View" is a main-record + any relevant sub-ability records.
    local hero = hero_by_key_or_name(hero_name)
-- Each view entry also carries "cardprops", the display labels of the
    if(hero == nil) then return "Hero Not Found" end
-- 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
    if not (hero.Weapon and hero.Weapon.AltFire) then
-- walks the whole record, never sees them.
         return "No Alt Fire"
-- ============================================================
 
-- 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
 
    for _, section in ipairs({ "Info1", "Info2", "Info3" }) do
        walk(card[section])
     end
     end
     local rest = {}
   
     for k, v in pairs(card) do
     local var_value = hero.Weapon.AltFire[stat_key]
        if type(v) == "table" and k ~= "Upgrades"
     if(var_value == nil) then return 0 end
            and k ~= "Info1" and k ~= "Info2" and k ~= "Info3"
   
         then
    --round
            table.insert(rest, k)
    if (sig_figs_or_localize ~= nil and tonumber(sig_figs_or_localize) ~= nil) then
        end
         var_value = util_module.round_to_sig_fig(var_value, sig_figs_or_localize)
     end
     end
     table.sort(rest, function(a, b) return tostring(a) < tostring(b) end)
      
    for _, k in ipairs(rest) do walk(card[k]) end
     --localize
 
     if (sig_figs_or_localize == "true") then
     -- Header attributes are stored on the card without a Name.
         return lang_module.get_string(var_value)
     local header_attrs = {
        { "AbilityCastRange",            "Cast Range" },
         { "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


     return labels
     return var_value
end
end


local _views = nil  -- card key -> { view = record, heroName = ..., abilityNum = ..., cardprops = ... }
--{{#invoke:HeroData|get_list_elem|HERO_NAME|VAR|NUMBER|LOCALIZE}}
local _order = nil   -- array of card keys, sorted by hero then slot
p.get_list_elem = function(frame)
local hero_name = frame.args[1]
local var = frame.args[2]
local number_int = tonumber(frame.args[3])
local localize_arg = frame.args[4]
local hero = hero_by_key_or_name(hero_name)
if(hero == nil) then return "Hero Not Found" end
local list = p.get_stat(hero, var)
if (list == nil) then return "" end
local element = list[number_int]
if (element == nil) then return "" end
if localize_arg=="true" then  
element = lang_module.get_string(element)
end
return element
end


local function build_views()
p.get_ability_key = function(frame)
    if _views then return _views, _order end
local hero_key = frame.args[1]
    _views, _order = {}, {}
local bound_slot_number = frame.args[2]
local hero_data = heroes_data[hero_key]
if (hero_data == nil) then return "Hero key "..hero_key.. " not found" end
local bound_abilities_data = hero_data["BoundAbilities"]
if (bound_abilities_data == nil) then return "Hero key " .. hero_key.. " has no BoundAbilities" end
return bound_abilities_data[tonumber(bound_slot_number)]["Key"]
end


    local hero_data = mw.loadJsonData("Data:HeroData.json")
p.write_role_playstyle_quote = function(frame)
    local active_heroes = {}
local hero_key = frame.args[1]
    for _, hero in pairs(hero_data) do
local hero_data = heroes_data[hero_key]
        if type(hero) == "table"
if (hero_data == nil) then return hero_key.." not found" end
            and hero["IsDisabled"] == false
local role_key = hero_data["Role"]
            and type(hero["Name"]) == "string"
local role_localized = lang_module.get_string(role_key, nil, 'en')
            and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false)
local playstyle_key = hero_data["Playstyle"]
        then
local playstyle_localized = lang_module.get_string(playstyle_key, nil, 'en')
            active_heroes[hero["Name"]] = true
        end
local str = "<b>" .. role_localized .. '</b><br>' .. playstyle_localized
    end
local template_args = {}
template_args[1] = ""
template_args[2] = str
return frame:expandTemplate{title = 'Quotation', args = template_args}
end


    local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
p.write_default_items = function(frame)
    local cards        = mw.loadJsonData("Data:AbilityCards.json")
local hero_key = frame.args[1] --unlocalized
 
if (hero_key == nil) then return "No hero key provided" end
    local card_of = {}
local str = ""
    for _, hero_entry in pairs(cards) do
local hero = heroes_data[hero_key]
        if type(hero_entry) == "table" then
if (hero == nil) then return "Hero not found, must be unlocalized" end
            local hero_name = hero_entry["Name"]
local template_title = 'PageRef'
            if hero_name and active_heroes[hero_name] then
for i, item_key in ipairs(hero["RecommendedItems"] or {}) do
                for slot_str, ability in pairs(hero_entry) do
local template_args = {}
                    local slot_num = tonumber(slot_str)
template_args[1] = lang_module.get_string(item_key, 'en')
                    local key = type(ability) == "table" and ability["Key"] or nil
template_args['alt_name'] = localize(item_key, item_key)
                    if slot_num and key and ability_data[key] then
local expanded_template = mw.getCurrentFrame():expandTemplate{ title = template_title, args = template_args }
                        card_of[key] = { heroName = hero_name, abilityNum = slot_num, card = ability }
str = str .. "* " .. expanded_template .. "\n"
                    end
end
                end
return str
            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
 
    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


-- ============================================================
--If the hero scales with the stat, it returns {{Ss|value}} or {{Ls|value}}, else blank string
-- Membership
--{{#invoke:HeroData|get_hero_scalar|HERO_NAME|SCALING_TYPE|STAT_NAME|sig_figs_or_localize}}--
-- Pieces of code that help in telling  "is ability X a member of list Y".
p.get_hero_scalar = function(frame)
-- main function is: p.is_member()
local hero_key = frame.args[1]
-- ============================================================
local scaling_type = frame.args[2]
local hero_stat_key = frame.args[3]
local sig_figs_or_localize = frame.args[4]
local no_template = frame.args["no_template"]
local hero_data = hero_by_key_or_name(hero_key)
if(hero_data == nil) then return "Hero not found." end
local scaling_value = raw_scaling(hero_data, scaling_type, hero_stat_key)
if scaling_value == nil then return "" end


-- Memoized name-exclusion set per list (from Lists.exclude_abilities).
--round
local _excluded = {}
if (sig_figs_or_localize ~= nil and tonumber(sig_figs_or_localize) ~= nil) then
local function excluded_set(stat)
scaling_value = util_module.round_to_sig_fig(scaling_value, sig_figs_or_localize)
    if _excluded[stat] then return _excluded[stat] end
if (scaling_value == nil) then return "get_hero_scalar() error with rounding" end
    local set = {}
end
    for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do
        set[name] = true
--localize
    end
if (sig_figs_or_localize == "true") then
    _excluded[stat] = set
return lang_module.get_string(scaling_value)
    return set
end
if no_template == "true" then
return scaling_value
end
return p.write_scalar_str(scaling_value, scaling_type)
end
end


-- Per-list rules that can't be expressed as prop lists or exclusions.
-- Check if a hero has a stat (Base, Boons, or Spirit Scaling)
-- Returns true to keep the ability, false to drop it.
-- Returns "true" if found, nil if not (for #if templates)
local function special_ok(stat, record, ability_num)
p.hero_has_stat = function(frame)
    if stat == "dispelmagic" and ability_num == 4 then return false end
local hero_key = frame.args[1]
    return true
local stat_key = frame.args[2]
if (hero_key == nil) then return nil end
local hero_data = hero_by_key_or_name(hero_key)
if (hero_data == nil) then return nil end
if p.has_stat(hero_data, stat_key) then return "true" end
return nil
end
end


-- Public: does ability `ability_key` belong to list `stat`?
-- Function to call hero tags based on name
function p.is_member(stat, ability_key, ability_num)
p.get_hero_tag = function (frame)
    local props = Lists.lists[stat]
local name = frame.args[1]
    if not props then return false end
local number = tonumber(frame.args[2])


    local views = build_views()
-- Validate input
    local entry = views[ability_key]
if not name then
    local view  = entry and entry.view
return "Error: Missing name input."
end


    if not view then
if number ~= 1 and number ~= 2 and number ~= 3 then
        view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]
return string.format("Error: Invalid number '%s'. Must be 1, 2, or 3.", tostring(frame.args[2]))
        if not view then return false end
end
    end


    local name = view["Name"]
if TAG_NAME_MAP[name] == nil then
    if name and excluded_set(stat)[name] then return false end
return string.format("Error: Unknown name '%s'.", name)
end


    ability_num = ability_num or (entry and entry.abilityNum)
return p.hero_tag(name, number)
    if not special_ok(stat, view, ability_num) then return false end
end


    return GameData.entity_matches(view, props)
--{{#invoke:HeroData|has_tags|HERO_ENGLISH_NAME}}--
-- 'true' when the hero has roster tags, '' otherwise.
function p.has_tags(frame)
if p.hero_tag(frame.args[1], 1) ~= '' then return 'true' end
return ''
end
end


-- ============================================================
-- Retrieve scaling string of a hero's given stat, if it has scaling, else return blank
-- Entry point
-- Scaling string meaning the expanded template {{Ss|scalar}} or {{Ls|scalar}}
-- ============================================================
function p.write_scalar_str(scaling_value, scaling_type, compact)
local scaling_abbrevs = {Spirit = "Ss", Level = "PI"}
-- Return blank if it doesnt scale
if (scaling_value == 0) then return "" end
-- Round it
scaling_value = util_module.round_to_sig_fig(scaling_value, 3)
--The hero has a scaling value with this stat
local template_title = "Template:" .. scaling_abbrevs[scaling_type]
local template_args = {}
template_args["1"] = scaling_value
if compact then
template_args["compact"] = "yes"
template_args["show_value"] = "yes"
end
local template_call = mw.getCurrentFrame():expandTemplate{ title = template_title, args = template_args }
   
return template_call:gsub("\n", ""):gsub("\r", "")
end


function p.render(frame)
-- Outputs a wikitable of heroes grouped by GroundDashDuration using {{HeroIcon|hero_name}}
     local stat = mw.text.trim(frame.args[1] or ""):lower()
-- {{#invoke:HeroData|write_ground_dash_buckets}}
p.write_ground_dash_buckets = function(frame)
     local buckets = {
        [0.62] = {},
        [0.68] = {},
        [0.72] = {},
    }


     local internal_keys = Lists.lists[stat]
     -- Collect heroes
     if not internal_keys then
     for hero_key, hero_data in pairs(heroes_data) do
        return '<span class="error">AbilityTable: unknown list "' .. stat .. '".</span>'
        if not hero_data.InDevelopment and not hero_data.IsDisabled then
            local dash = p.get_stat(hero_data, "GroundDashDuration")
            if dash and buckets[dash] then
                table.insert(buckets[dash], hero_data.Name or hero_key)
            end
        end
     end
     end


     local hide_set = {}
     -- Sort alphabetically
     local hide_param = mw.text.trim(frame.args["hide"] or "")
     for _, list in pairs(buckets) do
    if hide_param ~= "" then
        table.sort(list)
        for col in hide_param:gmatch("[^,]+") do hide_set[mw.text.trim(col)] = true end
     end
     end


     local compact = mw.text.trim(frame.args["compact"] or "") ~= ""
    -- Determine max rows dynamically
     local max_rows = 0
    for _, list in pairs(buckets) do
        if #list > max_rows then max_rows = #list end
    end


     local collapsed = mw.text.trim(frame.args["collapsed"] or ""):lower()
     local output = {"{| class='wikitable mw-collapsible'",
    "|+Hero Dash Buckets", "! Bucket 1 !! Bucket 2 !! Bucket 3"}
    local current_frame = frame or mw.getCurrentFrame()


     local views, order = build_views()
     for i = 1, max_rows do
    local spec      = extra_columns[stat]
        local row = {}
    local headers    = spec and spec.headers or {}
        for _, bucket in ipairs({0.62, 0.68, 0.72}) do
    local get_cells  = spec and spec.get_cells or function() return {} end
            local hero_name = buckets[bucket][i]
    local stat_notes = Notes[stat]
             table.insert(row, hero_name and current_frame:expandTemplate{title = "HeroIcon", args = {hero_name}} or "")
 
    local filtered = {}
    for _, key in ipairs(order) do
        if p.is_member(stat, key) then
             table.insert(filtered, key)
         end
         end
        table.insert(output, "|-\n| " .. table.concat(row, " || "))
     end
     end


     if #filtered == 0 then
     table.insert(output, "|}")
        return "No abilities found for list: " .. stat
    return table.concat(output, "\n")
    end
end
--------------------------------------------------------------------------------
-- TEMPORARY parity harness - NOT part of the live module.
-- {{#invoke:Sandbox/Vergir|test|SUITE}} where SUITE is one of:
--  stats | list | scalars | scalings | has_stat | keys | tags
--------------------------------------------------------------------------------


    local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
local old_module = require('Module:HeroData')
    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 function repr(v, depth)
        local hero_order    = {}
depth = depth or 0
        local hero_abilities = {}
if type(v) ~= 'table' then return tostring(v) end
        for _, key in ipairs(filtered) do
if depth > 3 then return '<deep>' end
            local hero = views[key].heroName
local entries = {}
            if not hero_abilities[hero] then
for k, val in pairs(v) do
                table.insert(hero_order, hero)
table.insert(entries, { k = tostring(k), v = val })
                hero_abilities[hero] = {}
end
            end
table.sort(entries, function(a, b) return a.k < b.k end)
            table.insert(hero_abilities[hero], views[key])
local parts = {}
        end
for _, e in ipairs(entries) do
table.insert(parts, e.k .. '=' .. repr(e.v, depth + 1))
end
return '{' .. table.concat(parts, ',') .. '}'
end


        local items = {}
-- Calls fn(...) and returns a normalised string, so that a raised error on one
        for _, hero in ipairs(hero_order) do
-- side compares equal to the same error on the other (line numbers stripped).
            local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
local function attempt(fn, ...)
            local ability_parts = {}
local ok, result = pcall(fn, ...)
            for _, entry in ipairs(hero_abilities[hero]) do
if ok then return repr(result) end
                local ability = entry.view
return 'ERR:' .. tostring(result):gsub("^.-:%d+: ", "")
                local name = ability["Name"]
end
                local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } }


                local extra = get_cells(ability, entry.cardprops)
local function sorted_hero_keys()
                enrich_notes_compact(extra, stat_notes and stat_notes[name])
local list = {}
for hero_key in pairs(heroes_data) do table.insert(list, hero_key) end
table.sort(list)
return list
end


                local note = extra["Notes"]
-- Every key appearing anywhere in the hero data, so sweeps are exhaustive.
                if note then
local function all_stat_keys()
                    table.insert(ability_parts, ability_cell .. " (" .. note .. ")")
local seen = {}
                else
for _, hero in pairs(heroes_data) do
                    table.insert(ability_parts, ability_cell)
local sources = {
                end
hero,
            end
hero.Weapon or {},
            table.insert(items, "<li>" .. hero_cell .. ": " .. table.concat(ability_parts, ", ") .. "</li>")
(hero.Weapon and hero.Weapon.AltFire) or {},
        end
hero.LevelScaling or {},
hero.SpiritScaling or {},
}
for _, source in ipairs(sources) do
for stat in pairs(source) do
if type(stat) == 'string' then seen[stat] = true end
end
end
end
local list = {}
for stat in pairs(seen) do table.insert(list, stat) end
table.sort(list)
return list
end


        return disclaimer .. "<ul>" .. table.concat(items) .. "</ul>"
local MAX_ROWS = 50
    end


    local has_notes_header = false
local function report(title, checked, mismatches)
    local final_headers = {}
local out = {
    for _, h in ipairs(headers) do
"'''" .. title .. "'''",
        if h == "Notes" then
'',
            has_notes_header = true
'Comparisons: ' .. checked,
        elseif not hide_set[h] then
'Mismatches: ' .. #mismatches,
            table.insert(final_headers, h)
'',
        end
}
    end
if #mismatches == 0 then
    local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set["Notes"]
table.insert(out, ':<span style="color:#13f278">No differences.</span>')
    if needs_notes then table.insert(final_headers, "Notes") end
return table.concat(out, '\n')
end
table.insert(out, '{| class="wikitable"')
table.insert(out, '! Case !! old !! new')
for i, row in ipairs(mismatches) do
if i > MAX_ROWS then
table.insert(out, '|-\n| colspan="3" | ... and ' .. (#mismatches - MAX_ROWS) .. ' more')
break
end
table.insert(out, '|-\n| <code>' .. row[1] .. '</code> || <code>' ..
mw.text.nowiki(row[2]) .. '</code> || <code>' .. mw.text.nowiki(row[3]) .. '</code>')
end
table.insert(out, '|}')
return table.concat(out, '\n')
end


    local table_class = 'wikitable sortable ability-table'
local suites = {}
    if collapsed ~= "" then
        table_class = table_class .. ' mw-collapsible'
        if collapsed == "yes" or collapsed == "true" or collapsed == "1" then
            table_class = table_class .. ' mw-collapsed'
        end
    end


    local out = {}
-- p.get_stat, via get_hero_var
    table.insert(out, disclaimer)
suites.stats = function()
    table.insert(out, '{| class="' .. table_class .. '"')
local checked, mismatches = 0, {}
local stat_keys = all_stat_keys()
for _, hero_key in ipairs(sorted_hero_keys()) do
for _, stat in ipairs(stat_keys) do
local frame = { args = { hero_key, stat } }
local a = attempt(old_module.get_hero_var, frame)
local b = attempt(p.get_hero_var, frame)
checked = checked + 1
if a ~= b then
table.insert(mismatches, { hero_key .. ' / ' .. stat, a, b })
end
end
end
return report('get_hero_var (p.get_stat)', checked, mismatches)
end


    local header_row = '! class="ability-table-col-hero" | Hero'
-- p.get_stat, via get_list_elem
                    .. ' !! class="ability-table-col-ability" | Ability'
suites.list = function()
    for _, h in ipairs(final_headers) do
local checked, mismatches = 0, {}
        header_row = header_row .. ' !! class="ability-table-col-'
local stat_keys = all_stat_keys()
            .. h:lower():gsub("%s+", "-") .. '" | ' .. h
for _, hero_key in ipairs(sorted_hero_keys()) do
    end
for _, stat in ipairs(stat_keys) do
    table.insert(out, header_row)
for index = 1, 2 do
local frame = { args = { hero_key, stat, tostring(index) } }
local a = attempt(old_module.get_list_elem, frame)
local b = attempt(p.get_list_elem, frame)
checked = checked + 1
if a ~= b then
table.insert(mismatches, { hero_key .. ' / ' .. stat .. ' [' .. index .. ']', a, b })
end
end
end
end
return report('get_list_elem (p.get_stat)', checked, mismatches)
end


        local hero_counts = {}
-- raw_scaling, via get_hero_scalar (both template and no_template modes)
    for _, key in ipairs(filtered) do
suites.scalars = function()
        local hero = views[key].heroName
local checked, mismatches = 0, {}
        hero_counts[hero] = (hero_counts[hero] or 0) + 1
local stat_keys = all_stat_keys()
    end
for _, hero_key in ipairs(sorted_hero_keys()) do
for _, stat in ipairs(stat_keys) do
for _, scaling_type in ipairs({ 'Level', 'Spirit' }) do
for _, no_template in ipairs({ 'true', '' }) do
local frame = { args = { hero_key, scaling_type, stat, no_template = no_template } }
local a = attempt(old_module.get_hero_scalar, frame)
local b = attempt(p.get_hero_scalar, frame)
checked = checked + 1
if a ~= b then
table.insert(mismatches, {
hero_key .. ' / ' .. scaling_type .. ' / ' .. stat ..
(no_template == 'true' and ' (raw)' or ''), a, b })
end
end
end
end
end
return report('get_hero_scalar (raw_scaling)', checked, mismatches)
end


    local hero_seen = {}
-- The deprecated shim must stay byte-identical for Module:HeroComparisonTable
suites.scalings = function()
local checked, mismatches = 0, {}
local stat_keys = all_stat_keys()
for _, hero_key in ipairs(sorted_hero_keys()) do
local hero_data = heroes_data[hero_key]
for _, stat in ipairs(stat_keys) do
local a = attempt(old_module.get_hero_scaling_data, hero_data, stat)
local b = attempt(p.get_hero_scaling_data, hero_data, stat)
checked = checked + 1
if a ~= b then
table.insert(mismatches, { hero_key .. ' / ' .. stat, a, b })
end
end
end
return report('get_hero_scaling_data (deprecated shim)', checked, mismatches)
end


    for _, key in ipairs(filtered) do
-- EXPECTED to differ: new version is Weapon/AltFire aware
        local entry  = views[key]
suites.has_stat = function()
        local ability = entry.view
local checked, mismatches = 0, {}
        local hero    = entry.heroName
local stat_keys = all_stat_keys()
for _, hero_key in ipairs(sorted_hero_keys()) do
for _, stat in ipairs(stat_keys) do
local frame = { args = { hero_key, stat } }
local a = attempt(old_module.hero_has_stat, frame)
local b = attempt(p.hero_has_stat, frame)
checked = checked + 1
if a ~= b then
table.insert(mismatches, { hero_key .. ' / ' .. stat, a, b })
end
end
end
return report('hero_has_stat (EXPECTED to widen)', checked, mismatches)
end


        local ability_cell = frame:expandTemplate{
-- EXPECTED to differ: new version also accepts a hero_* key
            title = "AbilityIcon",
suites.keys = function()
            args = { ability["Name"] }
local checked, mismatches = 0, {}
        }
local inputs = {}
 
for _, hero_key in ipairs(sorted_hero_keys()) do
        local extra = get_cells(ability, entry.cardprops)
table.insert(inputs, hero_key)
        enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]])
local name = heroes_data[hero_key]['Name']
 
if name then
        local row
table.insert(inputs, name)
 
table.insert(inputs, mw.ustring.upper(name))
        if not hero_seen[hero] then
table.insert(inputs, mw.ustring.lower(name))
            local hero_cell = frame:expandTemplate{
end
                title = "HeroIcon",
end
                args = { hero }
for _, extra in ipairs({ '', ' ', 'Not A Hero', 'hero_nope' }) do
            }
table.insert(inputs, extra)
 
end
            if hero_counts[hero] > 1 then
for _, input in ipairs(inputs) do
                row = '| rowspan="' .. hero_counts[hero] .. '" class="ability-table-cell-hero" | '
local frame = { args = { input } }
                    .. hero_cell
local a = attempt(old_module.get_hero_key, frame)
                    .. ' || class="ability-table-cell-ability" | ' .. ability_cell
local b = attempt(p.get_hero_key, frame)
            else
checked = checked + 1
                row = '| class="ability-table-cell-hero" | '
if a ~= b then
                    .. hero_cell
table.insert(mismatches, { input, a, b })
                    .. ' || class="ability-table-cell-ability" | ' .. ability_cell
end
            end
end
 
return report('get_hero_key (EXPECTED to widen)', checked, mismatches)
            hero_seen[hero] = true
end
        else
            row = '| class="ability-table-cell-ability" | ' .. ability_cell
        end


        for _, header in ipairs(final_headers) do
-- get_hero_tag must be unchanged; the has_tags predicate may narrow
            local value = extra[header]
suites.tags = function()
            local cls  = "ability-table-cell-" .. header:lower():gsub("%s+", "-")
local checked, mismatches = 0, {}
            local content = value ~= nil and tostring(value) or "—"
local names = {}
            row = row .. ' || class="' .. cls .. '" | ' .. content
for _, hero_key in ipairs(sorted_hero_keys()) do
        end
local name = heroes_data[hero_key]['Name']
if name then table.insert(names, name) end
end
for _, extra in ipairs({ 'Not A Hero', '' }) do table.insert(names, extra) end


        table.insert(out, "|-")
for _, name in ipairs(names) do
        table.insert(out, row)
for _, index in ipairs({ '0', '1', '2', '3', '4', 'x' }) do
    end
local frame = { args = { name, index } }
 
local a = attempt(old_module.get_hero_tag, frame)
    table.insert(out, "|}")
local b = attempt(p.get_hero_tag, frame)
    return table.concat(out, "\n")
checked = checked + 1
if a ~= b then
table.insert(mismatches, { name .. ' [' .. index .. ']', a, b })
end
end
-- Predicate: old Infobox_hero.hero_has_tags vs new p.has_tags
local old_tag = old_module.get_hero_tag({ args = { name, '1' } })
local old_pred = tostring(string.find(old_tag, 'Error:') == nil)
local new_pred = tostring(p.has_tags({ args = { name } }) == 'true')
checked = checked + 1
if old_pred ~= new_pred then
table.insert(mismatches, { 'PREDICATE ' .. name, old_pred, new_pred })
end
end
return report('get_hero_tag + has_tags predicate', checked, mismatches)
end
end


-- TEST ONLY: plain-text dump of a list for review, one line per ability.
function p.test(frame)
function p.dump(frame)
local suite = frame.args[1]
    local stat = mw.text.trim(frame.args[1] or ""):lower()
if suite == nil or suites[suite] == nil then
    local views, order = build_views()
local names = {}
    local spec = extra_columns[stat]
for name in pairs(suites) do table.insert(names, name) end
    local get_cells = spec and spec.get_cells or function() return {} end
table.sort(names)
    local out = {}
return 'Pick a suite: ' .. table.concat(names, ', ')
    for _, key in ipairs(order) do
end
        if p.is_member(stat, key) then
return suites[suite]()
            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: