Module:Buildup: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
LVL (talk | contribs)
Fix for Silver's transformed state. She seems to use her normal form buildup numbers.
LVL (talk | contribs)
Fix shotgun buildup by calculating per-bullet instead of per-shot; add Bullets/Shot column.
 
(One intermediate revision by the same user not shown)
Line 7: Line 7:
-- Handles both flat numbers and lookup in the ItemData file
-- Handles both flat numbers and lookup in the ItemData file
local function get_item_bb(target_name)
local function get_item_bb(target_name)
if target_name == nil then return nil end
    if target_name == nil then return nil end
   
-- Clean input
    -- Clean input
local clean_name = mw.text.trim(target_name)
    local clean_name = mw.text.trim(target_name)
   
-- If user input a number directly
    -- If user input a number directly
if tonumber(clean_name) then return tonumber(clean_name) end
    if tonumber(clean_name) then return tonumber(clean_name) end


-- Iterate Item Data
    -- Iterate Item Data
for key, item in pairs(items_data) do
    for key, item in pairs(items_data) do
if item["Name"] == clean_name then
        if item["Name"] == clean_name then
           
-- Valve data structure (nested)
            -- Valve data structure (nested)
if item["m_mapAbilityProperties"] and item["m_mapAbilityProperties"]["BuildUpPerShot"] then
            if item["m_mapAbilityProperties"] and item["m_mapAbilityProperties"]["BuildUpPerShot"] then
return tonumber(item["m_mapAbilityProperties"]["BuildUpPerShot"]["m_strValue"])
                return tonumber(item["m_mapAbilityProperties"]["BuildUpPerShot"]["m_strValue"])
end
            end
           
-- Flat data structure (fallback)
            -- Flat data structure (fallback)
if item["BuildUpPerShot"] then
            if item["BuildUpPerShot"] then
return tonumber(item["BuildUpPerShot"])
                return tonumber(item["BuildUpPerShot"])
end
            end
end
        end
end
    end
return nil
    return nil
end
end


-- Helper to find Hero Data by name or key
-- Helper to find Hero Data by name or key
local function get_hero_data(hero_name)
local function get_hero_data(hero_name)
if hero_name == nil then return nil end
    if hero_name == nil then return nil end
   
local clean_name = mw.text.trim(hero_name)
    local clean_name = mw.text.trim(hero_name)
   
-- Try direct Key lookup
    -- Try direct Key lookup
if heroes_data[clean_name] then return heroes_data[clean_name] end
    if heroes_data[clean_name] then return heroes_data[clean_name] end
   
-- Try adding "hero_" prefix
    -- Try adding "hero_" prefix
local key_lower = "hero_" .. string.lower(clean_name)
    local key_lower = "hero_" .. string.lower(clean_name)
if heroes_data[key_lower] then return heroes_data[key_lower] end
    if heroes_data[key_lower] then return heroes_data[key_lower] end
   
-- Search by Name field
    -- Search by Name field
for k, v in pairs(heroes_data) do
    for k, v in pairs(heroes_data) do
if v["Name"] == clean_name then return v end
        if v["Name"] == clean_name then return v end
end
    end
return nil
    return nil
end
 
-- Helper: number of bullets/pellets fired per trigger pull
-- For shotguns (Abrams, etc.) this is the pellet count.
-- For burst weapons this is the burst size.
-- For normal weapons it is 1.
local function get_bullets_per_shot(hero)
    if not hero or not hero.Weapon then return 1 end
    local bps = tonumber(
        hero.Weapon.BulletsPerShot or
        hero.Weapon.PelletsPerShot or
        hero.Weapon.BulletsPerBurst or
        1
    )
    if bps == nil or bps < 1 then bps = 1 end
    return bps
end
end


