Editing
Module
:
Sandbox/LVL
Give feedback
Jump to navigation
Jump to search
Warning:
You are not logged in. Once you make an edit, a temporary account will be created for you.
Learn more
.
Log in
or
create an account
to continue receiving notifications after this account expires, and to access other features.
Anti-spam check. Do
not
fill this in!
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) 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 -- ========================================================================= -- DPS calculation helpers -- ========================================================================= local function calculate_dps(stats, dps_type) local rps = stats.RoundsPerSecond or 0 if rps == 0 then return 0 end local bullets_per_shot = (stats.HitOnceAcrossAllBullets and 1) or (stats.BulletsPerShot or 1) local burst_count = stats.BulletsPerBurst or 1 local cycle_time = 1 / rps local total_cycle_time = cycle_time * burst_count if total_cycle_time == 0 then return 0 end local base_damage = stats.BulletDamage or 0 if dps_type == 'burst' then return base_damage * bullets_per_shot * burst_count / total_cycle_time end local clip_size = stats.ClipSize or 0 if clip_size <= 0 then return base_damage * bullets_per_shot * burst_count / total_cycle_time end local reload_time if stats.ReloadSingle then reload_time = (stats.ReloadTime or 0) * clip_size else reload_time = stats.ReloadTime or 0 end reload_time = reload_time + (stats.ReloadDelay or 0) local time_to_empty_clip = (clip_size / burst_count) * total_cycle_time local damage_from_clip = base_damage * bullets_per_shot * clip_size local total_time = time_to_empty_clip + reload_time if total_time == 0 then return 0 end return damage_from_clip / total_time end -- 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 scaling_source = hero_data[scaling_type .. "Scaling"] or {} local component_scalings = {} local possible_keys = { "BulletDamage", "RoundsPerSecond", "ClipSize", "ReloadTime", "ReloadDelay", "BulletsPerShot", "BulletsPerBurst" } for _, key in ipairs(possible_keys) do local alt_key = key .. "AltFire" if scaling_source[alt_key] then component_scalings[key] = scaling_source[alt_key] end end -- ClipSize is shared between fire modes; fall back to primary scaling if base_stats.ClipSize and not component_scalings.ClipSize then local primary_clip_scale = scaling_source["ClipSize"] if primary_clip_scale then component_scalings.ClipSize = primary_clip_scale end end if next(component_scalings) == nil then return 0 end local base_dps = calculate_dps(base_stats, dps_type) 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 scaled_dps = calculate_dps(scaled_stats, dps_type) return scaled_dps - base_dps end -- ========================================================================= -- Main module function -- ========================================================================= 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 stats_to_include = { Weapon = { "DPS", "SustainedDPS", "BulletDamage", "RoundsPerSecond", "FireRate", "ClipSize", "ReloadTime", "ReloadDelay", "ReloadSingle", "BulletsPerShot", "BulletsPerBurst", "BurstInterShotInterval", "LightMeleeDamage", "HeavyMeleeDamage", "BulletSpeed", "FalloffStartRange", "FalloffEndRange", "CritDamageBonusPercent", "BulletRadius", "RoundsPerSecondAtMaxSpin", "SpinAcceleration", "SpinDeceleration" }, Vitality = { "MaxHealth", "BaseHealthRegen", "BulletResist", "TechResist", "MeleeResist", "CritDamageReceivedPercent", "DebuffResist", "BulletLifesteal", "MaxMoveSpeed", "SprintSpeed", "StaminaCooldown", "Stamina", "GroundDashSpeed", "GravityChange" }, Spirit = { "TechPower" } } -- Collect and sort heroes alphabetically, excluding 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) -- 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) -- 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 if is_alt_fire then if attr_key == "ClipSize" or attr_key == "ReloadTime" 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 -- Apply unit conversion to the base value if type(base_value) == "number" then base_value = base_value * conv_factor end local stat_value = base_value -- Scaling lookup local scaling_data if is_alt_fire then if attr_key == "ClipSize" or attr_key == "ReloadTime" then scaling_data = hero_data_module.get_hero_scaling_data(hero_data, attr_key) elseif (attr_key == "DPS" or attr_key == "SustainedDPS") 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") if direct_scaling and next(direct_scaling) ~= nil then scaling_data = direct_scaling else local alt_stats = hero_data.Weapon.AltFire local dps_type = attr_key == "DPS" and 'burst' or 'sustained' local level_scale = compute_dps_scaling(hero_data, alt_stats, dps_type, "Level") local spirit_scale = compute_dps_scaling(hero_data, alt_stats, dps_type, "Spirit") 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 end 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 local scaling_strs = "" local spirit_scale = 0 local level_scale = 0 local spirit_val = 0 local level_val = 0 if scaling_data ~= nil then 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 -- 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 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 if attr_key == "TechPower" and spirit_scale == 0 then spirit_scale = 1.0 if type(stat_value) == "number" then stat_value = stat_value + (spirit_power * spirit_scale) end 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, 5) 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 ) return string.format('<td style="white-space: nowrap;" %s>%s</td>', data_attrs, cell_inner) end -- Generate a hero's primary stat row local function generatePrimaryRow(hero_data, hero_key, has_alt) local row_str = "" 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 hero_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args } local hero_cell_content = hero_icon if has_alt then hero_cell_content = '[[#alt-fire-' .. hero_key .. '|+]] ' .. hero_cell_content end 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 if stats_to_include[category] ~= nil then for _, attr_key in ipairs(stats_to_include[category]) do row_str = row_str .. buildStatCell(hero_data, hero_key, attr_key, false, category) end end end return "<tr>" .. row_str .. "</tr>" end -- Generate an expandable alt-fire sub-row (weapon stats only) local function generateAltFireSubRow(hero_data, hero_key) local row_str = "" 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 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 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 if stats_to_include[category] ~= nil then for _, attr_key in ipairs(stats_to_include[category]) do if category == "Weapon" then row_str = row_str .. buildStatCell(hero_data, hero_key, attr_key, true, category) else row_str = row_str .. '<td></td>' end end end end return '<tr class="alt-fire-sub" data-parent="' .. hero_key .. '">' .. row_str .. '</tr>' end -- Build all body rows for _, hero_entry in ipairs(sorted_heroes) do local hero_key = hero_entry.key local hero_data = hero_entry.data local has_alt = hero_data.Weapon and hero_data.Weapon.AltFire and true or false body_str = body_str .. generatePrimaryRow(hero_data, hero_key, has_alt) if has_alt then body_str = body_str .. generateAltFireSubRow(hero_data, hero_key) end end -- 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_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 -- Primary scaling local scaling_data = hero_data_module.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, has_level = false, false for _, scaling_type in pairs(scaling_data) do if scaling_type == "Spirit" then has_spirit = true elseif 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 -- Alt-fire scaling (only for stats present in the AltFire table) if hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[attr_key] ~= nil then if attr_key ~= "ClipSize" then local alt_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire") 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 if has_spirit_alt and has_level_alt then stats_with_both_scaling[attr_key] = true end else if attr_key == "DPS" or attr_key == "SustainedDPS" then local alt_stats = hero_data.Weapon.AltFire local dps_type = attr_key == "DPS" and 'burst' or 'sustained' local lv = compute_dps_scaling(hero_data, alt_stats, dps_type, "Level") local sp = compute_dps_scaling(hero_data, alt_stats, dps_type, "Spirit") lv = util_module.round_to_sig_fig(lv, 5) sp = util_module.round_to_sig_fig(sp, 5) if lv ~= 0 or sp ~= 0 then stats_with_any_scaling[attr_key] = true if lv ~= 0 and sp ~= 0 then stats_with_both_scaling[attr_key] = true end end end end end end end end end end -- Build table header row 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 postfix_key_map = { ["ReloadDelay"] = "StatDesc_ReloadTime_postfix", ["BulletsPerShot"] = "", ["BulletsPerBurst"] = "", ["BurstInterShotInterval"] = "StatDesc_ReloadTime_postfix", ["ReloadSingle"] = "", ["BonusAttackRange"] = "StatDesc_WeaponRangeFalloffMax_postfix", ["SustainedDPS"] = "DPS_postfix", ["RoundsPerSecondAtMaxSpin"] = "", ["CritDamageBonusPercent"] = "StatDesc_CritDamageBonusScale_postfix", ["CritDamageReceivedPercent"] = "StatDesc_CritDamageReceivedScale_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] -- 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 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 if attr_key == "BulletRadius" then postfix = " (cm)" 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
Summary:
Please note that all contributions to The Deadlock Wiki are considered to be released under the Creative Commons Attribution-NonCommercial-ShareAlike (see
Deadlock:Copyrights
for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)
Preview page with this template
Page included on this page:
Module:Sandbox/LVL/doc
(
view source
)
Navigation menu
Personal tools
Create account
Log in
associated-pages
Module
Discussion
English
Views
Read
Edit source
History
Tools
Search
Discord
Discord
Navigation
Main page
About us
Random page
Special pages
Recent changes
Unstable Rift
1h ago
·
~2026-IvyGankStone1157
Haze/Sounds
2h ago
·
Gammaton32
Haze/Voice lines
2h ago
·
Gammaton32
Grey Talon/Voice lines
2h ago
·
Gammaton32
Show more...
Interface Admin
Common CSS
Vector CSS
Minerva CSS
Citizen CSS
Brown theme CSS
Gadgets
Gadgets definition
Editing
How to edit
Style guide
All guides
New files
Unresolved feedback
Tools
What links here
Related changes
Page information