Module:Sandbox/LVL: Difference between revisionsGive feedback
No edit summary |
No edit summary |
||
| Line 30: | Line 30: | ||
end | end | ||
-- ========================================================================= | |||
-- DPS calculation helpers (mirroring bot's calculate_dps) | |||
-- ========================================================================= | |||
local function calculate_dps(stats, dps_type) | |||
-- stats table with all needed fields (may contain nil) | |||
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 | |||
-- sustained DPS | |||
local clip_size = stats.ClipSize or 0 | |||
if clip_size <= 0 then | |||
-- weapons with no clip use burst DPS | |||
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 DPS scaling delta for a given dps_type and scaling source | |||
-- base_stats: table of alt-fire weapon stats (from Weapon.AltFire) | |||
-- hero_data: full hero data (to look up LevelScaling/SpiritScaling) | |||
-- scaling_type: "Level" or "Spirit" | |||
-- returns a single numeric scaling value (e.g., how much the DPS increases per boon or per spirit point) | |||
local function compute_dps_scaling(hero_data, base_stats, dps_type, scaling_type) | |||
local scaling_source = hero_data[scaling_type .. "Scaling"] or {} | |||
-- Collect all alt-fire component scalings that exist | |||
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 | |||
-- If no component scalings, scaling is zero | |||
if next(component_scalings) == nil then | |||
return 0 | |||
end | |||
-- Compute base DPS from the original base stats | |||
local base_dps = calculate_dps(base_stats, dps_type) | |||
-- Apply all scalings to a copy of base stats | |||
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) | 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 87: | Line 176: | ||
local stat_value = base_value | local stat_value = base_value | ||
-- Scaling lookup | |||
local scaling_data | local scaling_data | ||
if is_alt_fire then | if is_alt_fire then | ||
| Line 93: | Line 184: | ||
elseif hero_key == "hero_shiv" and attr_key == "BulletDamage" then | elseif hero_key == "hero_shiv" and attr_key == "BulletDamage" 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) | ||
-- NEW: compute DPS scaling if missing | |||
elseif (attr_key == "DPS" or attr_key == "SustainedDPS") | |||
and hero_data.Weapon and hero_data.Weapon.AltFire then | |||
-- try direct scaling key first | |||
local direct_scaling = hero_data_module.get_hero_scaling_data(hero_data, attr_key .. "AltFire") | |||
if direct_scaling ~= nil then | |||
scaling_data = direct_scaling | |||
else | |||
-- compute from component scalings | |||
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") | |||
-- build scaling_data table like get_hero_scaling_data returns: { [scale_val] = "Level"/"Spirit" } | |||
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 | elseif hero_data.Weapon and hero_data.Weapon.AltFire | ||
and hero_data.Weapon.AltFire[attr_key] ~= nil then | and hero_data.Weapon.AltFire[attr_key] ~= nil then | ||
| Line 161: | Line 271: | ||
end | end | ||
-- Primary row | -- Primary row generator (unchanged) | ||
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 168: | Line 278: | ||
local template_args = {[1] = hero_name_en, l1 = hero_name_local} | 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_icon = mw.getCurrentFrame():expandTemplate{ title = "Template:HeroIcon", args = template_args } | ||
local hero_cell_content = hero_icon | local hero_cell_content = hero_icon | ||
if has_alt then | if has_alt then | ||
| Line 174: | Line 283: | ||
end | end | ||
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate;">' .. hero_cell_content .. '</td>' | row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 10; background-color: #202122; isolation: isolate;">' .. 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 185: | Line 293: | ||
end | end | ||
-- Alt-fire sub-row | -- Alt-fire sub-row generator (unchanged except hero cell with icon) | ||
local function generateAltFireSubRow(hero_data, hero_key) | 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>' | |||
row_str = row_str .. '<td style="position: sticky; left: 0; z-index: 9; background-color: #202122; isolation: isolate;">' .. 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 body | -- Build body | ||
| Line 241: | Line 345: | ||
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 checks (unchanged, but now DPS/SustainedDPS might be computed and should be caught) | |||
if hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[attr_key] ~= nil then | if hero_data.Weapon and hero_data.Weapon.AltFire and hero_data.Weapon.AltFire[attr_key] ~= nil then | ||
if attr_key ~= "ClipSize" then | if attr_key ~= "ClipSize" then | ||
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 | if alt_scaling and next(alt_scaling) then | ||
if not (hero_entry.key == "hero_shiv" and attr_key == "BulletDamage") then | if not (hero_entry.key == "hero_shiv" and attr_key == "BulletDamage") then | ||
stats_with_any_scaling[attr_key] = true | stats_with_any_scaling[attr_key] = true | ||
local has_spirit_alt, has_level_alt = false, false | local has_spirit_alt, has_level_alt = false, false | ||
for _, | for _, stype in pairs(alt_scaling) do | ||
if | if stype == "Spirit" then has_spirit_alt = true | ||
elseif | 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 | if has_spirit_alt and has_level_alt then stats_with_both_scaling[attr_key] = true end | ||
end | |||
else | |||
-- check computed scaling (for DPS/SustainedDPS) | |||
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") | |||
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 | ||