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.
local Lists    = require("Module:AbilityTable/Lists")
--
local Notes    = require("Module:AbilityTable/Notes")
-- Usage on wiki pages: {{AbilityTable|charges}}
local Renderers = require("Module:AbilityTable/ComplexRenderers")
--
local GameData  = require("Module:GameData")
-- Adding a new stat:
--  1. Add an entry to friendly_to_internal below, mapping the friendly
--      name (lowercase) to the list of internal AbilityData property keys.
--  2. The table will appear automatically on any page using {{AbilityTable|<stat>}}.


local p = {}
local p = {}
local GameData = require("Module:GameData")


local extra_columns = {
-- Maps lowercase friendly stat names to internal AbilityData property keys.
    ["barrier"] = {
-- A single friendly name may map to multiple internal keys if the stat can
        headers  = { "Barrier" },
-- be represented differently across abilities.
        get_cells = Renderers.RenderBarrier,
local friendly_to_internal = {
    },
     ["charges"] = { "AbilityCharges" },
    ["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 = 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
-- Hero lookup (ability key -> hero display name)
-- Built lazily from AbilityCards.json and HeroData.json.
-- ============================================================
-- ============================================================


-- Merges auto-generated and human notes for full-table mode.
local _hero_lookup = nil
-- 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


-- Merges auto-generated and human notes for compact mode.
local function build_hero_lookup()
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
     if _hero_lookup then return _hero_lookup end
-- joins with "; ", strips trailing period.
     _hero_lookup = {}
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
 
-- ============================================================
-- Ability views
--
-- In-game Ability may containe 1 or more records in AbilityData.json
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
 
-- "Ability View" is a main-record + any relevant sub-ability records.
-- ============================================================
 
local _views = nil  -- card key -> { view = record, heroName = ..., abilityNum = ... }
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 = {}, {}


    -- Build set of active hero display names from HeroData
     local hero_data = mw.loadJsonData("Data:HeroData.json")
     local hero_data = mw.loadJsonData("Data:HeroData.json")
     local active_heroes = {}
     local active_heroes = {}
Line 105: Line 44:
     end
     end


     local ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
     -- Map ability key -> { heroName, abilityNum } using AbilityCards
     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 52:
                 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 63:
     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 70:
-- ============================================================
-- ============================================================


function p.render(frame)
-- Entry point invoked by Template:AbilityTable.
-- frame.args[1] is the friendly stat name (e.g. "charges").
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 = friendly_to_internal[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
 
    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
     end


     local compact = mw.text.trim(frame.args["compact"] or "") ~= ""
     local lookup = build_hero_lookup()
 
     local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)
     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]


    -- Filter to abilities belonging to active, released heroes only
     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 (primary), then ability slot number (secondary)
     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 = {}
     -- Render wikitable
        for _, hero in ipairs(hero_order) do
            local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
            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 final_headers = {}
    for _, h in ipairs(headers) do
        if h == "Notes" then
            has_notes_header = true
        elseif not hide_set[h] then
            table.insert(final_headers, h)
        end
    end
    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 = {}
     local out = {}
     table.insert(out, disclaimer)
     table.insert(out, '{| class="wikitable sortable"')
     table.insert(out, '{| class="' .. table_class .. '"')
     table.insert(out, "! Hero !! Ability")


    local header_row = '! class="ability-table-col-hero" | Hero'
     for _, ability in ipairs(filtered) do
                    .. ' !! class="ability-table-col-ability" | Ability'
         local info = lookup[ability["Key"]]
     for _, h in ipairs(final_headers) do
        header_row = header_row .. ' !! class="ability-table-col-'
            .. h:lower():gsub("%s+", "-") .. '" | ' .. h
    end
    table.insert(out, header_row)
 
        local hero_counts = {}
    for _, key in ipairs(filtered) do
         local hero = views[key].heroName
        hero_counts[hero] = (hero_counts[hero] or 0) + 1
    end
 
    local hero_seen = {}
 
    for _, key in ipairs(filtered) do
        local entry  = views[key]
        local ability = entry.view
        local hero    = entry.heroName


        local 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)
        enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]])
        local row
        if not hero_seen[hero] then
            local hero_cell = frame:expandTemplate{
                title = "HeroIcon",
                args = { hero }
            }
            if hero_counts[hero] > 1 then
                row = '| rowspan="' .. hero_counts[hero] .. '" class="ability-table-cell-hero" | '
                    .. hero_cell
                    .. ' || class="ability-table-cell-ability" | ' .. ability_cell
            else
                row = '| class="ability-table-cell-hero" | '
                    .. hero_cell
                    .. ' || class="ability-table-cell-ability" | ' .. ability_cell
            end
            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


         table.insert(out, "|-")
         table.insert(out, "|-")
         table.insert(out, row)
         table.insert(out, "| " .. hero_cell .. " || " .. ability_cell)
     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: