Editing Module:AbilityTableGive feedback
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: | ||
-- Module:AbilityTables | |||
-- Renders sortable wikitables of abilities filtered by a named stat. | -- Renders sortable wikitables of abilities filtered by a named stat. | ||
-- Backing module for Template:AbilityTable. | |||
-- | |||
-- Usage on wiki pages: {{AbilityTable|charges}} | |||
-- | |||
-- ============================================================ | |||
-- ADDING A NEW STAT | |||
-- ============================================================ | |||
-- 1. Add an entry to stat_to_props mapping the friendly name | |||
-- (lowercase) to the internal AbilityData property keys that | |||
-- determine whether an ability appears in the table. | |||
-- | |||
-- 2. Add a matching entry to extra_columns: | |||
-- - headers: ordered list of column header strings. | |||
-- Include "Notes" if this stat generates notes. | |||
-- - get_cells: function(ability) -> { ["Column Name"] = value } | |||
-- The ability is guaranteed to match the stat. | |||
-- Omit a key or return nil to show "—" in that cell. | |||
-- If no extra_columns entry is added, only Hero and Ability | |||
-- columns are shown (plus Notes if human notes exist for the stat). | |||
-- | |||
-- 3. Optionally add human-authored notes to human_notes: | |||
-- human_notes["stat"]["Ability Name"] = "Note text." | |||
-- Notes are appended to any renderer-generated note automatically. | |||
-- | |||
-- 4. Optionally add entries to column_suffix to append a unit | |||
-- string (e.g. "s", "%") after cell values in specific columns. | |||
-- ============================================================ | |||
local | local p = {} | ||
local GameData = require("Module:GameData") | |||
local GameData | |||
local | -- ============================================================ | ||
-- stat_to_props | |||
-- Maps lowercase friendly stat names to internal AbilityData | |||
-- property keys used to filter which abilities appear. | |||
-- ============================================================ | |||
local stat_to_props = { | |||
["charges"] = { "AbilityCharges" }, | |||
} | |||
-- ============================================================ | |||
-- human_notes | |||
-- Editor-authored notes appended to the Notes column. | |||
-- Keyed first by stat name, then by ability display Name. | |||
-- ============================================================ | |||
local human_notes = { | |||
["charges"] = { | ["charges"] = { | ||
["Jar of Dead"] = "Charges by being near when anything dies.", | |||
}, | }, | ||
} | } | ||
-- ============================================================ | -- ============================================================ | ||
-- | -- 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 | |||
-- ============================================================ | |||
-- 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 | local base_cooldown = ability["AbilityCooldownBetweenCharge"] | ||
if | 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 | end | ||
if | |||
local cells = {} | |||
if has_base then | |||
cells["Charges"] = tostring(base_charges) | |||
if cooldown then | |||
cells["Time Between Charges"] = tostring(cooldown) | |||
end | |||
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 | 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 | ||
-- ============================================================ | -- ============================================================ | ||
-- | -- extra_columns | ||
-- | -- Declares headers and cell renderer for each stat. | ||
-- | -- ============================================================ | ||
-- | local extra_columns = { | ||
["charges"] = { | |||
headers = { "Charges", "Time Between Charges", "Notes" }, | |||
get_cells = charges_cells, | |||
}, | |||
} | |||
-- ============================================================ | |||
-- column_suffix | |||
-- Unit strings appended to non-nil cell values by column name. | |||
-- ============================================================ | |||
local column_suffix = { | |||
["Time Between Charges"] = "s", | |||
} | |||
-- | -- ============================================================ | ||
-- Hero lookup (ability key -> { heroName, abilityNum }) | |||
-- ============================================================ | -- ============================================================ | ||
local | local _hero_lookup = nil | ||
local function | local function build_hero_lookup() | ||
if | if _hero_lookup then return _hero_lookup end | ||
_hero_lookup = {} | |||
local hero_data = mw.loadJsonData("Data:HeroData.json") | local hero_data = mw.loadJsonData("Data:HeroData.json") | ||
| Line 105: | Line 174: | ||
end | end | ||
local cards = mw.loadJsonData("Data:AbilityCards.json") | |||
local cards | |||
for _, hero_entry in pairs(cards) do | for _, hero_entry in pairs(cards) do | ||
if type(hero_entry) == "table" then | if type(hero_entry) == "table" then | ||
| Line 115: | Line 181: | ||
for slot_str, ability in pairs(hero_entry) do | for slot_str, ability in pairs(hero_entry) do | ||
local slot_num = tonumber(slot_str) | 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 | ||
| Line 124: | Line 192: | ||
end | end | ||
return _hero_lookup | |||
end | end | ||
| Line 212: | Line 199: | ||
-- ============================================================ | -- ============================================================ | ||
function p. | function p.abilityPropTable(frame) | ||
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 | ||
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 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 = human_notes[stat] | ||
local abilities = GameData.get_entities(GameData.Dataset.ABILITIES, internal_keys) | |||
-- Keep only abilities belonging to active, released heroes | |||
local filtered = {} | local filtered = {} | ||
for _, | for _, ability in ipairs(abilities) do | ||
if | local key = ability["Key"] | ||
table.insert(filtered, | if key and lookup[key] then | ||
table.insert(filtered, ability) | |||
end | end | ||
end | end | ||
if #filtered == 0 then | if #filtered == 0 then | ||
return "No abilities found for | return "No abilities found for stat: " .. stat | ||
end | end | ||
-- Sort by hero name, then ability slot number | |||
table.sort(filtered, function(a, b) | |||
local ia = lookup[a["Key"]] | |||
local ib = lookup[b["Key"]] | |||
if ia.heroName ~= ib.heroName then | |||
return ia.heroName < ib.heroName | |||
. | |||
end | end | ||
return ia.abilityNum < ib.abilityNum | |||
end) | |||
-- Determine final header list. "Notes" is included if declared in headers | |||
-- (meaning the renderer may produce notes) or if human notes exist for | |||
-- this stat. Either way it appears exactly once at the end. | |||
local has_notes_header = false | local has_notes_header = false | ||
local final_headers = {} | local final_headers = {} | ||
| Line 300: | Line 246: | ||
if h == "Notes" then | if h == "Notes" then | ||
has_notes_header = true | has_notes_header = true | ||
else | |||
table.insert(final_headers, h) | table.insert(final_headers, h) | ||
end | end | ||
end | end | ||
local needs_notes = | local needs_notes = has_notes_header or stat_notes ~= nil | ||
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:AbilityTables", { 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) | |||
for _, ability in ipairs(filtered) do | |||
local info = lookup[ability["Key"]] | |||
for _, | |||
local | |||
local hero_cell = frame:expandTemplate{ | |||
title = "HeroIcon", | |||
args = { info.heroName } | |||
} | |||
local ability_cell = frame:expandTemplate{ | local ability_cell = frame:expandTemplate{ | ||
title = "AbilityIcon", | title = "AbilityIcon", | ||
args = { ability["Name"] } | args = { ability["Name"] } | ||
} | } | ||
local extra = get_cells(ability) | local extra = get_cells(ability) | ||
-- Append human note to any renderer-generated note | |||
if stat_notes and stat_notes[ability["Name"]] then | |||
if | local human = stat_notes[ability["Name"]] | ||
local | 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 "" | |||
content = value .. suffix | |||
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 | ||