Module:Sandbox/LVL: Difference between revisions

LVL (talk | contribs)
No edit summary
LVL (talk | contribs)
No edit summary
Line 40: Line 40:


     local body_str  = ""
     local body_str  = ""
    local hero_name
    local hero_name_en
    local hero_icon
    local template_args = {}


     local stats_to_include = {
     local stats_to_include = {
Line 104: Line 100:
     end)
     end)


     -- Helper to generate a single row (primary or alt-fire)
     -- Shared helper: build a single stat cell (returns the <td> string)
     local function generateRow(hero_data, hero_key, is_alt_fire)
     local function buildStatCell(hero_data, hero_key, attr_key, is_alt_fire, category)
         local row_str = ""
         local base_value
        local hero_name_local = localize(hero_key, hero_key)
        if is_alt_fire then
        local hero_name_en = hero_data["Name"]
            -- ClipSize always uses primary
        local template_args = {[1] = hero_name_en, l1 = hero_name_local}
            if attr_key == "ClipSize" then
         local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args }
                base_value = get_nested_stat_value(hero_data, attr_key)
         local hero_cell_content = hero_icon
            elseif hero_data.Weapon and hero_data.Weapon.AltFire
                and hero_data.Weapon.AltFire[attr_key] ~= nil then
                base_value = hero_data.Weapon.AltFire[attr_key]
            else
                base_value = get_nested_stat_value(hero_data, attr_key)
            end
         else
            base_value = get_nested_stat_value(hero_data, attr_key)
        end
 
        local stat_value = base_value
 
        -- Scaling lookup
         local scaling_data
         if is_alt_fire then
         if is_alt_fire then
             hero_cell_content = hero_icon .. ' <small>(Alt-fire)</small>'
             if attr_key == "ClipSize" then
                scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
            elseif hero_key == "hero_shiv" and attr_key == "BulletDamage" then
                scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
            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
         end
        row_str = row_str ..
            '<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate; overflow: hidden;">' ..
            hero_cell_content .. "</td>"


         for _, category in ipairs(attribute_orders["category_order"]) do
         local scaling_strs = ""
            if stats_to_include[category] ~= nil then
        local spirit_scale = 0
                for _, attr_key in ipairs(stats_to_include[category]) do
        local level_scale  = 0
                    -- Base value: special cases for alt-fire row
                    local base_value
                    if is_alt_fire then
                        -- ClipSize always uses primary
                        if attr_key == "ClipSize" then
                            base_value = get_nested_stat_value(hero_data, attr_key)
                        elseif hero_data.Weapon and hero_data.Weapon.AltFire
                            and hero_data.Weapon.AltFire[attr_key] ~= nil then
                            base_value = hero_data.Weapon.AltFire[attr_key]
                        else
                            base_value = get_nested_stat_value(hero_data, attr_key)
                        end
                    else
                        base_value = get_nested_stat_value(hero_data, attr_key)
                    end


                     local stat_value = base_value
        if scaling_data ~= nil then
            for scaling_val, scaling_type in pairs(scaling_data) do
                local scaling_str = hero_data_module.write_scalar_str(scaling_val, scaling_type, true)
                if scaling_str ~= "" then scaling_str = " " .. scaling_str end
                if scaling_str ~= nil then
                     scaling_strs = scaling_strs .. scaling_str
                end


                    -- Scaling: special cases for alt-fire row
                if scaling_type == "Spirit" then
                    local scaling_data
                    spirit_scale = scaling_val
                    if is_alt_fire then
                    stat_value  = stat_value + (spirit_power * scaling_val)
                        -- ClipSize always uses primary scaling
                elseif scaling_type == "Level" then
                        if attr_key == "ClipSize" then
                    level_scale = scaling_val
                            scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
                    stat_value  = stat_value + (power_increases * scaling_val)
                        -- Shiv's alt‑fire BulletDamage uses primary scaling
                end
                        elseif hero_key == "hero_shiv" and attr_key == "BulletDamage" then
            end
                            scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
        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 = ""
        if attr_key == "TechPower" and spirit_scale == 0 then
                    local spirit_scale = 0
            spirit_scale = 1.0
                    local level_scale  = 0
            stat_value  = stat_value + (spirit_power * spirit_scale)
        end


                    if scaling_data ~= nil then
        local innate_spirit_scale =
                        for scaling_val, scaling_type in pairs(scaling_data) do
            (hero_data["LevelScaling"] and hero_data["LevelScaling"]["TechPower"]) or 0
                            local scaling_str = hero_data_module.write_scalar_str(scaling_val, scaling_type, true)
                            if scaling_str ~= "" then scaling_str = " " .. scaling_str end
                            if scaling_str ~= nil then
                                scaling_strs = scaling_strs .. scaling_str
                            end


                            if scaling_type == "Spirit" then
        if not display_scaling_icons then scaling_strs = "" end
                                spirit_scale = scaling_val
                                stat_value  = stat_value + (spirit_power * scaling_val)
                            elseif scaling_type == "Level" then
                                level_scale = scaling_val
                                stat_value  = stat_value + (power_increases * scaling_val)
                            end
                        end
                    end


                    if attr_key == "TechPower" and spirit_scale == 0 then
        if type(stat_value) == "boolean" then
                        spirit_scale = 1.0
            stat_value = tostring(stat_value)
                        stat_value   = stat_value + (spirit_power * spirit_scale)
        else
                    end
            stat_value = util_module.round_to_sig_fig(stat_value, 3)
        end


                    local innate_spirit_scale =
        local cell_inner = string.format(
                        (hero_data["LevelScaling"] and hero_data["LevelScaling"]["TechPower"]) or 0
            '<span class="stat-num">%s</span><span class="stat-scaling">%s</span>',
            stat_value,
            scaling_strs
        )


                    if not display_scaling_icons then scaling_strs = "" end
        local weapon_table = hero_data.Weapon
        if is_alt_fire and weapon_table and weapon_table.AltFire then
            weapon_table = weapon_table.AltFire
        end
        local hit_once = "false"
        if (attr_key == "DPS" or attr_key == "SustainedDPS")
            and weapon_table
            and weapon_table.HitOnceAcrossAllBullets
        then
            hit_once = "true"
        end


                    if type(stat_value) == "boolean" then
        local data_attrs = string.format(
                        stat_value = tostring(stat_value)
            '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"',
                    else
            attr_key,
                        stat_value = util_module.round_to_sig_fig(stat_value, 3)
            tostring(type(base_value) == "number" and util_module.round_to_sig_fig(base_value, 3) or base_value),
                    end
            tonumber(spirit_scale) or 0,
            (attr_key == "TechPower") and "0" or (tonumber(level_scale) or 0),
            innate_spirit_scale or 0,
            stat_value,
            hit_once
        )


                    local cell_inner = string.format(
        return string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner)
                        '<span class="stat-num">%s</span><span class="stat-scaling">%s</span>',
    end
                        stat_value,
                        scaling_strs
                    )


                    local weapon_table = hero_data.Weapon
    -- Build primary row (with optional rowspan on hero cell)
                    if is_alt_fire and weapon_table and weapon_table.AltFire then
    local function generatePrimaryRow(hero_data, hero_key, has_alt)
                        weapon_table = weapon_table.AltFire
        local row_str = ""
                    end
        local hero_name_local = localize(hero_key, hero_key)
                    local hit_once = "false"
        local hero_name_en = hero_data["Name"]
                    if (attr_key == "DPS" or attr_key == "SustainedDPS")
        local template_args = {[1] = hero_name_en, l1 = hero_name_local}
                        and weapon_table
        local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args }
                        and weapon_table.HitOnceAcrossAllBullets
                    then
                        hit_once = "true"
                    end


                    local data_attrs = string.format(
        local hero_cell_content = hero_icon
                        '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"',
        if has_alt then
                        attr_key,
            hero_cell_content = '<button class="toggle-alt-fire" data-hero-key="' .. hero_key .. '">+</button> ' .. hero_cell_content
                        tostring(type(base_value) == "number" and util_module.round_to_sig_fig(base_value, 3) or base_value),
        end
                        tonumber(spirit_scale) or 0,
        local rowspan_attr = has_alt and ' rowspan="2"' or ''
                        (attr_key == "TechPower") and "0" or (tonumber(level_scale) or 0),
        row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate;"' .. rowspan_attr .. '>' .. hero_cell_content .. '</td>'
                        innate_spirit_scale or 0,
                        stat_value,
                        hit_once
                    )


                     row_str = row_str ..
        for _, category in ipairs(attribute_orders["category_order"]) do
                        string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner)
            if stats_to_include[category] ~= nil then
                for _, attr_key in ipairs(stats_to_include[category]) do
                     row_str = row_str .. buildStatCell(hero_data, hero_key, attr_key, false, category)
                 end
                 end
             end
             end
