Module:AbilityTable: Difference between revisionsGive feedback
Jump to navigation
Jump to search
mNo edit summary |
healreduce: remove Duration column; iterate stat_to_props for amount; drop find_base_duration (with help from vergir-bot LLM) |
||
| Line 52: | Line 52: | ||
}, | }, | ||
["moveslow"] = { | ["moveslow"] = { | ||
["Grapple Arm"] = "Applies a short strong slow when the victim reaches Bebop.", | ["Grapple Arm"] = "Applies a short strong slow when the victim reaches Bebop.", | ||
["Heavy Barrage"] = "Slows McGinnis by unknown amount." | ["Heavy Barrage"] = "Slows McGinnis by unknown amount.", | ||
}, | }, | ||
} | } | ||
| Line 77: | Line 77: | ||
local column_suffix = { | local column_suffix = { | ||
["Time Between Charges"] = "s", | ["Time Between Charges"] = "s", | ||
} | } | ||
| Line 238: | Line 237: | ||
if #upgrade_notes > 0 then cells["Notes"] = table.concat(upgrade_notes, " ") end | if #upgrade_notes > 0 then cells["Notes"] = table.concat(upgrade_notes, " ") end | ||
return cells | return cells | ||
end | end | ||
| Line 251: | Line 242: | ||
local cells = {} | local cells = {} | ||
local notes = {} | local notes = {} | ||
local | local props = stat_to_props["healreduce"] | ||
-- Find the first heal reduction value, at base or in upgrades. | |||
-- DisableHealing is a flag (value=100%); all other props are negative percentages. | |||
local first_pct, first_tier, first_is_disable = nil, nil, false | 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"] | local upgrades = ability["Upgrades"] | ||
if type(upgrades) == "table" then | if type(upgrades) == "table" then | ||
for i, tier in ipairs(upgrades) do | for i, tier in ipairs(upgrades) do | ||
if type(tier) == "table" then | 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 | ||
end | end | ||
if first_pct or first_is_disable then break end | |||
end | end | ||
end | end | ||
| Line 285: | Line 286: | ||
cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "") | cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "") | ||
end | end | ||
-- Upgrade notes: percent changes after the first value. | |||
local upgrades = ability["Upgrades"] | local upgrades = ability["Upgrades"] | ||
if type(upgrades) == "table" then | if type(upgrades) == "table" then | ||
local | local running = first_pct | ||
local first_pct_seen = (first_tier == nil) | local first_pct_seen = (first_tier == nil) | ||
for i, tier in ipairs(upgrades) do | for i, tier in ipairs(upgrades) do | ||
if type(tier) == "table" then | if type(tier) == "table" then | ||
if first_tier == i then | if first_tier == i then | ||
first_pct_seen = true | first_pct_seen = true | ||
if first_pct then | if first_pct then running = first_pct end | ||
elseif first_pct_seen | elseif first_pct_seen then | ||
local | 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 | |||
local | table.insert(notes, "T" .. i .. " upgrade increases heal reduction by " | ||
table.insert( | .. delta .. "% (total " .. total .. "%).") | ||
running = total | |||
.. | |||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
| Line 449: | Line 444: | ||
}, | }, | ||
["healreduce"] = { | ["healreduce"] = { | ||
headers = { "Heal Reduction | headers = { "Heal Reduction", "Notes" }, | ||
get_cells = healreduce_cells, | get_cells = healreduce_cells, | ||
}, | }, | ||
Revision as of 23:53, 15 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"] = { "melee", "heavy_melee", "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",
-- can also just list abilities if they don't have a reusable property
"Hotel Guest", "Life Drain", "Combo", "Light Eater", "Spirit Lasso", "Card Trick"
},
["moveslow"] = { "SlowPercent", "EnemySlowPct", "MoveSlowPercent", "MaxSlowPercent", "MovementSpeedSlow", "MovementSlowPct", "ClubSlowPercent" },
["dashslow"] = { "GroundDashReductionPercent", "MaxGroundDashReductionPercent", "MoveSpeedAndDashSlowPct", "EnemyDashSlowPercent" },
}
-- 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.",
},
}
-- 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.
},
}
-- 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
-- ============================================================
-- 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"]
-- Find the first heal reduction value, at base or in upgrades.
-- DisableHealing is a flag (value=100%); all other props are negative percentages.
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
-- Upgrade notes: percent changes after the first value.
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 single percentage stat (move slow, dash slow).
-- col_name: the column header string (e.g. "Move Slow")
-- fields: ordered list of property names to check (first non-zero wins for display)
-- Upgrade deltas are additive; notes track totals.
local function pct_stat_cells(ability, col_name, fields)
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) .. "%"
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
.. "% (total " .. total .. "%).")
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 = pct_stat_cells(ability, "Move Slow", stat_to_props["moveslow"])
-- If the ability slows the caster during channel, note it.
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"]
-- If GroundDashReductionPercent is set at base AND any other dashslow prop
-- exists anywhere (base or upgrades), the base value is a caster self-slow
-- rather than an enemy debuff. Exclude it from the main value and note it instead.
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 = pct_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]
-- 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)
-- 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>"
)
if compact then
-- Compact mode: <ul> with one <li> per hero.
-- Each item: "{HeroIcon}: {AbilityIcon} (note), {AbilityIcon}, ..."
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
-- Normal mode: wikitable with one row per ability and stat-specific columns.
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