Module:AbilityTable: Difference between revisionsGive feedback
Jump to navigation
Jump to search
fix debuff_cells: remove debug, replace #upgrades loop with sorted pairs iteration to fix empty notes (with help from vergir-bot LLM) |
fix typo in val_stat_cells: #parts should be #notes (with help from vergir-bot LLM) |
||
| Line 335: | Line 335: | ||
end | end | ||
-- Build note in tier order | -- Build note in tier order. Use sorted keys since pairs() order is not guaranteed. | ||
local tier_indices = {} | local tier_indices = {} | ||
for i in pairs(tier_debuffs) do | for i in pairs(tier_debuffs) do | ||
| Line 575: | Line 575: | ||
end | end | ||
if # | if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end | ||
return cells | return cells | ||
end | end | ||
Revision as of 16:50, 25 May 2026
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", "LateCheckoutStun", "BulletDamageAmpDuration", "BulletResistReduction", "BurnDuration",
"EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "StealDuration", "MaxSlowDuration", "EffectDuration",
"BleedDuration", "VenomDuration", "TossDuration",
-- can also just list abilities if they don't have a reusable property
"Life Drain", "Combo", "Light Eater", "Spirit Lasso", "Card Trick"
},
["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"] = {
"SilenceDuration", "SilenceDebuff", "ImmobilizeDuration", "LiftDuration", "HoldInPlaceDuration",
"RestrictionDuration",
"SlowPercent", "EnemySlowPct", "MoveSlowPercent", "GroundDashReductionPercent", "SlowDuration",
"DebuffDuration", "ExplodeDebuffDuration", "BulletDamageAmpDuration", "BulletResistReduction",
"StealDuration", "EffectDuration", "DebuffAccuracy", "TimeSlowDuration",
"BurnDuration", "BleedDuration", "VenomDuration", "MaxSlowDuration",
-- raw abilities
"Life Drain", "Card Trick", "Sticky Bomb", "Light Eater", "Luggage Cart", "Essence Theft"
},
["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.",
},
}
-- 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
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
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
-- ============================================================
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
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
-- ============================================================
local debuff_short_name = {
["StunDuration"] = "stun",
["SleepDuration"] = "sleep",
["SilenceDuration"] = "silence",
["SilenceDebuff"] = "silence",
["PetrifyDuration"] = "petrify",
["HexDuration"] = "hex",
["ImmobilizeDuration"] = "immobilize",
["LiftDuration"] = "lift",
["HoldInPlaceDuration"] = "immobilize",
["RestrictionDuration"] = "immobilize",
["DebuffDuration"] = "debuff",
["ExplodeDebuffDuration"] = "debuff",
["SlowPercent"] = "slow",
["EnemySlowPct"] = "slow",
["MoveSlowPercent"] = "slow",
["GroundDashReductionPercent"] = "dash slow",
["SlowDuration"] = "slow",
["MaxSlowDuration"] = "slow",
["BulletDamageAmpDuration"] = "bullet amp",
["BulletResistReduction"] = "bullet resist reduction",
["StealDuration"] = "debuff",
["EffectDuration"] = "debuff",
["DebuffAccuracy"] = "debuff",
["TimeSlowDuration"] = "time slow",
["LateCheckoutStun"] = "stun",
["BurnDuration"] = "burn",
["BleedDuration"] = "bleed",
["VenomDuration"] = "venom",
["TossDuration"] = "Knockback",
}
-- Returns true if prop is found with a non-zero value anywhere in the record
-- excluding the Upgrades key. Mirrors GameData.record_matches_prop logic.
local function prop_in_base(record, prop)
for k, v in pairs(record) do
if k == "Upgrades" then
-- skip
elseif k == prop then
if type(v) == "table" then v = v["Value"] end
if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
return true
end
elseif type(v) == "table" then
if prop_in_base(v, prop) then return true end
elseif type(v) == "string" then
if v == prop then return true end
end
end
return false
end
-- Renderer shared by debuffresist and dispelmagic.
-- Outputs a Notes cell describing which debuffs are added only by upgrades.
-- If any prop from the stat's list is found in the base ability, outputs nothing.
-- Otherwise lists per-tier debuff names: "T2 slow", "T1 slow, T2 silence", etc.
local function debuff_cells(ability, stat)
local props = stat_to_props[stat]
local cells = {}
-- If any prop exists in base, this is a base-level debuff — no note needed.
for _, prop in ipairs(props) do
if prop_in_base(ability, prop) then
return cells
end
end
local upgrades = ability["Upgrades"]
if type(upgrades) ~= "table" then return cells end
-- Collect debuff short names per upgrade tier.
local tier_debuffs = {}
local seen_names = {} -- deduplicate short names across all tiers
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
local names_this_tier = {}
local seen_this_tier = {}
for _, prop in ipairs(props) do
local v = tier[prop]
if v ~= nil then
if type(v) == "table" then v = v["Value"] end
if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
local short = debuff_short_name[prop] or "debuff"
if not seen_this_tier[short] then
seen_this_tier[short] = true
if not seen_names[short] then
seen_names[short] = true
table.insert(names_this_tier, short)
end
end
end
end
end
if #names_this_tier > 0 then
tier_debuffs[i] = names_this_tier
end
end
end
-- Build note in tier order. Use sorted keys since pairs() order is not guaranteed.
local tier_indices = {}
for i in pairs(tier_debuffs) do
table.insert(tier_indices, i)
end
table.sort(tier_indices)
local parts = {}
for _, i in ipairs(tier_indices) do
table.insert(parts, "T" .. i .. " " .. table.concat(tier_debuffs[i], " + "))
end
if #parts > 0 then
cells["Notes"] = table.concat(parts, ", ")
end
return cells
end
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
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,
},
["debuffresist"] = {
headers = { "Notes" },
get_cells = function(ability) return debuff_cells(ability, "debuffresist") end,
},
["dispelmagic"] = {
headers = { "Notes" },
get_cells = function(ability) return debuff_cells(ability, "dispelmagic") end,
},
["moveslow"] = {
headers = { "Move Slow", "Notes" },
get_cells = moveslow_cells,
},
["dashslow"] = {
headers = { "Dash Slow", "Notes" },
get_cells = dashslow_cells,
},
}
-- ============================================================
-- Hero lookup
-- ============================================================
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