Module:AbilityTable/ComplexRenderers: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
Vergir (talk | contribs)
mNo edit summary
Vergir (talk | contribs)
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)
Line 100: Line 100:


-- ============================================================
-- ============================================================
-- Generic renderer for a single numeric stat column.
-- Generic renderer for a single numeric stat that can increase
-- col_name: column header string
-- additively through upgrades. Shows value in a named column
-- fields:  ordered list of property names (first non-zero wins)
-- and generates upgrade notes with running totals.
-- suffix:  string appended to values in cell and upgrade notes
-- ============================================================
-- ============================================================


local function val_stat_cells(ability, col_name, fields, suffix)
local function render_upgradable_stat(ability, col_name, fields, suffix)
     suffix = suffix or ""
     suffix = suffix or ""
     local cells = {}
     local cells = {}
Line 259: Line 258:
-- HealReduce: shows heal reduction percentage and upgrade notes.
-- HealReduce: shows heal reduction percentage and upgrade notes.
function p.RenderHealReduce(ability)
function p.RenderHealReduce(ability)
     local cells = {}
     -- DisableHealing is always 100%, handle it separately
    local notes = {}
     local v = unwrap_value(ability["DisableHealing"])
    local stat_props = Lists.props["healreduce"]
    if v and v ~= 0 then
 
         return { ["Heal Reduction"] = "100%" }
    local first_pct, first_tier, first_is_disable = nil, nil, false
 
     for _, prop in ipairs(stat_props) do
        local v = unwrap_value(ability[prop])
        if v and v ~= 0 then
            if prop == "DisableHealing" then
                first_is_disable = true
            else
                first_pct = math.abs(v)
            end
            break
        end
    end
    if not first_pct and not first_is_disable then
         local upgrades = ability["Upgrades"]
        if type(upgrades) == "table" then
            for i, tier in ipairs(upgrades) do
                if type(tier) == "table" then
                    for _, prop in ipairs(stat_props) do
                        local v = unwrap_value(tier[prop])
                        if v and v ~= 0 then
                            if prop == "DisableHealing" then
                                first_is_disable, first_tier = true, i
                            else
                                first_pct, first_tier = math.abs(v), i
                            end
                            break
                        end
                    end
                end
                if first_pct or first_is_disable then break end
            end
        end
     end
     end
 
     local upgrade = find_in_upgrades(ability, "DisableHealing")
     if first_is_disable then
     if upgrade then
        cells["Heal Reduction"] = "100%" .. (first_tier and " (T" .. first_tier .. ")" or "")
         return { ["Heal Reduction"] = "100% (T" .. upgrade.tier .. ")" }
     elseif first_pct then
         cells["Heal Reduction"] = tostring(first_pct) .. "%" .. (first_tier and " (T" .. first_tier .. ")" or "")
     end
     end


     local upgrades = ability["Upgrades"]
     -- Everything else goes through the generic renderer
     if type(upgrades) == "table" then
     local filtered = {}
        local running        = first_pct
    for _, prop in ipairs(Lists.lists["healreduce"]) do
        local first_pct_seen = (first_tier == nil)
        if prop ~= "DisableHealing" then table.insert(filtered, prop) end
        for i, tier in ipairs(upgrades) do
            if type(tier) == "table" then
                if first_tier == i then
                    first_pct_seen = true
                    if first_pct then running = first_pct end
                elseif first_pct_seen then
                    local delta = nil
                    for _, prop in ipairs(stat_props) do
                        if prop ~= "DisableHealing" then
                            local v = unwrap_value(tier[prop])
                            if v and v ~= 0 then delta = math.abs(v); break end
                        end
                    end
                    if delta then
                        local total = (running or 0) + delta
                        table.insert(notes, "T" .. i .. " upgrade increases heal reduction by "
                            .. delta .. "% (total " .. total .. "%).")
                        running = total
                    end
                end
            end
        end
     end
     end
 
     return render_upgradable_stat(ability, "Heal Reduction", filtered, "%")
     if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
    return cells
