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", | |||
"DebuffAccuracy", "TimeSlowDuration", "LateCheckoutStun", | |||
}, | |||
} | |||
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 whose effects cannot be inferred from | |||
-- data alone (e.g. engine-side CC with no matching data fields). | |||
-- The hero is resolved automatically from AbilityCards.json via the ability name. | |||
-- Each entry requires: | |||
-- effects = { "EffectName", ... } (supports "Effect (TN)" for upgrade-only) | |||
local manual_entries = { | |||
[" | ["debuff"] = { | ||
["Combo"] = { | |||
effects = { "Stun" }, | |||
}, | |||
["Sand Blast"] = { | |||
effects = { "Disarm" }, | |||
}, | |||
["Spirit Lasso"] = { | |||
effects = { "Stun" }, | |||
}, | |||
}, | }, | ||
} | } | ||
-- 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 | ||
if | |||
end | 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 function | local upgrades = ability["Upgrades"] | ||
if | if type(upgrades) ~= "table" then return nil end | ||
for i, tier in ipairs(upgrades) do | |||
if | if type(tier) == "table" and tier[field] ~= nil then | ||
return { tier = i, value = num_val(tier[field]) } | |||
end | end | ||
end | end | ||
return nil | |||
end | 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 | local cooldown, upgrade_cooldown | ||
local | 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 | local cells = {} | ||
if has_base then | |||
cells["Charges"] = tostring(base_charges) | |||
if cooldown then | |||
cells["Time Between Charges"] = tostring(cooldown) | |||
then | |||
end | end | ||
end | end | ||
local | 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 | |||
local | return cells | ||
for _, | end | ||
if type( | |||
local | local function find_base_melee_scale(ability) | ||
if | for _, v in pairs(ability) do | ||
for | if type(v) == "table" then | ||
local scale = v["Scale"] | |||
local | 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 | ||
| Line 123: | Line 179: | ||
end | end | ||
end | end | ||
return notes | |||
end | |||
local function melee_cells(ability) | |||
local cells = {} | |||
local base_scale = find_base_melee_scale(ability) | |||
if base_scale then cells["Light Melee Scaling"] = "×" .. base_scale end | |||
local upgrade_notes = build_melee_upgrade_notes(ability, base_scale) | |||
if #upgrade_notes > 0 then cells["Notes"] = table.concat(upgrade_notes, " ") end | |||
return cells | |||
end | |||
local function find_base_duration(ability) | |||
for _, field in ipairs({ "VenomDuration", "DebuffDuration", "AbilityDuration", "BurnDuration" }) do | |||
local v = num_val(ability[field]) | |||
if v and v > 0 then return v, field end | |||
end | |||
return nil, nil | |||
end | |||
local function healreduce_cells(ability) | |||
local | local cells = {} | ||
local notes = {} | |||
local base_dur, dur_field = find_base_duration(ability) | |||
for | local first_pct, first_tier, first_is_disable = nil, nil, false | ||
if | local base_receive = num_val(ability["HealAmpReceivePenaltyPercent"]) | ||
and | local base_regen = num_val(ability["HealAmpRegenPenaltyPercent"]) | ||
local base_pct_raw = base_receive or base_regen | |||
if base_pct_raw and base_pct_raw ~= 0 then | |||
first_pct = math.abs(base_pct_raw) | |||
elseif ability["DisableHealing"] and ability["DisableHealing"] ~= 0 then | |||
first_is_disable = true | |||
else | |||
local upgrades = ability["Upgrades"] | |||
if type(upgrades) == "table" then | |||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
local up_raw = num_val(tier["HealAmpReceivePenaltyPercent"]) | |||
or num_val(tier["HealAmpRegenPenaltyPercent"]) | |||
if up_raw and up_raw ~= 0 then | |||
first_pct, first_tier = math.abs(up_raw), i | |||
break | |||
elseif tier["DisableHealing"] and tier["DisableHealing"] ~= 0 then | |||
first_is_disable, first_tier = true, i | |||
break | |||
end | |||
end | end | ||
end | end | ||
end | |||
end | |||
table.insert( | |||
if first_is_disable then | |||
cells["Heal Reduction"] = "100%" .. (first_tier and " (T" .. first_tier .. ")" or "") | |||
elseif first_pct then | |||
cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "") | |||
end | |||
if base_dur then cells["Duration"] = tostring(base_dur) end | |||
local upgrades = ability["Upgrades"] | |||
if type(upgrades) == "table" then | |||
local running_pct, running_dur = first_pct, base_dur | |||
local first_pct_seen = (first_tier == nil) | |||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
local tier_notes = {} | |||
local pct_delta = num_val(tier["HealAmpReceivePenaltyPercent"]) | |||
or num_val(tier["HealAmpRegenPenaltyPercent"]) | |||
if first_tier == i then | |||
first_pct_seen = true | |||
if first_pct then running_pct = first_pct end | |||
elseif first_pct_seen and pct_delta and pct_delta ~= 0 then | |||
local delta_abs = math.abs(pct_delta) | |||
local total = (running_pct or 0) + delta_abs | |||
table.insert(tier_notes, "T" .. i .. " upgrade increases heal reduction by " | |||
.. delta_abs .. "% (total " .. total .. "%).") | |||
running_pct = total | |||
end | |||
if dur_field then | |||
local dur_delta = num_val(tier[dur_field]) | |||
if dur_delta and dur_delta ~= 0 and running_dur then | |||
local total_dur = running_dur + dur_delta | |||
table.insert(tier_notes, "T" .. i .. " upgrade " | |||
.. (dur_delta > 0 and "increases" or "decreases") | |||
.. " duration by " .. math.abs(dur_delta) | |||
.. "s (total " .. total_dur .. "s).") | |||
running_dur = total_dur | |||
end | |||
end | |||
for _, n in ipairs(tier_notes) do table.insert(notes, n) end | |||
end | end | ||
end | end | ||
end | end | ||
for | 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. | |||
table. | -- Props not listed here fall back to using the prop name itself as the effect label. | ||
-- DebuffDuration is absent: it signals "(See notes)" on Duration 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", | |||
DebuffAccuracy = "Inaccuracy", | |||
TimeSlowDuration = "Time Slow", | |||
LateCheckoutStun = "Stun", | |||
} | |||
local function debuff_cells(ability, stat_manual) | |||
local cells = {} | |||
-- Manual entry takes full precedence. | |||
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 | |||
return cells | |||
end | end | ||
local effects_seen = {} -- ordered list of { name, tier } | |||
local effects_set = {} -- effect name -> true, for dedup | |||
-- Iterate stat_to_props["debuff"], skipping DebuffDuration (it has no named effect). | |||
end | -- Use debuff_prop_to_effect for the display name; fall back to the prop name itself. | ||
local function collect_effects(obj, tier) | |||
for _, prop in ipairs(stat_to_props["debuff"]) do | |||
local val = obj[prop] | |||
if val and val ~= 0 then | |||
local effect = debuff_prop_to_effect[prop] or prop | |||
if not effects_set[effect] then | |||
table.insert(effects_seen, { name = effect, tier = tier }) | |||
effects_set[effect] = true | |||
end | |||
end | |||
end | |||
end | |||
collect_effects(ability, nil) | |||
local upgrades = ability["Upgrades"] | |||
if type(upgrades) == "table" then | |||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then collect_effects(tier, i) end | |||
end | |||
end | |||
local effect_parts = {} | |||
for _, e in ipairs(effects_seen) do | |||
table.insert(effect_parts, e.tier and (e.name .. " (T" .. e.tier .. ")") or e.name) | |||
local | |||
for _, | |||
end | end | ||
if #effect_parts > 0 then | |||
return | cells["Effects"] = table.concat(effect_parts, ", ") | ||
end | |||
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" }, | |||
get_cells = debuff_cells, | |||
}, | |||
} | |||
-- ============================================================ | |||
-- Name lookup: ability name -> { heroName, abilityNum } | |||
-- Built from HeroData (active hero filter) + AbilityCards. | |||
-- Used for both prop-matched and manual abilities. | |||
-- ============================================================ | |||
local _name_lookup = nil | |||
local function build_name_lookup() | |||
if _name_lookup then return _name_lookup end | |||
_name_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["Name"] then | |||
_name_lookup[ability["Name"]] = { | |||
heroName = hero_name, | |||
abilityNum = slot_num, | |||
} | |||
end | |||
end | |||
end | |||
end | |||
end | |||
return _name_lookup | |||
end | end | ||
| Line 215: | Line 416: | ||
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 | ||
| Line 226: | Line 427: | ||
end | end | ||
local | local name_lookup = build_name_lookup() | ||
local spec = extra_columns[stat] | |||
local headers = spec and spec.headers or {} | |||
local stat_notes = human_notes[stat] | |||
local stat_manual = manual_entries[stat] | |||
local | local get_cells | ||
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 | |||
else | |||
get_cells = function() return {} end | |||
end | |||
local | local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys) | ||
-- Keep only abilities belonging to active, released heroes | |||
local filtered = {} | local filtered = {} | ||
for _, | local filtered_names = {} | ||
if | for _, ability in ipairs(abilities) do | ||
table.insert(filtered, | if ability["Name"] and name_lookup[ability["Name"]] then | ||
table.insert(filtered, ability) | |||
filtered_names[ability["Name"]] = true | |||
end | end | ||
end | end | ||
-- Inject manual entries not already covered by prop-matching. | |||
-- Hero and slot are resolved via name_lookup; if the ability isn't in AbilityCards | |||
-- the row shows "—" in the Hero column and sorts to the end. | |||
if stat_manual then | |||
for name, _ in pairs(stat_manual) do | |||
if not filtered_names[name] then | |||
table.insert(filtered, { Name = name }) | |||
if | |||
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. | |||
-- Abilities not in name_lookup sort to the end alphabetically. | |||
table.sort(filtered, function(a, b) | |||
local ia = name_lookup[a["Name"]] | |||
local ib = name_lookup[b["Name"]] | |||
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) | |||
end | |||
return | |||
end | |||
-- Determine final headers | |||
local has_notes_header = false | local has_notes_header = false | ||
local final_headers = {} | local final_headers = {} | ||
| Line 307: | Line 496: | ||
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>" | |||
) | |||
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 header_row = '! class="ability-table-col-hero" | Hero' | local header_row = '! class="ability-table-col-hero" | Hero' | ||
| Line 327: | Line 518: | ||
table.insert(out, header_row) | table.insert(out, header_row) | ||
for _, ability in ipairs(filtered) do | |||
for _, | local info = name_lookup[ability["Name"]] | ||
local | |||
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 ability_cell = frame:expandTemplate{ | |||
local extra = get_cells(ability) | 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 | end | ||
local row = '| 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 "" | |||
content = (type(value) == "string" and value:sub(1,1) == "(") | |||
and value or (value .. suffix) | |||
else | |||
content = "—" | |||
end | |||
row = row .. ' || class="' .. cls .. '" | ' .. content | row = row .. ' || class="' .. cls .. '" | ' .. content | ||
end | end | ||