Editing Module:Sandbox/LVL

Warning: You are not logged in. Once you make an edit, a temporary account will be created for you. Learn more. Log in or create an account to continue receiving notifications after this account expires, and to access other features.
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:
local p = {}
local p = {}
local lang = require "Module:Lang"
local util_module = require "Module:Utilities"
local data = mw.loadJsonData("Data:Monster_Domosed/ItemData.json")


local heroes_data      = mw.loadJsonData("Data:HeroData.json")
local function get_component_of_sorted(item_input)
local attributes_data  = mw.loadJsonData("Data:AttributeData.json")
    -- Find the item's key
local attribute_orders  = mw.loadJsonData("Data:StatInfoboxOrder.json")
    local target_key = nil
local util_module      = require('Module:Utilities')
    if data[item_input] then
local lang_module      = require('Module:Lang')
        target_key = item_input
local dictionary_module = require('Module:Dictionary')
    else
local attribute_module  = require('Module:AttributeData')
        for k, v in pairs(data) do
local hero_data_module  = require('Module:HeroData')
            if v.Name == item_input and (v.IsDisabled == false or v.IsDisabled == nil) then
                target_key = k; break
            end
        end
    end
    if not target_key then return {} end


local function get_nested_stat_value(hero_data, stat_key)
    -- Find all items that have this item as a component
    if hero_data[stat_key] ~= nil then
    local parents = {}
        return hero_data[stat_key]
    for key, item in pairs(data) do
    elseif hero_data.Weapon and hero_data.Weapon[stat_key] ~= nil then
        if item.IsDisabled ~= true and item.Components and type(item.Components) == "table" then
        return hero_data.Weapon[stat_key]
            for _, comp in ipairs(item.Components) do
    elseif hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[stat_key] ~= nil then
                if comp == target_key then
         return hero_data.Weapon.AltFire[stat_key]
                    table.insert(parents, key)
                    break
                end
            end
         end
     end
     end
     return 0
 
     -- Sort by Tier instead of alphabetically
    table.sort(parents, function(a, b)
        local tier_a = tonumber(data[a] and data[a]["Tier"]) or 0
        local tier_b = tonumber(data[b] and data[b]["Tier"]) or 0
        return tier_a < tier_b
    end)
 
    return parents
end
end


local function localize(key, fallback)
-- Get raw value, CSS class, and LocTokenOverride for a property
     local result = lang_module.get_string(key)
local function get_raw_value(item_name, prop)
     if result == "" or result == nil then
     local item = data[item_name] or nil
         result = util_module.add_space_before_cap(fallback) ..
    if not item then
                mw.getCurrentFrame():expandTemplate{title = "MissingValveTranslationTooltip"}
        for k,v in pairs(data) do
            if v.Name == item_name then item = v; break end
        end
    end
    if not item then return nil, nil, nil end
 
    local raw_prop = item[prop]
     if type(raw_prop) == "table" then
         local loc_override = raw_prop.LocTokenOverride or prop
        return raw_prop.Value, raw_prop.CSSClass, loc_override
    else
        return raw_prop, nil, prop
     end
     end
    return result
end
end


-- =========================================================================
-- Check if property exists in language data
--  DPS calculation helpers
local function is_property_in_lang_data(prop)
-- =========================================================================
    local keys = {
        prop .. "_label",
        prop .. "_Label",
        prop .. "_prefix",
        prop .. "_Prefix",
        prop .. "_postfix",
        prop .. "_Postfix",
        prop .. "_postvalue_label",
        prop .. "_Postvalue_Label"
    }


local function calculate_dps(stats, dps_type)
    for _, key in ipairs(keys) do
    local rps = stats.RoundsPerSecond or 0
        if lang.get_string(key) then
     if rps == 0 then return 0 end
            return true
        end
     end


     local bullets_per_shot = (stats.HitOnceAcrossAllBullets and 1) or (stats.BulletsPerShot or 1)
     return false
     local burst_count = stats.BulletsPerBurst or 1
end
     local cycle_time = 1 / rps
 
     local total_cycle_time = cycle_time * burst_count
-- Get item by name, ignoring disabled if possible
local function get_json_item(name)
    for _, v in pairs(data) do
        if v.Name == name and (v.IsDisabled == false or v.IsDisabled == nil) then
            return v
        end
     end
    for _, v in pairs(data) do
        if v.Name == name then return v end
    end
     return nil
end
 
-- Format stat value for Infobox, handling prefix/postfix, CSS, scaling, and conditional
local function format_stat(item_name, prop, skip_label, debug, allow_conditional)
     if allow_conditional == nil then allow_conditional = true end


     if total_cycle_time == 0 then return 0 end
     if prop == "AbilityCooldown" and not skip_label then
        return nil
    end


     local base_damage = stats.BulletDamage or 0
     local value, css, loc_key = get_raw_value(item_name, prop)
     if dps_type == 'burst' then
     if value == nil or value == 0 or value == "" then
        return base_damage * bullets_per_shot * burst_count / total_cycle_time
    return nil
     end
     end


     local clip_size = stats.ClipSize or 0
    -- Convert string like "0m" or "0.0" to number for zero check
     if clip_size <= 0 then
     local numeric_val = tonumber(tostring(value):match("^-?%d+%.?%d*")) or 0
         return base_damage * bullets_per_shot * burst_count / total_cycle_time
     if numeric_val == 0 then
         return nil
     end
     end


     local reload_time
     -- Skip if property has no lang data
    if stats.ReloadSingle then
    local label_key = loc_key:gsub("^#", "")
        reload_time = (stats.ReloadTime or 0) * clip_size
