Module:HeroDataArrays: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
added more alt fire stats
optimization (WIP)
Line 22: Line 22:
     end
     end
     return value
     return value
end
-- Generate tables from given properties
local function buildTable(frame, rows, tableDef, options)
    options = options or {}
    -- option to ignore heroes who don't have any of the given properties. true by default
    local filterZero = options.filterZero or true
    local filteredRows = {}
    for _, r in ipairs(rows) do
        local hasValue = not filterZero
        if filterZero then
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 and r[col.field] ~= nil then
                    hasValue = true
                    break
                end
            end
        end
        if hasValue then
            table.insert(filteredRows, r)
        end
    end
    if #filteredRows == 0 then
        return ""
    end
    -- Build header
    local header = ""
    for _, col in ipairs(tableDef.columns) do
        header = header .. " !! " .. col.label
    end
    -- Start table
    local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n" ..
        "|+ " .. tableDef.title .. " \n" ..
        "! Hero" .. header .. " \n"
    -- Rows
    for _, r in ipairs(filteredRows) do
        local cells = ""
        for _, col in ipairs(tableDef.columns) do
            cells = cells .. " || " .. (r[col.field] or 0)
        end
        t = t ..
            "|- \n" ..
            "| style=\"text-align:left;\" | " ..
            frame:expandTemplate{ title = "PageRef", args = { r.name } } ..
            cells .. " \n"
    end
    t = t .. "|}"
    return t
end
end


Line 139: Line 198:
         })
         })
     end
     end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Hero Base Damage \n"..
        "! Hero !! Damage per sec !! Reload Delay (s) !! Reload Time (s) !! Ammo !! Bullets per sec !! Bullet Damage !! Bullets Per Shot \n"
       
    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.DPS.." || "..r.reloadDelay.." || "..r.reloadTime.."  || "..r.clipSize..
                " || "..r.roundsPerSecond.." || "..r.bulletDamage.." || "..r.bulletsPerShot.." \n"
    end
    z = z.."|}"
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)


     local tableDefs = {
     local tableDefs = {
    {
            title  = "Hero Base Damage",
            columns = {
                { label = "Damage per sec", field = "DPS"  },
                { label = "Reload Delay (s)", field = "reloadDelay"  },
                { label = "Reload Time (s)", field = "reloadTime"  },
                { label = "Ammo", field = "clipSize"  },
                { label = "Bullets per sec", field = "roundsPerSecond"  },
                { label = "Bullet Damage", field = "bulletDamage"  },
                { label = "Bullets Per Shot", field = "bulletsPerShot"  },
            }
        },
     {
     {
             title  = "Hero Base Alt-Fire Damage",
             title  = "Hero Base Alt-Fire Damage",
Line 170: Line 225:
     }
     }


     local output = ""
     local output = {}
 
     for _, def in ipairs(tableDefs) do
     for _, tableDef in ipairs(tableDefs) do
         local t = buildTable(frame, rows, def, def.options)
         local filteredRows = {}
        if t ~= "" then
        for _, r in ipairs(rows) do
             table.insert(output, t)
            local hasValue = false
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 then
                    hasValue = true
                    break
                end
             end
            if hasValue then
                table.insert(filteredRows, r)
            end
        end
 
        if #filteredRows > 0 then
            local header = ""
            for _, col in ipairs(tableDef.columns) do
                header = header.." !! "..col.label
            end
 
            local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
                "|+ "..tableDef.title.." \n"..
                "! Hero"..header.." \n"
 
            for _, r in ipairs(filteredRows) do
                local cells = ""
                for _, col in ipairs(tableDef.columns) do
                    cells = cells.." || "..r[col.field]
                end
                t = t..
                    "|- \n"..
                    "| style=\"text-align:left;\" | "..
                        frame:expandTemplate{title="PageRef", args={r.name}}..
                        cells.." \n"
            end
 
            t = t.."|}"
            if output ~= "" then
                output = output.."\n\n"
            end
            output = output..t
         end
         end
     end
     end


     return z .. "\n\n" .. output
     return table.concat(output, "\n\n")
end
end


p.heroBulletSpeedTable = function(frame)
p.heroBulletSpeedTable = function(frame)
     local heroBulletSpeedData = heroDataArray({"Name", "Weapon>BulletSpeed", "Weapon>AltFire>BulletSpeed"})
     local heroBulletSpeedData = heroDataArray({
   
        "Name",
        "Weapon>BulletSpeed",
        "Weapon>AltFire>BulletSpeed"
    })
 
     local rows = {}
     local rows = {}
     for name, data in pairs(heroBulletSpeedData) do
     for name, data in pairs(heroBulletSpeedData) do
Line 231: Line 251:
         })
         })
     end
     end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)


    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
     table.sort(rows, function(a, b)
        "|+ Base Bullet Velocity \n"..
        return a.name:lower() < b.name:lower()
        "! Hero !! m/s \n"
    end)
 
    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.bulletSpeed.." \n"
    end
    z = z.."|}"
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)


     local tableDefs = {
     local tableDefs = {
    {
        {
             title   = "Base Alt-Fire Bullet Velocity",
            title = "Base Bullet Velocity",
            columns = {
                { label = "m/s", field = "bulletSpeed" }
            }
        },
        {
             title = "Base Alt-Fire Bullet Velocity",
             columns = {
             columns = {
                 { label = "m/s", field = "altBulletSpeed" },
                 { label = "m/s", field = "altBulletSpeed" }
             }
             }
         }  
         }
     }
     }


     local output = ""
     local output = {}
 
     for _, def in ipairs(tableDefs) do
     for _, tableDef in ipairs(tableDefs) do
         local t = buildTable(frame, rows, def, def.options)
         local filteredRows = {}
        if t ~= "" then
        for _, r in ipairs(rows) do
             table.insert(output, t)
            local hasValue = false
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 then
                    hasValue = true
                    break
                end
             end
            if hasValue then
                table.insert(filteredRows, r)
            end
        end
 
        if #filteredRows > 0 then
            local header = ""
            for _, col in ipairs(tableDef.columns) do
                header = header.." !! "..col.label
            end
 
            local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
                "|+ "..tableDef.title.." \n"..
                "! Hero"..header.." \n"
 
            for _, r in ipairs(filteredRows) do
                local cells = ""
                for _, col in ipairs(tableDef.columns) do
                    cells = cells.." || "..r[col.field]
                end
                t = t..
                    "|- \n"..
                    "| style=\"text-align:left;\" | "..
                        frame:expandTemplate{title="PageRef", args={r.name}}..
                        cells.." \n"
            end
 
            t = t.."|}"
            if output ~= "" then
                output = output.."\n\n"
            end
            output = output..t
         end
         end
     end
     end


     return z .. "\n\n" .. output
     return table.concat(output, "\n\n")