end
end


Line 339: Line 279:
-- Outputs upgrade-tier notes for debuffs added only in upgrades (not at base).
-- Outputs upgrade-tier notes for debuffs added only in upgrades (not at base).
function p.RenderDebuff(ability, stat)
function p.RenderDebuff(ability, stat)
     local stat_props = Lists.props[stat]
     local stat_props = Lists.lists[stat]
     local cells = {}
     local cells = {}


Line 396: Line 336:
-- MoveSlow: shows move slow percentage and caster slow note if present.
-- MoveSlow: shows move slow percentage and caster slow note if present.
function p.RenderMoveSlow(ability)
function p.RenderMoveSlow(ability)
     local cells = val_stat_cells(ability, "Move Slow", Lists.props["moveslow"], "%")
     local cells = render_upgradable_stat(ability, "Move Slow", Lists.lists["moveslow"], "%")


     local channel_slow = unwrap_value(ability["ChannelSlowPercent"])
     local channel_slow = unwrap_value(ability["ChannelSlowPercent"])
Line 409: Line 349:
-- DashSlow: shows dash slow percentage, auto-detects caster self-slow.
-- DashSlow: shows dash slow percentage, auto-detects caster self-slow.
function p.RenderDashSlow(ability)
function p.RenderDashSlow(ability)
     local stat_props = Lists.props["dashslow"]
     local stat_props = Lists.lists["dashslow"]
     local props = {}
     local props = {}
     for _, p in ipairs(stat_props) do table.insert(props, p) end
     for _, pr in ipairs(stat_props) do table.insert(props, pr) end


     local caster_dash_slow = nil
     local caster_dash_slow = nil
Line 451: Line 391:
     end
     end


     local cells = val_stat_cells(ability, "Dash Slow", props, "%")
     local cells = render_upgradable_stat(ability, "Dash Slow", props, "%")


     if caster_dash_slow then
     if caster_dash_slow then

Revision as of 21:11, 28 May 2026

Documentation for this module may be created at Module:AbilityTable/ComplexRenderers/doc

-- Complex cell renderers for AbilityTable.
local Lists   = require("Module:AbilityTable/Lists")
local GameData = require("Module:GameData")

-- ============================================================
-- 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

-- Returns true if prop exists with a non-zero value anywhere in the record
-- excluding the Upgrades key.
local function is_base_property(record, prop)
    for k, v in pairs(record) do
        if k == "Upgrades" then
            -- skip
        elseif k == prop then
            if type(v) == "table" then v = v["Value"] end
            if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
                return true
            end
        elseif type(v) == "table" then
            if is_base_property(v, prop) then return true end
        elseif type(v) == "string" then
            if v == prop then return true end
        end
    end
    return false
end

-- ============================================================
-- Debuff short names (used by RenderDebuff)
-- ============================================================

local debuff_short_name = {
    ["StunDuration"]               = "stun",
    ["SleepDuration"]              = "sleep",
    ["SilenceDuration"]            = "silence",
    ["SilenceDebuff"]              = "silence",
    ["PetrifyDuration"]            = "petrify",
    ["HexDuration"]                = "hex",
    ["ImmobilizeDuration"]         = "immobilize",
    ["LiftDuration"]               = "lift",
    ["HoldInPlaceDuration"]        = "immobilize",
    ["RestrictionDuration"]        = "immobilize",
    ["SlowPercent"]                = "slow",
    ["EnemySlowPct"]               = "slow",
    ["MoveSlowPercent"]            = "slow",
    ["GroundDashReductionPercent"] = "dash slow",
    ["SlowDuration"]               = "slow",
    ["MaxSlowDuration"]            = "slow",
    ["BulletDamageAmpDuration"]    = "bullet amp",
    ["BulletResistReduction"]      = "bullet resist reduction",
    ["TimeSlowDuration"]           = "time slow",
    ["LateCheckoutStun"]           = "stun",
    ["BurnDuration"]               = "burn",
    ["BleedDuration"]              = "bleed",
    ["VenomDuration"]              = "venom",
    ["TossDuration"]               = "knockback",
}

