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 friendly stat names to internal property keys or values.
local stat_to_props = {
["charges"] = { "AbilityCharges" },
["melee"] = { "melee", "heavy_melee" },
["wpndmg"] = { "weapon_damage_increase" },
["healreduce"] = { "HealAmpReceivePenaltyPercent", "HealAmpRegenPenaltyPercent", "DisableHealing" },
["debuffresist"] = {
-- Do not insert: AbilityDuration, TickRate, SlowDuration
-- they do more harm than good and include a lot of excessive abilities to the list
"StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", "PetrifyDuration", "HexDuration",
"ImmobilizeDuration", "LiftDuration", "DebuffDuration", "HoldInPlaceDuration", "ExplodeDebuffDuration", "SlowPercent",
"DebuffAccuracy", "TimeSlowDuration", "BulletDamageAmpDuration", "BulletResistReduction", "BurnDuration",
"EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "StealDuration", "EffectDuration", "TossDuration",
-- can also just list abilities if they don't have a reusable property
"Hotel Guest", "Life Drain", "Combo", "Light Eater", "Spirit Lasso", "Card Trick", "Lethal Venom", "Serrated Knives", "Kinetic Carbine"
},
["moveslow"] = {
"SlowPercent", "EnemySlowPct", "MoveSlowPercent", "MaxSlowPercent", "MoveSpeedSlowPct", "MovementSpeedSlow",
"MovementSlowPct", "ClubSlowPercent", "MoveSpeedPenaltyPerStack", "MovementSlow"
},
["dashslow"] = { "GroundDashReductionPercent", "MaxGroundDashReductionPercent", "MoveSpeedAndDashSlowPct", "EnemyDashSlowPercent" },
["reactivebarrier"] = {
-- WIP, do not use yet.
"StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", "PetrifyDuration", "HexDuration",
"ImmobilizeDuration", "LiftDuration", "HoldInPlaceDuration", "RestrictionDuration", "PullDuration", "PullDownDuration",
-- engine-side CCs with no matching data prop
"Itani Lo Sahn", "Dazzling Trick", "Ground Strike", "Hotel Guest",
},
["dispelmagic"] = {
-- Excludes effects that prevent casting (StunDuration, SleepDuration, PetrifyDuration, HexDuration)
-- since you cannot self-dispel while affected by those.
-- Slot 4 (ultimate) abilities are excluded via filter_abilities.
"SilenceDuration", "SilenceDebuff", "ImmobilizeDuration", "LiftDuration", "HoldInPlaceDuration",
"RestrictionDuration",
"SlowPercent", "EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "SlowDuration",
"DebuffDuration", "ExplodeDebuffDuration", "BulletDamageAmpDuration", "BulletResistReduction",
"StealDuration", "EffectDuration", "DebuffAccuracy", "TimeSlowDuration", "BurnDuration", "TossDuration",
-- raw abilities
"Life Drain", "Card Trick", "Sticky Bomb", "Light Eater", "Luggage Cart", "Lethal Venom", "Serrated Knives", "Essence Theft", "Kinetic Carbine",
},
["heal"] = {
"HealingPerSecond", "HealAmount", "HealFixedHealth", "FlatHealthHealing", "HealingFactor", "MaxHealthRegen",
"ExternalBonusHealthRegen", "BonusHealthRegen", "LifeStealPercentOnHit", "LifestealPercentHero", "MissingHealthPercentHeal", "MissingHealthBurstPct",
"MissingHPHeal", "TechLifestealPercent", "AbilityLifestealPercentHero", "HealthStealPctHero", "BulletLifestealPercent", "TotalHealthRegen",
"DamageHealMult", "HealPctVsHeroes", "InfestHeal", "HealOnKillPct", "HealMaxHealth", "HealMaxHealthPercent",
"RegenIncomingDamagePercent",
-- raw abilities
"Rake", "Card Trick",
},
}
-- Editor-authored notes go here.
local human_notes = {
["charges"] = {
["Jar of Dead"] = "Charges by being near when anything dies.",
},
["melee"] = {
["Puddle Punch"] = "Can be parried.",
},
["debuffresist"] = {
["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.",
["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.",
},
["moveslow"] = {
["Grapple Arm"] = "Applies a short strong slow when the victim reaches Bebop.",
["Heavy Barrage"] = "Slows McGinnis by unknown amount.",
},
["reactivebarrier"] = {
["Rend"] = "Triggers on T3 upgrade only.",
["Soul Exchange"] = "Triggers on T2 upgrade only.",
["Air Drop"] = "Triggers on T3 upgrade only.",
["Entangling Thorns"] = "Triggers on T3 upgrade only.",
["Ground Strike"] = "Triggers only when slamming a mid-air enemy into the ground.",
["Djinn's Mark"] = "Triggers on T3 upgrade only.",
["Time Wall"] = "Triggers on T2 upgrade only.",
},
["heal"] = {
["Siphon Life"] = "Lifesteals off damage dealt while active.",
["Infernal Resilience"] = "Passively restores health for a percentage of all incoming damage.",
["Flawless Advance"] = "Heals a flat amount per hit. Unlocked by T1 upgrade.",
["Exploding Uppercut"] = "Restores a portion of missing health on use. Unlocked by T3 upgrade.",
["Leaping Slash"] = "Heals a flat amount on hero hit. Unlocked by T1 upgrade.",
["Return to Shadows"] = "Heals a flat amount on use. Unlocked by T3 upgrade.",
["Light Eater"] = "Grants a buff that gives spirit lifesteal while active.",
["Bloodscent"] = "Restores a portion of missing health on kill.",
["Rain of Arrows"] = "Grants bullet and spirit lifesteal while active. Unlocked by T3 upgrade.",
["Napalm"] = "Lifesteals off all damage dealt to napalmed targets. Unlocked by T2 upgrade.",
["Concussive Combustion"] = "Lifesteals off damage dealt. Unlocked by T2 upgrade.",
["Kudzu Connection"] = "Grants bullet lifesteal. Also replicates a portion of healing.",
["Rejuvenating Aurora"] = "Heals per second while channeling. T3 upgrade also adds passive max health regen.",
["Stone Form"] = "Heals a percentage of max HP on cast.",
["Frost Grenade"] = "Heals Kelvin and nearby allies for a flat amount.",
["Frozen Shelter"] = "Regenerates health for all allies inside the dome.",
["Flog"] = "Heals Lash for a percentage of damage dealt to heroes.",
["Medicinal Specter"] = "Heals nearby allies per second.",
["Rake"] = "Heals a flat amount on kill.",
["Scorn"] = "Heals for a percentage of damage dealt.",
["Combo"] = "Lifesteals off damage dealt. Unlocked by T1 upgrade.",
["Rallying Charge"] = "Heal affected allies and Paige for a flat amount.",
["Tag Along"] = "Restores a portion of target's missing health on attachment, then heals per second for a short duration.",
["Lil Helpers"] = "Regenerates health for any trooper the helpers are attached to.",
["Lycan Curse"] = "Restores a percentage of missing health on activation.",
["Go For The Throat"] = "Lifesteals off melee damage dealt.",
["Pain Battery"] = "Restores a portion of missing health on hero hit. Unlocked by T3 upgrade.",
["Jumpstart"] = "Grants a flat health regen buff on use.",
["The Cube"] = "Regenerates health for the target.",
["Last Stand"] = "Lifesteals off all damage dealt while active.",
["Card Trick"] = "Heals Wraith for a flat amount when a Heart or Joker card hits.",
["Crimson Slash"] = "Heals a flat amount on hero hit.",
["Shadow Transformation"] = "Restores a percentage of max health per hero kill.",
},
["dispelmagic"] = {
}
}
-- Type here to exclude ability from a list. If adding please include a comment.
local exclude_list = {
["debuffresist"] = {
"Bookwyrm", -- has DebuffDuration for some reason
"Itani Lo Sahn", -- has DebuffDuration but does not get reduced
"Slam Fire", -- has DebuffDuration for some reason
"Splatter", -- has SlowPercent but no duration to shave off.
},
["moveslow"] = {
"Riposte", -- has a 40% 4s slow somehow? (SlowPercent + SlowDuration)
"Grasping Hands", -- has SlowPercent
},
["dashslow"] = {
"Itani Lo Sahn", -- has GroundDashReductionPercent but also disables stamina.
},
["heal"] = {
"Riposte", -- has AbilityLifestealPercentHero, but doesn't deal damage to heal off of.
},
["dispelmagic"] = {
"Bookwyrm", -- has DebuffDuration for some reason
"Slam Fire", -- has DebuffDuration for some reason
},
}
-- Unit strings appended to cell values by column name.
local column_suffix = {
["Time Between Charges"] = "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
-- Finds the first non-zero value for any of the given fields, checking base
-- then upgrades in order. Returns field, tier (nil=base, N=upgrade), abs value.
local function find_first_value(ability, fields)
for _, field in ipairs(fields) do
local v = num_val(ability[field])
if v and v ~= 0 then return field, nil, math.abs(v) end
end
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
for _, field in ipairs(fields) do
local v = num_val(tier[field])
if v and v ~= 0 then return field, i, math.abs(v) end
end
end
end
end
return nil, nil, nil
end
-- ============================================================
-- Filtering
-- ============================================================
-- Filters a list of prop-matched abilities down to those that should appear
-- in the table for the given stat. Handles:
-- 1. key_lookup membership (ability belongs to an active released hero)
-- 2. exclude_list (manual false-positive suppressions)
-- 3. stat-specific logic (e.g. dispelmagic strips slot-4 ultimates)
local function filter_abilities(stat, abilities, key_lookup)
local excluded = {}
if exclude_list[stat] then
for _, name in ipairs(exclude_list[stat]) do
excluded[name] = true
end
end
local result = {}
for _, ability in ipairs(abilities) do
local key = ability["Key"]
local info = key and key_lookup[key]
if info and not excluded[ability["Name"]] then
local skip = false
-- dispelmagic: skip ultimates (slot 4)
if stat == "dispelmagic" and info.abilityNum == 4 then
skip = true
end
if not skip then
table.insert(result, ability)
end
end
end
return result
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 healreduce_cells(ability)
local cells = {}
local notes = {}
local props = stat_to_props["healreduce"]
local first_pct, first_tier, first_is_disable = nil, nil, false
for _, prop in ipairs(props) do
local v = num_val(ability[prop])
if v and v ~= 0 then
if prop == "DisableHealing" then
first_is_disable = true
else
first_pct = math.abs(v)
end
break
end
end
if not first_pct and not first_is_disable then
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
for _, prop in ipairs(props) do
local v = num_val(tier[prop])
if v and v ~= 0 then
if prop == "DisableHealing" then
first_is_disable, first_tier = true, i
else
first_pct, first_tier = math.abs(v), i
end
break
end
end
end
if first_pct or first_is_disable then break 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
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
local running = first_pct
local first_pct_seen = (first_tier == nil)
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
if first_tier == i then
first_pct_seen = true
if first_pct then running = first_pct end
elseif first_pct_seen then
local delta = nil
for _, prop in ipairs(props) do
if prop ~= "DisableHealing" then
local v = num_val(tier[prop])
if v and v ~= 0 then delta = math.abs(v); break end
end
end
if delta then
local total = (running or 0) + delta
table.insert(notes, "T" .. i .. " upgrade increases heal reduction by "
.. delta .. "% (total " .. total .. "%).")
running = total
end
end
end
end
end
if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
return cells
end
-- Generic renderer for a numeric stat column.
-- col_name: the column header string
-- fields: ordered list of property names (first non-zero wins)
-- suffix: string appended to values in cell and notes (e.g. "%" or "")
-- Upgrade deltas are additive; notes track running totals.
local function val_stat_cells(ability, col_name, fields, suffix)
suffix = suffix or ""
local cells = {}
local notes = {}
local first_field, first_tier, first_val = find_first_value(ability, fields)
if not first_field then return cells end
local cell_str = tostring(first_val) .. suffix
if first_tier then cell_str = cell_str .. " (T" .. first_tier .. ")" end
cells[col_name] = cell_str
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
local running = first_val
local seen_first = (first_tier == nil)
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
if first_tier == i then
seen_first = true
elseif seen_first then
local delta = nil
for _, field in ipairs(fields) do
local v = num_val(tier[field])
if v and v ~= 0 then delta = math.abs(v); break end
end
if delta then
local total = running + delta
table.insert(notes, "T" .. i .. " upgrade increases "
.. col_name:lower() .. " by " .. delta .. suffix
.. " (total " .. total .. suffix .. ").")
running = total
end
end
end
end
end
if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
return cells
end
local function moveslow_cells(ability)
local cells = val_stat_cells(ability, "Move Slow", stat_to_props["moveslow"], "%")
local channel_slow = num_val(ability["ChannelSlowPercent"])
if channel_slow and channel_slow ~= 0 then
local note = "Also slows the caster for " .. math.abs(channel_slow) .. "%."
cells["Notes"] = cells["Notes"] and (cells["Notes"] .. " " .. note) or note
end
return cells
end
local function dashslow_cells(ability)
local props = stat_to_props["dashslow"]
local caster_dash_slow = nil
local base_ground = num_val(ability["GroundDashReductionPercent"])
if base_ground and base_ground ~= 0 then
local has_enemy_prop = false
for _, p in ipairs(stat_to_props["dashslow"]) do
if p ~= "GroundDashReductionPercent" then
local v = num_val(ability[p])
if v and v ~= 0 then has_enemy_prop = true; break end
end
end
if not has_enemy_prop then
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
for _, tier in ipairs(upgrades) do
if type(tier) == "table" then
for _, p in ipairs(stat_to_props["dashslow"]) do
if p ~= "GroundDashReductionPercent" then
local v = num_val(tier[p])
if v and v ~= 0 then has_enemy_prop = true; break end
end
end
end
if has_enemy_prop then break end
end
end
end
if has_enemy_prop then
caster_dash_slow = math.abs(base_ground)
local filtered = {}
for _, p in ipairs(props) do
if p ~= "GroundDashReductionPercent" then
table.insert(filtered, p)
end
end
props = filtered
end
end
local cells = val_stat_cells(ability, "Dash Slow", props, "%")
if caster_dash_slow then
local note = "Gives " .. caster_dash_slow .. "% dash slow for the caster."
cells["Notes"] = cells["Notes"] and (cells["Notes"] .. " " .. note) or note
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", "Notes" },
get_cells = healreduce_cells,
},
["moveslow"] = {
headers = { "Move Slow", "Notes" },
get_cells = moveslow_cells,
},
["dashslow"] = {
headers = { "Dash Slow", "Notes" },
get_cells = dashslow_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 compact = mw.text.trim(frame.args["compact"] or "") ~= ""
local key_lookup, _ = 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]
local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys)
local filtered = filter_abilities(stat, abilities, key_lookup)
if #filtered == 0 then
return "No abilities found for stat: " .. stat
end
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)
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>"
)
if compact then
local hero_order = {}
local hero_abilities = {}
for _, ability in ipairs(filtered) do
local info = key_lookup[ability["Key"]]
local hero = info and info.heroName or "—"
if not hero_abilities[hero] then
table.insert(hero_order, hero)
hero_abilities[hero] = {}
end
table.insert(hero_abilities[hero], ability["Name"])
end
local items = {}
for _, hero in ipairs(hero_order) do
local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
local ability_parts = {}
for _, name in ipairs(hero_abilities[hero]) do
local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } }
local note = stat_notes and stat_notes[name]
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 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