end
end



Revision as of 00:47, 23 April 2026

Overview

This module consists of one generic function, heroDataArray, and a series of functions that create specific wikitables using the output from heroDataArray.


local p = {};
local util_module = require('Module:Utilities')
local allHeroData = mw.loadJsonData("Data:HeroData.json")
local soul_unlock = require('Module:SoulUnlock')
local inGameHeroData = {}

-- Just get heroes that are playable and have a "name" in HeroData
for i, heroData in pairs(allHeroData) do
	if heroData["InDevelopment"] == false then
    if heroData["IsDisabled"] == false then
    if heroData["Name"] ~= nil then
        table.insert(inGameHeroData, heroData)
        end
        end
    end
end

-- Helper function to round numeric values
local function roundValue(value)
    if type(value) == "number" then
        return util_module.round_to_sig_fig(value, 3)
    end
    return value
end

-- Generate tables from given properties
local function buildTable(frame, rows, tableDef, options)
    options = options or {}
    -- option to ignore heroes who don't have any of the given properties. true by default
    local filterZero = options.filterZero or true

    local filteredRows = {}

    for _, r in ipairs(rows) do
        local hasValue = not filterZero

        if filterZero then
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 and r[col.field] ~= nil then
                    hasValue = true
                    break
                end
            end
        end

        if hasValue then
            table.insert(filteredRows, r)
        end
    end

    if #filteredRows == 0 then
        return ""
    end

    -- Build header
    local header = ""
    for _, col in ipairs(tableDef.columns) do
        header = header .. " !! " .. col.label
    end

    -- Start table
    local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n" ..
        "|+ " .. tableDef.title .. " \n" ..
        "! Hero" .. header .. " \n"

    -- Rows
    for _, r in ipairs(filteredRows) do
        local cells = ""
        for _, col in ipairs(tableDef.columns) do
            cells = cells .. " || " .. (r[col.field] or 0)
        end

        t = t ..
            "|- \n" ..
            "| style=\"text-align:left;\" | " ..
            frame:expandTemplate{ title = "PageRef", args = { r.name } } ..
            cells .. " \n"
    end

    t = t .. "|}"

    return t
