Module:Sandbox/LVL: Difference between revisionsGive feedback
No edit summary |
No edit summary |
||
| (9 intermediate revisions by the same user not shown) | |||
| Line 31: | Line 31: | ||
-- ========================================================================= | -- ========================================================================= | ||
-- DPS calculation helpers | -- DPS calculation helpers | ||
-- ========================================================================= | -- ========================================================================= | ||
local function calculate_dps(stats, dps_type) | local function calculate_dps(stats, dps_type) | ||
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 | ||
local clip_size = stats.ClipSize or 0 | local clip_size = stats.ClipSize or 0 | ||
if clip_size <= 0 then | if clip_size <= 0 then | ||
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 72: | Line 70: | ||
end | end | ||
-- Compute DPS | -- Compute how much DPS changes when all relevant component scalings are applied. | ||
-- | -- Used for alt-fire rows where explicit DPS/SustainedDPS scaling keys are missing. | ||
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 92: | Line 87: | ||
end | end | ||
-- ClipSize is shared between | -- ClipSize is shared between fire modes; fall back to primary scaling | ||
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 113: | Line 107: | ||
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 134: | Line 114: | ||
-- 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 142: | Line 123: | ||
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", "BulletsPerShot", "BulletsPerBurst", | "ClipSize", "ReloadTime", "ReloadDelay", "ReloadSingle", "BulletsPerShot", "BulletsPerBurst", | ||
"BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage", | "BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage", | ||
"BulletSpeed", "FalloffStartRange", "FalloffEndRange", | |||
"CritDamageBonusPercent", "RoundsPerSecondAtMaxSpin", "SpinAcceleration", "SpinDeceleration" | "CritDamageBonusPercent", "BulletRadius", "RoundsPerSecondAtMaxSpin", "SpinAcceleration", "SpinDeceleration" | ||
}, | }, | ||
Vitality = { | Vitality = { | ||
| Line 160: | Line 141: | ||
} | } | ||
-- Collect and sort heroes | -- Collect and sort heroes alphabetically, excluding disabled/in-development | ||
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 174: | Line 155: | ||
end) | end) | ||
-- | -- 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 | ||
if attr_key == "ClipSize" then | if attr_key == "ClipSize" or attr_key == "ReloadTime" 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 188: | 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 195: | Line 187: | ||
local scaling_data | local scaling_data | ||
if is_alt_fire then | if is_alt_fire then | ||
if attr_key == "ClipSize" 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 (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 | ||
-- Alt-fire DPS/SustainedDPS: use explicit key if present, otherwise compute from components | |||
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 256: | Line 220: | ||
local spirit_scale = 0 | local spirit_scale = 0 | ||
local level_scale = 0 | local level_scale = 0 | ||
local spirit_val = 0 | |||
local level_val = 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 | ||
if scaling_type == "Spirit" then | |||
spirit_val = scaling_val * conv_factor -- apply unit conversion | |||
elseif scaling_type == "Level" then | |||
level_val = scaling_val * conv_factor -- apply unit conversion | |||
end | end | ||
end | |||
if | -- Output Spirit icon before Level icon | ||
local function append_scaling(val, stype) | |||
if val ~= 0 then | |||
local scaling_str = hero_data_module.write_scalar_str(val, stype, true) | |||
if scaling_str ~= "" then | |||
scaling_strs = scaling_strs .. " " .. scaling_str | |||
end | |||
end | end | ||
end | end | ||
append_scaling(spirit_val, "Spirit") | |||
append_scaling(level_val, "Level") | |||
if type(stat_value) == "number" then | |||
stat_value = stat_value + (spirit_power * spirit_val) | |||
stat_value = stat_value + (power_increases * level_val) | |||
end | |||
spirit_scale = spirit_val | |||
level_scale = level_val | |||
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 | if type(stat_value) == "number" then | ||
stat_value = stat_value + (spirit_power * spirit_scale) | |||
end | |||
end | end | ||
| Line 287: | Line 267: | ||
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, 5) | ||
end | end | ||
| Line 322: | Line 302: | ||
end | end | ||
-- | -- Generate a hero's primary stat row | ||
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 333: | Line 313: | ||
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: | 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: 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 344: | Line 325: | ||
end | end | ||
-- | -- Generate an expandable alt-fire sub-row (weapon stats only) | ||
local function generateAltFireSubRow(hero_data, hero_key) | local function generateAltFireSubRow(hero_data, hero_key) | ||
local row_str = "" | local row_str = "" | ||
| Line 352: | Line 333: | ||
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: | local sort_name = (hero_data["Name"] or hero_key):gsub("^The ", "") | ||
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>' | |||
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 367: | Line 349: | ||
end | end | ||
-- Build body | -- Build all body 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 | ||
| Line 378: | Line 360: | ||
end | end | ||
-- Pre-pass | -- Pre-pass: determine which stats have any scaling, and which have both types. | ||
-- Used to set column widths in the header. | |||
local stats_with_any_scaling = {} | local stats_with_any_scaling = {} | ||
local stats_with_both_scaling = {} | local stats_with_both_scaling = {} | ||
| Line 386: | Line 369: | ||
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 | ||
-- Primary scaling | |||
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 396: | Line 380: | ||
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 scaling (only | -- Alt-fire scaling (only for stats present in the AltFire table) | ||
if hero_data.Weapon | if hero_data.Weapon | ||
and hero_data.Weapon.AltFire | and hero_data.Weapon.AltFire | ||
| Line 405: | 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 | ||
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 | end | ||
if has_spirit_alt and has_level_alt then stats_with_both_scaling[attr_key] = true end | |||
else | else | ||
if attr_key == "DPS" or attr_key == "SustainedDPS" then | |||
if | |||
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 456: | Line 419: | ||
end | end | ||
-- | -- Build table header row | ||
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: 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 468: | Line 431: | ||
["SustainedDPS"] = "DPS_postfix", | ["SustainedDPS"] = "DPS_postfix", | ||
["RoundsPerSecondAtMaxSpin"] = "", | ["RoundsPerSecondAtMaxSpin"] = "", | ||
[" | ["CritDamageBonusPercent"] = "StatDesc_CritDamageBonusScale_postfix", | ||
["CritDamageReceivedPercent"] = "StatDesc_CritDamageReceivedScale_postfix", | |||
["BulletLifesteal"] = "BulletLifestealPercentHero_postfix", | ["BulletLifesteal"] = "BulletLifestealPercentHero_postfix", | ||
["GroundDashSpeed"] = "DashSpeed_postfix" | ["GroundDashSpeed"] = "DashSpeed_postfix" | ||
| Line 479: | 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 488: | 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) | ||