Module:Sandbox/LVL: Difference between revisions

LVL (talk | contribs)
No edit summary
LVL (talk | contribs)
No edit summary
 
(5 intermediate revisions by the same user not shown)
Line 107: Line 107:
         end
         end
     end
     end
    local scaled_dps = calculate_dps(scaled_stats, dps_type)
    return scaled_dps - base_dps
end
-- Compute DPS scaling delta from explicit component scalings.
-- Used for Shiv, whose alt-fire BulletDamage uses primary scaling.
local function compute_dps_scaling_from_components(base_stats, dps_type, component_scalings)
    local scaled_stats = {}
    for k, v in pairs(base_stats) do scaled_stats[k] = v end
    for comp_key, scale_val in pairs(component_scalings) do
        if scaled_stats[comp_key] ~= nil then
            scaled_stats[comp_key] = scaled_stats[comp_key] + scale_val
        end
    end
    local base_dps = calculate_dps(base_stats, dps_type)
     local scaled_dps = calculate_dps(scaled_stats, dps_type)
     local scaled_dps = calculate_dps(scaled_stats, dps_type)
     return scaled_dps - base_dps
     return scaled_dps - base_dps
Line 143: Line 128:
         Weapon = {
         Weapon = {
             "DPS", "SustainedDPS", "BulletDamage", "RoundsPerSecond", "FireRate",
             "DPS", "SustainedDPS", "BulletDamage", "RoundsPerSecond", "FireRate",
             "ClipSize", "ReloadTime", "ReloadDelay", "BulletsPerShot", "BulletsPerBurst",
             "ClipSize", "ReloadTime", "ReloadDelay", "ReloadSingle", "BulletsPerShot", "BulletsPerBurst",
             "BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage",
             "BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage",
             "ReloadSingle", "BulletSpeed", "FalloffStartRange", "FalloffEndRange",
             "BulletSpeed", "FalloffStartRange", "FalloffEndRange",
             "CritDamageBonusPercent", "RoundsPerSecondAtMaxSpin", "SpinAcceleration", "SpinDeceleration"
             "CritDamageBonusPercent", "BulletRadius", "RoundsPerSecondAtMaxSpin", "SpinAcceleration", "SpinDeceleration"
         },
         },
         Vitality = {
         Vitality = {
Line 172: Line 157:
     -- Build a single stat cell (<td>) with data attributes for JS recalculation
     -- 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)
     local function buildStatCell(hero_data, hero_key, attr_key, is_alt_fire, category)
        -- Conversion factors for stats that need unit changes (e.g. metres → centimetres)
        local unit_conversion = {
            BulletRadius = 100  -- metres to centimetres
        }
        local conv_factor = unit_conversion[attr_key] or 1
         local base_value
         local base_value
         if is_alt_fire then
         if is_alt_fire then
Line 184: Line 175:
         else
         else
             base_value = get_nested_stat_value(hero_data, attr_key)
             base_value = get_nested_stat_value(hero_data, attr_key)
        end
        -- Apply unit conversion to the base value
        if type(base_value) == "number" then
            base_value = base_value * conv_factor
         end
         end


Line 193: Line 189:
             if attr_key == "ClipSize" or attr_key == "ReloadTime" then
             if attr_key == "ClipSize" or attr_key == "ReloadTime" then
                 scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
                 scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
            elseif hero_key == "hero_shiv" and attr_key == "BulletDamage" then
                -- Shiv's alt-fire BulletDamage uses the primary fire's scaling
                scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
            elseif hero_key == "hero_shiv" and (attr_key == "DPS" or attr_key == "SustainedDPS")
                and hero_data.Weapon and hero_data.Weapon.AltFire then
                -- Shiv's alt-fire DPS scales from primary BulletDamage scaling
                local alt_stats = hero_data.Weapon.AltFire
                local dps_type = attr_key == "DPS" and 'burst' or 'sustained'
                local bd_scaling = hero_data_module.get_hero_scaling_data(hero_data, "BulletDamage")
                local level_scale = 0
                local spirit_scale = 0
                if bd_scaling and next(bd_scaling) ~= nil then
                    for val, stype in pairs(bd_scaling) do
                        local component = { ["BulletDamage"] = val }
                        local delta = compute_dps_scaling_from_components(alt_stats, dps_type, component)
                        if stype == "Level" then
                            level_scale = delta
                        elseif stype == "Spirit" then
                            spirit_scale = delta
                        end
                    end
                end
                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
             elseif (attr_key == "DPS" or attr_key == "SustainedDPS")
             elseif (attr_key == "DPS" or attr_key == "SustainedDPS")
                 and hero_data.Weapon and hero_data.Weapon.AltFire then
                 and hero_data.Weapon and hero_data.Weapon.AltFire then
Line 257: Line 225:
             for scaling_val, scaling_type in pairs(scaling_data) do
             for scaling_val, scaling_type in pairs(scaling_data) do
                 if scaling_type == "Spirit" then
                 if scaling_type == "Spirit" then
                     spirit_val = scaling_val
                     spirit_val = scaling_val * conv_factor  -- apply unit conversion
                 elseif scaling_type == "Level" then
                 elseif scaling_type == "Level" then
                     level_val = scaling_val
                     level_val = scaling_val * conv_factor  -- apply unit conversion
                 end
                 end
             end
             end
Line 299: Line 267:
             stat_value = tostring(stat_value)
             stat_value = tostring(stat_value)
         else
         else
             stat_value = util_module.round_to_sig_fig(stat_value, 3)
             stat_value = util_module.round_to_sig_fig(stat_value, 5)
         end
         end


Line 346: Line 314:
         end
         end
         local sort_name = (hero_data["Name"] or hero_key):gsub("^The ", "")
         local sort_name = (hero_data["Name"] or hero_key):gsub("^The ", "")
         row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate; overflow: hidden; min-width: 150px;" data-sort-value="' .. sort_name .. '">' .. hero_cell_content .. '</td>'
         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
         for _, category in ipairs(attribute_orders["category_order"]) do
             if stats_to_include[category] ~= nil then
             if stats_to_include[category] ~= nil then
Line 421: Line 389:
                             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 and next(alt_scaling) ~= nil then
                             if alt_scaling and next(alt_scaling) ~= nil then
                                 if not (hero_entry.key == "hero_shiv" and attr_key == "BulletDamage") then
                                 stats_with_any_scaling[attr_key] = true
                                    stats_with_any_scaling[attr_key] = true
                                local has_spirit_alt, has_level_alt = false, false
                                    local has_spirit_alt, has_level_alt = false, false
                                for _, stype in pairs(alt_scaling) do
                                    for _, stype in pairs(alt_scaling) do
                                    if stype == "Spirit" then has_spirit_alt = true
                                        if stype == "Spirit" then has_spirit_alt = true
                                    elseif stype == "Level" then has_level_alt = true end
                                        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
                                 end
                                 end
                                if has_spirit_alt and has_level_alt then stats_with_both_scaling[attr_key] = true end
                             else
                             else
                                -- Shiv's alt-fire DPS/SustainedDPS uses primary BulletDamage scaling
                                 if attr_key == "DPS" or attr_key == "SustainedDPS" then
                                 if hero_entry.key == "hero_shiv" and (attr_key == "DPS" or attr_key == "SustainedDPS") then
                                    local bd_scaling = hero_data_module.get_hero_scaling_data(hero_data, "BulletDamage")
                                    if bd_scaling and next(bd_scaling) ~= nil then
                                        local alt_stats = hero_data.Weapon.AltFire
                                        local dps_type = attr_key == "DPS" and 'burst' or 'sustained'
                                        for val, stype in pairs(bd_scaling) do
                                            local component = { BulletDamage = val }
                                            local delta = compute_dps_scaling_from_components(alt_stats, dps_type, component)
                                            if delta ~= 0 then
                                                stats_with_any_scaling[attr_key] = true
                                            end
                                        end
                                    end
                                -- Alt-fire DPS/SustainedDPS for other heroes: compute from component scalings
                                elseif attr_key == "DPS" or attr_key == "SustainedDPS" then
                                     local alt_stats = hero_data.Weapon.AltFire
                                     local alt_stats = hero_data.Weapon.AltFire
                                     local dps_type = attr_key == "DPS" and 'burst' or 'sustained'
                                     local dps_type = attr_key == "DPS" and 'burst' or 'sustained'
Line 469: Line 420:


     -- Build table header row
     -- Build table header row
     local headers_str = '<th style="position: sticky; left: 0; top: -1px; z-index: 12; background-color: #444; isolation: isolate; min-width: 150px;">Hero</th>'
     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>'
     local category_data = attribute_module.get_category_data()
     local category_data = attribute_module.get_category_data()
     local postfix_key_map = {
     local postfix_key_map = {
Line 480: Line 431:
         ["SustainedDPS"] = "DPS_postfix",
         ["SustainedDPS"] = "DPS_postfix",
         ["RoundsPerSecondAtMaxSpin"] = "",
         ["RoundsPerSecondAtMaxSpin"] = "",
         ["CritDamageBonusScale"] = "StatDesc_CritDamageBonusScale_postfix",
         ["CritDamageBonusPercent"]   = "StatDesc_CritDamageBonusScale_postfix",
        ["BonusCritDamagePercent"] = "BonusCritDamagePercent_postfix",
["CritDamageReceivedPercent"] = "StatDesc_CritDamageReceivedScale_postfix",
         ["BulletLifesteal"] = "BulletLifestealPercentHero_postfix",
         ["BulletLifesteal"] = "BulletLifestealPercentHero_postfix",
         ["GroundDashSpeed"] = "DashSpeed_postfix"
         ["GroundDashSpeed"] = "DashSpeed_postfix"
Line 491: Line 442:
             for _, attr_key in ipairs(stats_to_include[category]) do
             for _, attr_key in ipairs(stats_to_include[category]) do
                 local attr_data = category_attrs[attr_key]
                 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
                 local attr_localized, postfix
                 if attr_data ~= nil then
                 if attr_data ~= nil then
Line 500: Line 462:
                     if postfix == nil or postfix == "" then postfix = ""
                     if postfix == nil or postfix == "" then postfix = ""
                     else postfix = " (" .. postfix .. ")" end
                     else postfix = " (" .. postfix .. ")" end
                    if attr_key == "BulletRadius" then postfix = " (cm)" end
                 else
                 else
                     attr_localized = dictionary_module.translate(attr_key)
                     attr_localized = dictionary_module.translate(attr_key)