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/ItemCards.json")
local stat_links = mw.loadJsonData("Data:StatLinks.json")


local heroes_data      = mw.loadJsonData("Data:HeroData.json")
local LANG_CODE = lang.get_lang_code()
local attributes_data  = mw.loadJsonData("Data:AttributeData.json")
local IS_EN = (LANG_CODE == "en")
local attribute_orders  = mw.loadJsonData("Data:StatInfoboxOrder.json")
local util_module      = require('Module:Utilities')
local lang_module      = require('Module:Lang')
local dictionary_module = require('Module:Dictionary')
local attribute_module  = require('Module:AttributeData')
local hero_data_module  = require('Module:HeroData')


local function get_nested_stat_value(hero_data, stat_key)
local MOVE_SPEED_RATES = mw.loadJsonData("Data:LocalizedUnits.json")
     if hero_data[stat_key] ~= nil then
 
        return hero_data[stat_key]
--------------------------------------------------------------------------------
     elseif hero_data.Weapon and hero_data.Weapon[stat_key] ~= nil then
-- Utility helpers
        return hero_data.Weapon[stat_key]
--------------------------------------------------------------------------------
    elseif hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[stat_key] ~= nil then
 
        return hero_data.Weapon.AltFire[stat_key]
local function has_rate_unit(str)
    end
     if type(str) ~= "string" then return false end
    return 0
     return str:find("/") ~= nil
end
end


local function localize(key, fallback)
local function get_movespeed_rate_suffix()
    local result = lang_module.get_string(key)
     return MOVE_SPEED_RATES[LANG_CODE]
    if result == "" or result == nil then
        result = util_module.add_space_before_cap(fallback) ..
                mw.getCurrentFrame():expandTemplate{title = "MissingValveTranslationTooltip"}
    end
     return result
end
end


-- =========================================================================
local function get_missing_rate_warning()
--  DPS calculation helpers
    return '<sup style="white-space:nowrap; font-size: smaller;"><span title="Missing translated m/s. Please add an entry for this language to Data:Lang_MoveSpeedRates.json" style="color:red; cursor:help;">!?</span></sup>'
-- =========================================================================
end


local function calculate_dps(stats, dps_type)
local function first_non_nil(...)
    local rps = stats.RoundsPerSecond or 0
    for i = 1, select("#", ...) do
    if rps == 0 then return 0 end
        local v = select(i, ...)
        if v and v ~= "" then return v end
    end
    return ""
end


     local bullets_per_shot = (stats.HitOnceAcrossAllBullets and 1) or (stats.BulletsPerShot or 1)
local function get_lang_with_fallback(base_key, suffix1, suffix2)
    local burst_count = stats.BulletsPerBurst or 1
     return first_non_nil(
     local cycle_time = 1 / rps
        lang.get_string(base_key .. suffix1),
    local total_cycle_time = cycle_time * burst_count
        lang.get_string(base_key .. suffix2)
     )
end


    if total_cycle_time == 0 then return 0 end
--------------------------------------------------------------------------------
-- Data access helpers (adapted for new Info-section format)
--------------------------------------------------------------------------------


     local base_damage = stats.BulletDamage or 0
-- Get item by name, preferring non-disabled
     if dps_type == 'burst' then
local function get_json_item(name)
         return base_damage * bullets_per_shot * burst_count / total_cycle_time
     if data[name] and not data[name].IsDisabled then
        return data[name]
    end
     for _, v in pairs(data) do
        if v.Name == name and not v.IsDisabled then return v end
    end
    for _, v in pairs(data) do
         if v.Name == name then return v end
     end
     end
    return nil
end


     local clip_size = stats.ClipSize or 0
-- Get ordered Info sections (Info1, Info2, ...) from an item
     if clip_size <= 0 then
local function get_info_sections(item)
         return base_damage * bullets_per_shot * burst_count / total_cycle_time
     local sections = {}
     local i = 1
    while item["Info" .. i] do
         table.insert(sections, item["Info" .. i])
        i = i + 1
     end
     end
    return sections
end


    local reload_time
-- Find a property object by key across all Info sections and Other
     if stats.ReloadSingle then
local function find_property_obj(item, prop_key)
         reload_time = (stats.ReloadTime or 0) * clip_size
     for _, section in ipairs(get_info_sections(item)) do
     else
        for _, obj in ipairs(section.Main or {}) do
         reload_time = stats.ReloadTime or 0
            if obj.Key == prop_key then return obj end
        end
         for _, obj in ipairs(section.Alt or {}) do
            if obj.Key == prop_key then return obj end
        end
     end
    if item.Other and item.Other[prop_key] then
         return item.Other[prop_key]
     end
     end
     reload_time = reload_time + (stats.ReloadDelay or 0)
     return nil
end
 
