Editing Module:AbilityTable

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:
-- Module:AbilityTables
-- Renders sortable wikitables of abilities filtered by a named stat.
-- Renders sortable wikitables of abilities filtered by a named stat.
-- Backing module for Template:AbilityTable.
--
-- Usage on wiki pages: {{AbilityTable|charges}}
--
-- ============================================================
-- ADDING A NEW STAT
-- ============================================================
-- 1. Add an entry to stat_to_props mapping the friendly name
--    (lowercase) to the internal AbilityData property keys that
--    determine whether an ability appears in the table.
--
-- 2. Add a matching entry to extra_columns:
--      - headers:    ordered list of column header strings.
--                    Include "Notes" if this stat generates notes.
--      - get_cells:  function(ability) -> { ["Column Name"] = value }
--                    The ability is guaranteed to match the stat.
--                    Omit a key or return nil to show "—" in that cell.
--    If no extra_columns entry is added, only Hero and Ability
--    columns are shown (plus Notes if human notes exist for the stat).
--
-- 3. Optionally add human-authored notes to human_notes:
--      human_notes["stat"]["Ability Name"] = "Note text."
--    Notes are appended to any renderer-generated note automatically.
--
-- 4. Optionally add entries to column_suffix to append a unit
--    string (e.g. "s", "%") after cell values in specific columns.
-- ============================================================


local Lists    = require("Module:AbilityTable/Lists")
local p = {}
local Notes    = require("Module:AbilityTable/Notes")
local GameData = require("Module:GameData")
local Renderers = require("Module:AbilityTable/ComplexRenderers")
local GameData = require("Module:GameData")


local p = {}
-- ============================================================
-- stat_to_props
-- Maps lowercase friendly stat names to internal AbilityData
-- property keys used to filter which abilities appear.
-- ============================================================
local stat_to_props = {
    ["charges"] = { "AbilityCharges" },
}