end

-- heroDataArray takes as argument a table of any length containing keys or 
-- paths of keys in the HeroData JSON, e.g. {Name, MaxHealth, LevelScaling>Health}.
-- A path of keys should be specified as such, with the ">" character separating
-- subsequent keys.
local heroDataArray = function(args)
    local outData = {}
    
    for i, heroData in ipairs(inGameHeroData) do            -- Iterate over each hero
        local out = {}                                      -- First creating a table to hold data
        for j, key in pairs(args) do                        -- Iterate over each key:
            if string.find(key, ">") ~= nil then            -- If the key is actually a path of keys,
                local node = heroData                       -- start with the biggest node heroData, and
                for k in string.gmatch(key, "([^>]+)") do   -- iterate over each key, splitting on ">",
                    if node and node[k] ~= nil then         -- trying the key as a string e.g. "1", (Added 'node and' for safety)
                        node = node[k]                      
                    elseif node and node[tonumber(k)] ~= nil then -- else using the key as numeric, (Added 'node and' for safety)
                        node = node[tonumber(k)]
                    else                                    -- If at any point the path breaks, the result is nil
                        node = nil
                        break
                    end                                     -- drilling down the JSON node after node
                end
                out[key] = roundValue(node)                 -- until outputting the final node (rounded)
            else
                out[key] = roundValue(heroData[key])        -- or just grab the data if not a path (rounded)
            end
        end
        outData[heroData["Name"]] = out                     -- Save data in the big table of heroes
    end
    return(outData)
end