-- Get raw value, type (CSS class), and LocTokenOverride for a property
local function get_raw_value(item_name, prop)
    local item = data[item_name] or get_json_item(item_name)
    if not item then return nil, nil, nil end
 
    local obj = find_property_obj(item, prop)
    if not obj then return nil, nil, nil end


     local time_to_empty_clip = (clip_size / burst_count) * total_cycle_time
     return obj.Value, obj.Type, obj.LocTokenOverride or prop
    local damage_from_clip = base_damage * bullets_per_shot * clip_size
    local total_time = time_to_empty_clip + reload_time
    if total_time == 0 then return 0 end
    return damage_from_clip / total_time
end
end


-- Compute how much DPS changes when all relevant component scalings are applied.
-- Check if property has language data (checks both key and optional loc override)
-- Used for alt-fire rows where explicit DPS/SustainedDPS scaling keys are missing.
local function is_property_in_lang_data(prop, loc_override)
local function compute_dps_scaling(hero_data, base_stats, dps_type, scaling_type)
     local suffixes = {
     local scaling_source = hero_data[scaling_type .. "Scaling"] or {}
        "_label", "_Label", "_prefix", "_Prefix",
        "_postfix", "_Postfix", "_postvalue_label", "_Postvalue_Label"
    }
    local keys = { prop }
    if loc_override and loc_override ~= prop then
        table.insert(keys, loc_override)
    end
    for _, key in ipairs(keys) do
        for _, suffix in ipairs(suffixes) do
            if lang.get_string(key .. suffix) then return true end
        end
    end
    return false
end


    local component_scalings = {}