Line 235: Line 232:
     end
     end


     -- Build table body rows (primary + optional alt-fire)
     -- Build alt-fire sub-row (only weapon cells, other cells empty)
    local function generateAltFireSubRow(hero_data, hero_key)
        local row_str = ""
        -- No hero cell (covered by rowspan)
 
        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
                        -- Empty cell to preserve column layout
                        row_str = row_str .. '<td></td>'
                    end
                end
            end
        end
 
        return '<tr class="alt-fire-sub" data-parent="' .. hero_key .. '">' .. row_str .. '</tr>'
    end
 
    -- Generate all rows
     for _, hero_entry in ipairs(sorted_heroes) do
     for _, hero_entry in ipairs(sorted_heroes) do
         local hero_key  = hero_entry.key
         local hero_key  = hero_entry.key
         local hero_data = hero_entry.data
         local hero_data = hero_entry.data
        local has_alt = hero_data.Weapon and hero_data.Weapon.AltFire and true or false


         body_str = body_str .. generateRow(hero_data, hero_key, false)
         body_str = body_str .. generatePrimaryRow(hero_data, hero_key, has_alt)
         if hero_data.Weapon and hero_data.Weapon.AltFire then
         if has_alt then
             body_str = body_str .. generateRow(hero_data, hero_key, true)
             body_str = body_str .. generateAltFireSubRow(hero_data, hero_key)
         end
         end
     end
     end


     -- Pre-pass: determine which stats have any scaling, and which have both types
     -- Pre-pass: scaling info (unchanged, but now includes alt-fire where appropriate)
    -- (now includes alt-fire scaling for weapon columns)
     local stats_with_any_scaling  = {}
     local stats_with_any_scaling  = {}
     local stats_with_both_scaling = {}
     local stats_with_both_scaling = {}