local extra_columns = {
-- ============================================================
    ["barrier"] = {
-- human_notes
        headers  = { "Barrier" },
-- Editor-authored notes appended to the Notes column.
        get_cells = Renderers.RenderBarrier,
-- Keyed first by stat name, then by ability display Name.
    },
-- ============================================================
local human_notes = {
     ["charges"] = {
     ["charges"] = {
         headers  = { "Charges", "Time Between Charges", "Notes" },
         ["Jar of Dead"] = "Charges by being near when anything dies.",
        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 = Renderers.RenderSpiritScaling,
    },
    ["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
-- Shared helpers
-- ============================================================
-- ============================================================


-- Merges auto-generated and human notes for full-table mode.
local function num_val(v)
-- Capitalizes human note, joins with ". ", ensures trailing period.
    if type(v) == "number" then return v end
local function enrich_notes_full(extra, human_note)
    if type(v) == "table"  then return v["Value"] end
     if human_note then
    return nil
        human_note = human_note:sub(1,1):upper() .. human_note:sub(2)
end
        extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human_note) or human_note
 
-- Searches Upgrades[] for a field and returns { tier = N, value = V } at its
-- first appearance, or nil if the field is absent from all upgrade tiers.
local function find_in_upgrades(ability, field)
    local upgrades = ability["Upgrades"]
     if type(upgrades) ~= "table" then return nil end
    for i, tier in ipairs(upgrades) do
        if type(tier) == "table" and tier[field] ~= nil then
            return { tier = i, value = num_val(tier[field]) }
        end
     end
     end
     if extra["Notes"] and extra["Notes"]:sub(-1) ~= "." then
     return nil
         extra["Notes"] = extra["Notes"] .. "."
end
 
-- ============================================================
-- Per-stat cell renderers
-- Each function receives a guaranteed-matching ability record
-- and returns { ["Column Name"] = value }.
-- Human notes are appended by the framework, not here.
-- ============================================================
 
local function charges_cells(ability)
    local base_charges  = num_val(ability["AbilityCharges"])
    local base_cooldown = ability["AbilityCooldownBetweenCharge"]
    local has_base      = base_charges ~= nil and base_charges > 0
    local upgrade_info  = find_in_upgrades(ability, "AbilityCharges")
 
    local cooldown, upgrade_cooldown
    if has_base then
        if type(base_cooldown) == "number" and base_cooldown > 0 then
            cooldown = base_cooldown
        else
            local cd = find_in_upgrades(ability, "AbilityCooldownBetweenCharge")
            if cd and cd.value and cd.value > 0 then cooldown = cd.value end
        end
    else
        local cd = find_in_upgrades(ability, "AbilityCooldownBetweenCharge")
        if cd and cd.value and cd.value > 0 then upgrade_cooldown = cd.value end
    end
 
    local cells = {}
 
    if has_base then
         cells["Charges"] = tostring(base_charges)
        if cooldown then
            cells["Time Between Charges"] = tostring(cooldown)
        end
     end
     end
end


-- Merges auto-generated and human notes for compact mode.
    local note_parts = {}
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
    if upgrade_info then
-- joins with "; ", strips trailing period.
        local n = upgrade_info.value or "?"
local function enrich_notes_compact(extra, human_note)
        local charge_str = n .. " charge" .. (n ~= 1 and "s" or "")
    if human_note then
        if has_base then
        local first = human_note:sub(1,1)
            table.insert(note_parts, "+" .. charge_str .. " on T" .. upgrade_info.tier .. " upgrade.")
         if first:match("%u") and not human_note:sub(2,2):match("%d") then
         else
            human_note = first:lower() .. human_note:sub(2)
            local cd_str = upgrade_cooldown and (upgrade_cooldown .. "s") or "unknown"
            table.insert(note_parts, "Becomes charged on T" .. upgrade_info.tier
                .. " upgrade with " .. charge_str
                .. " and " .. cd_str .. " time between charges.")
         end
         end
        if 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
    if ability["AbilityChargesConditionally"] ~= nil then
        table.insert(note_parts, "Has a conditional charge.")
    end
    if #note_parts > 0 then
        cells["Notes"] = table.concat(note_parts, " ")
    end
    return cells
end
end


-- ============================================================
-- ============================================================
-- Ability views
-- extra_columns
--
-- Declares headers and cell renderer for each stat.
-- In-game Ability may containe 1 or more records in AbilityData.json
-- ============================================================
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
local extra_columns = {
    ["charges"] = {
        headers  = { "Charges", "Time Between Charges", "Notes" },
        get_cells = charges_cells,
    },
}
 
-- ============================================================
-- column_suffix
-- Unit strings appended to non-nil cell values by column name.
-- ============================================================
local column_suffix = {
    ["Time Between Charges"] = "s",
}


-- "Ability View" is a main-record + any relevant sub-ability records.
-- ============================================================
-- Hero lookup (ability key -> { heroName, abilityNum })
-- ============================================================
-- ============================================================


local _views = nil   -- card key -> { view = record, heroName = ..., abilityNum = ... }
local _hero_lookup = nil
local _order = nil  -- array of card keys, sorted by hero then slot


local function build_views()
local function build_hero_lookup()
     if _views then return _views, _order end
     if _hero_lookup then return _hero_lookup end
     _views, _order = {}, {}
     _hero_lookup = {}


     local hero_data = mw.loadJsonData("Data:HeroData.json")
     local hero_data = mw.loadJsonData("Data:HeroData.json")
Line 105: Line 174:
     end
     end


    local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
     local cards = mw.loadJsonData("Data:AbilityCards.json")
     local cards       = mw.loadJsonData("Data:AbilityCards.json")
 
    local card_of = {}
     for _, hero_entry in pairs(cards) do
     for _, hero_entry in pairs(cards) do
         if type(hero_entry) == "table" then
         if type(hero_entry) == "table" then
Line 115: Line 181:
                 for slot_str, ability in pairs(hero_entry) do
                 for slot_str, ability in pairs(hero_entry) do
                     local slot_num = tonumber(slot_str)
                     local slot_num = tonumber(slot_str)
                     local key = type(ability) == "table" and ability["Key"] or nil
                     if slot_num and type(ability) == "table" and ability["Key"] then
                    if slot_num and key and ability_data[key] then
                         _hero_lookup[ability["Key"]] = {
                         card_of[key] = { heroName = hero_name, abilityNum = slot_num }
                            heroName   = hero_name,
                            abilityNum = slot_num,
                        }
                     end
                     end
                 end
                 end
Line 124: Line 192:
     end
     end


     -- Attach each non-card record to the longest card key it prefixes.
     return _hero_lookup
    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 }
        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
 
-- ============================================================
-- Membership
-- Pieces of code that help in telling  "is ability X a member of list Y".
-- main function is: p.is_member()
-- ============================================================
 
-- Memoized name-exclusion set per list (from Lists.exclude_abilities).
local _excluded = {}
local function excluded_set(stat)
    if _excluded[stat] then return _excluded[stat] end
    local set = {}
    for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do
        set[name] = true
    end
    _excluded[stat] = set
    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
 
-- Public: does ability `ability_key` belong to list `stat`?
function p.is_member(stat, ability_key, ability_num)
    local props = Lists.lists[stat]
    if not props then return false end
 
    local views = build_views()
    local entry = views[ability_key]
    local view  = entry and entry.view
 
    if not view then
        view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]
        if not view then return false end
    end
 
    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


Line 212: Line 199:
-- ============================================================
-- ============================================================


function p.render(frame)
function p.abilityPropTable(frame)
     local stat = mw.text.trim(frame.args[1] or ""):lower()
     local stat = mw.text.trim(frame.args[1] or ""):lower()


     local internal_keys = Lists.lists[stat]
     local internal_keys = stat_to_props[stat]
     if not internal_keys then
     if not internal_keys then
         return '<span class="error">AbilityTable: unknown list "' .. stat .. '".</span>'
         return '<span class="error">AbilityTable: unknown stat "' .. stat .. '".</span>'
     end
     end


     local hide_set = {}
     local lookup     = build_hero_lookup()
    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 spec      = extra_columns[stat]
     local headers    = spec and spec.headers or {}
     local headers    = spec and spec.headers or {}
     local get_cells  = spec and spec.get_cells or function() return {} end
     local get_cells  = spec and spec.get_cells or function() return {} end
     local stat_notes = Notes[stat]
     local stat_notes = human_notes[stat]
 
    local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)


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


     if #filtered == 0 then
     if #filtered == 0 then
         return "No abilities found for list: " .. stat
         return "No abilities found for stat: " .. stat
     end
     end


     local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
     -- Sort by hero name, then ability slot number
     local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" })
     table.sort(filtered, function(a, b)
    local disclaimer = frame:preprocess(
        local ia = lookup[a["Key"]]
        '<div class="ability-table-disclaimer">'
         local ib = lookup[b["Key"]]
         .. 'This is a dynamic list '
         if ia.heroName ~= ib.heroName then
        .. '<span class="ability-table-list-name">"' .. stat .. '"</span>. '
             return ia.heroName < ib.heroName
        .. '[' .. 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].view)
         end
         end
        return ia.abilityNum < ib.abilityNum
    end)


        local items = {}
    -- Determine final header list. "Notes" is included if declared in headers
        for _, hero in ipairs(hero_order) do
    -- (meaning the renderer may produce notes) or if human notes exist for
            local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
    -- this stat. Either way it appears exactly once at the end.
            local ability_parts = {}
            for _, ability in ipairs(hero_abilities[hero]) do
                local name = ability["Name"]
                local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } }
 
                local extra = get_cells(ability)
                enrich_notes_compact(extra, stat_notes and stat_notes[name])
 
                local note = extra["Notes"]
                if note then
                    table.insert(ability_parts, ability_cell .. " (" .. note .. ")")
                else
                    table.insert(ability_parts, ability_cell)
                end
            end
            table.insert(items, "<li>" .. hero_cell .. ": " .. table.concat(ability_parts, ", ") .. "</li>")
        end
 
        return disclaimer .. "<ul>" .. table.concat(items) .. "</ul>"
    end
 
     local has_notes_header = false
     local has_notes_header = false
     local final_headers = {}
     local final_headers = {}
