Editing Module:AbilityTableGive feedback
Jump to navigation
Jump to search
The edit can be undone.
Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
| Latest revision | Your text | ||
| Line 1: | Line 1: | ||
-- Renders sortable wikitables of abilities filtered by a named stat. | -- Renders sortable wikitables of abilities filtered by a named stat. | ||
local | -- 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" }, | |||
["debuff"] = { | |||
"StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", | |||
"PetrifyDuration", "HexDuration", "ImmobilizeDuration", "LiftDuration", | |||
"SlowPercent", "DebuffDuration", "HoldInPlaceDuration", | |||
}, | |||
} | |||
local | -- Editor-authored notes appended to the Notes column. | ||
local human_notes = { | |||
["charges"] = { | ["charges"] = { | ||
["Jar of Dead"] = "Charges by being near when anything dies.", | |||
}, | }, | ||
["melee"] = { | ["melee"] = { | ||
["Puddle Punch"] = "Can be parried.", | |||
}, | }, | ||
} | |||
-- Manually specified entries for abilities that have no matching props, | |||
-- or whose effects cannot be inferred from data alone. | |||
-- Each entry: { effects = { "EffectName", ... }, duration = <number or nil> } | |||
-- effects list may include "(TN)" suffix for upgrade-only effects: "Stun (T3)" | |||
-- duration nil renders as "—". | |||
local manual_entries = { | |||
[" | ["debuff"] = { | ||
["Combo"] = { | |||
effects = { "Stun" }, | |||
duration = 2.4, | |||
}, | |||
["Sand Blast"] = { | |||
effects = { "Disarm" }, | |||
duration = 2.5, | |||
}, | |||
["Spirit Lasso"] = { | |||
effects = { "Stun" }, | |||
duration = 2.25, | |||
}, | |||
}, | }, | ||
} | } | ||
-- 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 | ||
local function | if type(v) == "table" then return v["Value"] end | ||
if | 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 | 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 | 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 | end | ||
-- | -- Finds the base light melee scale value: the first top-level property | ||
-- | -- whose Scale.Type is "melee" and whose scale value is > 0. | ||
local function find_base_melee_scale(ability) | |||
local function | 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 | ||
end | end | ||
return nil | |||
end | end | ||
-- ====================== | -- Scans all upgrade tiers for melee-scale changes. Upgrade objects are | ||
-- additive in the engine, so the final scale = base + upgrade delta. | |||
-- Returns a list of note strings, one per relevant upgrade tier found. | |||
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 = base_melee_scale + delta | |||
final = math.floor(final * 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 | 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 | |||
-- Returns the base duration value using priority: VenomDuration > DebuffDuration > AbilityDuration > BurnDuration. | |||
-- Also returns the field name that was found (for upgrade scanning). | |||
for _, | 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 | |||
then | |||
end | end | ||
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 | local first_pct = nil | ||
local | local first_tier = nil | ||
local first_is_disable = false | |||
local | 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) | |||
first_tier = nil | |||
local | elseif ability["DisableHealing"] and ability["DisableHealing"] ~= 0 then | ||
local | first_is_disable = true | ||
if | first_tier = nil | ||
else | |||
local upgrades = ability["Upgrades"] | |||
if type(upgrades) == "table" then | |||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
local up_receive = num_val(tier["HealAmpReceivePenaltyPercent"]) | |||
local up_regen = num_val(tier["HealAmpRegenPenaltyPercent"]) | |||
local up_raw = up_receive or up_regen | |||
if up_raw and up_raw ~= 0 then | |||
first_pct = math.abs(up_raw) | |||
first_tier = i | |||
break | |||
elseif tier["DisableHealing"] and tier["DisableHealing"] ~= 0 then | |||
first_is_disable = true | |||
first_tier = i | |||
break | |||
end | end | ||
end | end | ||
| Line 124: | Line 263: | ||
end | end | ||
if first_is_disable then | |||
local | local val = "100%" | ||
if first_tier then val = val .. " (T" .. first_tier .. ")" end | |||
if | cells["Heal Reduction"] = val | ||
elseif first_pct then | |||
local val = tostring(first_pct) .. "%" | |||
if | if first_tier then val = val .. " (T" .. first_tier .. ")" end | ||
and | cells["Heal Reduction"] = val | ||
end | |||
if base_dur then | |||
cells["Duration"] = tostring(base_dur) | |||
end | |||
local upgrades = ability["Upgrades"] | |||
if type(upgrades) == "table" then | |||
local running_pct = first_pct | |||
local running_dur = 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 up_receive = num_val(tier["HealAmpReceivePenaltyPercent"]) | |||
local up_regen = num_val(tier["HealAmpRegenPenaltyPercent"]) | |||
local pct_delta = up_receive or up_regen | |||
local is_this_first = (first_tier == i) | |||
if is_this_first 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 | ||
end | |||
end | |||
if #notes > 0 then | |||
cells["Notes"] = table.concat(notes, " ") | |||
end | |||
return cells | |||
end | |||
-- Maps AbilityData property names to human-readable effect names for the debuff table. | |||
-- Properties listed here are the canonical CC props; DebuffDuration is intentionally | |||
-- absent because it renders as "(See notes)" rather than a named effect. | |||
local debuff_prop_to_effect = { | |||
StunDuration = "Stun", | |||
SleepDuration = "Sleep", | |||
SilenceDuration = "Silence", | |||
SilenceDebuff = "Silence", | |||
PetrifyDuration = "Petrify", | |||
HexDuration = "Hex", | |||
ImmobilizeDuration = "Root", | |||
LiftDuration = "Lift", | |||
SlowPercent = "Slow", | |||
HoldInPlaceDuration = "Stop", | |||
} | |||
-- Duration fields corresponding to each CC prop (for reading the numeric value). | |||
-- SlowPercent and SilenceDebuff/HoldInPlaceDuration have separate duration fields. | |||
local debuff_prop_duration_field = { | |||
StunDuration = "StunDuration", | |||
SleepDuration = "SleepDuration", | |||
SilenceDuration = "SilenceDuration", | |||
SilenceDebuff = "SilenceDuration", -- boolean flag; duration in SilenceDuration | |||
PetrifyDuration = "PetrifyDuration", | |||
HexDuration = "HexDuration", | |||
ImmobilizeDuration = "ImmobilizeDuration", | |||
LiftDuration = "LiftDuration", | |||
SlowPercent = "SlowDuration", | |||
HoldInPlaceDuration = "HoldInPlaceDuration", | |||
} | |||
local function debuff_cells(ability, stat_manual) | |||
local cells = {} | |||
-- If a manual entry exists for this ability, use it directly. | |||
local manual = stat_manual and stat_manual[ability["Name"]] | |||
if manual then | |||
if manual.effects and #manual.effects > 0 then | |||
cells["Effects"] = table.concat(manual.effects, ", ") | |||
end | |||
if manual.duration then | |||
cells["Duration"] = tostring(manual.duration) | |||
end | |||
return cells | |||
end | |||
-- Collect effects from base props and upgrades. | |||
-- effects_seen: effect_name -> { tier = N or nil, dur_field, value } | |||
-- tier nil = present at base; tier N = first appears at upgrade N | |||
local effects_seen = {} -- ordered list of { name, tier } | |||
local effects_set = {} -- name -> true, for dedup | |||
local CC_PROPS = { | |||
"StunDuration", "SleepDuration", "SilenceDuration", "SilenceDebuff", | |||
"PetrifyDuration", "HexDuration", "ImmobilizeDuration", "LiftDuration", | |||
"SlowPercent", "HoldInPlaceDuration", | |||
} | |||
-- Check base fields | |||
for _, prop in ipairs(CC_PROPS) do | |||
local val = ability[prop] | |||
local effect = debuff_prop_to_effect[prop] | |||
if val and val ~= 0 and effect and not effects_set[effect] then | |||
table.insert(effects_seen, { name = effect, tier = nil }) | |||
effects_set[effect] = true | |||
end | |||
end | |||
-- Check upgrades | |||
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(CC_PROPS) do | |||
local val = tier[prop] | |||
local effect = debuff_prop_to_effect[prop] | |||
if val and val ~= 0 and effect and not effects_set[effect] then | |||
table.insert(effects_seen, { name = effect, tier = i }) | |||
effects_set[effect] = true | |||
end | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
-- Check DebuffDuration separately (adds "(See notes)" sentinel if present) | |||
local has_debuff_dur = (ability["DebuffDuration"] and ability["DebuffDuration"] ~= 0) | |||
for | if not has_debuff_dur and type(upgrades) == "table" then | ||
for _, tier in ipairs(upgrades) do | |||
if type(tier) == "table" and tier["DebuffDuration"] and tier["DebuffDuration"] ~= 0 then | |||
table.insert( | has_debuff_dur = true | ||
break | |||
end | |||
end | |||
end | |||
-- Build Effects cell string | |||
local effect_parts = {} | |||
for _, e in ipairs(effects_seen) do | |||
if e.tier then | |||
table.insert(effect_parts, e.name .. " (T" .. e.tier .. ")") | |||
else | |||
table.insert(effect_parts, e.name) | |||
end | |||
end | |||
if #effect_parts > 0 then | |||
cells["Effects"] = table.concat(effect_parts, ", ") | |||
end | end | ||
-- Build Duration cell. | |||
-- Rule: find all non-DebuffDuration duration fields present (base or upgrade). | |||
-- If exactly one distinct field found -> use its base or first-appearing value. | |||
-- If DebuffDuration present and it's the only duration -> "(See notes)". | |||
-- If multiple distinct fields -> "(See notes)". | |||
-- If none -> nil (renders as "—"). | |||
local dur_fields_found = {} -- field_name -> value | |||
local DUR_FIELDS = { | |||
"StunDuration", "SleepDuration", "SilenceDuration", "PetrifyDuration", | |||
"HexDuration", "ImmobilizeDuration", "LiftDuration", "SlowDuration", | |||
"HoldInPlaceDuration", | |||
} | |||
for _, f in ipairs(DUR_FIELDS) do | |||
end | local val = num_val(ability[f]) | ||
if val and val > 0 then | |||
dur_fields_found[f] = val | |||
end | |||
end | |||
-- Also check upgrades for any duration field not present at base | |||
if type(upgrades) == "table" then | |||
for _, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
for _, f in ipairs(DUR_FIELDS) do | |||
if not dur_fields_found[f] then | |||
local val = num_val(tier[f]) | |||
if val and val > 0 then | |||
dur_fields_found[f] = val | |||
end | |||
end | |||
end | |||
end | |||
end | |||
end | |||
local distinct_dur_count = 0 | |||
local sole_dur_val = nil | |||
for _, val in pairs(dur_fields_found) do | |||
distinct_dur_count = distinct_dur_count + 1 | |||
sole_dur_val = val | |||
end | |||
if distinct_dur_count == 1 then | |||
cells["Duration"] = tostring(sole_dur_val) | |||
elseif distinct_dur_count > 1 or has_debuff_dur then | |||
if | cells["Duration"] = "(See notes)" | ||
end | end | ||
-- distinct_dur_count == 0 and no debuff_dur -> Duration stays nil -> "—" | |||
return | |||
return cells | |||
end | 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, | |||
}, | |||
["debuff"] = { | |||
headers = { "Effects", "Duration" }, | |||
get_cells = debuff_cells, | |||
}, | |||
} | |||
-- ============================================================ | |||
-- Hero lookup (ability key -> { heroName, abilityNum }) | |||
-- ============================================================ | |||
local _hero_lookup = nil | |||
local function build_hero_lookup() | |||
if _hero_lookup then return _hero_lookup end | |||
_hero_lookup = {} | |||
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 | end | ||
local | 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" and ability["Key"] then | |||
_hero_lookup[ability["Key"]] = { | |||
heroName = hero_name, | |||
abilityNum = slot_num, | |||
} | |||
end | |||
end | |||
end | |||
end | |||
end | |||
return _hero_lookup | |||
end | end | ||
| Line 215: | Line 560: | ||
local stat = mw.text.trim(frame.args[1] or ""):lower() | local stat = mw.text.trim(frame.args[1] or ""):lower() | ||
local internal_keys = | local internal_keys = stat_to_props[stat] | ||
if not internal_keys then | if not internal_keys then | ||
return '<span class="error">AbilityTable: unknown | return '<span class="error">AbilityTable: unknown stat "' .. stat .. '".</span>' | ||
end | end | ||
-- Build set of columns to hide from the "hide" parameter | |||
local hide_set = {} | local hide_set = {} | ||
local hide_param = mw.text.trim(frame.args["hide"] or "") | local hide_param = mw.text.trim(frame.args["hide"] or "") | ||
if hide_param ~= "" then | if hide_param ~= "" then | ||
for col in hide_param:gmatch("[^,]+") do hide_set[mw.text.trim(col)] = true end | for col in hide_param:gmatch("[^,]+") do | ||
hide_set[mw.text.trim(col)] = true | |||
end | |||
end | end | ||
local | local lookup = build_hero_lookup() | ||
local spec = extra_columns[stat] | local spec = extra_columns[stat] | ||
local headers = spec and spec.headers or {} | local headers = spec and spec.headers or {} | ||
local | local stat_notes = human_notes[stat] | ||
local | local stat_manual = manual_entries[stat] | ||
local | -- Wrap the renderer to inject manual_entries for stats that need it | ||
local get_cells | |||
if | if spec then | ||
if stat == "debuff" then | |||
get_cells = function(ability) return debuff_cells(ability, stat_manual) end | |||
else | |||
get_cells = spec.get_cells | |||
end | end | ||
else | |||
get_cells = function() return {} end | |||
end | end | ||
local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys) | |||
-- Keep only abilities belonging to active, released heroes | |||
local filtered = {} | |||
for _, ability in ipairs(abilities) do | |||
local key = ability["Key"] | |||
if key and lookup[key] then | |||
table.insert(filtered, ability) | |||
end | |||
end | end | ||
-- Add manual entries whose ability key is not already in filtered | |||
if stat_manual then | |||
local filtered_names = {} | |||
for _, ability in ipairs(filtered) do | |||
filtered_names[ability["Name"]] = true | |||
end | |||
for name, _ in pairs(stat_manual) do | |||
if not filtered_names[name] then | |||
-- Find the ability record by name | |||
-- We search all abilities returned by get_entities with an empty key list | |||
-- instead: search the GameData for this name via a synthetic record | |||
-- Since we can't query by name, we inject a minimal synthetic record | |||
if | -- that debuff_cells will resolve via the manual entry | ||
local | table.insert(filtered, { Name = name, Key = "__manual__" .. name }) | ||
for _, | |||
if not | |||
table.insert( | |||
end | end | ||
end | end | ||
end | |||
if #filtered == 0 then | |||
for | return "No abilities found for stat: " .. stat | ||
end | |||
-- Sort by hero name, then ability slot number. | |||
-- Manual entries (key starts with __manual__) go at the end if not in lookup. | |||
table.sort(filtered, function(a, b) | |||
local ia = lookup[a["Key"]] | |||
local ib = 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 | end | ||
return ia.abilityNum < ib.abilityNum | |||
end) | |||
-- Determine final header list, respecting hide_set. | |||
local has_notes_header = false | local has_notes_header = false | ||
local final_headers = {} | local final_headers = {} | ||
| Line 305: | Line 650: | ||
end | end | ||
local needs_notes = (has_notes_header or stat_notes ~= nil) and not hide_set["Notes"] | 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 | 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>" | |||
) | |||
-- Render wikitable | |||
local out = {} | local out = {} | ||
table.insert(out, disclaimer) | table.insert(out, disclaimer) | ||
table.insert(out, '{| class=" | table.insert(out, '{| class="wikitable sortable ability-table"') | ||
local | local header_cells = '! class="ability-table-col-hero" | Hero' | ||
.. ' !! class="ability-table-col-ability" | Ability' | |||
for _, h in ipairs(final_headers) do | for _, h in ipairs(final_headers) do | ||
local cls = "ability-table-col-" .. h:lower():gsub("%s+", "-") | |||
header_cells = header_cells .. ' !! class="' .. cls .. '" | ' .. h | |||
end | end | ||
table.insert(out, header_cells) | |||
local | for _, ability in ipairs(filtered) do | ||
local info = lookup[ability["Key"]] | |||
local hero_cell, ability_cell | |||
if info then | |||
hero_cell = frame:expandTemplate{ | |||
local ability_cell | |||
if | |||
title = "HeroIcon", | title = "HeroIcon", | ||
args = { hero } | args = { info.heroName } | ||
} | |||
ability_cell = frame:expandTemplate{ | |||
title = "AbilityIcon", | |||
args = { ability["Name"] } | |||
} | |||
else | |||
-- Manual entry with no hero lookup: render name only | |||
hero_cell = "—" | |||
ability_cell = frame:expandTemplate{ | |||
title = "AbilityIcon", | |||
args = { ability["Name"] } | |||
} | } | ||
end | |||
local extra = get_cells(ability) | |||
-- Append human note to any renderer-generated note | |||
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 | end | ||
local row_cells = '| class="ability-table-cell-hero" | ' .. hero_cell | |||
.. ' || class="ability-table-cell-ability" | ' .. ability_cell | |||
for _, header in ipairs(final_headers) do | for _, header in ipairs(final_headers) do | ||
local value = extra[header] | local value = extra[header] | ||
local cls = "ability-table-cell-" .. header:lower():gsub("%s+", "-") | local cls = "ability-table-cell-" .. header:lower():gsub("%s+", "-") | ||
local content | local content | ||
if value ~= nil then | |||
local suffix = column_suffix[header] or "" | |||
-- Don't append suffix to already-complete strings like "(See notes)" | |||
if type(value) == "string" and value:sub(1, 1) == "(" then | |||
content = value | |||
else | |||
content = value .. suffix | |||
end | |||
else | |||
content = "—" | |||
end | |||
row_cells = row_cells .. ' || class="' .. cls .. '" | ' .. content | |||
end | end | ||
table.insert(out, "|-") | table.insert(out, "|-") | ||
table.insert(out, | table.insert(out, row_cells) | ||
end | end | ||