-- Returns item keys that build from this item, sorted by tier
     local possible_keys = {
local function get_component_of_sorted(item_input)
        "BulletDamage", "RoundsPerSecond", "ClipSize", "ReloadTime",
     local target_key = nil
         "ReloadDelay", "BulletsPerShot", "BulletsPerBurst"
    if data[item_input] then
     }
         target_key = item_input
    for _, key in ipairs(possible_keys) do
     else
        local alt_key = key .. "AltFire"
        for k, v in pairs(data) do
        if scaling_source[alt_key] then
            if v.Name == item_input and not v.IsDisabled then
             component_scalings[key] = scaling_source[alt_key]
                target_key = k; break
             end
         end
         end
     end
     end
    if not target_key then return {} end


     -- ClipSize is shared between fire modes; fall back to primary scaling
     local parents = {}
     if base_stats.ClipSize and not component_scalings.ClipSize then
     for key, item in pairs(data) do
        local primary_clip_scale = scaling_source["ClipSize"]
        if not item.IsDisabled and item.Components and type(item.Components) == "table" then
        if primary_clip_scale then
            for _, comp in ipairs(item.Components) do
             component_scalings.ClipSize = primary_clip_scale
                if comp == target_key then
                    table.insert(parents, key)
                    break
                end
             end
         end
         end
     end
     end


     if next(component_scalings) == nil then
    table.sort(parents, function(a, b)
         return 0
        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
 
--------------------------------------------------------------------------------
-- Shared formatting sub-routines
--------------------------------------------------------------------------------
 
local function resolve_label_key(prop_key, loc_override)
    local label_key = (loc_override or prop_key):gsub("^#", "")
     if prop_key == "BuildUpPerShot" then label_key = "BuildupPerShot"
    elseif prop_key == "DotDuration" then label_key = "DOTDuration" end
    return label_key
end
 
local function resolve_prefix(prefix, numeric_val)
    if prefix == "{s:sign}" then
         return (numeric_val >= 0) and "+" or ""
    end
    return prefix
end
 
local function resolve_suffix(postfix, css_type)
    local suffix = postfix
    if postfix == "HP/s" then suffix = "&#32;" .. postfix end
    if (css_type == "move_speed" or css_type == "sprint_speed")
      and postfix ~= "%" and not has_rate_unit(postfix) then
        local override = get_movespeed_rate_suffix()
        suffix = override or ("m/s" .. get_missing_rate_warning())
     end
     end
    return suffix
end


    local base_dps = calculate_dps(base_stats, dps_type)
local function append_label(val_str, prop_key, label)
     local scaled_stats = {}
     if label == "" then return val_str end
     for k, v in pairs(base_stats) do scaled_stats[k] = v end
     local link_page = stat_links[prop_key]
     for comp_key, scale_val in pairs(component_scalings) do
     if link_page then
         if scaled_stats[comp_key] ~= nil then
         if link_page == label then
             scaled_stats[comp_key] = scaled_stats[comp_key] + scale_val
             return val_str .. " [[" .. label .. "]]"
        else
            return val_str .. " [[" .. link_page .. "|" .. label .. "]]"
         end
         end
     end
     end
    local scaled_dps = calculate_dps(scaled_stats, dps_type)
     return val_str .. " " .. label
     return scaled_dps - base_dps
end
end


-- =========================================================================
local function append_conditional(val_str, allow_conditional, usage_flags)
--  Main module function
    if allow_conditional
-- =========================================================================
      and (usage_flags == "ConditionallyApplied"
        or usage_flags == "ConditionallyEnemyApplied") then
        return val_str .. string.format(
            ' <span style="color:#C0C0C0">(%s)</span>',
            lang.get_string("Citadel_Shop_ConditionalAttribute")
        )
    end
    return val_str
end


p.write_hero_comparison_table = function(frame)
--------------------------------------------------------------------------------
    local power_increases = tonumber(frame.args[1]) or 0
-- Core formatting: non-enhanced
    local spirit_power    = tonumber(frame.args[2]) or 0
--------------------------------------------------------------------------------
    local max_power      = tonumber(frame.args[3]) or 25
    local max_spirit      = tonumber(frame.args[4]) or 500


     local display_scaling_icons = (power_increases == 0 and spirit_power == 0)
local function format_value(prop_key, value, css_type, loc_override,
                            skip_label, allow_conditional, usage_flags, scale_info)
     if value == nil or value == 0 or value == "" then return nil end


     local body_str = ""
     local numeric_val = tonumber(tostring(value):match("^-?%d+%.?%d*")) or 0
    if numeric_val == 0 then return nil end


     local stats_to_include = {
     local label_key = resolve_label_key(prop_key, loc_override)
        Weapon = {
    local label  = skip_label and "" or get_lang_with_fallback(label_key, "_label", "_Label")
            "DPS", "SustainedDPS", "BulletDamage", "RoundsPerSecond", "FireRate",
    local prefix  = get_lang_with_fallback(label_key, "_prefix", "_Prefix")
            "ClipSize", "ReloadTime", "ReloadDelay", "ReloadSingle", "BulletsPerShot", "BulletsPerBurst",
    local postfix = get_lang_with_fallback(label_key, "_postfix", "_Postfix")
            "BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage",
 
            "BulletSpeed", "FalloffStartRange", "FalloffEndRange",
    if prefix == "" and postfix == "" and label == "" then return nil end
            "CritDamageBonusPercent", "BulletRadius", "RoundsPerSecondAtMaxSpin", "SpinAcceleration", "SpinDeceleration"
 
        },
    prefix = resolve_prefix(prefix, numeric_val)
        Vitality = {
    local suffix = resolve_suffix(postfix, css_type)
            "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
     -- Compose value string
     local sorted_heroes = {}
     local val_str = tostring(numeric_val)
     for hero_key, hero_data in pairs(heroes_data) do
     if prefix ~= "" then val_str = prefix .. val_str end
        if not hero_data["InDevelopment"] and not hero_data["IsDisabled"] then
    if suffix ~= "" and not val_str:find(suffix, 1, true) then
            table.insert(sorted_heroes, {key = hero_key, data = hero_data})
         val_str = val_str .. suffix
         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
     -- Scale handling
     local function buildStatCell(hero_data, hero_key, attr_key, is_alt_fire, category)
     if scale_info and scale_info.Value and scale_info.Value ~= 0 then
         -- Conversion factors for stats that need unit changes (e.g. metres → centimetres)
        local scale_val = util_module.round_to_sig_fig(scale_info.Value, 2)
         local unit_conversion = {
         local tmpl = (scale_info.Type == "power_increase") and "Boon" or "Ss"
             BulletRadius = 100  -- metres to centimetres
         val_str = val_str .. " " .. mw.getCurrentFrame():expandTemplate{
             title = tmpl, args = { scale_val }
         }
         }
         local conv_factor = unit_conversion[attr_key] or 1
    end
 
    val_str = append_label(val_str, prop_key, label)
    val_str = append_conditional(val_str, allow_conditional, usage_flags)
 
    return val_str
end
 
-- Convenience wrapper: look up a property from item data, then format
local function format_stat(item_name, prop, skip_label, debug, allow_conditional)
    skip_label = skip_label or false
    if allow_conditional == nil then allow_conditional = true end
 
    local item = get_json_item(item_name)
    if not item then return nil end
 
    local obj = find_property_obj(item, prop)
    if not obj then return nil end
 
    return format_value(
        prop, obj.Value, obj.Type, obj.LocTokenOverride,
        skip_label, allow_conditional, obj.UsageFlags, obj.Scale
    )
end
 
--------------------------------------------------------------------------------
-- Core formatting: enhanced
--------------------------------------------------------------------------------
 
local function format_value_enhanced(item_name, prop_key, value, css_type,
                                    loc_override, skip_label,
                                    allow_conditional, usage_flags)
    if value == nil or value == 0 or value == "" then return nil end
 
    local numeric_val = tonumber(tostring(value):match("^-?%d+%.?%d*")) or 0
    if numeric_val == 0 then return nil end
 
    local label_key = resolve_label_key(prop_key, loc_override)
 
    -- Enhanced requires lang data to exist
    if not (
        lang.get_string(label_key.."_label") or 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
 
    local label  = skip_label and "" or get_lang_with_fallback(label_key, "_label", "_Label")
    local prefix  = get_lang_with_fallback(label_key, "_prefix", "_Prefix")
    local postfix = get_lang_with_fallback(label_key, "_postfix", "_Postfix")
 
    if prefix == "" and postfix == "" and label == "" then return nil end
 
    prefix = resolve_prefix(prefix, numeric_val)
    local suffix = resolve_suffix(postfix, css_type)
 
    -- Build Enhanced invoke
    local extras = {}
    if prefix ~= "" then table.insert(extras, "prefix=" .. prefix) end
    if suffix ~= "" and suffix ~= "%" then table.insert(extras, "suffix=" .. suffix) end
    table.insert(extras, "noicon=true")
 
    local val_str = string.format(
        "{{#invoke:Enhanced|render|%s|%s|%s}}",
        item_name, prop_key, table.concat(extras, "|")
    )
 
    -- Unstyled postfix (if not already present)
    if postfix ~= "" and postfix ~= "%" and not val_str:find(postfix, 1, true) then
         val_str = val_str .. (postfix == "HP/s" and (" " .. postfix) or postfix)
    end
 
    val_str = append_label(val_str, prop_key, label)
    val_str = append_conditional(val_str, allow_conditional, usage_flags)
 
    return val_str
end
 
-- Convenience wrapper: look up a property from item data, then format (enhanced)
local function format_stat_enhanced(item_name, prop, skip_label, allow_conditional)
    skip_label = skip_label or false
    if allow_conditional == nil then allow_conditional = true end
 
    local item = get_json_item(item_name)
    if not item then return nil end
 
    local obj = find_property_obj(item, prop)
    if not obj then return nil end
 
    return format_value_enhanced(
        item_name, prop, obj.Value, obj.Type,
        obj.LocTokenOverride, skip_label, allow_conditional, obj.UsageFlags
    )
end
 
--------------------------------------------------------------------------------
-- StatusEffect special formatting
--------------------------------------------------------------------------------
 
local function format_property_for_infobox(item_name, prop, skip_label, debug)
    if prop:match("^StatusEffect") then
        return string.format(
            "[[{{#invoke:Lang|get_string|Citadel_%s}}]] [[{{#invoke:Lang|get_string|Citadel_StatusEffect}}]]",
            prop
        )
    end
    return format_stat(item_name, prop, skip_label, debug, true)
end
 
local function format_property_for_infobox_enhanced(item_name, prop)
    if prop:match("^StatusEffect") then
        return string.format(
            "[[{{#invoke:Lang|get_string|Citadel_%s}}]] [[{{#invoke:Lang|get_string|Citadel_StatusEffect}}]]",
            prop
        )
    end
    return format_stat_enhanced(item_name, prop)
end
 
--------------------------------------------------------------------------------
-- Tooltip builder helpers
--------------------------------------------------------------------------------


        local base_value
-- Format the Cooldown / ChargeUp fields that sit directly on an Info section
         if is_alt_fire then
local function format_section_cooldown(section, item_name, enhanced)
             if attr_key == "ClipSize" or attr_key == "ReloadTime" then
    local result = {}
                 base_value = get_nested_stat_value(hero_data, attr_key)
    if section.Cooldown and section.Cooldown ~= 0 then
             elseif hero_data.Weapon and hero_data.Weapon.AltFire
         if enhanced then
                 and hero_data.Weapon.AltFire[attr_key] ~= nil then
             result.cooldown = format_value_enhanced(
                 base_value = hero_data.Weapon.AltFire[attr_key]
                item_name, "AbilityCooldown", section.Cooldown,
            else
                 nil, nil, true, false, nil
                 base_value = get_nested_stat_value(hero_data, attr_key)
            )
             end
        else
             result.cooldown = format_value(
                "AbilityCooldown", section.Cooldown,
                 nil, nil, true, false, nil, nil
            )
        end
    end
    if section.ChargeUp and section.ChargeUp ~= 0 then
        if enhanced then
            result.chargeup = format_value_enhanced(
                 item_name, "AbilityChargeUpTime", section.ChargeUp,
                 nil, nil, true, false, nil
             )
         else
         else
             base_value = get_nested_stat_value(hero_data, attr_key)
             result.chargeup = format_value(
                "AbilityChargeUpTime", section.ChargeUp,
                nil, nil, true, false, nil, nil
            )
         end
         end
    end
    return result
end


        -- Apply unit conversion to the base value
-- Iterate a property list (Main or Alt), format each, and append to lines
         if type(base_value) == "number" then
-- Returns the next stat_index
             base_value = base_value * conv_factor
local function process_stats(lines, fmt, counter, start_index, prop_list,
                            item_name, seen, formatter)
    local stat_index = start_index
    for _, prop_obj in ipairs(prop_list or {}) do
        local key = prop_obj.Key
         if is_property_in_lang_data(key, prop_obj.LocTokenOverride) and not seen[key] then
            local val = formatter(prop_obj)
            if val then
                table.insert(lines, string.format(fmt, counter, stat_index, val))
                stat_index = stat_index + 1
                seen[key] = true
             end
         end
         end
    end
    return stat_index
end
--------------------------------------------------------------------------------
-- Build default (non-enhanced) tooltip
--------------------------------------------------------------------------------
local function build_tooltip(frame)
    local item_name = frame.args[1]
    if not item_name then return "Item name missing", 0 end
    local debug = tonumber(frame.args.debug) or 0
    local item = get_json_item(item_name)
    if not item then return "Item not found", debug end
    local ItemData = require("Module:ItemData")
    local lines = {
        "{{Infobox item",
        "| item_name = " .. item_name
    }
    if item.StreetBrawl == true then
        table.insert(lines, "| street_brawl = true")
    end


        local stat_value = base_value
    local sections = get_info_sections(item)


        -- Scaling lookup
    ------------------------------------------------------------------
        local scaling_data
    -- Innate stats
        if is_alt_fire then
    ------------------------------------------------------------------
            if attr_key == "ClipSize" or attr_key == "ReloadTime" then
    local innate_index = 1
                scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
    for _, section in ipairs(sections) do
            elseif (attr_key == "DPS" or attr_key == "SustainedDPS")
        if section.Type == "Innate" then
                and hero_data.Weapon and hero_data.Weapon.AltFire then
            for _, prop_obj in ipairs(section.Alt or {}) do
                -- Alt-fire DPS/SustainedDPS: use explicit key if present, otherwise compute from components
                 if is_property_in_lang_data(prop_obj.Key, prop_obj.LocTokenOverride) then
                local direct_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
                     local val = format_value(
                 if direct_scaling and next(direct_scaling) ~= nil then
                        prop_obj.Key, prop_obj.Value, prop_obj.Type,
                     scaling_data = direct_scaling
                        prop_obj.LocTokenOverride, false, false,
                else
                        prop_obj.UsageFlags, prop_obj.Scale
                    local alt_stats = hero_data.Weapon.AltFire
                     )
                    local dps_type = attr_key == "DPS" and 'burst' or 'sustained'
                     if val then
                    local level_scale = compute_dps_scaling(hero_data, alt_stats, dps_type, "Level")
                        table.insert(lines, string.format("| item_stat%d = %s", innate_index, val))
                    local spirit_scale = compute_dps_scaling(hero_data, alt_stats, dps_type, "Spirit")
                        innate_index = innate_index + 1
                    level_scale = util_module.round_to_sig_fig(level_scale, 5)
                     end
                     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
             end
        else
            local cd = format_section_cooldown(section, item_name, false)
             scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
             if cd.cooldown then table.insert(lines, "| item_stat_cooldown = " .. cd.cooldown) end
            if cd.chargeup then table.insert(lines, "| item_stat_chargeup = " .. cd.chargeup) end
         end
         end
    end
    ------------------------------------------------------------------
    -- Passive stats (any section that is not Innate or Active)
    ------------------------------------------------------------------
    local passive_counter = 1
    for _, section in ipairs(sections) do
        if section.Type ~= "Innate" and section.Type ~= "Active" then
            local loc = section.DescKey and section.DescKey:gsub("#", "") or nil
            if loc and 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 scaling_strs = ""
            local stat_index = 1
        local spirit_scale = 0
            local seen = {}
        local level_scale  = 0
 
        local spirit_val = 0
            -- Main (important properties) — allow_conditional = true
        local level_val  = 0
            stat_index = process_stats(
        if scaling_data ~= nil then
                lines, "| passive%d_stat%d = %s", passive_counter, stat_index,
            for scaling_val, scaling_type in pairs(scaling_data) do
                section.Main, item_name, seen,
                if scaling_type == "Spirit" then
                function(obj)
                    spirit_val = scaling_val * conv_factor  -- apply unit conversion
                    return format_value(
                elseif scaling_type == "Level" then
                        obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                     level_val = scaling_val * conv_factor  -- apply unit conversion
                        false, true, obj.UsageFlags, obj.Scale
                     )
                 end
                 end
             end
             )


             -- Output Spirit icon before Level icon
             -- Alt (regular properties) — allow_conditional = false
             local function append_scaling(val, stype)
             process_stats(
                 if val ~= 0 then
                 lines, "| passive%d_stat%d = %s", passive_counter, stat_index,
                    local scaling_str = hero_data_module.write_scalar_str(val, stype, true)
                section.Alt, item_name, seen,
                     if scaling_str ~= "" then
                function(obj)
                         scaling_strs = scaling_strs .. " " .. scaling_str
                     return format_value(
                     end
                         obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                        false, false, obj.UsageFlags, obj.Scale
                     )
                 end
                 end
            )
            local cd = format_section_cooldown(section, item_name, false)
            if cd.cooldown then
                table.insert(lines, string.format("| passive%d_cooldown = %s", passive_counter, cd.cooldown))
             end
             end
             append_scaling(spirit_val, "Spirit")
             if cd.chargeup then
            append_scaling(level_val, "Level")
                table.insert(lines, string.format("| passive%d_chargeup = %s", passive_counter, cd.chargeup))
 
            if type(stat_value) == "number" then
                stat_value = stat_value + (spirit_power * spirit_val)
                stat_value = stat_value + (power_increases * level_val)
             end
             end


             spirit_scale = spirit_val
             passive_counter = passive_counter + 1
            level_scale  = level_val
         end
         end
    end


         if attr_key == "TechPower" and spirit_scale == 0 then
    ------------------------------------------------------------------
             spirit_scale = 1.0
    -- Active stats
             if type(stat_value) == "number" then
    ------------------------------------------------------------------
                 stat_value = stat_value + (spirit_power * spirit_scale)
    local active_index = 1
    for _, section in ipairs(sections) do
         if section.Type == "Active" then
             local loc = section.DescKey and section.DescKey:gsub("#", "") or nil
             if loc and loc ~= "" then
                 table.insert(lines, string.format(
                    "| active%d_description = {{#invoke:Lang|get_string|%s|item_name=%s}}",
                    active_index, loc, item_name
                ))
             end
             end
        end


        local innate_spirit_scale =
            local stat_index = 1
             (hero_data["LevelScaling"] and hero_data["LevelScaling"]["TechPower"]) or 0
            local seen = {}
 
             stat_index = process_stats(
                lines, "| active%d_stat%d = %s", active_index, stat_index,
                section.Main, item_name, seen,
                function(obj)
                    return format_value(
                        obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                        false, true, obj.UsageFlags, obj.Scale
                    )
                end
            )
 
            process_stats(
                lines, "| active%d_stat%d = %s", active_index, stat_index,
                section.Alt, item_name, seen,
                function(obj)
                    return format_value(
                        obj.Key, obj.Value, obj.Type, obj.LocTokenOverride,
                        false, false, obj.UsageFlags, obj.Scale
                    )
                end
            )


        if not display_scaling_icons then scaling_strs = "" end
            local cd = format_section_cooldown(section, item_name, false)
            if cd.cooldown then
                table.insert(lines, string.format("| active%d_cooldown = %s", active_index, cd.cooldown))
            end
            if cd.chargeup then
                table.insert(lines, string.format("| active%d_chargeup = %s", active_index, cd.chargeup))
            end


        if type(stat_value) == "boolean" then
             active_index = active_index + 1
            stat_value = tostring(stat_value)
        else
             stat_value = util_module.round_to_sig_fig(stat_value, 5)
         end
         end
    end


         local cell_inner = string.format(
    ------------------------------------------------------------------
            '<span class="stat-num">%s</span><span class="stat-scaling">%s</span>',
    -- Components
            stat_value,
    ------------------------------------------------------------------
            scaling_strs
    for i = 1, 2 do
        )
         local comp = ItemData.get_component_name { args = { item_name, i } }
        if comp and comp ~= "" then
            table.insert(lines, string.format("| component%d_name = %s", i, comp))
        end
    end


         local weapon_table = hero_data.Weapon
    ------------------------------------------------------------------
         if is_alt_fire and weapon_table and weapon_table.AltFire then
    -- Is Component Of (sorted by tier)
             weapon_table = weapon_table.AltFire
    ------------------------------------------------------------------
    local parents = get_component_of_sorted(item_name)
    for i, key in ipairs(parents) do
         local parent_name = data[key] and data[key].Name or ""
         if parent_name ~= "" then
             table.insert(lines, string.format("| iscomponentof%d_name = %s", i, parent_name))
         end
         end
         local hit_once = "false"
    end
         if (attr_key == "DPS" or attr_key == "SustainedDPS")
 
             and weapon_table
    ------------------------------------------------------------------
            and weapon_table.HitOnceAcrossAllBullets
    -- Sounds
        then
    ------------------------------------------------------------------
            hit_once = "true"
    for key, value in pairs(frame.args) do
         local index = tostring(key):match("^sound(%d+)$")
         if index and value ~= "" then
             table.insert(lines, string.format("| sound%s = %s", index, value))
         end
         end
    end


        local data_attrs = string.format(
    table.insert(lines, "}}")
            '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"',
    return table.concat(lines, "\n"), debug
            attr_key,
end
            tostring(type(base_value) == "number" and util_module.round_to_sig_fig(base_value, 3) or base_value),
 
            tonumber(spirit_scale) or 0,
--------------------------------------------------------------------------------
            (attr_key == "TechPower") and "0" or (tonumber(level_scale) or 0),
-- Build enhanced tooltip
            innate_spirit_scale or 0,
--------------------------------------------------------------------------------
            stat_value,
 
            hit_once
local function build_tooltip_enhanced(frame)
        )
    local item_name = frame.args[1]
    if not item_name then return "Item name missing" end
 
    local item = get_json_item(item_name)
    if not item then return "Item not found" end
 
    local ItemData = require("Module:ItemData")
    local lines = {
        "{{Infobox item",
        "| enhanced = true",
        "| item_name = " .. item_name
    }


         return string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner)
    if item.StreetBrawl == true then
         table.insert(lines, "| street_brawl = true")
     end
     end


    -- Generate a hero's primary stat row
     local sections = get_info_sections(item)
     local function generatePrimaryRow(hero_data, hero_key, has_alt)
 
        local row_str = ""
    ------------------------------------------------------------------
        local hero_name_local = localize(hero_key, hero_key)
    -- Innate stats (Enhanced)
        local hero_name_en = hero_data["Name"]
    ------------------------------------------------------------------
        local template_args = {[1] = hero_name_en, l1 = hero_name_local}
    local innate_index = 1
        local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args }
    for _, section in ipairs(sections) do
        local hero_cell_content = hero_icon
        if section.Type == "Innate" then
        if has_alt then
            for _, prop_obj in ipairs(section.Alt or {}) do
            hero_cell_content = '[[#alt-fire-' .. hero_key .. '|+]] ' .. hero_cell_content
                if is_property_in_lang_data(prop_obj.Key, prop_obj.LocTokenOverride) then
        end
                     local val = format_value_enhanced(
        local sort_name = (hero_data["Name"] or hero_key):gsub("^The ", "")
                        item_name, prop_obj.Key, prop_obj.Value, prop_obj.Type,
        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>'
                        prop_obj.LocTokenOverride, nil, false, prop_obj.UsageFlags
        for _, category in ipairs(attribute_orders["category_order"]) do
                    )
            if stats_to_include[category] ~= nil then
                    if val then
                for _, attr_key in ipairs(stats_to_include[category]) do
                        table.insert(lines, string.format("| item_stat%d = %s", innate_index, val))
                     row_str = row_str .. buildStatCell(hero_data, hero_key, attr_key, false, category)
                        innate_index = innate_index + 1
                    end
                 end
                 end
             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)
    -- Passive stats (Enhanced)
        local row_str = ""
     ------------------------------------------------------------------
        local hero_name_local = localize(hero_key, hero_key)
    local passive_counter = 1
         local hero_name_en = hero_data["Name"]
    for _, section in ipairs(sections) do
        local template_args = {[1] = hero_name_en, l1 = hero_name_local}
         if section.Type ~= "Innate" and section.Type ~= "Active" then
        local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args }
            local loc = section.DescKey and section.DescKey:gsub("#", "") or nil
        local hero_cell_content = hero_icon .. ' <small style="color:#aaa;">(Alt‑fire)</small>'
            if loc and loc ~= "" then
        local sort_name = (hero_data["Name"] or hero_key):gsub("^The ", "")
                table.insert(lines, string.format(
        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>'
                    "| passive%d_description = {{#invoke:Lang|get_string|%s|item_name=%s}}",
        for _, category in ipairs(attribute_orders["category_order"]) do
                    passive_counter, loc, item_name
             if stats_to_include[category] ~= nil then
                ))
                 for _, attr_key in ipairs(stats_to_include[category]) do
            end
                     if category == "Weapon" then
 
                         row_str = row_str .. buildStatCell(hero_data, hero_key, attr_key, true, category)
            local stat_index = 1
                    else
            local seen = {}
                         row_str = row_str .. '<td></td>'
 
                     end
            -- Main — use format_property_for_infobox_enhanced (handles StatusEffect)
            stat_index = process_stats(
                lines, "| passive%d_stat%d = %s", passive_counter, stat_index,
                section.Main, item_name, seen,
                function(obj)
                    return format_property_for_infobox_enhanced(item_name, obj.Key)
                end
            )
 
            -- Alt — use format_value_enhanced directly
             process_stats(
                lines, "| passive%d_stat%d = %s", passive_counter, stat_index,
                 section.Alt, item_name, seen,
                function(obj)
                     return format_value_enhanced(
                         item_name, obj.Key, obj.Value, obj.Type,
                         obj.LocTokenOverride, nil, false, obj.UsageFlags
                     )
                 end
                 end
            )
            local cd = format_section_cooldown(section, item_name, true)
            if cd.cooldown then
                table.insert(lines, string.format("| passive%d_cooldown = %s", passive_counter, cd.cooldown))
            end
            if cd.chargeup then
                table.insert(lines, string.format("| passive%d_chargeup = %s", passive_counter, cd.chargeup))
             end
             end
            passive_counter = passive_counter + 1
         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
    -- Active stats (Enhanced)
         local hero_key  = hero_entry.key
    ------------------------------------------------------------------
        local hero_data = hero_entry.data
    local active_index = 1
        local has_alt = hero_data.Weapon and hero_data.Weapon.AltFire and true or false
     for _, section in ipairs(sections) do
        body_str = body_str .. generatePrimaryRow(hero_data, hero_key, has_alt)
         if section.Type == "Active" then
        if has_alt then
            local loc = section.DescKey and section.DescKey:gsub("#", "") or nil
            body_str = body_str .. generateAltFireSubRow(hero_data, hero_key)
            if loc and loc ~= "" then
        end
                table.insert(lines, string.format(
    end
                    "| active%d_description = {{#invoke:Lang|get_string|%s|item_name=%s}}",
                    active_index, loc, item_name
                ))
            end
 
            local stat_index = 1
            local seen = {}


    -- Pre-pass: determine which stats have any scaling, and which have both types.
            stat_index = process_stats(
    -- Used to set column widths in the header.
                lines, "| active%d_stat%d = %s", active_index, stat_index,
    local stats_with_any_scaling  = {}
                 section.Main, item_name, seen,
    local stats_with_both_scaling = {}
                function(obj)
    for _, hero_entry in ipairs(sorted_heroes) do
                     return format_property_for_infobox_enhanced(item_name, obj.Key)
        local hero_data = hero_entry.data
                end
        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
                    -- 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)
            process_stats(
                    if hero_data.Weapon
                lines, "| active%d_stat%d = %s", active_index, stat_index,
                        and hero_data.Weapon.AltFire
                section.Alt, item_name, seen,
                        and hero_data.Weapon.AltFire[attr_key] ~= nil
                function(obj)
                    then
                    return format_value_enhanced(
                        if attr_key ~= "ClipSize" then
                        item_name, obj.Key, obj.Value, obj.Type,
                            local alt_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
                        obj.LocTokenOverride, nil, false, obj.UsageFlags
                            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
            )
            local cd = format_section_cooldown(section, item_name, true)
            if cd.cooldown then
                table.insert(lines, string.format("| active%d_cooldown = %s", active_index, cd.cooldown))
             end
             end
            if cd.chargeup then
                table.insert(lines, string.format("| active%d_chargeup = %s", active_index, cd.chargeup))
            end
            active_index = active_index + 1
        end
    end
    ------------------------------------------------------------------
    -- Components
    ------------------------------------------------------------------
    for i = 1, 2 do
        local comp = ItemData.get_component_name { args = { item_name, i } }
        if comp and comp ~= "" then
            table.insert(lines, string.format("| component%d_name = %s", i, comp))
         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>'
    -- Is Component Of (sorted by tier)
     local category_data = attribute_module.get_category_data()
     ------------------------------------------------------------------
    local postfix_key_map = {
     local parents = get_component_of_sorted(item_name)
        ["ReloadDelay"] = "StatDesc_ReloadTime_postfix",
     for i, key in ipairs(parents) do
        ["BulletsPerShot"] = "",
         local parent_name = data[key] and data[key].Name or ""
        ["BulletsPerBurst"] = "",
         if parent_name ~= "" then
        ["BurstInterShotInterval"] = "StatDesc_ReloadTime_postfix",
             table.insert(lines, string.format("| iscomponentof%d_name = %s", i, parent_name))
        ["ReloadSingle"] = "",
        end
        ["BonusAttackRange"] = "StatDesc_WeaponRangeFalloffMax_postfix",
    end
        ["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 attr_data ~= nil then
    -- Sounds
                    attr_localized = lang_module.get_string(attr_data["label"])
    ------------------------------------------------------------------
                    if attr_localized == nil or attr_localized == "" then
    for key, value in pairs(frame.args) do
                        attr_localized = util_module.add_space_before_cap(attr_key)
        local index = tostring(key):match("^sound(%d+)$")
                    end
        if index and value ~= "" then
                    postfix = lang_module.get_string(attr_data["postfix"])
            table.insert(lines, string.format("| sound%s = %s", index, value))
                    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
     end
    headers_str = "<tr>" .. headers_str .. "</tr>"


     return string.format(
    table.insert(lines, "}}")
        '<div id="hero-comparison-container" data-max-power="%s" data-max-spirit="%s">' ..
     return table.concat(lines, "\n")
        '<div style="overflow: auto; max-height: 70vh; width: 100%%;">' ..
end
        '<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
-- Public interface
    )
--------------------------------------------------------------------------------
 
function p.fill_tooltip(frame)
    local output, debug = build_tooltip(frame)
    if debug == 1 or debug == 2 then return output end
    return mw.getCurrentFrame():preprocess(output)
end
 
function p.fill_tooltip_enhanced(frame)
    return mw.getCurrentFrame():preprocess(build_tooltip_enhanced(frame))
end
 
function p.render(frame)
    local output_lines = { "<tabber>" }
    table.insert(output_lines, "Default=")
    local tooltip, _ = build_tooltip(frame)
    table.insert(output_lines, tooltip)
    table.insert(output_lines, "|-|")
    table.insert(output_lines, "Enhanced=")
    table.insert(output_lines, build_tooltip_enhanced(frame))
    table.insert(output_lines, "</tabber>")
    return mw.getCurrentFrame():preprocess(table.concat(output_lines, "\n"))
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: