Module:AbilityTable/ComplexRenderersGive feedback
Documentation for this module may be created at Module:AbilityTable/ComplexRenderers/doc
-- Complex cell renderers for AbilityTable.
local Lists = require("Module:AbilityTable/Lists")
-- ============================================================
-- Shared helpers
-- ============================================================
-- Unwraps a value that may be a raw number or a { Value = N } table.
local function unwrap_value(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 = unwrap_value(tier[field]) }
end
end
return nil
end
-- Finds the first non-zero value for any of the given fields, checking base
-- then upgrades in order. Returns field, tier (nil = base), absolute value.
local function find_first_occurrence(ability, fields)
for _, field in ipairs(fields) do
local v = unwrap_value(ability[field])
if v and v ~= 0 then return field, nil, math.abs(v) end
end
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
for _, field in ipairs(fields) do
local v = unwrap_value(tier[field])
if v and v ~= 0 then return field, i, math.abs(v) end
end
end
end
end
return nil, nil, nil
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 sum_fields(layer, fields)
local total = nil
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
end
return total
end
-- ============================================================
-- Generic renderer for a single numeric stat that can increase
-- additively through upgrades. Shows value in a named column
-- and generates upgrade notes with running totals.
-- Note: auto-generated notes omit trailing periods; the main
-- module's ensure_period handles that.
-- ============================================================
local function render_upgradable_stat(ability, col_name, fields, suffix)
suffix = suffix or ""
local cells = {}
local notes = {}
local first_field, first_tier, first_val = find_first_occurrence(ability, fields)
if not first_field then return cells end
local cell_str = tostring(first_val) .. suffix
if first_tier then cell_str = cell_str .. " (T" .. first_tier .. ")" end
cells[col_name] = cell_str
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
local running = first_val
local seen_first = (first_tier == nil)
for i, tier in ipairs(upgrades) do
if type(tier) == "table" then
if first_tier == i then
seen_first = true
elseif seen_first then
local delta = nil
for _, field in ipairs(fields) do
local v = unwrap_value(tier[field])
if v and v ~= 0 then delta = math.abs(v); break end
end
if delta then
local total = running + delta
table.insert(notes, "T" .. i .. " upgrade increases "
.. col_name:lower() .. " by " .. delta .. suffix
.. " (total " .. total .. suffix .. ")")
running = total
end
end
end
end
end
if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
return cells
end
-- ============================================================
-- Exported renderers
-- ============================================================
local p = {}
-- Renders charge count and time between charges.
-- Special case for abilities that gain charges only through upgrades.
function p.RenderCharges(ability)
local base_charges = unwrap_value(ability["AbilityCharges"])
local base_cooldown = unwrap_value(ability["AbilityCooldownBetweenCharge"])
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
local cells = {}
if has_base then
cells["Charges"] = tostring(base_charges)
if cooldown then
cells["Time Between Charges"] = tostring(cooldown) .. "s"
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
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
-- Renders base damage and melee scaling.
-- Special case for upgrades that switch from Light to Heavy Melee scaling.
function p.RenderMelee(ability)
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
local cells = {}
cells["Base Damage"] = tostring(base_damage or 0)
if base_scale then
cells["Light Melee Scaling"] = "×" .. base_scale
end
local upgrades = ability["Upgrades"]
if type(upgrades) ~= "table" then return cells end
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
if type(tier) == "table" then
local damage_delta = 0
local new_scale = nil
local new_type = nil
for k, v in pairs(tier) do
if type(v) == "table" then
local scale = v["Scale"]
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
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
if #notes > 0 then cells["Notes"] = table.concat(notes, ". ") end
return cells
end
-- Renders base damage and heavy melee scaling.
-- 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
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
-- Not at base — find in upgrades
local upgrades = ability["Upgrades"]
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
return cells
end
-- 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
-- Everything else goes through the generic renderer
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
-- Renders move slow percentage.
-- Appends a note if the ability also self-slows the caster.
function p.RenderMoveSlow(ability)
local cells = render_upgradable_stat(ability, "Move Slow", Lists.lists["moveslow"], "%")
local channel_slow = unwrap_value(ability["ChannelSlowPercent"])
if channel_slow and channel_slow ~= 0 then
local note = "Also slows the caster for " .. math.abs(channel_slow) .. "%"
cells["Notes"] = cells["Notes"] and (cells["Notes"] .. "; " .. note) or note
end
return cells
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)
local stat_props = Lists.lists["dashslow"]
local props = {}
for _, pr in ipairs(stat_props) do table.insert(props, pr) end
local caster_dash_slow = nil
local base_ground = unwrap_value(ability["GroundDashReductionPercent"])
if base_ground and base_ground ~= 0 then
local has_enemy_prop = false
for _, prop in ipairs(stat_props) do
if prop ~= "GroundDashReductionPercent" then
local v = unwrap_value(ability[prop])
if v and v ~= 0 then has_enemy_prop = true; break end
end
end
if not has_enemy_prop then
local upgrades = ability["Upgrades"]
if type(upgrades) == "table" then
for _, tier in ipairs(upgrades) do
if type(tier) == "table" then
for _, prop in ipairs(stat_props) do
if prop ~= "GroundDashReductionPercent" then
local v = unwrap_value(tier[prop])
if v and v ~= 0 then has_enemy_prop = true; break end
end
end
end
if has_enemy_prop then break end
end
end
end
if has_enemy_prop then
caster_dash_slow = math.abs(base_ground)
local filtered = {}
for _, prop in ipairs(props) do
if prop ~= "GroundDashReductionPercent" then
table.insert(filtered, prop)
end
end
props = filtered
end
end
local cells = render_upgradable_stat(ability, "Dash Slow", props, "%")
if caster_dash_slow then
local note = "Gives " .. caster_dash_slow .. "% dash slow for the caster"
cells["Notes"] = cells["Notes"] and (cells["Notes"] .. "; " .. note) or note
end
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
return p