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


local Lists    = require("Module:AbilityTable/Lists")
-- Just get heroes that are playable and have a "name" in HeroData
local Notes    = require("Module:AbilityTable/Notes")
for i, heroData in pairs(allHeroData) do
local Renderers = require("Module:AbilityTable/ComplexRenderers")
if heroData["InDevelopment"] == false then
local GameData  = require("Module:GameData")
     if heroData["IsDisabled"] == false then
 
     if heroData["Name"] ~= nil then
local p = {}
        table.insert(inGameHeroData, heroData)
 
        end
-- ============================================================
-- 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
-- ============================================================
 
-- Returns the Spirit scaling entry of a prop value, or nil. A prop's Scale is
-- either a single { Value, Type } table or an array of them (a duration that
-- scales with both Spirit and ability duration, for example).
local function spirit_scale(v)
    if type(v) ~= "table" then return nil end
    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
-- 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
     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
end


-- Formats one Spirit scaling for the table cell through the compact {{Ss}}
-- Use sort name for edge cases like "The Doorman"
-- form, the same way RenderBarrier does. The template is expanded via the
local function sortHeroes(rows)
-- frame the surrounding #invoke runs under, since module return values are
     local function getSortName(name)
-- not re-expanded; the plain "×N" fallback covers a nil frame (module
         return (name:gsub("^The ", ""))
-- 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
     end
    return "×" .. text
end


-- "A", "A and B", "A, B and C".
     table.sort(rows, function(a, b)
local function join_names(names)
        local nameA = getSortName(a.sortName or a.name or "")
    if #names <= 1 then return names[1] or "" end
         local nameB = getSortName(b.sortName or b.name or "")
     return table.concat(names, ", ", 1, #names - 1) .. " and " .. names[#names]
         return nameA:lower() < nameB:lower()
end
     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
 
    -- 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
    note_keys(base)
    for _, t in ipairs(tiers) do note_keys(t.list) 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
        cells["Spirit Scaling"] = table.concat(lines, "<br>")
    end
 
    local notes = {}
    for _, t in ipairs(tiers) do
        local i, list = t.tier, t.list
        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
 
    if #notes > 0 then cells["Notes"] = table.concat(notes, ". ") end
    return cells
end
end


-- get_cells(ability, cardprops) -> { [column header] = cell text, Notes = ... }
-- Helper function to round numeric values
-- `cardprops` maps the ability's prop keys to their display labels; most
local function roundValue(value)
-- renderers ignore it.
     if type(value) == "number" then
local extra_columns = {
         return util_module.round_to_sig_fig(value, 3)
    ["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
-- ============================================================
 
-- 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
    return value
end
end


-- Merges auto-generated and human notes for compact mode.
-- Round to an integer (up to 2 digits)
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
local function roundValueInteger(value)
-- joins with "; ", strips trailing period.
     if type(value) == "number" then
local function enrich_notes_compact(extra, human_note)
         return util_module.round_to_sig_fig(value, 2)
     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
    return value
end
end


-- ============================================================
-- Generate tables from given properties
-- Ability views
local function buildTable(frame, rows, tableDef, options)
--
    options = options or {}
-- In-game Ability may containe 1 or more records in AbilityData.json
    -- option to ignore heroes who don't have any of the given properties. true by default
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
    local filterZero = options.filterZero or true


-- "Ability View" is a main-record + any relevant sub-ability records.
    local filteredRows = {}
-- 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
    for _, r in ipairs(rows) do
-- from the Info sections of the card in AbilityCards.json. The flat
        local hasValue = not filterZero
-- 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 filterZero then
         if type(node) ~= "table" then return end
             for _, col in ipairs(tableDef.columns) do
        local key, name = node["Key"], node["Name"]
                 if r[col.field] ~= 0 and r[col.field] ~= nil then
        if type(key) == "string" and type(name) == "string" and not labels[key] then
                if r[col.field] ~= "+0" then
            n = n + 1
                    hasValue = true
            local title = node["Title"]
                    break
            if type(title) == "string" then
                     end
                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
             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


    for _, section in ipairs({ "Info1", "Info2", "Info3" }) do
         if hasValue then
        walk(card[section])
             table.insert(filteredRows, r)
    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.
     if #filteredRows == 0 then
    local header_attrs = {
         return ""
        { "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
     -- Build header
end
     local header = ""
 
     for _, col in ipairs(tableDef.columns) do
local _views = nil  -- card key -> { view = record, heroName = ..., abilityNum = ..., cardprops = ... }
         header = header .. " !! " .. col.label
local _order = nil  -- array of card keys, sorted by hero then slot
 
local function build_views()
    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
     end


     local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
    -- Start table
    local cards        = mw.loadJsonData("Data:AbilityCards.json")
     local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n" ..
        "|+ " .. tableDef.title .. " \n" ..
        "! Hero" .. header .. " \n"


     local card_of = {}
     -- Rows
     for _, hero_entry in pairs(cards) do
     for _, r in ipairs(filteredRows) do
         if type(hero_entry) == "table" then
         local cells = ""
            local hero_name = hero_entry["Name"]
        for _, col in ipairs(tableDef.columns) do
            if hero_name and active_heroes[hero_name] then
            cells = cells .. " || " .. (r[col.field] or 0)
                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


    -- Attach each non-card record to the longest card key it prefixes.
        -- Rows may carry an alternate sort key and a label suffix, used for
    local subs = {}
        -- secondary fire rows listed underneath their hero's main row
    for key, record in pairs(ability_data) do
        local nameCell = "| style=\"text-align:left;\""
         if not card_of[key] and record["IsDisabled"] ~= true then
         if r.sortName then
             local parent = nil
             nameCell = nameCell .. " data-sort-value=\"" .. r.sortName .. "\""
            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
        nameCell = nameCell .. " | " ..
            frame:expandTemplate{ title = "PageRef", args = { r.name } } ..
            (r.nameSuffix or "")


    for key, info in pairs(card_of) do
         t = t ..
         local view = {}
            "|- \n" ..
        for k, v in pairs(ability_data[key]) do view[k] = v end
             nameCell ..
        view["_subabilities"] = subs[key]
             cells .. " \n"
        _views[key] = {
            view      = view,
             heroName  = info.heroName,
            abilityNum = info.abilityNum,
             cardprops  = card_prop_labels(info.card),
        }
        table.insert(_order, key)
     end
     end


     table.sort(_order, function(a, b)
     t = t .. "|}"
        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
     return t
end
end


-- ============================================================
-- heroDataArray takes as argument a table of any length containing keys or
-- Membership
-- paths of keys in the HeroData JSON, e.g. {Name, MaxHealth, LevelScaling>Health}.
-- Pieces of code that help in telling  "is ability X a member of list Y".
-- A path of keys should be specified as such, with the ">" character separating
-- main function is: p.is_member()
-- subsequent keys.
-- ============================================================
local heroDataArray = function(args)
 
    local outData = {}
-- Memoized name-exclusion set per list (from Lists.exclude_abilities).
   
local _excluded = {}
    for i, heroData in ipairs(inGameHeroData) do            -- Iterate over each hero
local function excluded_set(stat)
        local out = {}                                      -- First creating a table to hold data
    if _excluded[stat] then return _excluded[stat] end
        for j, key in pairs(args) do                        -- Iterate over each key:
    local set = {}
            if string.find(key, ">") ~= nil then            -- If the key is actually a path of keys,
    for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do
                local node = heroData                      -- start with the biggest node heroData, and
         set[name] = true
                for k in string.gmatch(key, "([^>]+)") do  -- iterate over each key, splitting on ">",
                    if node and node[k] ~= nil then        -- trying the key as a string e.g. "1", (Added 'node and' for safety)
                        node = node[k]                     
                    elseif node and node[tonumber(k)] ~= nil then -- else using the key as numeric, (Added 'node and' for safety)
                        node = node[tonumber(k)]
                    else                                    -- If at any point the path breaks, the result is nil
                        node = nil
                        break
                    end                                     -- drilling down the JSON node after node
                end
                out[key] = roundValue(node)                -- until outputting the final node (rounded)
            else
                out[key] = roundValue(heroData[key])        -- or just grab the data if not a path (rounded)
            end
        end
         outData[heroData["Name"]] = out                    -- Save data in the big table of heroes
     end
     end
    _excluded[stat] = set
     return(outData)
     return set
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
end


-- Public: does ability `ability_key` belong to list `stat`?
p.heroBulletDamageTable = function(frame)
function p.is_member(stat, ability_key, ability_num)
     -- Pull data
    local props = Lists.lists[stat]
     local heroBulletDamageData = heroDataArray({"Name", "Weapon>BulletDamage", "LevelScaling>BulletDamage", "Weapon>AltFire>BulletDamage", "LevelScaling>BulletDamageAltFire",
    if not props then return false end
    "SpiritScaling>BulletDamage", "Weapon>BulletsPerShot", "Weapon>BulletsPerBurst"})
 
    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
 
    local name = view["Name"]
    if name and excluded_set(stat)[name] then return false end


     ability_num = ability_num or (entry and entry.abilityNum)
     local rows = {}
    if not special_ok(stat, view, ability_num) then return false end
    for name, data in pairs(heroBulletDamageData) do
        table.insert(rows, {
            name = name,
            bulletDamage = data["Weapon>BulletDamage"] or 0,
            bulletDamagePerShot = ((data["Weapon>BulletDamage"] or 0) * (data["Weapon>BulletsPerShot"] or 0) * (data["Weapon>BulletsPerBurst"] or 0)),
            scalingDamage = "+" .. data["LevelScaling>BulletDamage"] or 0,
            maxScalingDamage =  (roundValue(data["Weapon>BulletDamage"] + data["LevelScaling>BulletDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
            altBulletDamage = data["Weapon>AltFire>BulletDamage"] or 0,
            spiritScalingDamage = "+" .. (data["SpiritScaling>BulletDamage"] or 0),
        })


    return GameData.entity_matches(view, props)
        -- Secondary fire is listed as its own row, right under the hero's main row
end
        local altDamage = data["Weapon>AltFire>BulletDamage"]
 
        if altDamage and altDamage ~= 0 then
-- ============================================================
            local altScaling = data["LevelScaling>BulletDamageAltFire"] or 0
-- Entry point
            table.insert(rows, {
-- ============================================================
                name = name,
 
                sortName = name .. " (alt fire)",
function p.render(frame)
                nameSuffix = " (alt fire)",
    local stat = mw.text.trim(frame.args[1] or ""):lower()
                bulletDamage = altDamage,
 
                scalingDamage = "+" .. altScaling,
    local internal_keys = Lists.lists[stat]
                maxScalingDamage = roundValue(altDamage + altScaling * soul_unlock.get_max("PowerIncrease")),
    if not internal_keys then
                spiritScalingDamage = "+0",
        return '<span class="error">AbilityTable: unknown list "' .. stat .. '".</span>'
             })
    end
 
    local hide_set = {}
    local hide_param = mw.text.trim(frame.args["hide"] or "")
    if hide_param ~= "" then
        for col in hide_param:gmatch("[^,]+") do hide_set[mw.text.trim(col)] = true end
    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 = {}
    for _, key in ipairs(order) do
        if p.is_member(stat, key) then
             table.insert(filtered, key)
         end
         end
     end
     end
    sortHeroes(rows)


     if #filtered == 0 then
     local tableDefs = {
        return "No abilities found for list: " .. stat
    {
    end
            title  = "Base Bullet Damage Stats",
 
            columns = {
    local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
                { label = "Starting", field = "bulletDamage" },
    local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" })
                { label = "Added per Boon", field = "scalingDamage" },
    local disclaimer = frame:preprocess(
                { label = "At Max Boon", field = "maxScalingDamage" },
        '<div class="ability-table-disclaimer">'
                { label = "Spirit Scaling", field = "spiritScalingDamage" },
        .. '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 output = {}
        local hero_order    = {}
    for _, def in ipairs(tableDefs) do
        local hero_abilities = {}
        local t = buildTable(frame, rows, def, def.options)
        for _, key in ipairs(filtered) do
        if t ~= "" then
            local hero = views[key].heroName
             table.insert(output, t)
            if not hero_abilities[hero] then
                table.insert(hero_order, hero)
                hero_abilities[hero] = {}
            end
             table.insert(hero_abilities[hero], views[key])
         end
         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
     end


     local has_notes_header = false
     return table.concat(output, "\n\n")
    local final_headers = {}
end
    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
    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'
    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 = {}
-- Kept for regression testing: an untouched table that also goes through
    table.insert(out, disclaimer)
-- buildTable() and sortHeroes()
     table.insert(out, '{| class="' .. table_class .. '"')
p.heroHealthTable = function(frame)
     -- Pull data
    local heroHealthData = heroDataArray({"Name", "MaxHealth", "LevelScaling>MaxHealth"})


     local header_row = '! class="ability-table-col-hero" | Hero'
     local rows = {}
                    .. ' !! class="ability-table-col-ability" | Ability'
     for name, data in pairs(heroHealthData) do
     for _, h in ipairs(final_headers) do
         table.insert(rows, {
         header_row = header_row .. ' !! class="ability-table-col-'
            name = name,
             .. h:lower():gsub("%s+", "-") .. '" | ' .. h
            maxHealth = data["MaxHealth"] or 0,
             scalingHealth = "+" .. data["LevelScaling>MaxHealth"] or 0,
            maxScalingHealth = roundValue(data["MaxHealth"] + data["LevelScaling>MaxHealth"] * soul_unlock.get_max("PowerIncrease")) or 0
        })
     end
     end
     table.insert(out, header_row)
     sortHeroes(rows)


        local hero_counts = {}
    local tableDefs = {
    for _, key in ipairs(filtered) do
         {
         local hero = views[key].heroName
            title = "Base Health Stats",
        hero_counts[hero] = (hero_counts[hero] or 0) + 1
            columns = {
    end
                { label = "Starting", field = "maxHealth" },
 
                { label = "Added per Boon", field = "scalingHealth" },
    local hero_seen = {}
                { label = "At Max Boon", field = "maxScalingHealth" },
 
            }
    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)
local output = {}
        enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]])
    for _, def in ipairs(tableDefs) do
 
        local t = buildTable(frame, rows, def, def.options)
        local row
        if t ~= "" then
 
             table.insert(output, t)
        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
 
            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(output, "\n\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: