Module:HeroData: Difference between revisions

Jump to navigation Jump to search
LVL (talk | contribs)
Comparison table column widths based on scaling templates count
LVL (talk | contribs)
Moved hero comparison table to its own module Module:HeroComparisonTable
Line 310: Line 310:
return scaling_data_returned
return scaling_data_returned
end
local function get_nested_stat_value(hero_data, stat_key)
    -- Check top-level, then Weapon, then AltFire
    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
--writes the massive hero comparison table for a specific PI and SP
-- scaling icons are shown only if PI and SP are both 0 or both not provided
--{{#invoke:HeroData|write_hero_comparison_table|POWER_INCREASES|SPIRIT_POWER|MAX_POWER|MAX_SPIRIT}}
p.write_hero_comparison_table = function(frame)
-- process inputs (convert to numbers)
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
-- Show scaling icons if power/increases/spirit power are 0/not specified
local display_scaling_icons = false
if (power_increases == 0 and spirit_power == 0) then
display_scaling_icons = true
end
-- Initializations and declarations
local row_str = ""
local body_str = ""
local template_title = ""
local template_args = {}
local hero_icon
-- Add hero comp stats to
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 by name
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 getSortName(name)
        return (name:gsub("^The ", ""))
    end
    local nameA = getSortName(a.data["Name"] or a.key)
    local nameB = getSortName(b.data["Name"] or b.key)
    return nameA < nameB
end)
-- Iterate heroes
for _, hero_entry in ipairs(sorted_heroes) do
local hero_key = hero_entry.key
local hero_data = hero_entry.data
row_str = ""
-- Retrieve hero's localized name
hero_name = localize(hero_key, hero_key)
-- Retrieve hero's english name, used for hero icon
hero_name_en = hero_data["Name"]
-- Expand hero icon
template_title = "Template:HeroIcon"
template_args[1] = hero_name_en
template_args["l1"] = hero_name
hero_icon = mw.getCurrentFrame():expandTemplate{ title = template_title, args = template_args }
-- First column in each row is hero name (sticky)
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate; overflow: hidden;">' .. hero_icon .. "</td>"
-- Consecutive columns are then stats
-- Iterate categories
for _, category in ipairs(attribute_orders["category_order"]) do
category_attrs = attributes_data[category]
-- Iterate attributes within the category
if (stats_to_include[category] ~= nil) then
for _, attr_key in ipairs(stats_to_include[category]) do
--Retrieve the stats value from hero data
local base_value = get_nested_stat_value(hero_data, attr_key)
local stat_value = base_value
-- Retrieve scaling val and str if it scales
local scaling_data = p.get_hero_scaling_data(hero_data, attr_key)
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 = p.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
       
        -- Track scaling values for data attributes
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
-- TechPower gains 1:1 with user-input Spirit Power (only if not already defined)
if attr_key == 'TechPower' and spirit_scale == 0 then
    spirit_scale = 1.0
    stat_value = stat_value + (spirit_power * spirit_scale)
end
-- Calculate innate TechPower scaling for this hero
local innate_spirit_scale = (hero_data["LevelScaling"] and hero_data["LevelScaling"]["TechPower"]) or 0
-- Hide scaling icons if not at base values
if (not display_scaling_icons) then scaling_strs = "" end
-- Convert from boolean to string, or round it
if (type(stat_value)) == 'boolean' then
    stat_value = tostring(stat_value)
else
    stat_value = util_module.round_to_sig_fig(stat_value, 3)
end
-- Keep numeric value in its own span, scaling HTML as sibling
local cell_inner = string.format(
    '<span class="stat-num">%s</span><span class="stat-scaling">%s</span>',
    stat_value,
    scaling_strs
)
-- At the point where you build data_attrs for DPS/SustainedDPS cells
local hit_once = "false"
if (attr_key == 'DPS' or attr_key == 'SustainedDPS') and hero_data.Weapon and hero_data.Weapon.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
)
-- Add it to the row
row_str = row_str .. string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner)
end
end
end
-- Add row to the body
row_str = "<tr>" .. row_str .. "</tr>"
body_str = body_str .. row_str
end
-- Pre-pass: find which stats have scaling
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 = p.get_hero_scaling_data(hero_data, attr_key)
if scaling_data ~= nil and next(scaling_data) ~= nil then
stats_with_any_scaling[attr_key] = true
local has_spirit = false
local has_level = false
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
if has_spirit and has_level then
stats_with_both_scaling[attr_key] = true
end
end
end
end
end
end
-- First header is Hero (sticky)
local headers_str = '<th style="position: sticky; left: 0; top: -1px; z-index: 12; background-color: #27292d; isolation: isolate;">Hero</th>'
local header_style = ""
local category_data = attribute_module.get_category_data()
local postfix
-- Used for keys that are not localized by Valve
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"
}
-- Iterate stats again for displaying headers with their respective color
for _, category in ipairs(attribute_orders["category_order"]) do
category_attrs = attributes_data[category]
category_rgb = category_data[category]["rgb"]
-- Iterate attributes within the category
if (stats_to_include[category] ~= nil) then
for _, attr_key in ipairs(stats_to_include[category]) do
attr_data = category_attrs[attr_key]
if (attr_data ~= nil) then
-- Localize, fallback to attr_key with proper spaces
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
-- Get postfix
postfix = lang_module.get_string(attr_data["postfix"])
if (postfix == nil or postfix == "") then
postfix = ""
else
postfix = " (" .. postfix .. ")"
end
else -- If not in attributes data, use dictionary translate and postfix
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
header_style = ' style="' .. th_style .. '"'
headers_str = headers_str .. "<th" .. header_style .. ">" .. attr_localized .. postfix .. "</th>"
end
end
end
headers_str = "<tr>" .. headers_str .. "</tr>"
-- Wrap table in container with data attributes for JS
local table_str = 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
)
return table_str
end
end