if not (
    else
    lang.get_string(label_key.."_label") or
        reload_time = stats.ReloadTime or 0
    lang.get_string(label_key.."_Label") or
    lang.get_string(label_key.."_prefix") or
    lang.get_string(label_key.."_Prefix") or
    lang.get_string(label_key.."_postfix") or
    lang.get_string(label_key.."_Postfix") or
    lang.get_string(label_key.."_postvalue_label") or
    lang.get_string(label_key.."_Postvalue_Label")
) then
    return nil
end
if debug == 2 then
    local label_val, prefix_val, postfix_val
    -- determine which label key worked
    if lang.get_string(label_key.."_label") then
        label_val = label_key.."_label"
    elseif lang.get_string(label_key.."_Label") then
        label_val = label_key.."_Label"
    else
        label_val = "(none)"
    end
    -- determine which prefix key worked
    if lang.get_string(label_key.."_prefix") then
        prefix_val = label_key.."_prefix"
    elseif lang.get_string(label_key.."_Prefix") then
        prefix_val = label_key.."_Prefix"
    else
        prefix_val = "(none)"
    end
    -- determine which postfix key worked
    if lang.get_string(label_key.."_postfix") then
        postfix_val = label_key.."_postfix"
    elseif lang.get_string(label_key.."_Postfix") then
        postfix_val = label_key.."_Postfix"
    else
        postfix_val = "(none)"
    end
    return string.format(
        "raw invoke key: %s | raw value: %s | label key used: %s | prefix key used: %s | postfix key used: %s",
        loc_key,
        tostring(value),
        label_val,
        prefix_val,
        postfix_val
    )
end
 
    if prop == "BuildUpPerShot" then label_key = "BuildupPerShot"
    elseif prop == "DotDuration" then label_key = "DOTDuration"
     end
     end
    reload_time = reload_time + (stats.ReloadDelay or 0)


    local time_to_empty_clip = (clip_size / burst_count) * total_cycle_time
-- Helper function: get first non-nil string from variants
     local damage_from_clip = base_damage * bullets_per_shot * clip_size
local function first_non_nil(...)
    local total_time = time_to_empty_clip + reload_time
     for i = 1, select("#", ...) do
    if total_time == 0 then return 0 end
        local v = select(i, ...)
     return damage_from_clip / total_time
        if v and v ~= "" then return v end
    end
     return ""
end
end


-- Compute how much DPS changes when all relevant component scalings are applied.
-- Get actual label, prefix, postfix with fallbacks
-- Used for alt-fire rows where explicit DPS/SustainedDPS scaling keys are missing.
local label = skip_label and "" or first_non_nil(
local function compute_dps_scaling(hero_data, base_stats, dps_type, scaling_type)
    lang.get_string(label_key.."_label"),
    local scaling_source = hero_data[scaling_type .. "Scaling"] or {}
    lang.get_string(label_key.."_Label")
)
 
local prefix = first_non_nil(
    lang.get_string(label_key.."_prefix"),
    lang.get_string(label_key.."_Prefix")
)
 
local postfix = first_non_nil(
    lang.get_string(label_key.."_postfix"),
    lang.get_string(label_key.."_Postfix")
)
   
    if prefix == "" and postfix == "" and label == "" then
    return nil
end
 
    local val_str = tostring(value)


     local component_scalings = {}
     ------------------------------------------------------------------
     local possible_keys = {
    -- CSS-specific formatting
        "BulletDamage", "RoundsPerSecond", "ClipSize", "ReloadTime",
    ------------------------------------------------------------------
        "ReloadDelay", "BulletsPerShot", "BulletsPerBurst"
     if css == "move_speed" or css == "sprint_speed" then
    }
        if postfix == "" or not postfix:find("%%") then
    for _, key in ipairs(possible_keys) do
            val_str = val_str:gsub("/s", "") .. "/s"
         local alt_key = key .. "AltFire"
        end
         if scaling_source[alt_key] then
         local num = tonumber(val_str:match("^-?%d+%.?%d*"))
             component_scalings[key] = scaling_source[alt_key]
         if num and num > 0 and not val_str:find("^%+") then
             val_str = "+" .. val_str
         end
         end
     end
     end


     -- ClipSize is shared between fire modes; fall back to primary scaling
     ------------------------------------------------------------------
     if base_stats.ClipSize and not component_scalings.ClipSize then
    -- Apply prefix
        local primary_clip_scale = scaling_source["ClipSize"]
    ------------------------------------------------------------------
         if primary_clip_scale then
     if prefix ~= "" then
             component_scalings.ClipSize = primary_clip_scale
        if prefix == "{s:sign}" then
            local num = tonumber(val_str:match("^-?%d+%.?%d*"))
            if num and num >= 0 and not val_str:find("^%+") then
                val_str = "+"..val_str
            end
         else
             val_str = prefix..val_str
         end
         end
     end
     end


     if next(component_scalings) == nil then
     ------------------------------------------------------------------
         return 0
    -- Append postfix
    ------------------------------------------------------------------