Line 300: Line 246:
         if h == "Notes" then
         if h == "Notes" then
             has_notes_header = true
             has_notes_header = true
         elseif not hide_set[h] then
         else
             table.insert(final_headers, h)
             table.insert(final_headers, h)
         end
         end
     end
     end
     local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set["Notes"]
     local needs_notes = has_notes_header or stat_notes ~= nil
     if needs_notes then table.insert(final_headers, "Notes") end
     if needs_notes then
 
        table.insert(final_headers, "Notes")
    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
     end


    -- Render wikitable
     local out = {}
     local out = {}
    table.insert(out, disclaimer)
     table.insert(out, '{| class="wikitable sortable ability-table"')
     table.insert(out, '{| class="' .. table_class .. '"')


     local header_row = '! class="ability-table-col-hero" | Hero'
     local header_cells = '! class="ability-table-col-hero" | Hero'
                    .. ' !! class="ability-table-col-ability" | Ability'
                      .. ' !! class="ability-table-col-ability" | Ability'
     for _, h in ipairs(final_headers) do
     for _, h in ipairs(final_headers) do
         header_row = header_row .. ' !! class="ability-table-col-'
         local cls = "ability-table-col-" .. h:lower():gsub("%s+", "-")
            .. h:lower():gsub("%s+", "-") .. '" | ' .. h
        header_cells = header_cells .. ' !! class="' .. cls .. '" | ' .. h
     end
     end
     table.insert(out, header_row)
     table.insert(out, header_cells)


        local hero_counts = {}
     for _, ability in ipairs(filtered) do
     for _, key in ipairs(filtered) do
         local info = lookup[ability["Key"]]
         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 hero_cell = frame:expandTemplate{
            title = "HeroIcon",
            args  = { info.heroName }
        }
         local ability_cell = frame:expandTemplate{
         local ability_cell = frame:expandTemplate{
             title = "AbilityIcon",
             title = "AbilityIcon",
             args = { ability["Name"] }
             args = { ability["Name"] }
         }
         }


         local extra = get_cells(ability)
         local extra = get_cells(ability)
        enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]])


         local row
         -- Append human note to any renderer-generated note
        if stat_notes and stat_notes[ability["Name"]] then
            local human = stat_notes[ability["Name"]]
            extra["Notes"] = extra["Notes"] and (extra["Notes"] .. " " .. human) or human
        end


         if not hero_seen[hero] then
         local row_cells = '| class="ability-table-cell-hero" | '   .. hero_cell
            local hero_cell = frame:expandTemplate{
                      .. ' || class="ability-table-cell-ability" | ' .. ability_cell
                title = "HeroIcon",
        for _, header in ipairs(final_headers) do
                args = { hero }
            local value  = extra[header]
            }
            local cls    = "ability-table-cell-" .. header:lower():gsub("%s+", "-")
 
            local content
            if hero_counts[hero] > 1 then
            if value ~= nil then
                row = '| rowspan="' .. hero_counts[hero] .. '" class="ability-table-cell-hero" | '
                local suffix = column_suffix[header] or ""
                    .. hero_cell
                content = value .. suffix
                    .. ' || class="ability-table-cell-ability" | ' .. ability_cell
             else
             else
                 row = '| class="ability-table-cell-hero" | '
                 content = ""
                    .. hero_cell
                    .. ' || class="ability-table-cell-ability" | ' .. ability_cell
             end
             end
 
             row_cells = row_cells .. ' || class="' .. cls .. '" | ' .. content
             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, "|-")
         table.insert(out, row)
         table.insert(out, row_cells)
     end
     end


Please note that all contributions to The Deadlock Wiki are considered to be released under the Creative Commons Attribution-NonCommercial-ShareAlike (see Deadlock:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!
Cancel Editing help (opens in new window)
Preview page with this template

Page included on this page: