Module:AbilityTable/ComplexRenderers: Difference between revisionsGive feedback
rename val_stat_cells to render_upgradable_stat, simplify RenderHealReduce to use it, fix Lists.props to Lists.lists (with help from vergir-bot LLM) |
add scalingspirit |
||
| (18 intermediate revisions by 3 users not shown) | |||
| Line 1: | Line 1: | ||
-- Complex cell renderers for AbilityTable. | -- Complex cell renderers for AbilityTable. | ||
local Lists | local Lists = require("Module:AbilityTable/Lists") | ||
-- ============================================================ | -- ============================================================ | ||
| Line 48: | Line 47: | ||
end | end | ||
-- | -- Sums the non-zero values of `fields` within a single record layer (the base | ||
-- | -- ability record, or one upgrade tier). Returns nil when none are present. | ||
local function | local function sum_fields(layer, fields) | ||
for | local total = nil | ||
if | for _, field in ipairs(fields) do | ||
local v = unwrap_value(layer[field]) | |||
if v and v ~= 0 then total = (total or 0) + math.abs(v) end | |||
if type( | end | ||
return total | |||
end | |||
-- Sums the barrier value and Spirit scaling contributed by a single record | |||
-- layer (the base ability record, or one upgrade tier). Returns nil when the | |||
-- layer grants no barrier at all; a tier that only raises the scaling still | |||
-- counts, so "+0 value, +1.0 scaling" upgrades are not lost. | |||
local function sum_barrier(layer, fields) | |||
local value, scale, found = 0, 0, false | |||
for _, field in ipairs(fields) do | |||
local entry = layer[field] | |||
if entry ~= nil then | |||
local v = unwrap_value(entry) or 0 | |||
local s = 0 | |||
if type(entry) == "table" and type(entry["Scale"]) == "table" then | |||
s = entry["Scale"]["Value"] or 0 | |||
end | end | ||
if v ~= 0 or s ~= 0 then found = true end | |||
value = value + v | |||
scale = scale + s | |||
end | end | ||
end | end | ||
return | if not found then return nil end | ||
return value, scale | |||
end | end | ||
-- | -- Pads a whole-number scaling out to one decimal, so a running total of 2 | ||
-- | -- reads "2.0" next to its "1.5" neighbours instead of "2". Values that already | ||
-- carry decimals are left alone, which keeps precision on things like 0.75. | |||
local function format_scale(scale) | |||
local text = tostring(scale) | |||
if not text:find(".", 1, true) then text = text .. ".0" end | |||
return text | |||
end | |||
-- Formats one barrier step. Spirit scaling goes through the compact {{Ss}} | |||
-- form, expanded via the frame the surrounding #invoke is already running | |||
-- under: module return values are not re-expanded, so a literal "{{Ss|0.8}}" | |||
-- would reach the page as text. The plain fallback covers a nil frame | |||
-- (module console). | |||
local function format_barrier(value, scale) | |||
local out = tostring(value) | |||
if scale and scale ~= 0 then | |||
local scale_text = format_scale(scale) | |||
local frame = mw.getCurrentFrame() | |||
if frame then | |||
out = out .. " " .. frame:expandTemplate{ | |||
title = "Ss", | |||
args = { scale_text, compact = "1", show_value = "1" }, | |||
} | |||
else | |||
out = out .. " ×" .. scale_text | |||
end | |||
end | |||
return out | |||
end | |||
-- ============================================================ | -- ============================================================ | ||
| Line 103: | Line 116: | ||
-- additively through upgrades. Shows value in a named column | -- additively through upgrades. Shows value in a named column | ||
-- and generates upgrade notes with running totals. | -- and generates upgrade notes with running totals. | ||
-- Note: auto-generated notes omit trailing periods; the main | |||
-- module's ensure_period handles that. | |||
-- ============================================================ | -- ============================================================ | ||
| Line 136: | Line 151: | ||
table.insert(notes, "T" .. i .. " upgrade increases " | table.insert(notes, "T" .. i .. " upgrade increases " | ||
.. col_name:lower() .. " by " .. delta .. suffix | .. col_name:lower() .. " by " .. delta .. suffix | ||
.. " (total " .. total .. suffix .. ") | .. " (total " .. total .. suffix .. ")") | ||
running = total | running = total | ||
end | end | ||
| Line 154: | Line 169: | ||
local p = {} | local p = {} | ||
-- | -- Renders the barrier an ability grants, with its Spirit Power scaling. | ||
-- Both the value and the scaling accumulate across upgrades, so every step | |||
-- after the first shows the running total: "100 x0.8 → 180 x1.5 (T2)". | |||
-- A tier that only improves the scaling still gets its own segment. | |||
function p.RenderBarrier(ability) | |||
local fields = Lists.lists["barrier"] | |||
local segments = {} | |||
local value, scale = sum_barrier(ability, fields) | |||
if value then | |||
table.insert(segments, format_barrier(value, scale)) | |||
else | |||
value, scale = 0, 0 | |||
end | |||
local upgrades = ability["Upgrades"] | |||
if type(upgrades) == "table" then | |||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
local delta_value, delta_scale = sum_barrier(tier, fields) | |||
if delta_value then | |||
value, scale = value + delta_value, scale + delta_scale | |||
table.insert(segments, | |||
format_barrier(value, scale) .. " (T" .. i .. ")") | |||
end | |||
end | |||
end | |||
end | |||
if #segments == 0 then return {} end | |||
return { ["Barrier"] = table.concat(segments, " → ") } | |||
end | |||
-- Renders charge count and time between charges. | |||
-- Special case for abilities that gain charges only through upgrades. | |||
function p.RenderCharges(ability) | function p.RenderCharges(ability) | ||
local base_charges = unwrap_value(ability["AbilityCharges"]) | local base_charges = unwrap_value(ability["AbilityCharges"]) | ||
local base_cooldown = ability["AbilityCooldownBetweenCharge"] | local base_cooldown = unwrap_value(ability["AbilityCooldownBetweenCharge"]) | ||
local has_base = base_charges ~= nil and base_charges > 0 | local has_base = base_charges ~= nil and base_charges > 0 | ||
local upgrade_info = find_in_upgrades(ability, "AbilityCharges") | local upgrade_info = find_in_upgrades(ability, "AbilityCharges") | ||
| Line 188: | Line 237: | ||
local charge_str = n .. " charge" .. (n ~= 1 and "s" or "") | local charge_str = n .. " charge" .. (n ~= 1 and "s" or "") | ||
if has_base then | if has_base then | ||
table.insert(note_parts, "+" .. charge_str .. " on T" .. upgrade_info.tier .. " upgrade | table.insert(note_parts, "+" .. charge_str .. " on T" .. upgrade_info.tier .. " upgrade") | ||
else | else | ||
local cd_str = upgrade_cooldown and (upgrade_cooldown .. "s") or "unknown" | local cd_str = upgrade_cooldown and (upgrade_cooldown .. "s") or "unknown" | ||
table.insert(note_parts, "Becomes charged on T" .. upgrade_info.tier | table.insert(note_parts, "Becomes charged on T" .. upgrade_info.tier | ||
.. " upgrade with " .. charge_str | .. " upgrade with " .. charge_str | ||
.. " and " .. cd_str .. " time between charges | .. " and " .. cd_str .. " time between charges") | ||
end | end | ||
end | end | ||
if ability["AbilityChargesConditionally"] ~= nil then | if ability["AbilityChargesConditionally"] ~= nil then | ||
table.insert(note_parts, "Has a conditional charge | table.insert(note_parts, "Has a conditional charge") | ||
end | end | ||
if #note_parts > 0 then | if #note_parts > 0 then | ||
| Line 206: | Line 255: | ||
end | end | ||
-- Melee | -- Renders base damage and melee scaling. | ||
-- Special case for upgrades that switch from Light to Heavy Melee scaling. | |||
function p.RenderMelee(ability) | function p.RenderMelee(ability) | ||
local | local melee_types = {} | ||
for _, t in ipairs(Lists.lists["melee"]) do melee_types[t] = true end | |||
-- Find the base property with active melee/heavy_melee scaling (Scale.Value > 0) | |||
local base_prop, base_damage, base_scale, base_type | |||
for k, v in pairs(ability) do | |||
if k ~= "Upgrades" and type(v) == "table" then | |||
local scale = v["Scale"] | |||
if type(scale) == "table" and melee_types[scale["Type"]] | |||
and type(scale["Value"]) == "number" and scale["Value"] > 0 then | |||
base_prop = k | |||
base_damage = v["Value"] or 0 | |||
base_scale = scale["Value"] | |||
base_type = scale["Type"] | |||
break | |||
end | end | ||
end | end | ||
end | end | ||
local cells = {} | local cells = {} | ||
cells["Base Damage"] = tostring(base_damage or 0) | |||
if base_scale then cells["Light Melee Scaling"] = "×" .. base_scale end | if base_scale then | ||
cells["Light Melee Scaling"] = "×" .. base_scale | |||
end | |||
local upgrades = ability["Upgrades"] | local upgrades = ability["Upgrades"] | ||
if type(upgrades) ~= "table" then return cells end | if type(upgrades) ~= "table" then return cells end | ||
local notes = {} | local notes = {} | ||
local running_damage = base_damage or 0 | |||
local running_scale = base_scale or 0 | |||
local running_type = base_type | |||
for i, tier in ipairs(upgrades) do | for i, tier in ipairs(upgrades) do | ||
if type(tier) == "table" then | if type(tier) == "table" then | ||
for | local damage_delta = 0 | ||
local new_scale = nil | |||
local new_type = nil | |||
for k, v in pairs(tier) do | |||
if type(v) == "table" then | if type(v) == "table" then | ||
local scale = v["Scale"] | local scale = v["Scale"] | ||
if type(scale) == "table" and | if type(scale) == "table" and melee_types[scale["Type"]] then | ||
damage_delta = damage_delta + (v["Value"] or 0) | |||
if scale["Value"] > 0 then | |||
new_scale = scale["Value"] | |||
new_type = scale["Type"] | |||
end | end | ||
end | end | ||
end | end | ||
end | |||
local flat_delta = 0 | |||
if base_prop and type(tier[base_prop]) == "number" then | |||
flat_delta = tier[base_prop] | |||
end | |||
local tier_parts = {} | |||
if new_type and new_type ~= running_type then | |||
running_damage = running_damage + damage_delta | |||
running_scale = new_scale | |||
running_type = new_type | |||
local type_label = new_type == "heavy_melee" and "'''Heavy Melee'''" or "Light Melee" | |||
local parts = {} | |||
if running_damage > 0 then | |||
table.insert(parts, running_damage .. " base damage") | |||
end | |||
table.insert(parts, "×" .. new_scale .. " " .. type_label .. " scaling") | |||
table.insert(tier_parts, "on T" .. i .. " becomes " .. table.concat(parts, " with ")) | |||
elseif new_scale then | |||
running_damage = running_damage + damage_delta | |||
running_scale = running_scale + new_scale | |||
local parts = {} | |||
if damage_delta ~= 0 then | |||
table.insert(parts, "+" .. damage_delta .. " base damage") | |||
end | |||
table.insert(parts, "+×" .. new_scale .. " scaling (total ×" .. running_scale .. ")") | |||
table.insert(tier_parts, "on T" .. i .. " gets " .. table.concat(parts, ", ")) | |||
end | |||
if flat_delta ~= 0 and not new_scale then | |||
running_damage = running_damage + flat_delta | |||
table.insert(tier_parts, "on T" .. i .. " gets +" .. flat_delta .. " base damage (total " .. running_damage .. ")") | |||
end | |||
if #tier_parts > 0 then | |||
table.insert(notes, table.concat(tier_parts, ", ")) | |||
end | end | ||
end | end | ||
end | end | ||
if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end | |||
if #notes > 0 then cells["Notes"] = table.concat(notes, ". ") end | |||
return cells | return cells | ||
end | end | ||
-- | -- Renders base damage and heavy melee scaling. | ||
function p. | -- Notes when heavy melee is only available via upgrade. | ||
-- | function p.RenderHeavyMelee(ability) | ||
-- Find base heavy_melee property | |||
local base_damage, base_scale | |||
for k, v in pairs(ability) do | |||
if k ~= "Upgrades" and type(v) == "table" then | |||
local scale = v["Scale"] | |||
if type(scale) == "table" and scale["Type"] == "heavy_melee" | |||
and type(scale["Value"]) == "number" then | |||
base_damage = v["Value"] or 0 | |||
base_scale = scale["Value"] | |||
break | |||
end | |||
end | |||
end | end | ||
local | |||
if | local cells = {} | ||
-- Active at base | |||
if base_scale and base_scale > 0 then | |||
cells["Base Damage"] = tostring(base_damage) | |||
cells["Heavy Melee Scaling"] = "×" .. base_scale | |||
return cells | |||
end | end | ||
-- | -- Not at base — find in upgrades | ||
local | local upgrades = ability["Upgrades"] | ||
for | if type(upgrades) ~= "table" then return cells end | ||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
for k, v in pairs(tier) do | |||
if type(v) == "table" then | |||
local scale = v["Scale"] | |||
if type(scale) == "table" and scale["Type"] == "heavy_melee" | |||
and type(scale["Value"]) == "number" and scale["Value"] > 0 then | |||
cells["Base Damage"] = tostring(v["Value"] or 0) | |||
cells["Heavy Melee Scaling"] = "×" .. scale["Value"] | |||
cells["Notes"] = "Only becomes Heavy Melee on '''T" .. i .. "''' upgrade" | |||
return cells | |||
end | |||
end | |||
end | |||
end | |||
end | end | ||
return | |||
return cells | |||
end | end | ||
-- | -- Renders Spirit Power scaling. | ||
-- | -- Shows the Scale.Value for abilities whose Scale.Type is "spirit". | ||
function p. | function p.RenderSpiritScaling(ability) | ||
local base_scale = nil | |||
local base_prop = nil | |||
-- Find base Spirit scaling | |||
for k, v in pairs(ability) do | |||
if k ~= "Upgrades" and type(v) == "table" then | |||
local scale = v["Scale"] | |||
if type(scale) == "table" | |||
and scale["Type"] == "spirit" | |||
and type(scale["Value"]) == "number" | |||
and scale["Value"] ~= 0 then | |||
base_scale = scale["Value"] | |||
base_prop = k | |||
break | |||
end | |||
end | |||
end | |||
local cells = {} | local cells = {} | ||
if base_scale then | |||
cells["Spirit Scaling"] = "×" .. base_scale | |||
end | end | ||
| Line 292: | Line 434: | ||
if type(upgrades) ~= "table" then return cells end | if type(upgrades) ~= "table" then return cells end | ||
local | local notes = {} | ||
for i, tier in ipairs(upgrades) do | for i, tier in ipairs(upgrades) do | ||
if type(tier) == "table" then | if type(tier) == "table" then | ||
local | local upgrade_scale = nil | ||
for | for k, v in pairs(tier) do | ||
if type(v) == "table" then | |||
local scale = v["Scale"] | |||
if type(scale) == "table" | |||
if | and scale["Type"] == "spirit" | ||
and type(scale["Value"]) == "number" | |||
and scale["Value"] ~= 0 then | |||
upgrade_scale = scale["Value"] | |||
break | |||
end | end | ||
end | end | ||
end | end | ||
if | |||
if upgrade_scale then | |||
table.insert(notes, | |||
"on T" .. i .. " gets +×" .. upgrade_scale .. " Spirit scaling") | |||
end | end | ||
end | end | ||
end | end | ||
if #notes > 0 then | |||
cells["Notes"] = table.concat(notes, ". ") | |||
end | |||
return cells | |||
end | |||
local | -- Renders heal reduction percentage. | ||
-- Special case for DisableHealing, shown as 100%. | |||
function p.RenderHealReduce(ability) | |||
-- DisableHealing is always 100%, handle it separately | |||
local v = unwrap_value(ability["DisableHealing"]) | |||
if v and v ~= 0 then | |||
return { ["Heal Reduction"] = "100%" } | |||
end | |||
local upgrade = find_in_upgrades(ability, "DisableHealing") | |||
if upgrade then | |||
return { ["Heal Reduction"] = "100% (T" .. upgrade.tier .. ")" } | |||
end | end | ||
-- Everything else goes through the generic renderer | |||
return | local filtered = {} | ||
for _, prop in ipairs(Lists.lists["healreduce"]) do | |||
if prop ~= "DisableHealing" then table.insert(filtered, prop) end | |||
end | |||
return render_upgradable_stat(ability, "Heal Reduction", filtered, "%") | |||
end | end | ||
-- | -- Renders move slow percentage. | ||
-- Appends a note if the ability also self-slows the caster. | |||
function p.RenderMoveSlow(ability) | function p.RenderMoveSlow(ability) | ||
local cells = render_upgradable_stat(ability, "Move Slow", Lists.lists["moveslow"], "%") | local cells = render_upgradable_stat(ability, "Move Slow", Lists.lists["moveslow"], "%") | ||
| Line 340: | Line 495: | ||
local channel_slow = unwrap_value(ability["ChannelSlowPercent"]) | local channel_slow = unwrap_value(ability["ChannelSlowPercent"]) | ||
if channel_slow and channel_slow ~= 0 then | if channel_slow and channel_slow ~= 0 then | ||
local note = "Also slows the caster for " .. math.abs(channel_slow) .. "% | local note = "Also slows the caster for " .. math.abs(channel_slow) .. "%" | ||
cells["Notes"] = cells["Notes"] and (cells["Notes"] .. "; " .. note) or note | cells["Notes"] = cells["Notes"] and (cells["Notes"] .. "; " .. note) or note | ||
end | end | ||
| Line 347: | Line 502: | ||
end | end | ||
-- | -- Renders dash slow percentage. | ||
-- Special case for GroundDashReductionPercent coexisting with another dashslow | |||
-- prop, which means it's a caster self-slow and is noted separately. | |||
function p.RenderDashSlow(ability) | function p.RenderDashSlow(ability) | ||
local stat_props = Lists.lists["dashslow"] | local stat_props = Lists.lists["dashslow"] | ||
| Line 394: | Line 551: | ||
if caster_dash_slow then | if caster_dash_slow then | ||
local note = "Gives " .. caster_dash_slow .. "% dash slow for the caster | local note = "Gives " .. caster_dash_slow .. "% dash slow for the caster" | ||
cells["Notes"] = cells["Notes"] and (cells["Notes"] .. "; " .. note) or note | cells["Notes"] = cells["Notes"] and (cells["Notes"] .. "; " .. note) or note | ||
end | end | ||
return cells | return cells | ||
end | |||
-- Renders the amount of Debuff Resist an ability grants. | |||
-- Upgrades stack additively onto the base value, so every step after the first | |||
-- shows the running total: "20% (T3)" when granted only by an upgrade, | |||
-- "10% → 25% (T2)" when a base value is later increased. Chains for any number | |||
-- of steps. No ability currently has both a base and an upgrade value. | |||
function p.RenderExtraDebuffResist(ability) | |||
local fields = Lists.lists["extradebuffresist"] | |||
local segments = {} | |||
local running = sum_fields(ability, fields) | |||
if running then | |||
table.insert(segments, running .. "%") | |||
end | |||
local upgrades = ability["Upgrades"] | |||
if type(upgrades) == "table" then | |||
for i, tier in ipairs(upgrades) do | |||
if type(tier) == "table" then | |||
local delta = sum_fields(tier, fields) | |||
if delta then | |||
running = (running or 0) + delta | |||
table.insert(segments, running .. "% (T" .. i .. ")") | |||
end | |||
end | |||
end | |||
end | |||
if #segments == 0 then return {} end | |||
return { ["Debuff Resist"] = table.concat(segments, " → ") } | |||
end | end | ||
return p | return p | ||