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 30: | Line 30: | ||
end | end | ||
-- | -- {{#invoke:HeroComparisonTable|write_hero_comparison_table|POWER_INCREASES|SPIRIT_POWER|MAX_POWER|MAX_SPIRIT}} | ||
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 39: | ||
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 hero_name | |||
local hero_name_en | |||
local hero_icon | |||
local template_args = {} | |||
local stats_to_include = { | local stats_to_include = { | ||
Weapon = { | Weapon = { | ||
"DPS", "SustainedDPS", "BulletDamage", "RoundsPerSecond", "FireRate", | "DPS", | ||
"ClipSize", "ReloadTime", "ReloadDelay", | "SustainedDPS", | ||
"BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage", | "BulletDamage", | ||
"BulletSpeed", "FalloffStartRange", "FalloffEndRange", | "RoundsPerSecond", | ||
"CritDamageBonusPercent", | "FireRate", | ||
"ClipSize", | |||
"ReloadTime", | |||
"ReloadDelay", | |||
"BulletsPerShot", | |||
"BulletsPerBurst", | |||
"BurstInterShotInterval", | |||
"LightMeleeDamage", | |||
"HeavyMeleeDamage", | |||
"ReloadSingle", | |||
"BulletSpeed", | |||
"FalloffStartRange", | |||
"FalloffEndRange", | |||
"CritDamageBonusPercent", | |||
"RoundsPerSecondAtMaxSpin", | |||
"SpinAcceleration", | |||
"SpinDeceleration" | |||
}, | }, | ||
Vitality = { | Vitality = { | ||
"MaxHealth", "BaseHealthRegen", "BulletResist", "TechResist", "MeleeResist", | "MaxHealth", | ||
"CritDamageReceivedPercent", "DebuffResist", "BulletLifesteal", "MaxMoveSpeed", | "BaseHealthRegen", | ||
"SprintSpeed", "StaminaCooldown", "Stamina", "GroundDashSpeed", "GravityChange" | "BulletResist", | ||
"TechResist", | |||
"MeleeResist", | |||
"CritDamageReceivedPercent", | |||
"DebuffResist", | |||
"BulletLifesteal", | |||
"MaxMoveSpeed", | |||
"SprintSpeed", | |||
"StaminaCooldown", | |||
"Stamina", | |||
"GroundDashSpeed", | |||
"GravityChange" | |||
}, | }, | ||
Spirit = { "TechPower" } | Spirit = { | ||
"TechPower" | |||
} | |||
} | } | ||
-- Collect and sort heroes alphabetically, | -- Collect and sort heroes alphabetically, skipping 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 155: | Line 104: | ||
end) | end) | ||
-- | -- Helper to generate a single row (primary or alt-fire) | ||
local function | local function generateRow(hero_data, hero_key, is_alt_fire) | ||
local row_str = "" | |||
local | local hero_name_local = localize(hero_key, hero_key) | ||
local hero_name_en = hero_data["Name"] | |||
local template_args = {[1] = hero_name_en, l1 = hero_name_local} | |||
local | local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args } | ||
local hero_cell_content = hero_icon | |||
local | |||
if is_alt_fire then | if is_alt_fire then | ||
hero_cell_content = hero_icon .. ' <small>(Alt-fire)</small>' | |||
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 | ||
if stats_to_include[category] ~= nil then | |||
for _, attr_key in ipairs(stats_to_include[category]) do | |||
-- 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 | |||
-- Scaling: special cases for alt-fire row | |||
local scaling_data | |||
if is_alt_fire then | |||
-- ClipSize always uses primary scaling | |||
if attr_key == "ClipSize" then | |||
scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key) | |||
-- Shiv's alt‑fire BulletDamage uses primary scaling | |||
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 | ||
local scaling_strs = "" | |||
local spirit_scale = 0 | |||
local level_scale = 0 | |||
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 | |||
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 | |||
if attr_key == "TechPower" and spirit_scale == 0 then | |||
spirit_scale = 1.0 | |||
stat_value = stat_value + (spirit_power * spirit_scale) | |||
end | |||
local innate_spirit_scale = | |||
(hero_data["LevelScaling"] and hero_data["LevelScaling"]["TechPower"]) or 0 | |||
if not display_scaling_icons then scaling_strs = "" end | |||
if type(stat_value) == "boolean" then | |||
stat_value = tostring(stat_value) | |||
else | |||
stat_value = util_module.round_to_sig_fig(stat_value, 3) | |||
end | |||
local cell_inner = string.format( | |||
'<span class="stat-num">%s</span><span class="stat-scaling">%s</span>', | |||
stat_value, | |||
scaling_strs | |||
) | |||
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 | |||
local data_attrs = string.format( | |||
'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"', | |||
attr_key, | |||
tostring(type(base_value) == "number" and util_module.round_to_sig_fig(base_value, 3) or base_value), | |||
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 | |||
) | |||
row_str = row_str .. | |||
string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner) | |||
end | end | ||
end | end | ||
end | end | ||
return "<tr>" .. row_str .. "</tr>" | return "<tr>" .. row_str .. "</tr>" | ||
end | end | ||
-- | -- Build table body rows (primary + optional alt-fire) | ||
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 | ||
body_str = body_str .. | body_str = body_str .. generateRow(hero_data, hero_key, false) | ||
if | if hero_data.Weapon and hero_data.Weapon.AltFire then | ||
body_str = body_str .. | body_str = body_str .. generateRow(hero_data, hero_key, true) | ||
end | end | ||
end | end | ||
-- Pre-pass: determine which stats have any scaling, and which have both types | -- Pre-pass: determine which stats have any scaling, and which have both types | ||
-- | -- (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 373: | Line 259: | ||
if scaling_data ~= nil and next(scaling_data) ~= nil then | if scaling_data ~= nil and next(scaling_data) ~= nil then | ||
stats_with_any_scaling[attr_key] = true | stats_with_any_scaling[attr_key] = true | ||
local has_spirit | local has_spirit = false | ||
local has_level = false | |||
for _, scaling_type in pairs(scaling_data) do | for _, scaling_type in pairs(scaling_data) do | ||
if scaling_type == "Spirit" then has_spirit = true | if scaling_type == "Spirit" then has_spirit = true end | ||
if scaling_type == "Level" then has_level = true end | |||
end | |||
if has_spirit and has_level then | |||
stats_with_both_scaling[attr_key] = true | |||
end | end | ||
end | end | ||
-- Alt-fire scaling (only for stats | -- Alt-fire scaling (only for stats that truly appear in AltFire) | ||
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 | ||
if attr_key | -- Skip ClipSize (we always use primary scaling, no alt column needed) | ||
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 and next(alt_scaling) ~= nil then | if alt_scaling ~= nil and next(alt_scaling) ~= nil then | ||
stats_with_any_scaling[attr_key] = true | stats_with_any_scaling[attr_key] = true | ||
local has_spirit_alt | local has_spirit_alt = false | ||
for _, | local has_level_alt = false | ||
if | for _, scaling_type in pairs(alt_scaling) do | ||
if scaling_type == "Spirit" then has_spirit_alt = true end | |||
if scaling_type == "Level" then has_level_alt = true end | |||
end | end | ||
if has_spirit_alt and has_level_alt then | if has_spirit_alt and has_level_alt then | ||
stats_with_both_scaling[attr_key] = true | |||
end | end | ||
end | end | ||
| Line 419: | Line 301: | ||
end | end | ||
-- Build | -- Build 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: #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 = { | ||
["ReloadDelay"] = "StatDesc_ReloadTime_postfix", | ["ReloadDelay"] = "StatDesc_ReloadTime_postfix", | ||
["BulletsPerShot"] = "", | ["BulletsPerShot"] = "", | ||
["BulletsPerBurst"] = "", | ["BulletsPerBurst"] = "", | ||
["BurstInterShotInterval"] = "StatDesc_ReloadTime_postfix", | ["BurstInterShotInterval"] = "StatDesc_ReloadTime_postfix", | ||
["ReloadSingle"] = "", | ["ReloadSingle"] = "", | ||
["BonusAttackRange"] = "StatDesc_WeaponRangeFalloffMax_postfix", | ["BonusAttackRange"] = "StatDesc_WeaponRangeFalloffMax_postfix", | ||
["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" | ||
} | } | ||
for _, category in ipairs(attribute_orders["category_order"]) do | for _, category in ipairs(attribute_orders["category_order"]) do | ||
local category_attrs = attributes_data[category] | local category_attrs = attributes_data[category] | ||
local category_rgb = category_data[category]["rgb"] | local category_rgb = category_data[category]["rgb"] | ||
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 attr_data = category_attrs[attr_key] | local attr_data = category_attrs[attr_key] | ||
local attr_localized | |||
local postfix | |||
if attr_data ~= nil then | if attr_data ~= nil then | ||
attr_localized = lang_module.get_string(attr_data["label"]) | attr_localized = lang_module.get_string(attr_data["label"]) | ||
| Line 460: | Line 335: | ||
end | end | ||
postfix = lang_module.get_string(attr_data["postfix"]) | postfix = lang_module.get_string(attr_data["postfix"]) | ||
if postfix == nil or postfix == "" then postfix = "" | if postfix == nil or postfix == "" then | ||
else postfix = " (" .. postfix .. ")" | postfix = "" | ||
else | |||
postfix = " (" .. postfix .. ")" | |||
end | |||
else | else | ||
attr_localized = dictionary_module.translate(attr_key) | attr_localized = dictionary_module.translate(attr_key) | ||
postfix = lang_module.get_string(postfix_key_map[attr_key]) | postfix = lang_module.get_string(postfix_key_map[attr_key]) | ||
if postfix == nil then return "attr_key " .. attr_key .. " must be added to postfix_key_map" end | if postfix == nil then | ||
if postfix ~= "" then postfix = " (" .. postfix .. ")" end | return "attr_key " .. attr_key .. " must be added to postfix_key_map" | ||
end | |||
if postfix ~= "" then | |||
postfix = " (" .. postfix .. ")" | |||
end | |||
end | end | ||
local th_style = 'position: sticky; top: -1px; z-index: 3; background-color: rgb(' .. category_rgb .. ');' | local th_style = 'position: sticky; top: -1px; z-index: 3; background-color: rgb(' .. category_rgb .. ');' | ||
if stats_with_both_scaling[attr_key] then | if stats_with_both_scaling[attr_key] then | ||
| Line 475: | Line 357: | ||
th_style = th_style .. ' min-width: 75px;' | th_style = th_style .. ' min-width: 75px;' | ||
end | end | ||
headers_str = headers_str .. '<th style="' .. th_style .. '">' .. attr_localized .. postfix .. "</th>" | |||
headers_str = headers_str .. | |||
'<th style="' .. th_style .. '">' .. attr_localized .. postfix .. "</th>" | |||
end | end | ||
end | end | ||
| Line 486: | Line 370: | ||
'<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>' .. | ||
'</div></div>', | '</div></div>', | ||
max_power, max_spirit, headers_str, body_str | max_power, | ||
max_spirit, | |||
headers_str, | |||
body_str | |||
) | ) | ||
end | end | ||
return p | return p | ||