Line 270: Line 288:
                     end
                     end


                     -- Alt-fire scaling (only for stats that truly appear in AltFire)
                     -- Alt-fire scaling (only if the stat exists in AltFire and we actually use alt scaling)
                     if hero_data.Weapon
                     if hero_data.Weapon
                         and hero_data.Weapon.AltFire
                         and hero_data.Weapon.AltFire
                         and hero_data.Weapon.AltFire[attr_key] ~= nil
                         and hero_data.Weapon.AltFire[attr_key] ~= nil
                     then
                     then
                         -- Skip ClipSize (we always use primary scaling, no alt column needed)
                         -- ClipSize always uses primary, so skip alt check
                         if attr_key == "ClipSize" then
                         if attr_key ~= "ClipSize" then
                            -- do nothing
                        elseif hero_entry.key == "hero_shiv" and attr_key == "BulletDamage" then
                            -- Shiv's alt‑fire BulletDamage uses primary scaling; already counted
                        else
                             local alt_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
                             local alt_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
                             if alt_scaling ~= nil and next(alt_scaling) ~= nil then
                             if alt_scaling ~= nil and next(alt_scaling) ~= nil then
                                 stats_with_any_scaling[attr_key] = true
                                 -- Shiv BulletDamage uses primary scaling, already counted
                                local has_spirit_alt = false
                                if not (hero_entry.key == "hero_shiv" and attr_key == "BulletDamage") then
                                local has_level_alt  = false
                                    stats_with_any_scaling[attr_key] = true
                                for _, scaling_type in pairs(alt_scaling) do
                                    local has_spirit_alt = false
                                    if scaling_type == "Spirit" then has_spirit_alt = true end
                                    local has_level_alt  = false
                                    if scaling_type == "Level"  then has_level_alt  = true end
                                    for _, scaling_type in pairs(alt_scaling) do
                                end
                                        if scaling_type == "Spirit" then has_spirit_alt = true end
                                if has_spirit_alt and has_level_alt then
                                        if scaling_type == "Level"  then has_level_alt  = true end
                                    stats_with_both_scaling[attr_key] = true
                                    end
                                    if has_spirit_alt and has_level_alt then
                                        stats_with_both_scaling[attr_key] = true
                                    end
                                 end
                                 end
                             end
                             end
Line 301: Line 318:
     end
     end


     -- Build header row
     -- Build header row (unchanged)
     local headers_str = '<th style="position: sticky; left: 0; top: -1px; z-index: 12; background-color: #27292d; isolation: isolate;">Hero</th>'
     local headers_str = '<th style="position: sticky; left: 0; top: -1px; z-index: 12; background-color: #27292d; isolation: isolate;">Hero</th>'
     local category_data = attribute_module.get_category_data()
     local category_data = attribute_module.get_category_data()
Line 367: Line 384:
     return string.format(
     return string.format(
         '<div id="hero-comparison-container" data-max-power="%s" data-max-spirit="%s">' ..
         '<div id="hero-comparison-container" data-max-power="%s" data-max-spirit="%s">' ..
        '<style>.alt-fire-sub { display: none; } .alt-fire-sub.visible { display: table-row; } button.toggle-alt-fire { background: none; border: none; color: inherit; cursor: pointer; font-weight: bold; width: 1.8em; text-align: center; padding: 0; }</style>' ..
         '<div style="overflow: auto; max-height: 70vh; width: 100%%;">' ..
         '<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>' ..
         '<table class="wikitable sortable" style="table-layout: auto; width: 100%%;" id="hero-comparison-table">%s%s</table>' ..