Editing Module:Sandbox/LVLGive feedback
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 31: | Line 31: | ||
-- ========================================================================= | -- ========================================================================= | ||
-- DPS calculation helpers | -- DPS calculation helpers (mirroring bot's calculate_dps) | ||
-- ========================================================================= | -- ========================================================================= | ||
local function calculate_dps(stats, dps_type) | local function calculate_dps(stats, dps_type) | ||
-- stats table with all needed fields (may contain nil) | |||
local rps = stats.RoundsPerSecond or 0 | local rps = stats.RoundsPerSecond or 0 | ||
if rps == 0 then return 0 end | if rps == 0 then return 0 end | ||
| Line 50: | Line 50: | ||
end | end | ||
-- sustained DPS | |||
local clip_size = stats.ClipSize or 0 | local clip_size = stats.ClipSize or 0 | ||
if clip_size <= 0 then | if clip_size <= 0 then | ||
-- weapons with no clip use burst DPS | |||
return base_damage * bullets_per_shot * burst_count / total_cycle_time | return base_damage * bullets_per_shot * burst_count / total_cycle_time | ||
end | end | ||
| Line 70: | Line 72: | ||
end | end | ||
-- Compute | -- Compute DPS scaling delta for a given dps_type and scaling source | ||
-- | -- base_stats: table of alt-fire weapon stats (from Weapon.AltFire) | ||
-- hero_data: full hero data (to look up LevelScaling/SpiritScaling) | |||
-- scaling_type: "Level" or "Spirit" | |||
-- returns a single numeric scaling value (e.g., how much the DPS increases per boon or per spirit point) | |||
local function compute_dps_scaling(hero_data, base_stats, dps_type, scaling_type) | local function compute_dps_scaling(hero_data, base_stats, dps_type, scaling_type) | ||
local scaling_source = hero_data[scaling_type .. "Scaling"] or {} | local scaling_source = hero_data[scaling_type .. "Scaling"] or {} | ||
-- Collect all alt-fire component scalings that exist | |||
local component_scalings = {} | local component_scalings = {} | ||
local possible_keys = { | local possible_keys = { | ||
| Line 87: | Line 93: | ||
end | end | ||
-- | -- If no component scalings, scaling is zero | ||
if next(component_scalings) == nil then | if next(component_scalings) == nil then | ||
return 0 | return 0 | ||
end | end | ||
-- Compute base DPS from the original base stats | |||
local base_dps = calculate_dps(base_stats, dps_type) | local base_dps = calculate_dps(base_stats, dps_type) | ||
-- Apply all scalings to a copy of base stats | |||
local scaled_stats = {} | local scaled_stats = {} | ||
for k, v in pairs(base_stats) do scaled_stats[k] = v end | for k, v in pairs(base_stats) do | ||
scaled_stats[k] = v | |||
end | |||
for comp_key, scale_val in pairs(component_scalings) do | for comp_key, scale_val in pairs(component_scalings) do | ||
if scaled_stats[comp_key] ~= nil then | if scaled_stats[comp_key] ~= nil then | ||
| Line 107: | Line 111: | ||
end | end | ||
end | end | ||
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 114: | Line 119: | ||
-- Main module function | -- Main module function | ||
-- ========================================================================= | -- ========================================================================= | ||
p.write_hero_comparison_table = function(frame) | p.write_hero_comparison_table = function(frame) | ||
local power_increases = tonumber(frame.args[1]) or 0 | local power_increases = tonumber(frame.args[1]) or 0 | ||
| Line 123: | Line 127: | ||
local display_scaling_icons = (power_increases == 0 and spirit_power == 0) | local display_scaling_icons = (power_increases == 0 and spirit_power == 0) | ||
local body_str = "" | local body_str = "" | ||
local stats_to_include = { | local stats_to_include = { | ||
Weapon = { | Weapon = { | ||
"DPS", "SustainedDPS", "BulletDamage", "RoundsPerSecond", "FireRate", | "DPS", "SustainedDPS", "BulletDamage", "RoundsPerSecond", "FireRate", | ||
"ClipSize", "ReloadTime", "ReloadDelay | "ClipSize", "ReloadTime", "ReloadDelay", "BulletsPerShot", "BulletsPerBurst", | ||
"BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage", | "BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage", | ||
"BulletSpeed", "FalloffStartRange", "FalloffEndRange", | "ReloadSingle", "BulletSpeed", "FalloffStartRange", "FalloffEndRange", | ||
"CritDamageBonusPercent | "CritDamageBonusPercent", "RoundsPerSecondAtMaxSpin", "SpinAcceleration", "SpinDeceleration" | ||
}, | }, | ||
Vitality = { | Vitality = { | ||
| Line 141: | Line 145: | ||
} | } | ||
-- Collect and sort heroes | -- Collect and sort heroes | ||
local sorted_heroes = {} | local sorted_heroes = {} | ||
for hero_key, hero_data in pairs(heroes_data) do | for hero_key, hero_data in pairs(heroes_data) do | ||
| Line 155: | Line 159: | ||
end) | end) | ||
-- | -- Shared stat cell builder | ||
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) | ||
local base_value | local base_value | ||
if is_alt_fire then | if is_alt_fire then | ||
if attr_key == "ClipSize | if attr_key == "ClipSize" then | ||
base_value = get_nested_stat_value(hero_data, attr_key) | base_value = get_nested_stat_value(hero_data, attr_key) | ||
elseif hero_data.Weapon and hero_data.Weapon.AltFire | elseif hero_data.Weapon and hero_data.Weapon.AltFire | ||
| Line 175: | Line 173: | ||
else | else | ||
base_value = get_nested_stat_value(hero_data, attr_key) | base_value = get_nested_stat_value(hero_data, attr_key) | ||
end | end | ||
| Line 187: | Line 180: | ||
local scaling_data | local scaling_data | ||
if is_alt_fire then | if is_alt_fire then | ||
if attr_key == "ClipSize" | 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) | scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key) | ||
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 | ||
-- | -- try direct scaling key (e.g., SustainedDPSAltFire) | ||
local direct_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire") | local direct_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire") | ||
if direct_scaling and next(direct_scaling) ~= nil then | if direct_scaling and next(direct_scaling) ~= nil then | ||
scaling_data = direct_scaling | scaling_data = direct_scaling | ||
else | else | ||
-- compute from component scalings | |||
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' | ||
local level_scale = compute_dps_scaling(hero_data, alt_stats, dps_type, "Level") | 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") | local spirit_scale = compute_dps_scaling(hero_data, alt_stats, dps_type, "Spirit") | ||
-- round to 5 significant figures to match bot output | |||
level_scale = util_module.round_to_sig_fig(level_scale, 5) | level_scale = util_module.round_to_sig_fig(level_scale, 5) | ||
spirit_scale = util_module.round_to_sig_fig(spirit_scale, 5) | spirit_scale = util_module.round_to_sig_fig(spirit_scale, 5) | ||
| Line 220: | Line 217: | ||
local spirit_scale = 0 | local spirit_scale = 0 | ||
local level_scale = 0 | local level_scale = 0 | ||
if scaling_data ~= nil then | if scaling_data ~= nil then | ||
for scaling_val, scaling_type in pairs(scaling_data) do | for scaling_val, scaling_type in pairs(scaling_data) do | ||
local scaling_str | |||
-- For alt‑fire DPS/SustainedDPS with computed scaling, use a simple text indicator | |||
if is_alt_fire and (attr_key == "DPS" or attr_key == "SustainedDPS") then | |||
local icon = (scaling_type == "Spirit") and "★" or "▲" | |||
local rounded = math.floor(scaling_val * 1000 + 0.5) / 1000 -- 3 decimals | |||
scaling_str = " " .. icon .. "+" .. rounded | |||
else | |||
-- Use the normal template/icon function | |||
scaling_str = hero_data_module.write_scalar_str(scaling_val, scaling_type, true) | |||
if scaling_str ~= "" then scaling_str = " " .. scaling_str end | |||
end | |||
if scaling_str ~= nil then | |||
scaling_strs = scaling_strs .. scaling_str | |||
end | |||
if scaling_type == "Spirit" then | |||
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 | end | ||
if attr_key == "TechPower" and spirit_scale == 0 then | if attr_key == "TechPower" and spirit_scale == 0 then | ||
spirit_scale = 1.0 | spirit_scale = 1.0 | ||
stat_value = stat_value + (spirit_power * spirit_scale) | |||
end | end | ||
local innate_spirit_scale = | local innate_spirit_scale = (hero_data["LevelScaling"] and hero_data["LevelScaling"]["TechPower"]) or 0 | ||
if not display_scaling_icons then scaling_strs = "" end | if not display_scaling_icons then scaling_strs = "" end | ||
if type(stat_value) == "boolean" then | if type(stat_value) == "boolean" then stat_value = tostring(stat_value) | ||
else stat_value = util_module.round_to_sig_fig(stat_value, 3) end | |||
else | |||
local cell_inner = string.format( | local cell_inner = string.format( | ||
'<span class="stat-num">%s</span><span class="stat-scaling">%s</span>', | '<span class="stat-num">%s</span><span class="stat-scaling">%s</span>', | ||
stat_value, | stat_value, scaling_strs | ||
) | ) | ||
| Line 281: | Line 266: | ||
end | end | ||
local hit_once = "false" | local hit_once = "false" | ||
if (attr_key == "DPS" or attr_key == "SustainedDPS") | if (attr_key == "DPS" or attr_key == "SustainedDPS") and weapon_table and weapon_table.HitOnceAcrossAllBullets then | ||
hit_once = "true" | hit_once = "true" | ||
end | end | ||
| Line 298: | Line 280: | ||
hit_once | hit_once | ||
) | ) | ||
return string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner) | return string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner) | ||
end | end | ||
-- | -- Primary row generator (unchanged) | ||
local function generatePrimaryRow(hero_data, hero_key, has_alt) | local function generatePrimaryRow(hero_data, hero_key, has_alt) | ||
local row_str = "" | local row_str = "" | ||
| Line 313: | Line 294: | ||
hero_cell_content = '[[#alt-fire-' .. hero_key .. '|+]] ' .. hero_cell_content | hero_cell_content = '[[#alt-fire-' .. hero_key .. '|+]] ' .. hero_cell_content | ||
end | end | ||
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate;">' .. hero_cell_content .. '</td>' | |||
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 10; background-color: | |||
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 325: | Line 305: | ||
end | end | ||
-- | -- Alt-fire sub-row generator (unchanged except hero cell with icon) | ||
local function generateAltFireSubRow(hero_data, hero_key) | local function generateAltFireSubRow(hero_data, hero_key) | ||
local row_str = "" | local row_str = "" | ||
| Line 333: | Line 313: | ||
local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args } | 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>' | local hero_cell_content = hero_icon .. ' <small style="color:#aaa;">(Alt‑fire)</small>' | ||
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 9; background-color: #202122; isolation: isolate;">' .. hero_cell_content .. '</td>' | |||
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 9; background-color: #202122; isolation: isolate; | |||
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 349: | Line 328: | ||
end | end | ||
-- Build | -- Build body | ||
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 | ||
| Line 360: | Line 339: | ||
end | end | ||
-- Pre-pass | -- Pre-pass (unchanged) | ||
local stats_with_any_scaling = {} | local stats_with_any_scaling = {} | ||
local stats_with_both_scaling = {} | local stats_with_both_scaling = {} | ||
| Line 369: | Line 347: | ||
if stats_to_include[category] ~= nil then | if stats_to_include[category] ~= nil then | ||
for _, attr_key in ipairs(stats_to_include[category]) do | for _, attr_key in ipairs(stats_to_include[category]) do | ||
local scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key) | local scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key) | ||
if scaling_data ~= nil and next(scaling_data) ~= nil then | if scaling_data ~= nil and next(scaling_data) ~= nil then | ||
| Line 380: | Line 357: | ||
if has_spirit and has_level then stats_with_both_scaling[attr_key] = true end | if has_spirit and has_level then stats_with_both_scaling[attr_key] = true end | ||
end | end | ||
-- alt-fire checks (unchanged, but now DPS/SustainedDPS might be computed and should be caught) | |||
-- | |||
if hero_data.Weapon | if hero_data.Weapon | ||
and hero_data.Weapon.AltFire | and hero_data.Weapon.AltFire | ||
| Line 389: | Line 365: | ||
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 | ||
stats_with_any_scaling[attr_key] = true | if not (hero_entry.key == "hero_shiv" and attr_key == "BulletDamage") 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 | |||
end | end | ||
else | else | ||
-- Compute DPS scaling if direct key is missing/empty | |||
if attr_key == "DPS" or attr_key == "SustainedDPS" then | if attr_key == "DPS" or attr_key == "SustainedDPS" then | ||
local alt_stats = hero_data.Weapon.AltFire | local alt_stats = hero_data.Weapon.AltFire | ||
| Line 419: | Line 398: | ||
end | end | ||
-- | -- Header (unchanged) | ||
local headers_str = '<th style="position: sticky; left: 0; top: -1px; z-index: 12; background-color: | 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() | ||
local postfix_key_map = { | local postfix_key_map = { | ||
| Line 431: | Line 410: | ||
["SustainedDPS"] = "DPS_postfix", | ["SustainedDPS"] = "DPS_postfix", | ||
["RoundsPerSecondAtMaxSpin"] = "", | ["RoundsPerSecondAtMaxSpin"] = "", | ||
[" | ["CritDamageBonusScale"] = "StatDesc_CritDamageBonusScale_postfix", | ||
["BonusCritDamagePercent"] = "BonusCritDamagePercent_postfix", | |||
["BulletLifesteal"] = "BulletLifestealPercentHero_postfix", | ["BulletLifesteal"] = "BulletLifestealPercentHero_postfix", | ||
["GroundDashSpeed"] = "DashSpeed_postfix" | ["GroundDashSpeed"] = "DashSpeed_postfix" | ||
| Line 442: | Line 421: | ||
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] | ||
local attr_localized, postfix | local attr_localized, postfix | ||
if attr_data ~= nil then | if attr_data ~= nil then | ||
| Line 462: | Line 430: | ||
if postfix == nil or postfix == "" then postfix = "" | if postfix == nil or postfix == "" then postfix = "" | ||
else postfix = " (" .. postfix .. ")" end | else postfix = " (" .. postfix .. ")" end | ||
else | else | ||
attr_localized = dictionary_module.translate(attr_key) | attr_localized = dictionary_module.translate(attr_key) | ||