-- Append postfix
if prop ~= "BuildUpPerShot" and postfix ~= "" and not val_str:find(postfix, 1, true) then
    if postfix == "HP/s" then
        val_str = val_str .. " " .. postfix
    else
        val_str = val_str .. postfix
    end
end
 
    ------------------------------------------------------------------
    -- Append scaling template {{Boon}} or {{Ss}}
    ------------------------------------------------------------------
    local item = get_json_item(item_name)
    local raw_prop = item[prop]
    if type(raw_prop) == "table" and raw_prop.Scale and raw_prop.Scale.Value ~= 0 then
        local scale_val = util_module.round_to_sig_fig(raw_prop.Scale.Value, 2)
         local template_name = (raw_prop.Scale.Type == "power_increase") and "Boon" or "Ss"
        val_str = val_str .. " " .. mw.getCurrentFrame():expandTemplate{ title = template_name, args = { scale_val } }
     end
     end


     local base_dps = calculate_dps(base_stats, dps_type)
     ------------------------------------------------------------------
     local scaled_stats = {}
     -- Append label
     for k, v in pairs(base_stats) do scaled_stats[k] = v end
     ------------------------------------------------------------------
     for comp_key, scale_val in pairs(component_scalings) do
     if label ~= "" then
        if scaled_stats[comp_key] ~= nil then
        val_str = val_str.." "..label
            scaled_stats[comp_key] = scaled_stats[comp_key] + scale_val
        end
     end
     end
     local scaled_dps = calculate_dps(scaled_stats, dps_type)
 
     return scaled_dps - base_dps
     ------------------------------------------------------------------
    -- Append (Conditional) ONLY if allowed
    ------------------------------------------------------------------
if allow_conditional
    and type(raw_prop) == "table"
    and (raw_prop.UsageFlags == "ConditionallyApplied" or raw_prop.UsageFlags == "ConditionallyEnemyApplied")
then
    local conditional_text = lang.get_string("Citadel_Shop_ConditionalAttribute")
    val_str = val_str .. string.format(' <span style="color:#C0C0C0">(%s)</span>', conditional_text)
end
 
     return val_str
end
end


-- =========================================================================
-- Special formatting for StatusEffect
--  Main module function
local function format_property_for_infobox(item_name, prop, skip_label, debug)
-- =========================================================================
     if prop:match("^StatusEffect") then
 
        return string.format(
p.write_hero_comparison_table = function(frame)
            "[[{{#invoke:Lang|get_string|Citadel_%s}}]] [[{{#invoke:Lang|get_string|Citadel_StatusEffect}}]]",
     local power_increases = tonumber(frame.args[1]) or 0
            prop
    local spirit_power    = tonumber(frame.args[2]) or 0
        )
     local max_power      = tonumber(frame.args[3]) or 25
     else
     local max_spirit      = tonumber(frame.args[4]) or 500
        return format_stat(item_name, prop, skip_label, debug, true)
     end
end


     local display_scaling_icons = (power_increases == 0 and spirit_power == 0)
