Module:Sandbox/Vergir: Difference between revisionsGive feedback
Cleanup after audit (with help from vergir-bot LLM) Tag: Replaced |
Use {{Ss}} badge in the Spirit Scaling cell; add plain-text dump for review (with help from vergir-bot LLM) |
||
| (2 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- 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 p = {} | ||
-- ============================================================ | |||
-- TEST COPY of Module:AbilityTable with the proposed | |||
-- Module:AbilityTable/ComplexRenderers.RenderSpiritScaling inlined below, | |||
-- so the whole change can be previewed from one sandbox module. | |||
-- ============================================================ | |||
-- ============================================================ | |||
-- Spirit scaling helpers | |||
-- ============================================================ | |||
-- Returns the Spirit scaling entry of a prop value, or nil. A prop's Scale is | |||
-- either a single { Value, Type } table or an array of them (a duration that | |||
-- scales with both Spirit and ability duration, for example). | |||
local function spirit_scale(v) | |||
if type(v) ~= "table" then return nil end | |||
local scale = v["Scale"] | |||
if type(scale) ~= "table" then return nil end | |||
if scale[1] == nil then | |||
if scale["Type"] == "spirit" then return scale end | |||
return nil | |||
end | |||
for _, entry in ipairs(scale) do | |||
if type(entry) == "table" and entry["Type"] == "spirit" then return entry end | |||
end | |||
return nil | |||
end | |||
-- Collects the non-zero Spirit scalings of one record layer (the base record | |||
-- or one upgrade tier) as { key, value, multiply }, ordered the way the | |||
-- ability card lists its props; props the card does not list come last, in | |||
-- key order. `labels` is the view's cardprops map (see Module:AbilityTable). | |||
local function spirit_scalings(layer, labels) | |||
local out = {} | |||
for k, v in pairs(layer) do | |||
if k ~= "Upgrades" and tostring(k):sub(1, 1) ~= "_" then | |||
local s = spirit_scale(v) | |||
if s and type(s["Value"]) == "number" and s["Value"] ~= 0 then | |||
table.insert(out, { | |||
key = k, | |||
value = s["Value"], | |||
multiply = s["Multiply"] == true, | |||
}) | |||
end | |||
end | |||
end | |||
table.sort(out, function(a, b) | |||
local oa = labels[a.key] and labels[a.key].order or math.huge | |||
local ob = labels[b.key] and labels[b.key].order or math.huge | |||
if oa ~= ob then return oa < ob end | |||
return a.key < b.key | |||
end) | |||
return out | |||
end | |||
-- Formats a scaling value to 3 significant figures, as the ability cards do. | |||
local function format_spirit(v) | |||
return string.format("%.3g", v) | |||
end | |||
-- Formats one Spirit scaling for the table cell through the compact {{Ss}} | |||
-- form, the same way RenderBarrier does. The template is expanded via the | |||
-- frame the surrounding #invoke runs under, since module return values are | |||
-- not re-expanded; the plain "×N" fallback covers a nil frame (module | |||
-- console). | |||
local function format_spirit_badge(v) | |||
local text = format_spirit(v) | |||
local frame = mw.getCurrentFrame() | |||
if frame then | |||
return frame:expandTemplate{ | |||
title = "Ss", | |||
args = { text, compact = "1", show_value = "1" }, | |||
} | |||
end | |||
return "×" .. text | |||
end | |||
-- "A", "A and B", "A, B and C". | |||
local function join_names(names) | |||
if #names <= 1 then return names[1] or "" end | |||
return table.concat(names, ", ", 1, #names - 1) .. " and " .. names[#names] | |||
end | |||
-- Renders Spirit Power scaling. | |||
-- Lists every prop that scales with Spirit at base, one per line as an {{Ss}} | |||
-- badge labelled with the prop's name from the ability card: | |||
-- "x0.55 (Base Damage)". Two | |||
-- props sharing a name are told apart by the card's Title | |||
-- ("Damage: Cost of Stay"); props the card does not list show their raw key. | |||
-- Upgrades are reported in Notes per prop, with the running total: additive | |||
-- ones as "adds +×0.5 ... (total ×1.05)", ones that unlock scaling on a prop | |||
-- as "grants ×1.3 ...", and Multiply upgrades as "multiplies ... by 1.15", | |||
-- which is how the ability-upgrade gadget applies them. | |||
function p.RenderSpiritScaling(ability, cardprops) | |||
local labels = cardprops or {} | |||
local base = spirit_scalings(ability, labels) | |||
local tiers = {} -- { tier = i, list = ... } in tier order | |||
local upgrades = ability["Upgrades"] | |||
if type(upgrades) == "table" then | |||
-- ipairs rather than #: mw.loadJsonData tables have no length. | |||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
table.insert(tiers, { tier = i, list = spirit_scalings(tier, labels) }) | |||
end | |||
end | |||
end | |||
-- Names used by more than one scaling prop of this ability get their Title | |||
-- appended so the rows can be told apart. | |||
local keys_by_name = {} | |||
local function note_keys(list) | |||
for _, prop in ipairs(list) do | |||
local info = labels[prop.key] | |||
if info then | |||
keys_by_name[info.name] = keys_by_name[info.name] or {} | |||
keys_by_name[info.name][prop.key] = true | |||
end | |||
end | |||
end | |||
note_keys(base) | |||
for _, t in ipairs(tiers) do note_keys(t.list) end | |||
local function label_of(key) | |||
local info = labels[key] | |||
if not info then return key end | |||
local n = 0 | |||
for _ in pairs(keys_by_name[info.name] or {}) do n = n + 1 end | |||
if n > 1 and info.title then return info.name .. ": " .. info.title end | |||
return info.name | |||
end | |||
local cells = {} | |||
local running = {} -- prop key -> current total scaling | |||
if #base > 0 then | |||
local lines = {} | |||
for _, prop in ipairs(base) do | |||
table.insert(lines, format_spirit_badge(prop.value) .. " (" .. label_of(prop.key) .. ")") | |||
running[prop.key] = prop.value | |||
end | |||
cells["Spirit Scaling"] = table.concat(lines, "<br>") | |||
end | |||
local notes = {} | |||
for _, t in ipairs(tiers) do | |||
local i, list = t.tier, t.list | |||
local groups, group_order = {}, {} -- Multiply upgrades, grouped by multiplier | |||
for _, up in ipairs(list) do | |||
local label = label_of(up.key) | |||
local current = running[up.key] | |||
if up.multiply then | |||
local group = groups[up.value] | |||
if not group then | |||
group = { labels = {}, totals = {} } | |||
groups[up.value] = group | |||
table.insert(group_order, up.value) | |||
end | |||
table.insert(group.labels, label) | |||
if current then | |||
running[up.key] = current * up.value | |||
table.insert(group.totals, "×" .. format_spirit(running[up.key])) | |||
else | |||
table.insert(group.totals, "?") | |||
end | |||
elseif current then | |||
running[up.key] = current + up.value | |||
table.insert(notes, "T" .. i .. " upgrade adds +×" .. format_spirit(up.value) | |||
.. " " .. label .. " scaling (total ×" .. format_spirit(running[up.key]) .. ")") | |||
else | |||
running[up.key] = up.value | |||
table.insert(notes, "T" .. i .. " upgrade grants ×" .. format_spirit(up.value) | |||
.. " " .. label .. " scaling") | |||
end | |||
end | |||
for _, m in ipairs(group_order) do | |||
local group = groups[m] | |||
table.insert(notes, "T" .. i .. " upgrade multiplies " .. join_names(group.labels) | |||
.. " scaling by " .. format_spirit(m) | |||
.. " (total " .. table.concat(group.totals, ", ") .. ")") | |||
end | |||
end | |||
if #notes > 0 then cells["Notes"] = table.concat(notes, ". ") end | |||
return cells | |||
end | |||
-- get_cells(ability, cardprops) -> { [column header] = cell text, Notes = ... } | |||
-- `cardprops` maps the ability's prop keys to their display labels; most | |||
-- renderers ignore it. | |||
local extra_columns = { | |||
["barrier"] = { | |||
headers = { "Barrier" }, | |||
get_cells = Renderers.RenderBarrier, | |||
}, | |||
["bossdmgscale"] = { | |||
headers = { "Damage to Objectives" }, | |||
get_cells = Renderers.RenderBossDamageScale, | |||
}, | |||
["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 = p.RenderSpiritScaling, -- TEST: inlined copy above | |||
}, | |||
["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. | |||
-- Each view entry also carries "cardprops", the display labels of the | |||
-- ability's props taken from its card (see card_prop_labels). They are kept | |||
-- beside the record rather than inside it so that membership matching, which | |||
-- walks the whole record, never sees them. | |||
-- ============================================================ | |||
-- Display labels for a card's props: prop key -> { name, title, order }, read | |||
-- from the Info sections of the card in AbilityCards.json. The flat | |||
-- AbilityData records carry no display names, and a key such as "Damage" is | |||
-- reused across abilities under different labels, so the map is built per | |||
-- card rather than wiki-wide. `order` is the position of the prop on the | |||
-- card, so renderers can list props the way the in-game card does. | |||
local function card_prop_labels(card) | |||
local labels, n = {}, 0 | |||
local function walk(node) | |||
if type(node) ~= "table" then return end | |||
local key, name = node["Key"], node["Name"] | |||
if type(key) == "string" and type(name) == "string" and not labels[key] then | |||
n = n + 1 | |||
local title = node["Title"] | |||
if type(title) == "string" then | |||
title = title:gsub(":%s*$", "") | |||
else | |||
title = nil | |||
end | |||
labels[key] = { name = name, title = title, order = n } | |||
end | |||
if node[1] ~= nil then | |||
for _, child in ipairs(node) do walk(child) end | |||
else | |||
-- Sections such as Range or Move key each prop by its name: | |||
-- { AttackDashRange = { Name = "...", Value = ... } }. | |||
local keyed = {} | |||
for k, v in pairs(node) do | |||
if type(k) == "string" and type(v) == "table" and v["Key"] == nil | |||
and type(v["Name"]) == "string" and not labels[k] | |||
then | |||
table.insert(keyed, k) | |||
end | |||
end | |||
table.sort(keyed) | |||
for _, k in ipairs(keyed) do | |||
n = n + 1 | |||
labels[k] = { name = node[k]["Name"], order = n } | |||
end | |||
-- Main props before Alt props, then anything else in key order. | |||
if node["Main"] ~= nil then walk(node["Main"]) end | |||
if node["Alt"] ~= nil then walk(node["Alt"]) end | |||
local rest = {} | |||
for k, v in pairs(node) do | |||
if k ~= "Main" and k ~= "Alt" and k ~= "Upgrades" and type(v) == "table" then | |||
table.insert(rest, k) | |||
end | |||
end | |||
table.sort(rest, function(a, b) return tostring(a) < tostring(b) end) | |||
for _, k in ipairs(rest) do walk(node[k]) end | |||
end | |||
end | |||
for _, section in ipairs({ "Info1", "Info2", "Info3" }) do | |||
walk(card[section]) | |||
end | |||
local rest = {} | |||
for k, v in pairs(card) do | |||
if type(v) == "table" and k ~= "Upgrades" | |||
and k ~= "Info1" and k ~= "Info2" and k ~= "Info3" | |||
then | |||
table.insert(rest, k) | |||
end | |||
end | |||
table.sort(rest, function(a, b) return tostring(a) < tostring(b) end) | |||
for _, k in ipairs(rest) do walk(card[k]) end | |||
-- Header attributes are stored on the card without a Name. | |||
local header_attrs = { | |||
{ "AbilityCastRange", "Cast Range" }, | |||
{ "Radius", "Radius" }, | |||
{ "AbilityDuration", "Duration" }, | |||
{ "AbilityChannelTime", "Channel Time" }, | |||
{ "AbilityCooldown", "Cooldown" }, | |||
{ "AbilityCharges", "Charges" }, | |||
{ "AbilityCooldownBetweenCharge", "Charge Cooldown" }, | |||
} | |||
for _, attr in ipairs(header_attrs) do | |||
if not labels[attr[1]] then | |||
n = n + 1 | |||
labels[attr[1]] = { name = attr[2], order = n } | |||
end | |||
end | |||
return labels | |||
end | |||
local _views = nil -- card key -> { view = record, heroName = ..., abilityNum = ..., cardprops = ... } | |||
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, card = ability } | |||
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, | |||
cardprops = card_prop_labels(info.card), | |||
} | |||
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]) | |||
end | |||
local items = {} | |||
for _, hero in ipairs(hero_order) do | |||
local hero_cell = frame:expandTemplate{ title = "HeroIcon", args = { hero } } | |||
local ability_parts = {} | |||
for _, entry in ipairs(hero_abilities[hero]) do | |||
local ability = entry.view | |||
local name = ability["Name"] | |||
local ability_cell = frame:expandTemplate{ title = "AbilityIcon", args = { name } } | |||
local extra = get_cells(ability, entry.cardprops) | |||
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, entry.cardprops) | |||
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 | |||
-- TEST ONLY: plain-text dump of a list for review, one line per ability. | |||
function p.dump(frame) | |||
local stat = mw.text.trim(frame.args[1] or ""):lower() | |||
local views, order = build_views() | |||
local spec = extra_columns[stat] | |||
local get_cells = spec and spec.get_cells or function() return {} end | |||
local out = {} | |||
for _, key in ipairs(order) do | |||
if p.is_member(stat, key) then | |||
local entry = views[key] | |||
local extra = get_cells(entry.view, entry.cardprops) | |||
local cell = extra["Spirit Scaling"] or "—" | |||
cell = mw.text.killMarkers(cell):gsub("<br>", " / "):gsub("<[^>]+>", "") | |||
table.insert(out, entry.heroName .. " | " .. entry.view["Name"] .. " | " .. cell .. " | " .. (extra["Notes"] or "—")) | |||
end | |||
end | |||
return "<pre>" .. mw.text.nowiki(table.concat(out, "\n")) .. "</pre>" | |||
end | |||
return p | return p | ||