-- ============================================================
-- 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.
-- ============================================================

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 = {}

-- Charges: shows base charges, time between charges, and upgrade notes.
function p.RenderCharges(ability)
    local base_charges  = unwrap_value(ability["AbilityCharges"])
    local base_cooldown = 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

-- Melee: shows light melee scaling and upgrade notes.
function p.RenderMelee(ability)
    local function find_base_melee_scale(ability)
        for _, v in pairs(ability) do
            if type(v) == "table" then
                local scale = v["Scale"]
                if type(scale) == "table"
                    and scale["Type"] == "melee"
                    and type(scale["Value"]) == "number"
                    and scale["Value"] > 0
                then
                    return scale["Value"]
                end
            end
        end
        return nil
    end

    local cells = {}
    local base_scale = find_base_melee_scale(ability)
    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 = {}
    for i, tier in ipairs(upgrades) do
        if type(tier) == "table" then
            for _, v in pairs(tier) do
                if type(v) == "table" then
                    local scale = v["Scale"]
                    if type(scale) == "table" and type(scale["Value"]) == "number" then
                        local stype = scale["Type"]
                        local delta = scale["Value"]
                        if stype == "heavy_melee" and delta > 0 then
                            table.insert(notes, "T" .. i .. " upgrade changes scaling to ×"
                                .. delta .. " of Heavy Melee damage.")
                        elseif stype == "melee" and delta ~= 0 and base_scale then
                            local final = math.floor((base_scale + delta) * 1000 + 0.5) / 1000
                            table.insert(notes, "T" .. i .. " upgrade adds ×"
                                .. delta .. " scaling (total ×" .. final .. ").")
                        end
                    end
                end
            end
        end
    end
    if #notes > 0 then cells["Notes"] = table.concat(notes, " ") end
    return cells
end

-- HealReduce: shows heal reduction percentage and upgrade notes.
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

-- Debuff: shared by debuffresist and dispelmagic.
-- Outputs upgrade-tier notes for debuffs added only in upgrades (not at base).
function p.RenderDebuff(ability, stat)
    local stat_props = Lists.lists[stat]
    local cells = {}

    -- If any prop exists in base, this is a base-level debuff — no note needed.
    for _, prop in ipairs(stat_props) do
        if is_base_property(ability, prop) then
            return cells
        end
    end

    local upgrades = ability["Upgrades"]
    if type(upgrades) ~= "table" then return cells end

    local tier_debuffs = {}
    local seen_names   = {}

    for i, tier in ipairs(upgrades) do
        if type(tier) == "table" then
            local names_this_tier = {}
            local seen_this_tier  = {}
            for _, prop in ipairs(stat_props) do
                local v = tier[prop]
                if v ~= nil then
                    if type(v) == "table" then v = v["Value"] end
                    if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
                        local short = debuff_short_name[prop] or "debuff"
                        if not seen_this_tier[short] then
                            seen_this_tier[short] = true
                            if not seen_names[short] then
                                seen_names[short] = true
                                table.insert(names_this_tier, short)
                            end
                        end
                    end
                end
            end
            if #names_this_tier > 0 then
                tier_debuffs[i] = names_this_tier
            end
        end
    end

    local tier_indices = {}
    for i in pairs(tier_debuffs) do table.insert(tier_indices, i) end
    table.sort(tier_indices)

    local parts = {}
    for _, i in ipairs(tier_indices) do
        table.insert(parts, "T" .. i .. " " .. table.concat(tier_debuffs[i], " + "))
    end

    if #parts > 0 then cells["Notes"] = table.concat(parts, ", ") end
    return cells
end

-- MoveSlow: shows move slow percentage and caster slow note if present.
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

-- DashSlow: shows dash slow percentage, auto-detects caster self-slow.
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

return p