Module:Sandbox/LVL: Difference between revisionsGive feedback
Jump to navigation
Jump to search
No edit summary Tag: Manual revert |
No edit summary |
||
| Line 10: | Line 10: | ||
local hero_data_module = require('Module:HeroData') | local hero_data_module = require('Module:HeroData') | ||
local function get_nested_stat_value(hero_data, stat_key) | local function get_nested_stat_value(hero_data, stat_key, prefer_alt_fire) | ||
if prefer_alt_fire and hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[stat_key] ~= nil then | |||
return hero_data.Weapon.AltFire[stat_key] | |||
end | |||
if hero_data[stat_key] ~= nil then | if hero_data[stat_key] ~= nil then | ||
return hero_data[stat_key] | return hero_data[stat_key] | ||
| Line 104: | Line 107: | ||
end) | end) | ||
local function build_hero_row(hero_entry, is_alt_row) | |||
local hero_key = hero_entry.key | local hero_key = hero_entry.key | ||
local hero_data = hero_entry.data | local hero_data = hero_entry.data | ||
local row_str = "" | local row_str = "" | ||
hero_name = localize(hero_key, hero_key) | local hero_name = localize(hero_key, hero_key) | ||
hero_name_en = hero_data["Name"] | local hero_name_en = hero_data["Name"] | ||
if is_alt_row then | |||
hero_name = hero_name .. " (Alt-fire)" | |||
end | |||
template_args = {[1] = hero_name_en, l1 = hero_name} | local template_args = {[1] = hero_name_en, l1 = hero_name} | ||
hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args } | local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args } | ||
row_str = row_str .. | row_str = row_str .. | ||
| Line 124: | Line 129: | ||
for _, attr_key in ipairs(stats_to_include[category]) do | for _, attr_key in ipairs(stats_to_include[category]) do | ||
local base_value = get_nested_stat_value(hero_data, attr_key) | local base_value = get_nested_stat_value(hero_data, attr_key, is_alt_row) | ||
local stat_value = base_value | local stat_value = base_value | ||
local scaling_data = | local scaling_data = nil | ||
local scaling_strs = "" | local scaling_strs = "" | ||
local spirit_scale = 0 | local spirit_scale = 0 | ||
local level_scale = 0 | local level_scale = 0 | ||
if is_alt_row then | |||
scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire") | |||
end | |||
if scaling_data == nil then | |||
scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key) | |||
end | |||
if scaling_data ~= nil then | if scaling_data ~= nil then | ||
| Line 172: | Line 184: | ||
local hit_once = "false" | local hit_once = "false" | ||
if (attr_key == "DPS" or attr_key == "SustainedDPS") | if (attr_key == "DPS" or attr_key == "SustainedDPS") then | ||
and hero_data.Weapon | if not is_alt_row then | ||
and hero_data.Weapon.HitOnceAcrossAllBullets | if hero_data.Weapon and hero_data.Weapon.HitOnceAcrossAllBullets then | ||
hit_once = "true" | |||
end | |||
else | |||
if hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire.HitOnceAcrossAllBullets then | |||
hit_once = "true" | |||
end | |||
end | |||
end | end | ||
| Line 196: | Line 213: | ||
end | end | ||
return "<tr>" .. row_str .. "</tr>" | |||
end | |||
-- Build table body rows | |||
for _, hero_entry in ipairs(sorted_heroes) do | |||
body_str = body_str .. build_hero_row(hero_entry, false) | |||
if hero_entry.data.Weapon and hero_entry.data.Weapon.AltFire then | |||
body_str = body_str .. build_hero_row(hero_entry, true) | |||
end | |||
end | end | ||
| Line 208: | Line 234: | ||
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 | local alt_scaling_data = nil | ||
if hero_data.Weapon and hero_data.Weapon.AltFire then | |||
alt_scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire") | |||
end | |||
local has_scaling = (scaling_data ~= nil and next(scaling_data) ~= nil) | |||
local has_alt_scaling = (alt_scaling_data ~= nil and next(alt_scaling_data) ~= nil) | |||
if has_scaling or has_alt_scaling then | |||
stats_with_any_scaling[attr_key] = true | stats_with_any_scaling[attr_key] = true | ||
local has_spirit = false | local has_spirit = false | ||
local has_level = false | local has_level = false | ||
for _, scaling_type in pairs(scaling_data) do | if has_scaling then | ||
if scaling_type == "Spirit" then has_spirit = true end | for _, scaling_type in pairs(scaling_data) do | ||
if scaling_type == "Spirit" then has_spirit = true end | |||
if scaling_type == "Level" then has_level = true end | |||
end | |||
end | |||
if has_alt_scaling then | |||
for _, scaling_type in pairs(alt_scaling_data) do | |||
if scaling_type == "Spirit" then has_spirit = true end | |||
if scaling_type == "Level" then has_level = true end | |||
end | |||
end | end | ||
if has_spirit and has_level then | if has_spirit and has_level then | ||
Revision as of 19:59, 28 June 2026
Documentation for this module may be created at Module:Sandbox/LVL/doc
local p = {}
local heroes_data = mw.loadJsonData("Data:HeroData.json")
local attributes_data = mw.loadJsonData("Data:AttributeData.json")
local attribute_orders = mw.loadJsonData("Data:StatInfoboxOrder.json")
local util_module = require('Module:Utilities')
local lang_module = require('Module:Lang')
local dictionary_module = require('Module:Dictionary')
local attribute_module = require('Module:AttributeData')
local hero_data_module = require('Module:HeroData')
local function get_nested_stat_value(hero_data, stat_key, prefer_alt_fire)
if prefer_alt_fire and hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[stat_key] ~= nil then
return hero_data.Weapon.AltFire[stat_key]
end
if hero_data[stat_key] ~= nil then
return hero_data[stat_key]
elseif hero_data.Weapon and hero_data.Weapon[stat_key] ~= nil then
return hero_data.Weapon[stat_key]
elseif hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[stat_key] ~= nil then
return hero_data.Weapon.AltFire[stat_key]
end
return 0
end
local function localize(key, fallback)
local result = lang_module.get_string(key)
if result == "" or result == nil then
result = util_module.add_space_before_cap(fallback) ..
mw.getCurrentFrame():expandTemplate{title = "MissingValveTranslationTooltip"}
end
return result
end
-- {{#invoke:HeroComparisonTable|write_hero_comparison_table|POWER_INCREASES|SPIRIT_POWER|MAX_POWER|MAX_SPIRIT}}
p.write_hero_comparison_table = function(frame)
local power_increases = tonumber(frame.args[1]) or 0
local spirit_power = tonumber(frame.args[2]) or 0
local max_power = tonumber(frame.args[3]) or 25
local max_spirit = tonumber(frame.args[4]) or 500
local display_scaling_icons = (power_increases == 0 and spirit_power == 0)
local body_str = ""
local hero_name
local hero_name_en
local hero_icon
local template_args = {}
local stats_to_include = {
Weapon = {
"DPS",
"SustainedDPS",
"BulletDamage",
"RoundsPerSecond",
"FireRate",
"ClipSize",
"ReloadTime",
"ReloadDelay",
"BulletsPerShot",
"BulletsPerBurst",
"BurstInterShotInterval",
"LightMeleeDamage",
"HeavyMeleeDamage",
"ReloadSingle",
"BulletSpeed",
"BulletGravityScale",
"FalloffStartRange",
"FalloffEndRange",
"BonusAttackRange",
"CritDamageBonusPercent",
"RoundsPerSecondAtMaxSpin",
"SpinAcceleration",
"SpinDeceleration"
},
Vitality = {
"MaxHealth",
"BaseHealthRegen",
"BulletResist",
"TechResist",
"MeleeResist",
"BulletLifesteal",
"CritDamageReceivedPercent",
"MaxMoveSpeed",
"SprintSpeed",
"StaminaCooldown",
"Stamina",
"GroundDashSpeed"
},
Spirit = {
"TechPower"
}
}
-- Collect and sort heroes alphabetically, skipping disabled/in-development
local sorted_heroes = {}
for hero_key, hero_data in pairs(heroes_data) do
if not hero_data["InDevelopment"] and not hero_data["IsDisabled"] then
table.insert(sorted_heroes, {key = hero_key, data = hero_data})
end
end
table.sort(sorted_heroes, function(a, b)
local function get_sort_name(name)
return (name:gsub("^The ", ""))
end
return get_sort_name(a.data["Name"] or a.key) < get_sort_name(b.data["Name"] or b.key)
end)
local function build_hero_row(hero_entry, is_alt_row)
local hero_key = hero_entry.key
local hero_data = hero_entry.data
local row_str = ""
local hero_name = localize(hero_key, hero_key)
local hero_name_en = hero_data["Name"]
if is_alt_row then
hero_name = hero_name .. " (Alt-fire)"
end
local template_args = {[1] = hero_name_en, l1 = hero_name}
local hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args }
row_str = row_str ..
'<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate; overflow: hidden;">' ..
hero_icon .. "</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
local base_value = get_nested_stat_value(hero_data, attr_key, is_alt_row)
local stat_value = base_value
local scaling_data = nil
local scaling_strs = ""
local spirit_scale = 0
local level_scale = 0
if is_alt_row then
scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
end
if scaling_data == nil then
scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
end
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 hit_once = "false"
if (attr_key == "DPS" or attr_key == "SustainedDPS") then
if not is_alt_row then
if hero_data.Weapon and hero_data.Weapon.HitOnceAcrossAllBullets then
hit_once = "true"
end
else
if hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire.HitOnceAcrossAllBullets then
hit_once = "true"
end
end
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
return "<tr>" .. row_str .. "</tr>"
end
-- Build table body rows
for _, hero_entry in ipairs(sorted_heroes) do
body_str = body_str .. build_hero_row(hero_entry, false)
if hero_entry.data.Weapon and hero_entry.data.Weapon.AltFire then
body_str = body_str .. build_hero_row(hero_entry, true)
end
end
-- Pre-pass: determine which stats have any scaling, and which have both types
local stats_with_any_scaling = {}
local stats_with_both_scaling = {}
for _, hero_entry in ipairs(sorted_heroes) do
local hero_data = hero_entry.data
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
local scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key)
local alt_scaling_data = nil
if hero_data.Weapon and hero_data.Weapon.AltFire then
alt_scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire")
end
local has_scaling = (scaling_data ~= nil and next(scaling_data) ~= nil)
local has_alt_scaling = (alt_scaling_data ~= nil and next(alt_scaling_data) ~= nil)
if has_scaling or has_alt_scaling then
stats_with_any_scaling[attr_key] = true
local has_spirit = false
local has_level = false
if has_scaling then
for _, scaling_type in pairs(scaling_data) do
if scaling_type == "Spirit" then has_spirit = true end
if scaling_type == "Level" then has_level = true end
end
end
if has_alt_scaling then
for _, scaling_type in pairs(alt_scaling_data) do
if scaling_type == "Spirit" then has_spirit = true end
if scaling_type == "Level" then has_level = true end
end
end
if has_spirit and has_level then
stats_with_both_scaling[attr_key] = true
end
end
end
end
end
end
-- Build header row
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 postfix_key_map = {
["ReloadDelay"] = "StatDesc_ReloadTime_postfix",
["BulletsPerShot"] = "",
["BulletsPerBurst"] = "",
["BurstInterShotInterval"] = "StatDesc_ReloadTime_postfix",
["ReloadSingle"] = "",
["BonusAttackRange"] = "StatDesc_WeaponRangeFalloffMax_postfix",
["SustainedDPS"] = "DPS_postfix",
["RoundsPerSecondAtMaxSpin"] = "",
["CritDamageBonusScale"] = "StatDesc_CritDamageBonusScale_postfix",
["BonusCritDamagePercent"] = "BonusCritDamagePercent_postfix",
["BulletLifesteal"] = "BulletLifestealPercentHero_postfix",
["GroundDashSpeed"] = "DashSpeed_postfix"
}
for _, category in ipairs(attribute_orders["category_order"]) do
local category_attrs = attributes_data[category]
local category_rgb = category_data[category]["rgb"]
if stats_to_include[category] ~= nil then
for _, attr_key in ipairs(stats_to_include[category]) do
local attr_data = category_attrs[attr_key]
local attr_localized
local postfix
if attr_data ~= nil then
attr_localized = lang_module.get_string(attr_data["label"])
if attr_localized == nil or attr_localized == "" then
attr_localized = util_module.add_space_before_cap(attr_key)
end
postfix = lang_module.get_string(attr_data["postfix"])
if postfix == nil or postfix == "" then
postfix = ""
else
postfix = " (" .. postfix .. ")"
end
else
attr_localized = dictionary_module.translate(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 ~= "" then
postfix = " (" .. postfix .. ")"
end
end
local th_style = 'position: sticky; top: -1px; z-index: 3; background-color: rgb(' .. category_rgb .. ');'
if stats_with_both_scaling[attr_key] then
th_style = th_style .. ' min-width: 130px;'
elseif stats_with_any_scaling[attr_key] then
th_style = th_style .. ' min-width: 75px;'
end
headers_str = headers_str ..
'<th style="' .. th_style .. '">' .. attr_localized .. postfix .. "</th>"
end
end
end
headers_str = "<tr>" .. headers_str .. "</tr>"
return string.format(
'<div id="hero-comparison-container" data-max-power="%s" data-max-spirit="%s">' ..
'<div style="overflow: auto; max-height: 70vh; width: 100%%;">' ..
'<table class="wikitable sortable" style="table-layout: auto; width: 100%%;" id="hero-comparison-table">%s%s</table>' ..
'</div></div>',
max_power,
max_spirit,
headers_str,
body_str
)
end
return p