function p.fill_tooltip(frame)
     local item_name = frame.args[1]
    if not item_name then return "Item name missing" end


     local body_str = ""
     local debug = tonumber(frame.args.debug) or 0
    local item = get_json_item(item_name)
    if not item then return "Item not found" end


     local stats_to_include = {
     local ItemData = require("Module:ItemData")
        Weapon = {
    local lines = {
            "DPS", "SustainedDPS", "BulletDamage", "RoundsPerSecond", "FireRate",
        "{{Infobox item",
            "ClipSize", "ReloadTime", "ReloadDelay", "ReloadSingle", "BulletsPerShot", "BulletsPerBurst",
        "| item_name = " .. item_name
            "BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage",
            "BulletSpeed", "FalloffStartRange", "FalloffEndRange",
            "CritDamageBonusPercent", "BulletRadius", "RoundsPerSecondAtMaxSpin", "SpinAcceleration", "SpinDeceleration"
        },
        Vitality = {
            "MaxHealth", "BaseHealthRegen", "BulletResist", "TechResist", "MeleeResist",
            "CritDamageReceivedPercent", "DebuffResist", "BulletLifesteal", "MaxMoveSpeed",
            "SprintSpeed", "StaminaCooldown", "Stamina", "GroundDashSpeed", "GravityChange"
        },
        Spirit = { "TechPower" }
     }
     }


     -- Collect and sort heroes alphabetically, excluding disabled/in-development
     ----------------------------------------------------------------------
     local sorted_heroes = {}
    -- Check if AbilityCooldown or AbilityChargeUpTime exists in sections
    for hero_key, hero_data in pairs(heroes_data) do
    ----------------------------------------------------------------------
        if not hero_data["InDevelopment"] and not hero_data["IsDisabled"] then
     local cooldown_found_in_sections = false
             table.insert(sorted_heroes, {key = hero_key, data = hero_data})
    local chargeup_found_in_sections = false
    for _, section in ipairs(item.TooltipSections or {}) do
        for _, entry in ipairs(section.Entries or {}) do
            for _, prop in ipairs(entry.Properties or {}) do
                if prop == "AbilityCooldown" then cooldown_found_in_sections = true end
                if prop == "AbilityChargeUpTime" then chargeup_found_in_sections = true end
            end
             for _, prop in ipairs(entry.ImportantProperties or {}) do
                if prop == "AbilityCooldown" then cooldown_found_in_sections = true end
                if prop == "AbilityChargeUpTime" then chargeup_found_in_sections = true end
            end
         end
         end
     end
     end
    table.sort(sorted_heroes, function(a, b)
        local function get_sort_name(name)
            return (name:gsub("^The ", ""))
        end
        return get_sort_name(a.data["Name"] or a.key) < get_sort_name(b.data["Name"] or b.key)
    end)


     -- Build a single stat cell (<td>) with data attributes for JS recalculation
     ----------------------------------------------------------------------
     local function buildStatCell(hero_data, hero_key, attr_key, is_alt_fire, category)
     -- Innate stats
        -- Conversion factors for stats that need unit changes (e.g. metres → centimetres)
    ----------------------------------------------------------------------
        local unit_conversion = {
    local innate_index = 1
            BulletRadius = 100  -- metres to centimetres
    for _, section in ipairs(item.TooltipSections or {}) do
        }
         if section.Type == "EArea_Innate" then
        local conv_factor = unit_conversion[attr_key] or 1
             for _, entry in ipairs(section.Entries or {}) do
 
                for _, prop in ipairs(entry.Properties or {}) do
        local base_value
                    if prop ~= "AbilityCooldown" and prop ~= "AbilityChargeUpTime" then
         if is_alt_fire then
                        local val = format_stat(item_name, prop, false, debug, false)
             if attr_key == "ClipSize" or attr_key == "ReloadTime" then
                        if val then
                base_value = get_nested_stat_value(hero_data, attr_key)
                            table.insert(lines, string.format("| item_stat%d = %s", innate_index, val))
            elseif hero_data.Weapon and hero_data.Weapon.AltFire
                            innate_index = innate_index + 1
                and hero_data.Weapon.AltFire[attr_key] ~= nil then
                        end
                base_value = hero_data.Weapon.AltFire[attr_key]
                    end
            else
                 end
                 base_value = get_nested_stat_value(hero_data, attr_key)
             end
             end
        else
            base_value = get_nested_stat_value(hero_data, attr_key)
         end
         end
    end


        -- Apply unit conversion to the base value
    ----------------------------------------------------------------------
         if type(base_value) == "number" then
    -- Passive stats
            base_value = base_value * conv_factor
    ----------------------------------------------------------------------
        end
    local passive_counter = 1
    for _, section in ipairs(item.TooltipSections or {}) do
         if section.Type ~= "EArea_Innate" and section.Type ~= "EArea_Active" then
            for _, entry in ipairs(section.Entries or {}) do
                local loc = entry.LocString and entry.LocString:gsub("#","") or nil
                if loc then
                    table.insert(
                        lines,
                        string.format(
                            "| passive%d_description = {{#invoke:Lang|get_string|%s|item_name=%s}}",
                            passive_counter, loc, item_name
                        )
                    )
                end


        local stat_value = base_value
                local stat_index = 1
                local seen_props = {}


        -- Scaling lookup
                -- ImportantProperties
        local scaling_data
                 for _, prop in ipairs(entry.ImportantProperties or {}) do
        if is_alt_fire then
                    if prop ~= "AbilityCooldown" and prop ~= "AbilityChargeUpTime" and is_property_in_lang_data(prop) and not seen_props[prop] then
            if attr_key == "ClipSize" or attr_key == "ReloadTime" then
                        local val = format_property_for_infobox(item_name, prop, false, debug)
                 scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
                        if val then
            elseif (attr_key == "DPS" or attr_key == "SustainedDPS")
                            table.insert(lines, string.format("| passive%d_stat%d = %s", passive_counter, stat_index, val))
                and hero_data.Weapon and hero_data.Weapon.AltFire then
                            stat_index = stat_index + 1
                -- Alt-fire DPS/SustainedDPS: use explicit key if present, otherwise compute from components
                            seen_props[prop] = true
                local direct_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
                        end
                if direct_scaling and next(direct_scaling) ~= nil then
                     end
                    scaling_data = direct_scaling
                else
                    local alt_stats = hero_data.Weapon.AltFire
                    local dps_type = attr_key == "DPS" and 'burst' or 'sustained'
                    local level_scale = compute_dps_scaling(hero_data, alt_stats, dps_type, "Level")
                    local spirit_scale = compute_dps_scaling(hero_data, alt_stats, dps_type, "Spirit")
                    level_scale = util_module.round_to_sig_fig(level_scale, 5)
                    spirit_scale = util_module.round_to_sig_fig(spirit_scale, 5)
                    scaling_data = {}
                    if level_scale ~= 0 then scaling_data[level_scale] = "Level" end
                    if spirit_scale ~= 0 then scaling_data[spirit_scale] = "Spirit" end
                     if next(scaling_data) == nil then scaling_data = nil end
                 end
                 end
            elseif hero_data.Weapon and hero_data.Weapon.AltFire
                and hero_data.Weapon.AltFire[attr_key] ~= nil then
                scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
            else
                scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
            end
        else
            scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
        end


        local scaling_strs = ""
                -- Properties
        local spirit_scale = 0
                for _, prop in ipairs(entry.Properties or {}) do
        local level_scale  = 0
                    if prop ~= "AbilityCooldown" and prop ~= "AbilityChargeUpTime" and is_property_in_lang_data(prop) and not seen_props[prop] then
        local spirit_val = 0
                        local val = format_property_for_infobox(item_name, prop, false, debug)
        local level_val  = 0
                        if val then
        if scaling_data ~= nil then
                            table.insert(lines, string.format("| passive%d_stat%d = %s", passive_counter, stat_index, val))
            for scaling_val, scaling_type in pairs(scaling_data) do
                            stat_index = stat_index + 1
                if scaling_type == "Spirit" then
                            seen_props[prop] = true
                    spirit_val = scaling_val * conv_factor  -- apply unit conversion
                        end
                elseif scaling_type == "Level" then
                     end
                     level_val = scaling_val * conv_factor  -- apply unit conversion
                 end
                 end
            end


            -- Output Spirit icon before Level icon
                -- AbilityCooldown
            local function append_scaling(val, stype)
                for _, prop in ipairs(entry.Properties or {}) do
                if val ~= 0 then
                    if prop == "AbilityCooldown" then
                    local scaling_str = hero_data_module.write_scalar_str(val, stype, true)
                        local cd = format_stat(item_name, prop, true, debug)
                     if scaling_str ~= "" then
                        if cd then
                         scaling_strs = scaling_strs .. " " .. scaling_str
                            table.insert(lines, string.format("| passive%d_cooldown = %s", passive_counter, cd))
                        end
                    end
                end
                for _, prop in ipairs(entry.ImportantProperties or {}) do
                     if prop == "AbilityCooldown" then
                         local cd = format_stat(item_name, prop, true, debug)
                        if cd then
                            table.insert(lines, string.format("| passive%d_cooldown = %s", passive_counter, cd))
                        end
                     end
                     end
                 end
                 end
            end
            append_scaling(spirit_val, "Spirit")
            append_scaling(level_val, "Level")


            if type(stat_value) == "number" then
                -- AbilityChargeUpTime
                stat_value = stat_value + (spirit_power * spirit_val)
                for _, prop in ipairs(entry.Properties or {}) do
                stat_value = stat_value + (power_increases * level_val)
                    if prop == "AbilityChargeUpTime" then
            end
                        local cu = format_stat(item_name, prop, true, debug)
 
                        if cu then
            spirit_scale = spirit_val
                            table.insert(lines, string.format("| passive%d_chargeup = %s", passive_counter, cu))
            level_scale  = level_val
                        end
        end
                    end
                end
                for _, prop in ipairs(entry.ImportantProperties or {}) do
                    if prop == "AbilityChargeUpTime" then
                        local cu = format_stat(item_name, prop, true, debug)
                        if cu then
                            table.insert(lines, string.format("| passive%d_chargeup = %s", passive_counter, cu))
                        end
                    end
                end


        if attr_key == "TechPower" and spirit_scale == 0 then
                 passive_counter = passive_counter + 1
            spirit_scale = 1.0
            if type(stat_value) == "number" then
                 stat_value = stat_value + (spirit_power * spirit_scale)
             end
             end
         end
         end
    end


         local innate_spirit_scale =
    ----------------------------------------------------------------------
             (hero_data["LevelScaling"] and hero_data["LevelScaling"]["TechPower"]) or 0
    -- Active stats
    ----------------------------------------------------------------------
    local active_index = 1
    for _, section in ipairs(item.TooltipSections or {}) do
         if section.Type == "EArea_Active" then
             for _, entry in ipairs(section.Entries or {}) do
                if entry.LocString then
                    table.insert(
                        lines,
                        string.format(
                            "| active%d_description = {{#invoke:Lang|get_string|%s|item_name=%s}}",
                            active_index,
                            entry.LocString:gsub("#",""),
                            item_name
                        )
                    )
                end


        if not display_scaling_icons then scaling_strs = "" end
                local stat_index = 1
                local seen_props = {}


        if type(stat_value) == "boolean" then
                -- ImportantProperties
            stat_value = tostring(stat_value)
                for _, prop in ipairs(entry.ImportantProperties or {}) do
        else
                    if prop ~= "AbilityCooldown" and prop ~= "AbilityChargeUpTime" and is_property_in_lang_data(prop) and not seen_props[prop] then
            stat_value = util_module.round_to_sig_fig(stat_value, 5)
                        local val = format_property_for_infobox(item_name, prop, false, debug)
        end
                        if val then
                            table.insert(lines, string.format("| active%d_stat%d = %s", active_index, stat_index, val))
                            stat_index = stat_index + 1
                            seen_props[prop] = true
                        end
                    end
                end


        local cell_inner = string.format(
                -- Properties
            '<span class="stat-num">%s</span><span class="stat-scaling">%s</span>',
                for _, prop in ipairs(entry.Properties or {}) do
            stat_value,
                    if prop ~= "AbilityCooldown" and prop ~= "AbilityChargeUpTime" and is_property_in_lang_data(prop) and not seen_props[prop] then
            scaling_strs
                        local val = format_property_for_infobox(item_name, prop, false, debug)
        )
                        if val then
                            table.insert(lines, string.format("| active%d_stat%d = %s", active_index, stat_index, val))
                            stat_index = stat_index + 1
                            seen_props[prop] = true
                        end
                    end
                end


        local weapon_table = hero_data.Weapon
                -- AbilityCooldown
        if is_alt_fire and weapon_table and weapon_table.AltFire then
                for _, prop in ipairs(entry.Properties or {}) do
            weapon_table = weapon_table.AltFire
                    if prop == "AbilityCooldown" then
        end
                        local cd = format_stat(item_name, prop, true, debug)
        local hit_once = "false"
                        if cd then
        if (attr_key == "DPS" or attr_key == "SustainedDPS")
                            table.insert(lines, string.format("| active%d_cooldown = %s", active_index, cd))
            and weapon_table
                        end
            and weapon_table.HitOnceAcrossAllBullets
                    end
        then
                end
            hit_once = "true"
                for _, prop in ipairs(entry.ImportantProperties or {}) do
        end
                    if prop == "AbilityCooldown" then
                        local cd = format_stat(item_name, prop, true, debug)
                        if cd then
                            table.insert(lines, string.format("| active%d_cooldown = %s", active_index, cd))
                        end
                    end
                end


        local data_attrs = string.format(
                -- AbilityChargeUpTime
            'data-stat-name="%s" data-base="%s" data-spirit-scale="%s" data-level-scale="%s" data-innate-spirit-scale="%s" data-sort-value="%s" data-hit-once="%s"',
                for _, prop in ipairs(entry.Properties or {}) do
            attr_key,
                    if prop == "AbilityChargeUpTime" then
            tostring(type(base_value) == "number" and util_module.round_to_sig_fig(base_value, 3) or base_value),
                        local cu = format_stat(item_name, prop, true, debug)
            tonumber(spirit_scale) or 0,
                        if cu then
            (attr_key == "TechPower") and "0" or (tonumber(level_scale) or 0),
                            table.insert(lines, string.format("| active%d_chargeup = %s", active_index, cu))
            innate_spirit_scale or 0,
                        end
            stat_value,
                    end
            hit_once
                end
        )
                for _, prop in ipairs(entry.ImportantProperties or {}) do
                    if prop == "AbilityChargeUpTime" then
                        local cu = format_stat(item_name, prop, true, debug)
                        if cu then
                            table.insert(lines, string.format("| active%d_chargeup = %s", active_index, cu))
                        end
                    end
                end


         return string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner)
                active_index = active_index + 1
            end
         end
     end
     end


     -- Generate a hero's primary stat row
     ----------------------------------------------------------------------
     local function generatePrimaryRow(hero_data, hero_key, has_alt)
    -- Global fallback for AbilityCooldown and AbilityChargeUpTime
        local row_str = ""
     ----------------------------------------------------------------------
         local hero_name_local = localize(hero_key, hero_key)
    if not cooldown_found_in_sections and item.AbilityCooldown then
        local hero_name_en = hero_data["Name"]
         local cd = format_stat(item_name, "AbilityCooldown", true, debug)
        local template_args = {[1] = hero_name_en, l1 = hero_name_local}
         if cd then
        local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args }
             table.insert(lines, "| item_stat_cooldown = " .. cd)
        local hero_cell_content = hero_icon
         if has_alt then
             hero_cell_content = '[[#alt-fire-' .. hero_key .. '|+]] ' .. hero_cell_content
         end
         end
         local sort_name = (hero_data["Name"] or hero_key):gsub("^The ", "")
    end
         row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 10; background-color: var(--background-color-base-2); isolation: isolate; overflow: hidden; min-width: 150px;" data-sort-value="' .. sort_name .. '">' .. hero_cell_content .. '</td>'
 
        for _, category in ipairs(attribute_orders["category_order"]) do
    if not chargeup_found_in_sections and item.AbilityChargeUpTime then
            if stats_to_include[category] ~= nil then
         local cu = format_stat(item_name, "AbilityChargeUpTime", true, debug)
                for _, attr_key in ipairs(stats_to_include[category]) do
         if cu then
                    row_str = row_str .. buildStatCell(hero_data, hero_key, attr_key, false, category)
            table.insert(lines, "| item_stat_chargeup = " .. cu)
                end
            end
         end
         end
        return "<tr>" .. row_str .. "</tr>"
     end
     end


     -- Generate an expandable alt-fire sub-row (weapon stats only)
     ----------------------------------------------------------------------
     local function generateAltFireSubRow(hero_data, hero_key)
     -- Components
        local row_str = ""
    ----------------------------------------------------------------------
        local hero_name_local = localize(hero_key, hero_key)
    for i = 1, 2 do
         local hero_name_en = hero_data["Name"]
         local comp = ItemData.get_component_name {
        local template_args = {[1] = hero_name_en, l1 = hero_name_local}
            args = { item_name, i }
         local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args }
         }
         local hero_cell_content = hero_icon .. ' <small style="color:#aaa;">(Alt‑fire)</small>'
         if comp and comp ~= "" then
        local sort_name = (hero_data["Name"] or hero_key):gsub("^The ", "")
            table.insert(lines, string.format("| component%d_name = %s", i, comp))
        row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 9; background-color: #202122; isolation: isolate; overflow: hidden; min-width: 150px;" data-sort-value="' .. sort_name .. '">' .. hero_cell_content .. '</td>'
        for _, category in ipairs(attribute_orders["category_order"]) do
            if stats_to_include[category] ~= nil then
                for _, attr_key in ipairs(stats_to_include[category]) do
                    if category == "Weapon" then
                        row_str = row_str .. buildStatCell(hero_data, hero_key, attr_key, true, category)
                    else
                        row_str = row_str .. '<td></td>'
                    end
                end
            end
         end
         end
        return '<tr class="alt-fire-sub" data-parent="' .. hero_key .. '">' .. row_str .. '</tr>'
     end
     end


     -- Build all body rows
     ----------------------------------------------------------------------
     for _, hero_entry in ipairs(sorted_heroes) do
    -- Is Component Of
         local hero_key  = hero_entry.key
    ----------------------------------------------------------------------
        local hero_data = hero_entry.data
    local parents = get_component_of_sorted(item_name)
        local has_alt = hero_data.Weapon and hero_data.Weapon.AltFire and true or false
     for i, key in ipairs(parents) do
         body_str = body_str .. generatePrimaryRow(hero_data, hero_key, has_alt)
         local parent_name = data[key] and data[key]["Name"] or ""
        if has_alt then
         if parent_name ~= "" then
             body_str = body_str .. generateAltFireSubRow(hero_data, hero_key)
             table.insert(lines, string.format("| iscomponentof%d_name = %s", i, parent_name))
         end
         end
     end
     end


     -- Pre-pass: determine which stats have any scaling, and which have both types.
     ----------------------------------------------------------------------
     -- Used to set column widths in the header.
     -- Sounds (sound1, sound2, sound3, ...)
     local stats_with_any_scaling  = {}
     ----------------------------------------------------------------------
    local stats_with_both_scaling = {}
     for key, value in pairs(frame.args) do
     for _, hero_entry in ipairs(sorted_heroes) do
         local index = tostring(key):match("^sound(%d+)$")
         local hero_data = hero_entry.data
        if index and value ~= "" then
        for _, category in ipairs(attribute_orders["category_order"]) do
            table.insert(lines, string.format("| sound%s = %s", index, value))
            if stats_to_include[category] ~= nil then
                for _, attr_key in ipairs(stats_to_include[category]) do
                    -- Primary scaling
                    local scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
                    if scaling_data ~= nil and next(scaling_data) ~= nil then
                        stats_with_any_scaling[attr_key] = true
                        local has_spirit, has_level = false, false
                        for _, scaling_type in pairs(scaling_data) do
                            if scaling_type == "Spirit" then has_spirit = true
                            elseif scaling_type == "Level" then has_level = true end
                        end
                        if has_spirit and has_level then stats_with_both_scaling[attr_key] = true end
                    end
 
                    -- Alt-fire scaling (only for stats present in the AltFire table)
                    if hero_data.Weapon
                        and hero_data.Weapon.AltFire
                        and hero_data.Weapon.AltFire[attr_key] ~= nil
                    then
                        if attr_key ~= "ClipSize" then
                            local alt_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
                            if alt_scaling and next(alt_scaling) ~= nil then
                                stats_with_any_scaling[attr_key] = true
                                local has_spirit_alt, has_level_alt = false, false
                                for _, stype in pairs(alt_scaling) do
                                    if stype == "Spirit" then has_spirit_alt = true
                                    elseif stype == "Level" then has_level_alt = true end
                                end
                                if has_spirit_alt and has_level_alt then stats_with_both_scaling[attr_key] = true end
                            else
                                if attr_key == "DPS" or attr_key == "SustainedDPS" then
                                    local alt_stats = hero_data.Weapon.AltFire
                                    local dps_type = attr_key == "DPS" and 'burst' or 'sustained'
                                    local lv = compute_dps_scaling(hero_data, alt_stats, dps_type, "Level")
                                    local sp = compute_dps_scaling(hero_data, alt_stats, dps_type, "Spirit")
                                    lv = util_module.round_to_sig_fig(lv, 5)
                                    sp = util_module.round_to_sig_fig(sp, 5)
                                    if lv ~= 0 or sp ~= 0 then
                                        stats_with_any_scaling[attr_key] = true
                                        if lv ~= 0 and sp ~= 0 then
                                            stats_with_both_scaling[attr_key] = true
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            end
         end
         end
     end
     end


     -- Build table header row
     ----------------------------------------------------------------------
    local headers_str = '<th style="position: sticky; left: 0; top: -1px; z-index: 12; background-color: var(--background-color-base-5); isolation: isolate; min-width: 150px;">Hero</th>'
     table.insert(lines, "}}")
     local category_data = attribute_module.get_category_data()
    local output = table.concat(lines, "\n")
    local postfix_key_map = {
        ["ReloadDelay"] = "StatDesc_ReloadTime_postfix",
        ["BulletsPerShot"] = "",
        ["BulletsPerBurst"] = "",
        ["BurstInterShotInterval"] = "StatDesc_ReloadTime_postfix",
        ["ReloadSingle"] = "",
        ["BonusAttackRange"] = "StatDesc_WeaponRangeFalloffMax_postfix",
        ["SustainedDPS"] = "DPS_postfix",
        ["RoundsPerSecondAtMaxSpin"] = "",
        ["CritDamageBonusPercent"]  = "StatDesc_CritDamageBonusScale_postfix",
["CritDamageReceivedPercent"] = "StatDesc_CritDamageReceivedScale_postfix",
        ["BulletLifesteal"] = "BulletLifestealPercentHero_postfix",
        ["GroundDashSpeed"] = "DashSpeed_postfix"
    }
    for _, category in ipairs(attribute_orders["category_order"]) do
        local category_attrs = attributes_data[category]
        local category_rgb  = category_data[category]["rgb"]
        if stats_to_include[category] ~= nil then
            for _, attr_key in ipairs(stats_to_include[category]) do
                local attr_data = category_attrs[attr_key]
               
                -- Fallback: if the stat is missing from this category, check all other categories
                if attr_data == nil then
                    for _, fallback_cat_data in pairs(attributes_data) do
                        if fallback_cat_data[attr_key] ~= nil then
                            attr_data = fallback_cat_data[attr_key]
                            break
                        end
                    end
                end


                local attr_localized, postfix
    if debug == 1 or debug == 2 then
                if attr_data ~= nil then
        return output
                    attr_localized = lang_module.get_string(attr_data["label"])
                    if attr_localized == nil or attr_localized == "" then
                        attr_localized = util_module.add_space_before_cap(attr_key)
                    end
                    postfix = lang_module.get_string(attr_data["postfix"])
                    if postfix == nil or postfix == "" then postfix = ""
                    else postfix = " (" .. postfix .. ")" end
                    if attr_key == "BulletRadius" then postfix = " (cm)" end
                else
                    attr_localized = dictionary_module.translate(attr_key)
                    postfix = lang_module.get_string(postfix_key_map[attr_key])
                    if postfix == nil then return "attr_key " .. attr_key .. " must be added to postfix_key_map" end
                    if postfix ~= "" then postfix = " (" .. postfix .. ")" end
                end
                local th_style = 'position: sticky; top: -1px; z-index: 3; background-color: rgb(' .. category_rgb .. ');'
                if stats_with_both_scaling[attr_key] then
                    th_style = th_style .. ' min-width: 130px;'
                elseif stats_with_any_scaling[attr_key] then
                    th_style = th_style .. ' min-width: 75px;'
                end
                headers_str = headers_str .. '<th style="' .. th_style .. '">' .. attr_localized .. postfix .. "</th>"
            end
        end
     end
     end
    headers_str = "<tr>" .. headers_str .. "</tr>"


     return string.format(
     return mw.getCurrentFrame():preprocess(output)
        '<div id="hero-comparison-container" data-max-power="%s" data-max-spirit="%s">' ..
        '<div style="overflow: auto; max-height: 70vh; width: 100%%;">' ..
        '<table class="wikitable sortable" style="table-layout: auto; width: 100%%;" id="hero-comparison-table">%s%s</table>' ..
        '</div></div>',
        max_power, max_spirit, headers_str, body_str
    )
end
end


return p
return p
Please note that all contributions to The Deadlock Wiki are considered to be released under the Creative Commons Attribution-NonCommercial-ShareAlike (see Deadlock:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!
Cancel Editing help (opens in new window)
Preview page with this template

Page included on this page: