Module:AbilityTable: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Refactor logic so abilitytables now correctly account for subabilities (Sinclair's Bolt, Pocket's Cloak, Rem's Tag Along etc). Ability Hints will break for a minute |
grouped repeated hero cells with rowspan |
||
| (3 intermediate revisions by 2 users not shown) | |||
| Line 9: | Line 9: | ||
local extra_columns = { | local extra_columns = { | ||
["barrier"] = { | |||
headers = { "Barrier" }, | |||
get_cells = Renderers.RenderBarrier, | |||
}, | |||
["charges"] = { | ["charges"] = { | ||
headers = { "Charges", "Time Between Charges", "Notes" }, | headers = { "Charges", "Time Between Charges", "Notes" }, | ||
| Line 20: | Line 24: | ||
headers = { "Base Damage", "Heavy Melee Scaling", "Notes" }, | headers = { "Base Damage", "Heavy Melee Scaling", "Notes" }, | ||
get_cells = Renderers.RenderHeavyMelee, | get_cells = Renderers.RenderHeavyMelee, | ||
}, | |||
["scalingspirit"] = { | |||
headers = { "Spirit Scaling", "Notes" }, | |||
get_cells = Renderers.RenderSpiritScaling, | |||
}, | }, | ||
["healreduce"] = { | ["healreduce"] = { | ||
| Line 32: | Line 40: | ||
headers = { "Dash Slow", "Notes" }, | headers = { "Dash Slow", "Notes" }, | ||
get_cells = Renderers.RenderDashSlow, | get_cells = Renderers.RenderDashSlow, | ||
}, | |||
["extradebuffresist"] = { | |||
headers = { "Debuff Resist" }, | |||
get_cells = Renderers.RenderExtraDebuffResist, | |||
}, | }, | ||
} | } | ||
| Line 314: | Line 326: | ||
end | end | ||
table.insert(out, header_row) | table.insert(out, header_row) | ||
local hero_counts = {} | |||
for _, key in ipairs(filtered) do | |||
local hero = views[key].heroName | |||
hero_counts[hero] = (hero_counts[hero] or 0) + 1 | |||
end | |||
local hero_seen = {} | |||
for _, key in ipairs(filtered) do | for _, key in ipairs(filtered) do | ||
local entry = views[key] | local entry = views[key] | ||
local ability = entry.view | local ability = entry.view | ||
local hero = entry.heroName | |||
local | local ability_cell = frame:expandTemplate{ | ||
title = "AbilityIcon", | |||
args = { ability["Name"] } | |||
} | |||
local extra = get_cells(ability) | local extra = get_cells(ability) | ||
enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]]) | enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]]) | ||
local row = '| class="ability-table-cell-hero" | ' | local row | ||
if not hero_seen[hero] then | |||
local hero_cell = frame:expandTemplate{ | |||
title = "HeroIcon", | |||
args = { hero } | |||
} | |||
if hero_counts[hero] > 1 then | |||
row = '| rowspan="' .. hero_counts[hero] .. '" class="ability-table-cell-hero" | ' | |||
.. hero_cell | |||
.. ' || class="ability-table-cell-ability" | ' .. ability_cell | |||
else | |||
row = '| class="ability-table-cell-hero" | ' | |||
.. hero_cell | |||
.. ' || class="ability-table-cell-ability" | ' .. ability_cell | |||
end | |||
hero_seen[hero] = true | |||
else | |||
row = '| class="ability-table-cell-ability" | ' .. ability_cell | |||
end | |||
for _, header in ipairs(final_headers) do | for _, header in ipairs(final_headers) do | ||
local value = extra[header] | local value = extra[header] | ||
Latest revision as of 02:53, 22 August 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
[edit source]- 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.
local Lists = require("Module:AbilityTable/Lists")
local Notes = require("Module:AbilityTable/Notes")
local Renderers = require("Module:AbilityTable/ComplexRenderers")
local GameData = require("Module:GameData")
local p = {}
local extra_columns = {
["barrier"] = {
headers = { "Barrier" },
get_cells = Renderers.RenderBarrier,
},
["charges"] = {
headers = { "Charges", "Time Between Charges", "Notes" },
get_cells = Renderers.RenderCharges,
},
["melee"] = {
headers = { "Base Damage", "Light Melee Scaling", "Notes" },
get_cells = Renderers.RenderMelee,
},
["heavymelee"] = {
headers = { "Base Damage", "Heavy Melee Scaling", "Notes" },
get_cells = Renderers.RenderHeavyMelee,
},
["scalingspirit"] = {
headers = { "Spirit Scaling", "Notes" },
get_cells = Renderers.RenderSpiritScaling,
},
["healreduce"] = {
headers = { "Heal Reduction", "Notes" },
get_cells = Renderers.RenderHealReduce,
},
["moveslow"] = {
headers = { "Move Slow", "Notes" },
get_cells = Renderers.RenderMoveSlow,
},
["dashslow"] = {
headers = { "Dash Slow", "Notes" },
get_cells = Renderers.RenderDashSlow,
},
["extradebuffresist"] = {
headers = { "Debuff Resist" },
get_cells = Renderers.RenderExtraDebuffResist,
},
}
-- ============================================================
-- Note enrichment
-- ============================================================
-- Merges auto-generated and human notes for full-table mode.
-- Capitalizes human note, joins with ". ", ensures trailing period.
local function enrich_notes_full(extra, human_note)
if human_note then
human_note = human_note:sub(1,1):upper() .. human_note:sub(2)
extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human_note) or human_note
end
if extra["Notes"] and extra["Notes"]:sub(-1) ~= "." then
extra["Notes"] = extra["Notes"] .. "."
end
end
-- Merges auto-generated and human notes for compact mode.
-- Decapitalizes human note (skips T{N} prefixes like "T1 slow"),
-- joins with "; ", strips trailing period.
local function enrich_notes_compact(extra, human_note)
if human_note then
local first = human_note:sub(1,1)
if first:match("%u") and not human_note:sub(2,2):match("%d") then
human_note = first:lower() .. human_note:sub(2)
end
if human_note:sub(-1) == "." then human_note = human_note:sub(1, -2) end
extra["Notes"] = extra["Notes"] and (extra["Notes"] .. "; " .. human_note) or human_note
end
end
-- ============================================================
-- Ability views
--
-- In-game Ability may containe 1 or more records in AbilityData.json
-- Example: Vexing Bolt is 2 records (Launch Bolt + Redirect Bolt).
-- "Ability View" is a main-record + any relevant sub-ability records.
-- ============================================================
local _views = nil -- card key -> { view = record, heroName = ..., abilityNum = ... }
local _order = nil -- array of card keys, sorted by hero then slot
local function build_views()
if _views then return _views, _order end
_views, _order = {}, {}
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 ability_data = mw.loadJsonData(GameData.Dataset.ABILITIES)
local cards = mw.loadJsonData("Data:AbilityCards.json")
local card_of = {}
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)
local key = type(ability) == "table" and ability["Key"] or nil
if slot_num and key and ability_data[key] then
card_of[key] = { heroName = hero_name, abilityNum = slot_num }
end
end
end
end
end
-- Attach each non-card record to the longest card key it prefixes.
local subs = {}
for key, record in pairs(ability_data) do
if not card_of[key] and record["IsDisabled"] ~= true then
local parent = nil
for card_key in pairs(card_of) do
if key:sub(1, #card_key + 1) == card_key .. "_"
and (parent == nil or #card_key > #parent)
then
parent = card_key
end
end
if parent then
subs[parent] = subs[parent] or {}
table.insert(subs[parent], record)
end
end
end
for key, info in pairs(card_of) do
local view = {}
for k, v in pairs(ability_data[key]) do view[k] = v end
view["_subabilities"] = subs[key]
_views[key] = { view = view, heroName = info.heroName, abilityNum = info.abilityNum }
table.insert(_order, key)
end
table.sort(_order, function(a, b)
local ia, ib = _views[a], _views[b]
if ia.heroName ~= ib.heroName then return ia.heroName < ib.heroName end
return ia.abilityNum < ib.abilityNum
end)
return _views, _order
end
-- ============================================================
-- Membership
-- Pieces of code that help in telling "is ability X a member of list Y".
-- main function is: p.is_member()
-- ============================================================
-- Memoized name-exclusion set per list (from Lists.exclude_abilities).
local _excluded = {}
local function excluded_set(stat)
if _excluded[stat] then return _excluded[stat] end
local set = {}
for _, name in ipairs(Lists.exclude_abilities[stat] or {}) do
set[name] = true
end
_excluded[stat] = set
return set
end
-- Per-list rules that can't be expressed as prop lists or exclusions.
-- Returns true to keep the ability, false to drop it.
local function special_ok(stat, record, ability_num)
if stat == "dispelmagic" and ability_num == 4 then return false end
return true
end
-- Public: does ability `ability_key` belong to list `stat`?
function p.is_member(stat, ability_key, ability_num)
local props = Lists.lists[stat]
if not props then return false end
local views = build_views()
local entry = views[ability_key]
local view = entry and entry.view
if not view then
view = mw.loadJsonData(GameData.Dataset.ABILITIES)[ability_key]
if not view then return false end
end
local name = view["Name"]
if name and excluded_set(stat)[name] then return false end
ability_num = ability_num or (entry and entry.abilityNum)
if not special_ok(stat, view, ability_num) then return false end
return GameData.entity_matches(view, props)
end
-- ============================================================
-- Entry point
-- ============================================================
function p.render(frame)
local stat = mw.text.trim(frame.args[1] or ""):lower()
local internal_keys = Lists.lists[stat]
if not internal_keys then
return '<span class="error">AbilityTable: unknown list "' .. 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 collapsed = mw.text.trim(frame.args["collapsed"] or ""):lower()
local views, order = build_views()
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 = Notes[stat]
local filtered = {}
for _, key in ipairs(order) do
if p.is_member(stat, key) then
table.insert(filtered, key)
end
end
if #filtered == 0 then
return "No abilities found for list: " .. stat
end
local lists_url = mw.uri.fullUrl("Module:AbilityTable/Lists")
local notes_url = mw.uri.fullUrl("Module:AbilityTable/Notes", { action = "edit" })
local disclaimer = frame:preprocess(
'<div class="ability-table-disclaimer">'
.. 'This is a dynamic list '
.. '<span class="ability-table-list-name">"' .. stat .. '"</span>. '
.. '[' .. tostring(lists_url) .. ' Edit contents]'
.. ' · '
.. '[' .. tostring(notes_url) .. ' Edit notes].'
.. '</div>'
)
if compact then
local hero_order = {}
local hero_abilities = {}
for _, key in ipairs(filtered) do
local hero = views[key].heroName
if not hero_abilities[hero] then
table.insert(hero_order, hero)
hero_abilities[hero] = {}
end
table.insert(hero_abilities[hero], views[key].view)
end
local items = {}
for _, hero in ipairs(hero_order) do
local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } }
local ability_parts = {}
for _, ability in ipairs(hero_abilities[hero]) do
local name = ability["Name"]
local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } }
local extra = get_cells(ability)
enrich_notes_compact(extra, stat_notes and stat_notes[name])
local note = extra["Notes"]
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 table_class = 'wikitable sortable ability-table'
if collapsed ~= "" then
table_class = table_class .. ' mw-collapsible'
if collapsed == "yes" or collapsed == "true" or collapsed == "1" then
table_class = table_class .. ' mw-collapsed'
end
end
local out = {}
table.insert(out, disclaimer)
table.insert(out, '{| class="' .. table_class .. '"')
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)
local hero_counts = {}
for _, key in ipairs(filtered) do
local hero = views[key].heroName
hero_counts[hero] = (hero_counts[hero] or 0) + 1
end
local hero_seen = {}
for _, key in ipairs(filtered) do
local entry = views[key]
local ability = entry.view
local hero = entry.heroName
local ability_cell = frame:expandTemplate{
title = "AbilityIcon",
args = { ability["Name"] }
}
local extra = get_cells(ability)
enrich_notes_full(extra, stat_notes and stat_notes[ability["Name"]])
local row
if not hero_seen[hero] then
local hero_cell = frame:expandTemplate{
title = "HeroIcon",
args = { hero }
}
if hero_counts[hero] > 1 then
row = '| rowspan="' .. hero_counts[hero] .. '" class="ability-table-cell-hero" | '
.. hero_cell
.. ' || class="ability-table-cell-ability" | ' .. ability_cell
else
row = '| class="ability-table-cell-hero" | '
.. hero_cell
.. ' || class="ability-table-cell-ability" | ' .. ability_cell
end
hero_seen[hero] = true
else
row = '| class="ability-table-cell-ability" | ' .. ability_cell
end
for _, header in ipairs(final_headers) do
local value = extra[header]
local cls = "ability-table-cell-" .. header:lower():gsub("%s+", "-")
local content = value ~= nil and tostring(value) or "—"
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