p.heroHealthTable = function(frame)
    -- Pull data
    local heroHealthData = heroDataArray({"Name", "MaxHealth", "LevelScaling>MaxHealth"})

    local rows = {}
    for name, data in pairs(heroHealthData) do
        table.insert(rows, {
            name = name,
            maxHealth = tonumber(data["MaxHealth"]) or 0,
            scalingHealth = tonumber(data["LevelScaling>MaxHealth"]) or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    -- Input table will be called a, output wikitable will be called z
    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Hero Base Health Stats \n"..
        "! Hero !! Starting !! Added per Boon !! At Max Boon \n"

    -- Iterate over heroes adding a new row to the wikitable for each
    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.maxHealth.." || +"..r.scalingHealth.." || "..
                tostring(roundValue(r.maxHealth + r.scalingHealth * soul_unlock.get_max('PowerIncrease'))).." \n"
    end
    
    z = z.."|}"
    return z
end

p.heroRegenTable = function(frame)
    local heroRegenData = heroDataArray({"Name", "BaseHealthRegen"})
    
    local rows = {}
    for name, data in pairs(heroRegenData) do
        table.insert(rows, {
            name = name,
            baseHealthRegen = data["BaseHealthRegen"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Hero Base Health Regen \n"..
        "! Hero !! HP/s \n"

    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.baseHealthRegen.." \n"
    end
    z = z.."|}"
    return z
end

p.heroDpsTable = function(frame)
    local heroDpsData = heroDataArray({"Name","Weapon>DPS","Weapon>ReloadSingle","Weapon>ReloadDelay","Weapon>ReloadTime",
        "Weapon>ClipSize","Weapon>RoundsPerSecond","Weapon>BulletDamage","Weapon>BulletsPerShot","Weapon>AltFire>DPS",
        "Weapon>AltFire>AmmoConsumedPerShot","Weapon>AltFire>RoundsPerSecond","Weapon>AltFire>BulletDamage","Weapon>AltFire>BulletsPerShot"})
   
    local rows = {}
    for name, data in pairs(heroDpsData) do
        table.insert(rows, {
            name = name,
            DPS = data["Weapon>DPS"] or 0,
            reloadDelay = data["Weapon>ReloadDelay"] or 0,
            reloadTime = data["Weapon>ReloadTime"] or 0,
            clipSize = data["Weapon>ClipSize"] or 0,
            roundsPerSecond = data["Weapon>RoundsPerSecond"] or 0,
            bulletDamage = data["Weapon>BulletDamage"] or 0,
            bulletsPerShot = data["Weapon>BulletsPerShot"] or 0,
            altDPS = data["Weapon>AltFire>DPS"] or 0,
            altAmmoConsumedPerShot = data["Weapon>AltFire>AmmoConsumedPerShot"] or 0,
            altRoundsPerSecond = data["Weapon>AltFire>RoundsPerSecond"] or 0,
            altBulletDamage = data["Weapon>AltFire>BulletDamage"] or 0,
            altBulletsPerShot = data["Weapon>AltFire>BulletsPerShot"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local tableDefs = {
    	{
            title   = "Hero Base Damage",
            columns = {
                { label = "Damage per sec", field = "DPS"  },
                { label = "Reload Delay (s)", field = "reloadDelay"  },
                { label = "Reload Time (s)", field = "reloadTime"  },
                { label = "Ammo", field = "clipSize"  },
                { label = "Bullets per sec", field = "roundsPerSecond"  },
                { label = "Bullet Damage", field = "bulletDamage"  },
                { label = "Bullets Per Shot", field = "bulletsPerShot"  },
            }
        }, 
    	{
            title   = "Hero Base Alt-Fire Damage",
            columns = {
                { label = "Damage per sec", field = "altDPS"  },
                { label = "Ammo Per Shot", field = "altAmmoConsumedPerShot"  },
                { label = "Bullets per sec", field = "altRoundsPerSecond"  },
                { label = "Bullet Damage", field = "altBulletDamage"  },
                { label = "Bullets Per Shot", field = "altBulletsPerShot"  },
            }
        }    
    }

    local output = {}
    for _, def in ipairs(tableDefs) do
        local t = buildTable(frame, rows, def, def.options)
        if t ~= "" then
            table.insert(output, t)
        end
    end

    return table.concat(output, "\n\n")
end

p.heroBulletSpeedTable = function(frame)
    local heroBulletSpeedData = heroDataArray({
        "Name",
        "Weapon>BulletSpeed",
        "Weapon>AltFire>BulletSpeed"
    })

    local rows = {}
    for name, data in pairs(heroBulletSpeedData) do
        table.insert(rows, {
            name = name,
            bulletSpeed = data["Weapon>BulletSpeed"] or 0,
            altBulletSpeed = data["Weapon>AltFire>BulletSpeed"] or 0
        })
    end

    table.sort(rows, function(a, b)
        return a.name:lower() < b.name:lower()
    end)

    local tableDefs = {
        {
            title = "Base Bullet Velocity",
            columns = {
                { label = "m/s", field = "bulletSpeed" }
            }
        },
        {
            title = "Base Alt-Fire Bullet Velocity",
            columns = {
                { label = "m/s", field = "altBulletSpeed" }
            }
        }
    }

    local output = {}
    for _, def in ipairs(tableDefs) do
        local t = buildTable(frame, rows, def, def.options)
        if t ~= "" then
            table.insert(output, t)
        end
    end

    return table.concat(output, "\n\n")
end

p.heroClipSizeTable = function(frame)
    local heroClipSizeData = heroDataArray({"Name", "Weapon>ClipSize","Weapon>AltFire>AmmoConsumedPerShot"})
    
    local rows = {}
    for name, data in pairs(heroClipSizeData) do
        table.insert(rows, {
            name = name,
            clipSize = data["Weapon>ClipSize"] or 0,
            ammoConsumedPerShot = data["Weapon>AltFire>AmmoConsumedPerShot"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Base Ammo Count \n"..
        "! Hero !! Ammo \n"

    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.clipSize.." \n"
    end
    z = z.."|}"
    
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local tableDefs = {
    	{
            title   = "Alt-Fire Ammo Cost",
            columns = {
                { label = "Ammo Per Shot", field = "ammoConsumedPerShot"  },
            }
        }    
    }

    local output = ""

    for _, tableDef in ipairs(tableDefs) do
        local filteredRows = {}
        for _, r in ipairs(rows) do
            local hasValue = false
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 then
                    hasValue = true
                    break
                end
            end
            if hasValue then
                table.insert(filteredRows, r)
            end
        end

        if #filteredRows > 0 then
            local header = ""
            for _, col in ipairs(tableDef.columns) do
                header = header.." !! "..col.label
            end

            local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
                "|+ "..tableDef.title.." \n"..
                "! Hero"..header.." \n"

            for _, r in ipairs(filteredRows) do
                local cells = ""
                for _, col in ipairs(tableDef.columns) do
                    cells = cells.." || "..r[col.field]
                end
                t = t..
                    "|- \n"..
                    "| style=\"text-align:left;\" | "..
                        frame:expandTemplate{title="PageRef", args={r.name}}..
                        cells.." \n"
            end

            t = t.."|}"
            if output ~= "" then
                output = output.."\n\n"
            end
            output = output..t
        end
    end

    return z .. "\n\n" .. output
end

p.heroRoundsPerSecondTable = function(frame)
    local heroRoundsPerSecondData = heroDataArray({"Name", "Weapon>RoundsPerSecond", "Weapon>AltFire>RoundsPerSecond"})
    
    local rows = {}
    for name, data in pairs(heroRoundsPerSecondData) do
        table.insert(rows, {
            name = name,
            roundsPerSecond = data["Weapon>RoundsPerSecond"] or 0,
            altRoundsPerSecond = data["Weapon>AltFire>RoundsPerSecond"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Base Fire Rate \n"..
        "! Hero !! Rounds/s \n"

    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.roundsPerSecond.." \n"
    end
    z = z.."|}"
    
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local tableDefs = {
    	{
            title   = "Base Alt-Fire Fire Rate",
            columns = {
                { label = "Rounds/s", field = "altRoundsPerSecond"  },
            }
        }    
    }

    local output = ""

    for _, tableDef in ipairs(tableDefs) do
        local filteredRows = {}
        for _, r in ipairs(rows) do
            local hasValue = false
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 then
                    hasValue = true
                    break
                end
            end
            if hasValue then
                table.insert(filteredRows, r)
            end
        end

        if #filteredRows > 0 then
            local header = ""
            for _, col in ipairs(tableDef.columns) do
                header = header.." !! "..col.label
            end

            local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
                "|+ "..tableDef.title.." \n"..
                "! Hero"..header.." \n"

            for _, r in ipairs(filteredRows) do
                local cells = ""
                for _, col in ipairs(tableDef.columns) do
                    cells = cells.." || "..r[col.field]
                end
                t = t..
                    "|- \n"..
                    "| style=\"text-align:left;\" | "..
                        frame:expandTemplate{title="PageRef", args={r.name}}..
                        cells.." \n"
            end

            t = t.."|}"
            if output ~= "" then
                output = output.."\n\n"
            end
            output = output..t
        end
    end

    return z .. "\n\n" .. output
end

p.heroFalloffRangeTable = function(frame)
    local heroFalloffRangeData = heroDataArray({"Name", "Weapon>FalloffStartRange", "Weapon>FalloffEndRange",  "Weapon>AltFire>FalloffStartRange", "Weapon>AltFire>FalloffEndRange",
    	"Weapon>AltFire>ExplosionRadius"})
    
    local rows = {}
    for name, data in pairs(heroFalloffRangeData) do
        table.insert(rows, {
            name = name,
            falloffStartRange = data["Weapon>FalloffStartRange"] or 0,
            falloffEndRange = data["Weapon>FalloffEndRange"] or 0,
            altFalloffStartRange = data["Weapon>AltFire>FalloffStartRange"] or 0,
            altFalloffEndRange = data["Weapon>AltFire>FalloffEndRange"] or 0,
            altExplosionRadius = data["Weapon>AltFire>ExplosionRadius"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Base Falloff Range \n"..
        "! Hero !! Minimum Range (m) !! Maximum Range (m)  \n"

    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.falloffStartRange.." || "..r.falloffEndRange.." \n"
    end
    z = z.."|}"
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local tableDefs = {
    	{
            title   = "Base Alt-Fire Falloff Range",
            columns = {
                { label = "Minimum Range (m)", field = "altFalloffStartRange"  },
                { label = "Maximum Range (m)", field = "altFalloffEndRange"  },
                { label = "Explosion Radius (m)", field = "altExplosionRadius"  },
            }
        }    
    }

    local output = ""

    for _, tableDef in ipairs(tableDefs) do
        local filteredRows = {}
        for _, r in ipairs(rows) do
            local hasValue = false
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 then
                    hasValue = true
                    break
                end
            end
            if hasValue then
                table.insert(filteredRows, r)
            end
        end

        if #filteredRows > 0 then
            local header = ""
            for _, col in ipairs(tableDef.columns) do
                header = header.." !! "..col.label
            end

            local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
                "|+ "..tableDef.title.." \n"..
                "! Hero"..header.." \n"

            for _, r in ipairs(filteredRows) do
                local cells = ""
                for _, col in ipairs(tableDef.columns) do
                    cells = cells.." || "..r[col.field]
                end
                t = t..
                    "|- \n"..
                    "| style=\"text-align:left;\" | "..
                        frame:expandTemplate{title="PageRef", args={r.name}}..
                        cells.." \n"
            end

            t = t.."|}"
            if output ~= "" then
                output = output.."\n\n"
            end
            output = output..t
        end
    end

    return z .. "\n\n" .. output
end

p.heroBulletDamageTable = function(frame)
    -- Pull data
    local heroBulletDamageData = heroDataArray({"Name", "Weapon>BulletDamage", "LevelScaling>BulletDamage", "Weapon>AltFire>BulletDamage", "LevelScaling>BulletDamageAltFire"})

    local rows = {}
    for name, data in pairs(heroBulletDamageData) do
        table.insert(rows, {
            name = name,
            bulletDamage = tonumber(data["Weapon>BulletDamage"]) or 0,
            scalingDamage = tonumber(data["LevelScaling>BulletDamage"]) or 0,
            altBulletDamage = tonumber(data["Weapon>AltFire>BulletDamage"]) or 0,
            altScalingDamage =  tonumber(data["LevelScaling>BulletDamageAltFire"]) or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Hero Base Bullet Damage Stats \n"..
        "! Hero !! Starting !! Added per Boon !! At Max Boon \n"

    -- Iterate over heroes adding a new row to the wikitable for each
    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.bulletDamage.." || +"..r.scalingDamage.." || "..
                tostring(roundValue(r.bulletDamage + r.scalingDamage * soul_unlock.get_max('PowerIncrease'))).." \n"
    end
    
    z = z.."|}"
    return z
end

p.heroLightMeleeTable = function(frame)
    -- Pull data
    local heroLightMeleeData = heroDataArray({"Name", "LightMeleeDamage", "LevelScaling>LightMeleeDamage"})

    local rows = {}
    for name, data in pairs(heroLightMeleeData) do
        table.insert(rows, {
            name = name,
            lightMeleeDamage = tonumber(data["LightMeleeDamage"]) or 0,
            scalingDamage = tonumber(data["LevelScaling>LightMeleeDamage"]) or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Hero Base Light Melee Stats \n"..
        "! Hero !! Starting !! Added per Boon !! At Max Boon \n"

    -- Iterate over heroes adding a new row to the wikitable for each
    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.lightMeleeDamage.." || +"..r.scalingDamage.." || "..
                tostring(roundValue(r.lightMeleeDamage + r.scalingDamage * soul_unlock.get_max('PowerIncrease'))).." \n"
    end
    
    z = z.."|}"
    return z
end

p.heroHeavyMeleeTable = function(frame)
    -- Pull data
    local heroHeavyMeleeData = heroDataArray({"Name", "HeavyMeleeDamage", "LevelScaling>HeavyMeleeDamage"})

    local rows = {}
    for name, data in pairs(heroHeavyMeleeData) do
        table.insert(rows, {
            name = name,
            heavyMeleeDamage = tonumber(data["HeavyMeleeDamage"]) or 0,
            scalingDamage = tonumber(data["LevelScaling>HeavyMeleeDamage"]) or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Hero Base Heavy Melee Stats \n"..
        "! Hero !! Starting !! Added per Boon !! At Max Boon \n"

    -- Iterate over heroes adding a new row to the wikitable for each
    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.heavyMeleeDamage.." || +"..r.scalingDamage.." || "..
                tostring(roundValue(r.heavyMeleeDamage + r.scalingDamage * soul_unlock.get_max('PowerIncrease'))).." \n"
    end
    
    z = z.."|}"
    return z
end

p.heroStaminaTable = function(frame)
    local heroStaminaData = heroDataArray({"Name", "Stamina", "StaminaCooldown"})

    local rows = {}
    for name, data in pairs(heroStaminaData) do
        table.insert(rows, {
            name = name,
            stamina = data["Stamina"] or 0,
            staminaCooldown = data["StaminaCooldown"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Hero Base Stamina Stats \n"..
        "! Hero !! Stamina Bars !! Cooldown (s) \n"

    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                " || "..r.stamina.." || "..r.staminaCooldown.." \n"
    end
    z = z.."|}"
    return z
end

p.heroDashTable = function(frame)
    local heroDashData = heroDataArray({"Name", "GroundDashSpeed"})

    local rows = {}
    for name, data in pairs(heroDashData) do
        table.insert(rows, {
            name     = name,
            duration = tonumber(data.GroundDashSpeed) or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = '{| class="wikitable sortable mw-collapsible" style="text-align:center"\n' ..
              '|+ Hero Base Dash Speed Stats\n' ..
              '! Hero !! Dash Speed (m/s)\n'
    for _, r in ipairs(rows) do
        z = z .. '|-\n| style="text-align:left;" | ' ..
              frame:expandTemplate{title = 'PageRef', args = {r.name}} ..
              ' || ' .. r.duration .. '\n'
    end
    return z .. '|}'
end

p.heroReloadTable = function(frame)
    local heroReloadData = heroDataArray({"Name", "Weapon>ReloadTime"})

    -- 1. collect rows
    local rows = {}
    for name, data in pairs(heroReloadData) do
        table.insert(rows, {
            name       = name,
            reloadTime = tonumber(data["Weapon>ReloadTime"]) or 0
        })
    end

    -- 2. alphabetical by hero name
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    -- 3. build table
    local z = '{| class="wikitable sortable mw-collapsible" style="text-align:center"\n' ..
              '|+ Hero Base Reload Time Stats\n' ..
              '! Hero !! Reload Time (s) \n'

    for _, r in ipairs(rows) do
        z = z ..
            '|-\n' ..
            '| style="text-align:left;" | ' ..
            frame:expandTemplate{title = 'PageRef', args = {r.name}} ..
            ' || '.. r.reloadTime ..' \n'
    end
    return z .. '|}'
end

p.heroReloadDelayTable = function(frame)
    local heroReloadDelayData = heroDataArray({"Name", "Weapon>ReloadDelay"})

    local rows = {}
    for name, data in pairs(heroReloadDelayData) do
        table.insert(rows, {
            name = name,
            reloadDelay  = data["Weapon>ReloadDelay"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local tableDefs = {
    	{
            title   = "Hero Base Reload Delay Stats",
            columns = {
                { label = "Reload Delay (s)", field = "reloadDelay"  }
            }
        }    
    }

    local output = ""

    for _, tableDef in ipairs(tableDefs) do
        local filteredRows = {}
        for _, r in ipairs(rows) do
            local hasValue = false
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 then
                    hasValue = true
                    break
                end
            end
            if hasValue then
                table.insert(filteredRows, r)
            end
        end

        if #filteredRows > 0 then
            local header = ""
            for _, col in ipairs(tableDef.columns) do
                header = header.." !! "..col.label
            end

            local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
                "|+ "..tableDef.title.." \n"..
                "! Hero"..header.." \n"

            for _, r in ipairs(filteredRows) do
                local cells = ""
                for _, col in ipairs(tableDef.columns) do
                    cells = cells.." || "..r[col.field]
                end
                t = t..
                    "|- \n"..
                    "| style=\"text-align:left;\" | "..
                        frame:expandTemplate{title="PageRef", args={r.name}}..
                        cells.." \n"
            end

            t = t.."|}"
            if output ~= "" then
                output = output.."\n\n"
            end
            output = output..t
        end
    end

    return output
end

p.heroMoveSpeedTable = function(frame)
    local heroMoveSpeedData = heroDataArray({"Name", "MaxMoveSpeed", "SprintSpeed"})

    local rows = {}
    for name, data in pairs(heroMoveSpeedData) do
        table.insert(rows, {
            name = name,
            moveSpeed = tonumber(data["MaxMoveSpeed"]) or 0,
            sprintSpeed = tonumber(data["SprintSpeed"]) or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local z = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
        "|+ Hero Base Move Speed Stats \n"..
        "! Hero !! Move Speed (m/s) !! Sprint Speed (m/s) !! Total Speed (m/s) \n"

    for _, r in ipairs(rows) do
        z = z..
            "|- \n"..
            "| style=\"text-align:left;\" | "..
                frame:expandTemplate{title="PageRef", args={r.name}}..
                 " || "..r.moveSpeed.." || "..r.sprintSpeed.." || "..r.moveSpeed + r.sprintSpeed .." \n"
    end
    z = z.."|}"
    return z
end

p.heroDamageResistTable = function(frame)
    local heroDamageResistData = heroDataArray({"Name", "BulletResist", "LevelScaling>BulletResist", "TechResist", "LevelScaling>TechResist", "MeleeResist", "CritDamageReceivedPercent"})

    local rows = {}
    for name, data in pairs(heroDamageResistData) do
        table.insert(rows, {
            name = name,
            bulletResist  = data["BulletResist"] or 0,
            bulletScaling = data["LevelScaling>BulletResist"] or 0,
            spiritResist  = data["TechResist"] or 0,
            spiritScaling = data["LevelScaling>TechResist"] or 0,
            meleeResist   = data["MeleeResist"] or 0,
            critReduction = data["CritDamageReceivedPercent"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local tableDefs = {
        {
            title   = "Hero Base Bullet Resist Stats",
            columns = {
                { label = "Bullet Resist (%)", field = "bulletResist"  },
                { label = "BR per Boon (%)",   field = "bulletScaling" },
            }
        },
        {
            title   = "Hero Base Spirit Resist Stats",
            columns = {
                { label = "Spirit Resist (%)", field = "spiritResist"  },
                { label = "SR per Boon (%)",   field = "spiritScaling" },
            }
        },
        {
            title   = "Hero Base Melee Resist Stats",
            columns = {
                { label = "Melee Resist (%)", field = "meleeResist" },
            }
        },
        {
            title   = "Hero Base Crit Reduction Stats",
            columns = {
                { label = "Crit Reduction (%)", field = "critReduction" },
            }
        },
    }

    local output = ""

    for _, tableDef in ipairs(tableDefs) do
        local filteredRows = {}
        for _, r in ipairs(rows) do
            local hasValue = false
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 then
                    hasValue = true
                    break
                end
            end
            if hasValue then
                table.insert(filteredRows, r)
            end
        end

        if #filteredRows > 0 then
            local header = ""
            for _, col in ipairs(tableDef.columns) do
                header = header.." !! "..col.label
            end

            local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
                "|+ "..tableDef.title.." \n"..
                "! Hero"..header.." \n"

            for _, r in ipairs(filteredRows) do
                local cells = ""
                for _, col in ipairs(tableDef.columns) do
                    cells = cells.." || "..r[col.field]
                end
                t = t..
                    "|- \n"..
                    "| style=\"text-align:left;\" | "..
                        frame:expandTemplate{title="PageRef", args={r.name}}..
                        cells.." \n"
            end

            t = t.."|}"
            if output ~= "" then
                output = output.."\n\n"
            end
            output = output..t
        end
    end

    return output
end

p.heroCritTable = function(frame)
    local heroCritData = heroDataArray({"Name", "CritDamageBonusPercent"})

    local rows = {}
    for name, data in pairs(heroCritData) do
        table.insert(rows, {
            name = name,
            critBonusScale  = data["CritDamageBonusPercent"] or 0
        })
    end
    table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)

    local tableDefs = {
    	{
            title   = "Hero Base Crit Bonus Scale Stats",
            columns = {
                { label = "Crit Bonus Scale (%)", field = "critBonusScale"  }
            }
        }    
    }

    local output = ""

    for _, tableDef in ipairs(tableDefs) do
        local filteredRows = {}
        for _, r in ipairs(rows) do
            local hasValue = false
            for _, col in ipairs(tableDef.columns) do
                if r[col.field] ~= 0 then
                    hasValue = true
                    break
                end
            end
            if hasValue then
                table.insert(filteredRows, r)
            end
        end

        if #filteredRows > 0 then
            local header = ""
            for _, col in ipairs(tableDef.columns) do
                header = header.." !! "..col.label
            end

            local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n"..
                "|+ "..tableDef.title.." \n"..
                "! Hero"..header.." \n"

            for _, r in ipairs(filteredRows) do
                local cells = ""
                for _, col in ipairs(tableDef.columns) do
                    cells = cells.." || "..r[col.field]
                end
                t = t..
                    "|- \n"..
                    "| style=\"text-align:left;\" | "..
                        frame:expandTemplate{title="PageRef", args={r.name}}..
                        cells.." \n"
            end

            t = t.."|}"
            if output ~= "" then
                output = output.."\n\n"
            end
            output = output..t
        end
    end

    return output
end

return p