| Latest revision |
Your text |
| 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, 1 end
| | if not hero or not hero.Weapon then return 0 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 rps = tonumber(hero.Weapon.RoundsPerSecond or 0) |
| | local spin_rps = tonumber(hero.Weapon.RoundsPerSecondAtMaxSpin or 0) |
| | |
| | -- Handle Spin-up logic (McGinnis/Bebop) |
| | -- If they have a max spin speed, use that instead of base RPS |
| | if spin_rps > 0 then rps = spin_rps end |
|
| |
|
| local bullets_per_shot = get_bullets_per_shot(hero)
| | if rps == 0 then return 0 end |
|
| |
|
| -- Original Formula: 100 / (BB * (RPS + 1))
| | -- Formula: 100 / (BB * (RPS + 1)) |
| -- This calculates the buildup per trigger pull assuming all pellets connect
| | return 100 / (bb * (rps + 1)) |
| 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) representing buildup PER BULLET | | --Returns a single float value (rounded to 1 decimal) |
| 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 showing per-bullet buildup | | --Generates a sortable wikitable for all heroes |
| p.write_buildup_table = function(frame) | | p.write_buildup_table = function(frame) |
| local item_name = frame.args[1]
| | local item_name = frame.args[1] |
|
| | |
| local bb = get_item_bb(item_name)
| | local bb = get_item_bb(item_name) |
| if not bb then return "Error: Item '" .. (item_name or "nil") .. "' not found." end
| | 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)
| | local hero_list = {} |
| end)
| | |
| | -- Collect valid heroes |
| | 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"] then |
| | local val = calculate_bps(hero, bb) |
| | |
| | if val > 0 then |
| | -- Calculate shots needed (Rounding UP is crucial for 19.99% edge cases) |
| | local shots = math.ceil(100 / val) |
| | |
| | table.insert(hero_list, { |
| | name = hero["Name"], |
| | bps = val, |
| | shots = shots |
| | }) |
| | end |
| | end |
| | end |
|
| |
|
| -- Build Table
| | -- Sort Alphabetically |
| local html = mw.html.create('table')
| | table.sort(hero_list, function(a, b) return a.name < b.name end) |
| html:addClass('wikitable sortable mw-collapsible')
| |
|
| |
| if item_name then
| |
| html:tag('caption'):wikitext('Buildup Per Bullet: ' .. item_name)
| |
| end
| |
|
| |
|
| -- Headers
| | -- Build Table |
| local headerRow = html:tag('tr')
| | local html = mw.html.create('table') |
| headerRow:tag('th'):wikitext('Hero')
| | html:addClass('wikitable sortable mw-collapsible') |
| headerRow:tag('th'):wikitext('% per bullet')
| | |
| headerRow:tag('th'):wikitext('Bullets/Shot')
| | if item_name then |
| headerRow:tag('th'):wikitext('Shots to Proc<br/><small>(all bullets hit)</small>')
| | html:tag('caption'):wikitext('Buildup Per Shot: ' .. item_name) |
| | end |
|
| |
|
| -- Rows
| | -- Headers |
| for _, h in ipairs(hero_list) do
| | local headerRow = html:tag('tr') |
| local row = html:tag('tr')
| | headerRow:tag('th'):wikitext('Hero') |
| local hero_display
| | headerRow:tag('th'):wikitext('% per shot') |
|
| | headerRow:tag('th'):wikitext('Shots to Proc') |
| 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))
| | -- Rows |
| end
| | for _, h in ipairs(hero_list) do |
| | local row = html:tag('tr') |
| | |
| | -- Expand HeroIcon template |
| | local icon_template = frame:expandTemplate{ title = 'HeroIcon', args = { h.name } } |
| | |
| | row:tag('td'):wikitext(icon_template) |
| | row:tag('td'):wikitext(string.format("%.1f", h.bps)) |
| | row:tag('td'):wikitext(tostring(h.shots)) |
| | end |
|
| |
|
| return tostring(html)
| | return tostring(html) |
| end | | end |
|
| |
|
| Line 244: |
Line 152: |
| -- 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 pItem are present -> Return Single Number
| | -- If both Hero and Item 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 |