Module:AbilityTable: Difference between revisionsGive feedback
Jump to navigation
Jump to search
m dirtyfix for heavy melee notes |
refactor abilitytable to use submodules |
||
| 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 Lists = require("Module:AbilityTable/Lists") | |||
local | local Notes = require("Module:AbilityTable/Notes") | ||
local Renderers = require("Module:AbilityTable/ComplexRenderers") | |||
local GameData = require("Module:GameData") | |||
local p = {} | |||
-- ============================================================ | |||
-- Extra columns: maps stat names to their column headers and | |||
-- cell renderer functions from ComplexRenderers. | |||
-- Stats not listed here get a plain Hero + Ability + Notes table. | |||
-- ============================================================ | |||
local extra_columns = { | |||
local | |||
["charges"] = { | ["charges"] = { | ||
headers = { "Charges", "Time Between Charges", "Notes" }, | |||
get_cells = Renderers.RenderCharges, | |||
}, | }, | ||
["melee"] = { | ["melee"] = { | ||
headers = { "Light Melee Scaling", "Notes" }, | |||
get_cells = Renderers.RenderMelee, | |||
}, | }, | ||
[" | ["healreduce"] = { | ||
headers = { "Heal Reduction", "Notes" }, | |||
get_cells = Renderers.RenderHealReduce, | |||
}, | }, | ||
["debuffresist"] = { | ["debuffresist"] = { | ||
headers = { "Notes" }, | |||
get_cells = function(ability) return Renderers.RenderDebuff(ability, "debuffresist") end, | |||
}, | }, | ||
[" | ["dispelmagic"] = { | ||
headers = { "Notes" }, | |||
get_cells = function(ability) return Renderers.RenderDebuff(ability, "dispelmagic") end, | |||
}, | }, | ||
["moveslow"] = { | ["moveslow"] = { | ||
" | headers = { "Move Slow", "Notes" }, | ||
get_cells = Renderers.RenderMoveSlow, | |||
}, | }, | ||
["dashslow"] = { | ["dashslow"] = { | ||
headers = { "Dash Slow", "Notes" }, | |||
get_cells = Renderers.RenderDashSlow, | |||
}, | }, | ||
} | } | ||
-- ============================================================ | -- ============================================================ | ||
| Line 196: | Line 49: | ||
-- ============================================================ | -- ============================================================ | ||
-- Capitalises the first letter of a human note before merging | -- Capitalises the first letter of a human note before merging (full-table mode). | ||
local function human_note_full(note) | local function human_note_full(note) | ||
if not note or note == "" then return note end | if not note or note == "" then return note end | ||
| Line 203: | Line 55: | ||
end | end | ||
-- Ensures the final merged Notes string ends with "." | -- Ensures the final merged Notes string ends with "." (full-table mode). | ||
local function ensure_period(s) | local function ensure_period(s) | ||
if not s or s == "" then return s end | if not s or s == "" then return s end | ||
| Line 212: | Line 63: | ||
-- Compact mode: decapitalise first letter unless it looks like a tier prefix | -- Compact mode: decapitalise first letter unless it looks like a tier prefix | ||
-- (e.g. "T1", "T2") | -- (e.g. "T1", "T2"). Also strips any trailing ".". | ||
local function human_note_compact(note) | local function human_note_compact(note) | ||
if not note or note == "" then return note end | if not note or note == "" then return note end | ||
| Line 230: | Line 80: | ||
local function filter_abilities(stat, abilities, key_lookup) | local function filter_abilities(stat, abilities, key_lookup) | ||
local excluded = {} | local excluded = {} | ||
if | if Lists.exclude_abilities[stat] then | ||
for _, name in ipairs( | for _, name in ipairs(Lists.exclude_abilities[stat]) do | ||
excluded[name] = true | excluded[name] = true | ||
end | end | ||
| Line 252: | Line 102: | ||
return result | return result | ||
end | end | ||
-- ============================================================ | -- ============================================================ | ||
| Line 745: | Line 158: | ||
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 = Lists.lists[stat] | ||
if not internal_keys then | if not internal_keys then | ||
return '<span class="error">AbilityTable: unknown | return '<span class="error">AbilityTable: unknown list "' .. stat .. '".</span>' | ||
end | end | ||
| Line 762: | Line 175: | ||
local headers = spec and spec.headers or {} | local headers = spec and spec.headers or {} | ||
local get_cells = spec and spec.get_cells or function() return {} end | local get_cells = spec and spec.get_cells or function() return {} end | ||
local stat_notes = | local stat_notes = Notes[stat] | ||
local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys) | local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys) | ||
| Line 768: | Line 181: | ||
if #filtered == 0 then | if #filtered == 0 then | ||
return "No abilities found for | return "No abilities found for list: " .. stat | ||
end | end | ||
| Line 878: | Line 291: | ||
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 = value ~= nil and tostring(value) or "—" | ||
row = row .. ' || class="' .. cls .. '" | ' .. content | row = row .. ' || class="' .. cls .. '" | ' .. content | ||
end | end | ||
Revision as of 13:21, 29 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.
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 = {}
-- ============================================================
-- Extra columns: maps stat names to their column headers and
-- cell renderer functions from ComplexRenderers.
-- Stats not listed here get a plain Hero + Ability + Notes table.
-- ============================================================
local extra_columns = {
["charges"] = {
headers = { "Charges", "Time Between Charges", "Notes" },
get_cells = Renderers.RenderCharges,
},
["melee"] = {
headers = { "Light Melee Scaling", "Notes" },
get_cells = Renderers.RenderMelee,
},
["healreduce"] = {
headers = { "Heal Reduction", "Notes" },
get_cells = Renderers.RenderHealReduce,
},
["debuffresist"] = {
headers = { "Notes" },
get_cells = function(ability) return Renderers.RenderDebuff(ability, "debuffresist") end,
},
["dispelmagic"] = {
headers = { "Notes" },
get_cells = function(ability) return Renderers.RenderDebuff(ability, "dispelmagic") end,
},
["moveslow"] = {
headers = { "Move Slow", "Notes" },
get_cells = Renderers.RenderMoveSlow,
},
["dashslow"] = {
headers = { "Dash Slow", "Notes" },
get_cells = Renderers.RenderDashSlow,
},
}
-- ============================================================
-- Human-note formatters
-- ============================================================
-- Capitalises the first letter of a human note before merging (full-table mode).
local function human_note_full(note)
if not note or note == "" then return note end
return note:sub(1,1):upper() .. note:sub(2)
end
-- Ensures the final merged Notes string ends with "." (full-table mode).
local function ensure_period(s)
if not s or s == "" then return s end
if s:sub(-1) ~= "." then s = s .. "." end
return s
end
-- Compact mode: decapitalise first letter unless it looks like a tier prefix
-- (e.g. "T1", "T2"). Also strips any trailing ".".
local function human_note_compact(note)
if not note or note == "" then return note end
local first = note:sub(1,1)
if first:match("%u") and not note:sub(2,2):match("%d") then
note = first:lower() .. note:sub(2)
end
if note:sub(-1) == "." then note = note:sub(1, -2) end
return note
end
-- ============================================================
-- Filtering
-- ============================================================
local function filter_abilities(stat, abilities, key_lookup)
local excluded = {}
if Lists.exclude_abilities[stat] then
for _, name in ipairs(Lists.exclude_abilities[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
-- ============================================================
-- 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 = 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 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 = 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 list: " .. 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)
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)
if stat_notes and stat_notes[name] then
local human = human_note_compact(stat_notes[name])
extra["Notes"] = extra["Notes"] and (extra["Notes"] .. "; " .. human) or human
end
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 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 = human_note_full(stat_notes[ability["Name"]])
extra["Notes"] = extra["Notes"] and (extra["Notes"] .. ". " .. human) or human
end
extra["Notes"] = ensure_period(extra["Notes"])
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 = 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