Editing Module:Sandbox/LVLGive feedback
Jump to navigation
Jump to search
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 {} | ||
local component_scalings = {} | local component_scalings = {} | ||
local possible_keys = { | local possible_keys = { | ||
| Line 87: | Line 92: | ||
end | end | ||
-- ClipSize is shared between fire | -- ClipSize is shared between primary and alt-fire. If no AltFire scaling, | ||
-- use primary ClipSize scaling (the alt-fire row already uses primary base value) | |||
if base_stats.ClipSize and not component_scalings.ClipSize then | if base_stats.ClipSize and not component_scalings.ClipSize then | ||
local primary_clip_scale = scaling_source["ClipSize"] | local primary_clip_scale = scaling_source["ClipSize"] | ||
| Line 107: | Line 113: | ||
end | end | ||
end | end | ||
local scaled_dps = calculate_dps(scaled_stats, dps_type) | |||
return scaled_dps - base_dps | |||
end | |||
local function compute_dps_scaling_from_components(base_stats, dps_type, component_scalings) | |||
-- component_scalings is a table like { BulletDamage = 0.165 } | |||
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 114: | Line 134: | ||
-- 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 142: | ||
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 160: | ||
} | } | ||
-- 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 174: | ||
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 188: | ||
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 195: | ||
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) | 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) | |||
-- NEW: Shiv alt-fire DPS/SustainedDPS uses primary BulletDamage scaling | |||
elseif hero_key == "hero_shiv" and (attr_key == "DPS" or attr_key == "SustainedDPS") | |||
and hero_data.Weapon and hero_data.Weapon.AltFire then | |||
local alt_stats = hero_data.Weapon.AltFire | |||
local dps_type = attr_key == "DPS" and 'burst' or 'sustained' | |||
-- get primary BulletDamage scaling | |||
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 | |||
-- General DPS computation for other heroes | |||
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 | ||
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 | ||
| Line 220: | Line 256: | ||
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 = 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 scaling_type == "Spirit" then | ||
spirit_scale = scaling_val | |||
stat_value = stat_value + (spirit_power * scaling_val) | |||
elseif scaling_type == "Level" then | elseif scaling_type == "Level" then | ||
level_scale = scaling_val | |||
stat_value = stat_value + (power_increases * scaling_val) | |||
end | end | ||
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 | ||
| Line 267: | Line 287: | ||
stat_value = tostring(stat_value) | stat_value = tostring(stat_value) | ||
else | else | ||
stat_value = util_module.round_to_sig_fig(stat_value, | stat_value = util_module.round_to_sig_fig(stat_value, 3) | ||
end | end | ||
| Line 302: | Line 322: | ||
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 333: | ||
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: 9; background-color: #202122; isolation: isolate; overflow: hidden; min-width: 150px;">' .. hero_cell_content .. '</td>' | |||
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: | |||
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 344: | ||
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 352: | ||
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: 10; background-color: #202122; isolation: isolate; overflow: hidden; min-width: 150px;">' .. hero_cell_content .. '</td>' | |||
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: | |||
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 367: | ||
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 378: | ||
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 386: | ||
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 396: | ||
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) | |||
-- Alt-fire scaling (only | -- Alt-fire scaling (only if the stat exists in AltFire) | ||
if hero_data.Weapon | if hero_data.Weapon | ||
and hero_data.Weapon.AltFire | and hero_data.Weapon.AltFire | ||
| Line 389: | Line 405: | ||
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 | ||
if attr_key == "DPS" or attr_key == "SustainedDPS" then | -- Special handling for Shiv alt-fire DPS/SustainedDPS | ||
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 | |||
-- assume if only one type, it's not both | |||
if stype == "Level" then | |||
-- could still be both if both level and spirit scaling exist, but Shiv doesn't have spirit | |||
end | |||
end | |||
end | |||
end | |||
-- General DPS computation for other heroes | |||
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 419: | Line 456: | ||
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; 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 431: | Line 468: | ||
["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 479: | ||
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 488: | ||
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) | ||