-- Core calculation logic
-- Core calculation logic
-- Returns: per_bullet_bps (float), bullets_per_shot (int)
local function calculate_bps(hero, bb)
local function calculate_bps(hero, bb)
     if not hero or not hero.Weapon then return 0 end
     if not hero or not hero.Weapon then return 0, 1 end


     local rps = tonumber(hero.Weapon.RoundsPerSecond or 0)
     local rps = tonumber(hero.Weapon.RoundsPerSecond or 0)
Line 88: Line 105:
     end
     end


     if rps == 0 then return 0 end
     if rps == 0 then return 0, 1 end


     -- Formula: 100 / (BB * (RPS + 1))
    local bullets_per_shot = get_bullets_per_shot(hero)
     return 100 / (bb * (rps + 1))
 
     -- Original Formula: 100 / (BB * (RPS + 1))
     -- This calculates the buildup per trigger pull assuming all pellets connect
    local bps_per_shot = 100 / (bb * (rps + 1))
 
    -- Divide by bullets/pellets per shot to get the per-bullet buildup
    local bps_per_bullet = bps_per_shot / bullets_per_shot
 
    return bps_per_bullet, bullets_per_shot
end
end


--{{#invoke:Buildup|get_bps|HERO_NAME|ITEM_NAME}}
--{{#invoke:Buildup|get_bps|HERO_NAME|ITEM_NAME}}
--Returns a single float value (rounded to 1 decimal)
--Returns a single float value (rounded to 1 decimal) representing buildup PER BULLET
p.get_bps = function(frame)
p.get_bps = function(frame)
local hero_name = frame.args[1] or frame.args['hero']
    local hero_name = frame.args[1] or frame.args['hero']
local item_name = frame.args[2] or frame.args['item']
    local item_name = frame.args[2] or frame.args['item']


local hero = get_hero_data(hero_name)
    local hero = get_hero_data(hero_name)
if not hero then return "Hero Not Found" end
    if not hero then return "Hero Not Found" end


local bb = get_item_bb(item_name)
    local bb = get_item_bb(item_name)
if not bb then return "Item Not Found" end
    if not bb then return "Item Not Found" end


local result = calculate_bps(hero, bb)
    local result, _ = calculate_bps(hero, bb)
return string.format("%.1f", result)
    return string.format("%.1f", result)
end
end


--{{#invoke:Buildup|write_buildup_table|ITEM_NAME}}
--{{#invoke:Buildup|write_buildup_table|ITEM_NAME}}
--Generates a sortable wikitable for all heroes
--Generates a sortable wikitable for all heroes showing per-bullet buildup
p.write_buildup_table = function(frame)
p.write_buildup_table = function(frame)
     local item_name = frame.args[1]
     local item_name = frame.args[1]
Line 125: Line 150:
          
          
         if not is_dev and not is_disabled and hero["Weapon"] and hero["Name"] then
         if not is_dev and not is_disabled and hero["Weapon"] and hero["Name"] then
             local val = calculate_bps(hero, bb)
             local val, bullets_per_shot = calculate_bps(hero, bb)
              
              
             if val > 0 then
             if val > 0 then
                 local shots = math.ceil(100 / val)
                -- Shots to proc assumes every bullet in the shot connects
                local per_shot = val * bullets_per_shot
                 local shots = math.ceil(100 / per_shot)
                  
                  
                 table.insert(hero_list, {
                 table.insert(hero_list, {
                     name = hero["Name"],
                     name = hero["Name"],
                     bps = val,
                     bps = val,
                    bullets_per_shot = bullets_per_shot,
                     shots = shots
                     shots = shots
                 })
                 })
Line 157: Line 185:
      
      
     if item_name then
     if item_name then
         html:tag('caption'):wikitext('Buildup Per Shot: ' .. item_name)
         html:tag('caption'):wikitext('Buildup Per Bullet: ' .. item_name)
     end
     end


Line 163: Line 191:
     local headerRow = html:tag('tr')
     local headerRow = html:tag('tr')
     headerRow:tag('th'):wikitext('Hero')
     headerRow:tag('th'):wikitext('Hero')
     headerRow:tag('th'):wikitext('% per shot')
     headerRow:tag('th'):wikitext('% per bullet')
     headerRow:tag('th'):wikitext('Shots to Proc')
    headerRow:tag('th'):wikitext('Bullets/Shot')
     headerRow:tag('th'):wikitext('Shots to Proc<br/><small>(all bullets hit)</small>')


     -- Rows
     -- Rows
     for _, h in ipairs(hero_list) do
     for _, h in ipairs(hero_list) do
         local row = html:tag('tr')
         local row = html:tag('tr')
         local icon_template = frame:expandTemplate{ title = 'HeroIcon', args = { h.name } }
         local hero_display
   
        if h.name == "Silver (Transformed)" then
            -- Detect page language from subpage (e.g. /ru)
            local currentTitle = mw.title.getCurrentTitle().fullText
            local langCode = currentTitle:match("/([^/]+)$") or "en"
            local isRussian = (langCode == "ru")
       
            local displayName = isRussian and "Сильвер (Трансформация)" or "Silver (Transformed)"
            local linkTarget = isRussian and "Silver (Transformed)/ru" or "Silver (Transformed)"
          
          
         -- Use data-sort-value so the table sorts correctly even with the icon
            local icon_file = string.format('[[File:Silver.png|20px|link=%s]]', linkTarget)
            local name_link = string.format('[[%s|%s]]', linkTarget, displayName)
            hero_display = string.format(
                '<span style="white-space:nowrap;">'
                .. '<span style="position:relative; bottom:2px;">%s</span>'
                .. ' %s'
                .. '</span>',
                icon_file, name_link
            )
         else
            hero_display = frame:expandTemplate{ title = 'HeroIcon', args = { h.name } }
        end
   
         row:tag('td')
         row:tag('td')
             :attr('data-sort-value', h.name) -- Uses the name including "The" for consistent default sort
             :attr('data-sort-value', h.name)
             :wikitext(icon_template)
             :wikitext(hero_display)
           
   
         row:tag('td'):wikitext(string.format("%.1f", h.bps))
         row:tag('td'):wikitext(string.format("%.1f", h.bps))
       
        -- Show "—" for normal single-bullet weapons, the actual count for shotguns/bursts
        if h.bullets_per_shot > 1 then
            row:tag('td'):wikitext(tostring(h.bullets_per_shot))
        else
            row:tag('td'):wikitext('—')
        end
         row:tag('td'):wikitext(tostring(h.shots))
         row:tag('td'):wikitext(tostring(h.shots))
     end
     end
Line 186: Line 244:
-- Decides whether to return a single value or a full table based on arguments
-- Decides whether to return a single value or a full table based on arguments
p.main = function(frame)
p.main = function(frame)
local args = frame:getParent().args
    local args = frame:getParent().args
-- Handle direct #invoke calls vs Template calls
    -- Handle direct #invoke calls vs Template calls
if not args[1] and not args['item'] then args = frame.args end
    if not args[1] and not args['item'] then args = frame.args end


local arg1 = args[1] or args['hero']
    local arg1 = args[1] or args['hero']
local arg2 = args[2] or args['item']
    local arg2 = args[2] or args['item']


-- If both Hero and Item are present -> Return Single Number
    -- If both Hero and pItem are present -> Return Single Number
if arg1 and arg2 then
    if arg1 and arg2 then
-- Manually setting args for the helper function to read
        -- Manually setting args for the helper function to read
frame.args = {arg1, arg2}
        frame.args = {arg1, arg2}
return p.get_bps(frame)
        return p.get_bps(frame)
       
-- If only one arg (Item) is present -> Return Table
    -- If only one arg (Item) is present -> Return Table
elseif arg1 then
    elseif arg1 then
frame.args = {arg1}
        frame.args = {arg1}
return p.write_buildup_table(frame)
        return p.write_buildup_table(frame)
       
else
    else
return "Error: Provide [Item Name] for a table, or [Hero Name] [Item Name] for a value."
        return "Error: Provide [Item Name] for a table, or [Hero Name] [Item Name] for a value."
end
    end
end
end


return p
return p

Latest revision as of 03:18, 15 July 2026

Overview

[edit source]

The Buildup module provides functions for calculating the Status Effect Buildup per shot for heroes.

This is used for items that apply a status effect based on filling a meter (e.g., Toxic Bullets, Silencer, Inhibitor). The calculation normalizes the buildup based on the hero's Fire Rate (RPS) to ensure the time-to-activate is roughly consistent across different weapon types.

It references Data:HeroData.json for weapon stats (RPS, Spin-up) and Data:ItemData.json for the item's base buildup value.

Functions

[edit source]

The primary entry point used by Template:Buildup. It automatically detects whether to generate a full table or a single value based on the number of parameters provided.

get_bps

[edit source]

Calculates the specific buildup percentage per shot for a single hero.

Parameters

[edit source]
  • hero_name - Name of the hero in English (e.g., "Abrams") or the hero key.
  • item_name - Name of the item (e.g., "Toxic Bullets").

Example

[edit source]
{{#invoke:Buildup|get_bps|Haze|Toxic Bullets}}

Returns: 7.4

write_buildup_table

[edit source]

Generates a sortable wikitable listing the buildup percentage and shots-to-proc for every valid hero.

Parameters

[edit source]
  • item_name - Name of the item (e.g., "Silencer").

Example

[edit source]
{{#invoke:Buildup|write_buildup_table|Silencer}}

Internal Logic

[edit source]

The module uses the following formula derived from the game's mechanics: BPS=100BaseBuildup×(RPSeffective+1)

The Effective RPS is determined by analyzing specific hero mechanics:

  • Standard Heroes: Uses the standard RoundsPerSecond from the data files.
  • Spin-Up Heroes: For heroes like McGinnis, the calculation uses their Max Spin RPS.
  • Burst/Hybrid Heroes: For heroes like Paige and The Doorman, the game displays a Fire Rate that includes an "Intra-Burst" delay, but the Buildup mechanic ignores this delay. The module detects this configuration (Burst Count = 1, but Interval > 0) and mathematically removes the interval to determine the true Effective RPS.
  • Shots to Proc: The table uses math.ceil to always round up, ensuring accuracy for cases like Kelvin where the math results in 19.99% (requiring 6 shots, not 5).

local p = {}

local heroes_data = mw.loadJsonData("Data:HeroData.json")
local items_data = mw.loadJsonData("Data:ItemData.json")

-- Helper to find Item Buildup value by name
-- Handles both flat numbers and lookup in the ItemData file
local function get_item_bb(target_name)
    if target_name == nil then return nil end
    
    -- Clean input
    local clean_name = mw.text.trim(target_name)
    
    -- If user input a number directly
    if tonumber(clean_name) then return tonumber(clean_name) end

    -- Iterate Item Data
    for key, item in pairs(items_data) do
        if item["Name"] == clean_name then
            
            -- Valve data structure (nested)
            if item["m_mapAbilityProperties"] and item["m_mapAbilityProperties"]["BuildUpPerShot"] then
                return tonumber(item["m_mapAbilityProperties"]["BuildUpPerShot"]["m_strValue"])
            end
            
            -- Flat data structure (fallback)
            if item["BuildUpPerShot"] then
                return tonumber(item["BuildUpPerShot"])
            end
        end
    end
    return nil
end

-- Helper to find Hero Data by name or key
local function get_hero_data(hero_name)
    if hero_name == nil then return nil end
    
    local clean_name = mw.text.trim(hero_name)
    
    -- Try direct Key lookup
    if heroes_data[clean_name] then return heroes_data[clean_name] end
    
    -- Try adding "hero_" prefix
    local key_lower = "hero_" .. string.lower(clean_name)
    if heroes_data[key_lower] then return heroes_data[key_lower] end
    
    -- Search by Name field
    for k, v in pairs(heroes_data) do
        if v["Name"] == clean_name then return v end
    end
    return nil
end

-- Helper: number of bullets/pellets fired per trigger pull
-- For shotguns (Abrams, etc.) this is the pellet count.
-- For burst weapons this is the burst size.
-- For normal weapons it is 1.
local function get_bullets_per_shot(hero)
    if not hero or not hero.Weapon then return 1 end
    local bps = tonumber(
        hero.Weapon.BulletsPerShot or
        hero.Weapon.PelletsPerShot or
        hero.Weapon.BulletsPerBurst or
        1
    )
    if bps == nil or bps < 1 then bps = 1 end
    return bps
end

-- Core calculation logic
-- Returns: per_bullet_bps (float), bullets_per_shot (int)
local function calculate_bps(hero, bb)
    if not hero or not hero.Weapon then return 0, 1 end

    local rps = tonumber(hero.Weapon.RoundsPerSecond or 0)
    local spin_rps = tonumber(hero.Weapon.RoundsPerSecondAtMaxSpin or 0)
    
    -- 1. Handle Spin-up (McGinnis/Bebop)
    if spin_rps > 0 then 
        rps = spin_rps 
    end

    -- 2. Handle "Fake Burst" Logic (Paige/Doorman)
    -- If they have a burst interval, but only 1 bullet per burst,
    -- the game ignores that interval for Buildup calculation.
    local burst_count = tonumber(hero.Weapon.BulletsPerBurst or 1)
    local burst_interval = tonumber(hero.Weapon.BurstInterShotInterval or 0)

    if burst_count == 1 and burst_interval > 0 and rps > 0 then
        -- Convert RPS back to time
        local total_time = 1 / rps
        -- Remove the interval
        local effective_time = total_time - burst_interval
        
        -- Prevent division by zero if something is weird
        if effective_time > 0.001 then
            rps = 1 / effective_time
        end
    end

    -- 3. Handle Silver (Transformed) - uses base form's RPS for buildup
    if hero.Name == "Silver (Transformed)" then
        rps = 1.1111  -- Base Silver's RoundsPerSecond
    end

    if rps == 0 then return 0, 1 end

    local bullets_per_shot = get_bullets_per_shot(hero)

    -- Original Formula: 100 / (BB * (RPS + 1))
    -- This calculates the buildup per trigger pull assuming all pellets connect
    local bps_per_shot = 100 / (bb * (rps + 1))

    -- Divide by bullets/pellets per shot to get the per-bullet buildup
    local bps_per_bullet = bps_per_shot / bullets_per_shot

    return bps_per_bullet, bullets_per_shot
end

--{{#invoke:Buildup|get_bps|HERO_NAME|ITEM_NAME}}
--Returns a single float value (rounded to 1 decimal) representing buildup PER BULLET
p.get_bps = function(frame)
    local hero_name = frame.args[1] or frame.args['hero']
    local item_name = frame.args[2] or frame.args['item']

    local hero = get_hero_data(hero_name)
    if not hero then return "Hero Not Found" end

    local bb = get_item_bb(item_name)
    if not bb then return "Item Not Found" end

    local result, _ = calculate_bps(hero, bb)
    return string.format("%.1f", result)
end

--{{#invoke:Buildup|write_buildup_table|ITEM_NAME}}
--Generates a sortable wikitable for all heroes showing per-bullet buildup
p.write_buildup_table = function(frame)
    local item_name = frame.args[1]
    
    local bb = get_item_bb(item_name)
    if not bb then return "Error: Item '" .. (item_name or "nil") .. "' not found." end

    local hero_list = {}
    
    for key, hero in pairs(heroes_data) do
        local is_dev = hero["InDevelopment"] == true
        local is_disabled = hero["IsDisabled"] == true
        
        if not is_dev and not is_disabled and hero["Weapon"] and hero["Name"] then
            local val, bullets_per_shot = calculate_bps(hero, bb)
            
            if val > 0 then
                -- Shots to proc assumes every bullet in the shot connects
                local per_shot = val * bullets_per_shot
                local shots = math.ceil(100 / per_shot)
                
                table.insert(hero_list, {
                    name = hero["Name"],
                    bps = val,
                    bullets_per_shot = bullets_per_shot,
                    shots = shots
                })
            end
        end
    end

    -- Sorting Logic
    table.sort(hero_list, function(a, b) 
        -- Helper to strip "The " from names just for sorting
        local function clean_sort_name(name)
            if string.sub(name, 1, 4) == "The " then
                return string.sub(name, 5)
            end
            return name
        end

        return clean_sort_name(a.name) < clean_sort_name(b.name) 
    end)

    -- Build Table
    local html = mw.html.create('table')
    html:addClass('wikitable sortable mw-collapsible')
    
    if item_name then
        html:tag('caption'):wikitext('Buildup Per Bullet: ' .. item_name)
    end

    -- Headers
    local headerRow = html:tag('tr')
    headerRow:tag('th'):wikitext('Hero')
    headerRow:tag('th'):wikitext('% per bullet')
    headerRow:tag('th'):wikitext('Bullets/Shot')
    headerRow:tag('th'):wikitext('Shots to Proc<br/><small>(all bullets hit)</small>')

    -- Rows
    for _, h in ipairs(hero_list) do
        local row = html:tag('tr')
        local hero_display
    
        if h.name == "Silver (Transformed)" then
            -- Detect page language from subpage (e.g. /ru)
            local currentTitle = mw.title.getCurrentTitle().fullText
            local langCode = currentTitle:match("/([^/]+)$") or "en"
            local isRussian = (langCode == "ru")
        
            local displayName = isRussian and "Сильвер (Трансформация)" or "Silver (Transformed)"
            local linkTarget = isRussian and "Silver (Transformed)/ru" or "Silver (Transformed)"
        
            local icon_file = string.format('[[File:Silver.png|20px|link=%s]]', linkTarget)
            local name_link = string.format('[[%s|%s]]', linkTarget, displayName)
            hero_display = string.format(
                '<span style="white-space:nowrap;">'
                .. '<span style="position:relative; bottom:2px;">%s</span>'
                .. ' %s'
                .. '</span>',
                icon_file, name_link
            )
        else
            hero_display = frame:expandTemplate{ title = 'HeroIcon', args = { h.name } }
        end
    
        row:tag('td')
            :attr('data-sort-value', h.name)
            :wikitext(hero_display)
    
        row:tag('td'):wikitext(string.format("%.1f", h.bps))
        
        -- Show "—" for normal single-bullet weapons, the actual count for shotguns/bursts
        if h.bullets_per_shot > 1 then
            row:tag('td'):wikitext(tostring(h.bullets_per_shot))
        else
            row:tag('td'):wikitext('—')
        end

        row:tag('td'):wikitext(tostring(h.shots))
    end

    return tostring(html)
end

-- Main entry point for the template
-- Decides whether to return a single value or a full table based on arguments
p.main = function(frame)
    local args = frame:getParent().args
    -- Handle direct #invoke calls vs Template calls
    if not args[1] and not args['item'] then args = frame.args end

    local arg1 = args[1] or args['hero']
    local arg2 = args[2] or args['item']

    -- If both Hero and pItem are present -> Return Single Number
    if arg1 and arg2 then
        -- Manually setting args for the helper function to read
        frame.args = {arg1, arg2}
        return p.get_bps(frame)
        
    -- If only one arg (Item) is present -> Return Table
    elseif arg1 then
        frame.args = {arg1}
        return p.write_buildup_table(frame)
        
    else
        return "Error: Provide [Item Name] for a table, or [Hero Name] [Item Name] for a value."
    end
end

return p