Module:AbilityTableGive feedback
This module exposes two functions.
rendergenerates a table of hero abilities matching a named list, and is invoked via{{AbilityTable}}.is_member(list, ability_key, ability_num)is a shared function that decides whether a single ability belongs to a list (applying the same properties and exclusions); it is used both internally byrenderand externally by Module:AbilityHints, so the ability-hint icons always stay in sync with these tables.
See also
- Template:AbilityTable — the template that invokes this module
- Module:AbilityTable/Lists — defines which properties each list matches on, and which abilities to exclude
- Module:AbilityTable/Notes — editor-authored notes displayed in the Notes column
- Module:AbilityTable/ComplexRenderers — custom column renderers for lists that need more than just Notes
- Module:AbilityHints — reuses
is_memberto render interaction-hint icons on ability cards
-- Renders sortable wikitables of abilities filtered by a named stat.
-- Maps lowercase friendly stat names to internal AbilityData property keys or values.
local stat_to_props = {
["charges"] = { "AbilityCharges" },
["melee"] = { "melee", "heavy_melee" },
["wpndmg"] = { "melee", "heavy_melee", "weapon_damage_increase" },
["healreduce"] = { "HealAmpReceivePenaltyPercent", "HealAmpRegenPenaltyPercent", "DisableHealing" },
["debuffresist"] = {
"StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff",
"PetrifyDuration", "HexDuration", "ImmobilizeDuration", "LiftDuration",
"DebuffDuration", "HoldInPlaceDuration", "ExplodeDebuffDuration", "SlowPercent",
"DebuffAccuracy", "TimeSlowDuration", "LateCheckoutStun", "BulletDamageAmpDuration",
"BulletResistReduction", "BurnDuration", "EnemySlowPct", "MoveSlowPercent",
"GroundDashReductionPercent", "StealDuration", "MaxSlowDuration", "EffectDuration",
"BleedDuration", "VenomDuration",
-- can also just list abilities if they don't have a reusable property
"Hotel Guest", "Life Drain", "Combo", "Light Eater", "Spirit Lasso", "Card Trick"
},
}
-- Editor-authored notes appended to the Notes column.
local human_notes = {
["charges"] = {
["Jar of Dead"] = "Charges by being near when anything dies.",
},
["melee"] = {
["Puddle Punch"] = "Can be parried.",
},
["debuff_resist"] = {
["Aura of Suffering"] = "T1 slow, T3 extra damage. The effect is negligible.",
["Lightning Ball"] = "T2 Slow. The effect is negligible.",
["Static Charge"] = "Stun duration. Delay before stun is not affected.",
["Enchanter's Satchel"] = "T3 Slow.",
["Itani Lo Sahn"] = "Despite being described as \"debuff\", it can not be reduced with debuff resist.",
["Scorn"] = "T3 extra damage from Mo & Krill.",
["Hotel Guest"] = "Only T2 late check-out stun can be reduced.",
["Jar of Dead"] = "T2 slow.",
["Grapple Arm"] = "T1 extra damage from Bebop",
["Binding Word"] = "Immobilize Duration. Escape time is not affected.",
["Power Slash"] = "T1 slow",
["Kinetic Pulse"] = "T2 bullet resist reduction and slow.",
["Pain Battery"] = "T1 slow.",
["Power Surge"] = "T3 Spirit Resist reduction.",
["Rain of Arrows"] = "T2 slow.",
["Soul Exchange"] = "T2 silence.",
["Ground Strike"] = "T2 slow.",
["Flog"] = "T2 Fire rate slow.",
},
}
-- Abilities incorrectly matched by props that should not appear in the table.
-- Listed by ability name.
local exclude_list = {
["debuffresist"] = {
"Bookwyrm", -- has debuffdurattion for some reason
"Slam Fire", -- has debuffduration for some reason
"Splatter", -- has SlowPercent but no duration to shave off.
},
}
-- Unit strings appended to cell values by column name.
-- Values starting with "(" are treated as already-complete strings and
-- the suffix is NOT appended (e.g. "(See notes)" stays as-is).
local column_suffix = {
["Time Between Charges"] = "s",
["Duration"] = "s",
}
-- Actual code below
local p = {}
local GameData = require("Module:GameData")
-- ============================================================
-- Shared helpers
-- ============================================================
local function num_val(v)
if type(v) == "number" then return v end
if type(v) == "table" then return v["Value"] end
return nil
end
-- 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
return nil
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
local note_parts = {}
if upgrade_info then
local n = upgrade_info.value or "?"
local charge_str = n .. " charge" .. (n ~= 1 and "s" or "")
if has_base then
table.insert(note_parts, "+" .. charge_str .. " on T" .. upgrade_info.tier .. " upgrade.")
else
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 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
local function find_base_melee_scale(ability)
for _, v in pairs(ability) do
if type(v) == "table" then
local scale = v["Scale"]
if type(scale) == "table"
and scale["Type"] == "melee"
and type(scale["Value"]) == "number"
and scale["Value"] > 0
then
return scale["Value"]
end
end
end
return nil
end
local function build_melee_upgrade_notes(ability, base_melee_scale)
local upgrades = ability["Upgrades"]
if type(upgrades) ~= "table" then return {} end
local notes = {}
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
for _, v in pairs(tier) do
if type(v) == "table" then
local scale = v["Scale"]
if type(scale) == "table" and type(scale["Value"]) == "number" then
local stype = scale["Type"]
local delta = scale["Value"]
if stype == "heavy_melee" and delta > 0 then
table.insert(notes, "T" .. i .. " upgrade changes scaling to ×"
.. delta .. " of Heavy Melee damage.")
elseif stype == "melee" and delta ~= 0 and base_melee_scale then
local final = math.floor((base_melee_scale + delta) * 1000 + 0.5) / 1000
table.insert(notes, "T" .. i .. " upgrade adds ×"
.. delta .. " scaling (total ×" .. final .. ").")
end
end
end
end
end
end
return notes
end
local function melee_cells(ability)
local cells = {}
local base_scale = find_base_melee_scale(ability)
if base_scale then cells["Light Melee Scaling"] = "×" .. base_scale end
local upgrade_notes = build_melee_upgrade_notes(ability, base_scale)
if #upgrade_notes > 0 then cells["Notes"] = table.concat(upgrade_notes, " ") end
return cells
end
local function find_base_duration(ability)
for _, field in ipairs({ "VenomDuration", "DebuffDuration", "AbilityDuration", "BurnDuration" }) do
local v = num_val(ability[field])
if v and v > 0 then return v, field end
end
return nil, nil
end
local function healreduce_cells(ability)
local cells = {}
local notes = {}
local base_dur, dur_field = find_base_duration(ability)
local first_pct, first_tier, first_is_disable = nil, nil, false
local base_receive = num_val(ability["HealAmpReceivePenaltyPercent"])
local base_regen = num_val(ability["HealAmpRegenPenaltyPercent"])
local base_pct_raw = base_receive or base_regen
if base_pct_raw and base_pct_raw ~= 0 then
first_pct = math.abs(base_pct_raw)
elseif ability["DisableHealing"] and ability["DisableHealing"] ~= 0 then
first_is_disable = true
else
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
local up_raw = num_val(tier["HealAmpReceivePenaltyPercent"])
or num_val(tier["HealAmpRegenPenaltyPercent"])
if up_raw and up_raw ~= 0 then
first_pct, first_tier = math.abs(up_raw), i
break
elseif tier["DisableHealing"] and tier["DisableHealing"] ~= 0 then
first_is_disable, first_tier = true, i
break
end
end
end
end
end
if first_is_disable then
cells["Heal Reduction"] = "100%" .. (first_tier and " (T" .. first_tier .. ")" or "")
elseif first_pct then
cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "")
end
if base_dur then cells["Duration"] = tostring(base_dur) end
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
local running_pct, running_dur = first_pct, base_dur
local first_pct_seen = (first_tier == nil)
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
local tier_notes = {}
local pct_delta = num_val(tier["HealAmpReceivePenaltyPercent"])
or num_val(tier["HealAmpRegenPenaltyPercent"])
if first_tier == i then
first_pct_seen = true
if first_pct then running_pct = first_pct end
elseif first_pct_seen and pct_delta and pct_delta ~= 0 then
local delta_abs = math.abs(pct_delta)
local total = (running_pct or 0) + delta_abs
table.insert(tier_notes, "T" .. i .. " upgrade increases heal reduction by "
.. delta_abs .. "% (total " .. total .. "%).")
running_pct = total
end
if dur_field then
local dur_delta = num_val(tier[dur_field])
if dur_delta and dur_delta ~= 0 and running_dur then
local total_dur = running_dur + dur_delta
table.insert(tier_notes, "T" .. i .. " upgrade "
.. (dur_delta > 0 and "increases" or "decreases")
.. " duration by " .. math.abs(dur_delta)
.. "s (total " .. total_dur .. "s).")
running_dur = total_dur
end
end
for _, n in ipairs(tier_notes) do table.insert(notes, n) end
end
end
end
if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
return cells
end
local extra_columns = {
["charges"] = {
headers = { "Charges", "Time Between Charges", "Notes" },
get_cells = charges_cells,
},
["melee"] = {
headers = { "Light Melee Scaling", "Notes" },
get_cells = melee_cells,
},
["healreduce"] = {
headers = { "Heal Reduction", "Duration", "Notes" },
get_cells = healreduce_cells,
},
}
-- ============================================================
-- Hero lookup
-- _key_lookup : ability key -> { heroName, abilityNum }
-- _name_to_key : ability name -> ability key
-- Both built together from HeroData + AbilityCards.
-- ============================================================
local _key_lookup = nil
local _name_to_key = nil
local function build_lookup()
if _key_lookup then return _key_lookup, _name_to_key end
_key_lookup = {}
_name_to_key = {}
local hero_data = mw.loadJsonData("Data:HeroData.json")
local active_heroes = {}
for _, hero in pairs(hero_data) do
if type(hero) == "table"
and hero["IsDisabled"] == false
and type(hero["Name"]) == "string"
and (hero["IsSelectable"] == nil or hero["IsSelectable"] ~= false)
then
active_heroes[hero["Name"]] = true
end
end
local cards = mw.loadJsonData("Data:AbilityCards.json")
for _, hero_entry in pairs(cards) do
if type(hero_entry) == "table" then
local hero_name = hero_entry["Name"]
if hero_name and active_heroes[hero_name] then
for slot_str, ability in pairs(hero_entry) do
local slot_num = tonumber(slot_str)
if slot_num and type(ability) == "table" then
local info = { heroName = hero_name, abilityNum = slot_num }
if ability["Key"] then
_key_lookup[ability["Key"]] = info
end
if ability["Name"] and ability["Key"] then
_name_to_key[ability["Name"]] = ability["Key"]
end
end
end
end
end
end
return _key_lookup, _name_to_key
end
-- ============================================================
-- Entry point
-- ============================================================
function p.render(frame)
local stat = mw.text.trim(frame.args[1] or ""):lower()
local internal_keys = stat_to_props[stat]
if not internal_keys then
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
local key_lookup, name_to_key = build_lookup()
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 = human_notes[stat]
-- Build excluded-name set for this stat
local excluded = {}
if exclude_list[stat] then
for _, name in ipairs(exclude_list[stat]) do
excluded[name] = true
end
end
local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)
-- Keep only abilities belonging to active, released heroes and not excluded
local filtered = {}
for _, ability in ipairs(abilities) do
local key = ability["Key"]
if key and key_lookup[key] and not excluded[ability["Name"]] then
table.insert(filtered, ability)
end
end
if #filtered == 0 then
return "No abilities found for stat: " .. stat
end
-- Sort by hero name, then ability slot number.
table.sort(filtered, function(a, b)
local ia = key_lookup[a["Key"]]
local ib = key_lookup[b["Key"]]
if not ia and not ib then return (a["Name"] or "") < (b["Name"] or "") end
if not ia then return false end
if not ib then return true end
if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end
return ia.abilityNum < ib.abilityNum
end)
-- Determine final headers
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
-- Disclaimer
local edit_url = mw.uri.fullUrl("Module:AbilityTable", { action = "edit" })
local disclaimer = frame:preprocess(
'<div class="ability-table-disclaimer">'
.. "This is an automatic list. "
.. "[" .. tostring(edit_url) .. " Click here]"
.. " to add notes or correct the contents."
.. "</div>"
)
local out = {}
table.insert(out, disclaimer)
table.insert(out, '{| class="wikitable sortable ability-table"')
local header_row = '! class="ability-table-col-hero" | Hero'
.. ' !! class="ability-table-col-ability" | Ability'
for _, h in ipairs(final_headers) do
header_row = header_row .. ' !! class="ability-table-col-'
.. h:lower():gsub("%s+", "-") .. '" | ' .. h
end
table.insert(out, header_row)
for _, ability in ipairs(filtered) do
local info = key_lookup[ability["Key"]]
local hero_cell
if info then
hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { info.heroName } }
else
hero_cell = "—"
end
local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { ability["Name"] } }
local extra = get_cells(ability)
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
local row = '| class="ability-table-cell-hero" | ' .. hero_cell
.. ' || class="ability-table-cell-ability" | ' .. ability_cell
for _, header in ipairs(final_headers) do
local value = extra[header]
local cls = "ability-table-cell-" .. header:lower():gsub("%s+", "-")
local content
if value ~= nil then
local suffix = column_suffix[header] or ""
content = (type(value) == "string" and value:sub(1,1) == "(")
and value or (value .. suffix)
else
content = "—"
end
row = row .. ' || class="' .. cls .. '" | ' .. content
end
table.insert(out, "|-")
table.insert(out, row)
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
return p