Module:HeroDataArrays: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
added damage falloff to falloff range table
added alt fire stats to all tables where applicable
 
(25 intermediate revisions by 3 users not shown)
Line 14: Line 14:
         end
         end
     end
     end
end
-- Use sort name for edge cases like "The Doorman"
local function sortHeroes(rows)
    local function getSortName(name)
        return (name:gsub("^The ", ""))
    end
    table.sort(rows, function(a, b)
        local nameA = getSortName(a.sortName or a.name or "")
        local nameB = getSortName(b.sortName or b.name or "")
        return nameA:lower() < nameB:lower()
    end)
end
end


Line 20: Line 33:
     if type(value) == "number" then
     if type(value) == "number" then
         return util_module.round_to_sig_fig(value, 3)
         return util_module.round_to_sig_fig(value, 3)
    end
    return value
end
-- Round to 4 digits
local function roundValueFour(value)
    if type(value) == "number" then
        return util_module.round_to_sig_fig(value, 4)
    end
    return value
end
-- Round to an integer (up to 2 digits)
local function roundValueInteger(value)
    if type(value) == "number" then
        return util_module.round_to_sig_fig(value, 2)
     end
     end
     return value
     return value
Line 72: Line 101:
             cells = cells .. " || " .. (r[col.field] or 0)
             cells = cells .. " || " .. (r[col.field] or 0)
         end
         end
        -- A row may carry an alternate sort key and a label suffix, used by
        -- secondary fire rows so they stay underneath their hero's main row
        local nameCell = "| style=\"text-align:left;\""
        if r.sortName then
            nameCell = nameCell .. " data-sort-value=\"" .. r.sortName .. "\""
        end
        nameCell = nameCell .. " | " ..
            frame:expandTemplate{ title = "PageRef", args = { r.name } } ..
            (r.nameSuffix or "")


         t = t ..
         t = t ..
             "|- \n" ..
             "|- \n" ..
             "| style=\"text-align:left;\" | " ..
             nameCell ..
            frame:expandTemplate{ title = "PageRef", args = { r.name } } ..
             cells .. " \n"
             cells .. " \n"
     end
     end
Line 125: Line 163:
         table.insert(rows, {
         table.insert(rows, {
             name = name,
             name = name,
             maxHealth = tonumber(data["MaxHealth"]) or 0,
             maxHealth = data["MaxHealth"] or 0,
             scalingHealth = "+" .. tonumber(data["LevelScaling>MaxHealth"]) or 0,
             scalingHealth = "+" .. data["LevelScaling>MaxHealth"] or 0,
             maxScalingHealth = tonumber(roundValue(data["MaxHealth"] + data["LevelScaling>MaxHealth"] * soul_unlock.get_max("PowerIncrease"))) or 0
             maxScalingHealth = (data["MaxHealth"] + data["LevelScaling>MaxHealth"] * soul_unlock.get_max("PowerIncrease")) or 0
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 162: Line 200:
             name = name,
             name = name,
             baseHealthRegen = data["BaseHealthRegen"] or 0,
             baseHealthRegen = data["BaseHealthRegen"] or 0,
             spiritHealthRegen = "+".. (tonumber(data["SpiritScaling>BaseHealthRegen"]) or 0),
             spiritHealthRegen = "+".. (data["SpiritScaling>BaseHealthRegen"] or 0),
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 210: Line 248:
             altBulletsPerShot = data["Weapon>AltFire>BulletsPerShot"] or 0
             altBulletsPerShot = data["Weapon>AltFire>BulletsPerShot"] or 0
         })
         })
       
        local altBulletDamage = data["Weapon>AltFire>BulletDamage"]
        local altDPS = data["Weapon>AltFire>DPS"]
        local altRoundsPerSecond = data["Weapon>AltFire>RoundsPerSecond"]
        local altBulletsPerShot = data["Weapon>AltFire>BulletsPerShot"]
        if altBulletDamage and altBulletDamage ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                DPS = altDPS,
                reloadDelay = data["Weapon>ReloadDelay"] or 0,
                reloadTime = data["Weapon>ReloadTime"] or 0,
                clipSize = data["Weapon>ClipSize"] or 0,
                roundsPerSecond = altRoundsPerSecond,
                bulletDamage = altBulletDamage,
                bulletsPerShot = altBulletsPerShot
            })
        end
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 225: Line 282:
                 { label = "Bullets Per Shot", field = "bulletsPerShot"  },
                 { label = "Bullets Per Shot", field = "bulletsPerShot"  },
             }
             }
         },
         }   
    {
            title   = "Base Alt-Fire Damage Stats",
            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"  },
            }
        }   
     }
     }


Line 263: Line 310:
             altBulletSpeed = data["Weapon>AltFire>BulletSpeed"] or 0
             altBulletSpeed = data["Weapon>AltFire>BulletSpeed"] or 0
         })
         })
       
        local altBulletSpeed = data["Weapon>AltFire>BulletSpeed"]
        if altBulletSpeed and altBulletSpeed ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                bulletSpeed = altBulletSpeed
            })
        end
     end
     end


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


     local tableDefs = {
     local tableDefs = {
Line 274: Line 329:
             columns = {
             columns = {
                 { label = "m/s", field = "bulletSpeed" }
                 { label = "m/s", field = "bulletSpeed" }
            }
        },
        {
            title = "Base Alt-Fire Bullet Velocity",
            columns = {
                { label = "m/s", field = "altBulletSpeed" }
             }
             }
         }
         }
Line 303: Line 352:
             name = name,
             name = name,
             clipSize = data["Weapon>ClipSize"] or 0,
             clipSize = data["Weapon>ClipSize"] or 0,
             spiritClipSize = "+".. (tonumber(data["SpiritScaling>ClipSize"]) or 0),
             spiritClipSize = "+".. (data["SpiritScaling>ClipSize"] or 0),
             bulletsPerBurst = data["Weapon>BulletsPerBurst"] or 0,
             bulletsPerBurst = data["Weapon>BulletsPerBurst"] or 0,
             burstClipSize = data["Weapon>ClipSize"] or 0 / data["Weapon>BulletsPerBurst"] or 0,
             burstClipSize = data["Weapon>ClipSize"] or 0 / data["Weapon>BulletsPerBurst"] or 0,
             ammoConsumedPerShot = data["Weapon>AltFire>AmmoConsumedPerShot"] or 0
             ammoConsumedPerShot = data["Weapon>AltFire>AmmoConsumedPerShot"] or 0
         })
         })
       
        local ammoConsumedPerShot = data["Weapon>AltFire>AmmoConsumedPerShot"]
        if ammoConsumedPerShot and ammoConsumedPerShot ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                bulletsPerBurst = ammoConsumedPerShot,
                clipSize = data["Weapon>ClipSize"] or 0,
                spiritClipSize = "+".. (data["SpiritScaling>ClipSize"] or 0),
            })
        end
     end
     end
   
   
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 320: Line 381:
                 { label = "Spirit Scaling", field = "spiritClipSize"  },
                 { label = "Spirit Scaling", field = "spiritClipSize"  },
             }
             }
         },
         }  
    {
            title  = "Alt-Fire Ammo Cost",
            columns = {
                { label = "Ammo Per Shot", field = "ammoConsumedPerShot"  },
            }
        }   
     }
     }


Line 349: Line 404:
             roundsPerSecond = data["Weapon>RoundsPerSecond"] or 0,
             roundsPerSecond = data["Weapon>RoundsPerSecond"] or 0,
             altRoundsPerSecond = data["Weapon>AltFire>RoundsPerSecond"] or 0,
             altRoundsPerSecond = data["Weapon>AltFire>RoundsPerSecond"] or 0,
             spiritRoundsPerSecond = "+".. (tonumber(data["SpiritScaling>RoundsPerSecond"]) or 0),
             spiritRoundsPerSecond = "+".. (data["SpiritScaling>RoundsPerSecond"] or 0),
             spiritFireRate = "+".. (tonumber(data["SpiritScaling>FireRate"]) or 0),
             spiritFireRate = "+".. (data["SpiritScaling>FireRate"] or 0),
         })
         })
       
        local altRoundsPerSecond = data["Weapon>AltFire>RoundsPerSecond"]
        if altRoundsPerSecond and altRoundsPerSecond ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                roundsPerSecond = altRoundsPerSecond,
                spiritRoundsPerSecond = "+".. (data["SpiritScaling>RoundsPerSecond"] or 0),
            })
        end
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 362: Line 428:
                 { label = "Spirit Scaling", field = "spiritRoundsPerSecond"  },
                 { label = "Spirit Scaling", field = "spiritRoundsPerSecond"  },
             }
             }
         },
         }   
    {
            title  = "Base Alt-Fire Fire Rate",
            columns = {
                { label = "Rounds/s", field = "altRoundsPerSecond" },
            }
        }   
     }
     }


Line 384: Line 444:
p.heroFalloffRangeTable = function(frame)
p.heroFalloffRangeTable = function(frame)
     local heroFalloffRangeData = heroDataArray({"Name", "Weapon>FalloffStartRange", "Weapon>FalloffEndRange",  "Weapon>AltFire>FalloffStartRange", "Weapon>AltFire>FalloffEndRange",
     local heroFalloffRangeData = heroDataArray({"Name", "Weapon>FalloffStartRange", "Weapon>FalloffEndRange",  "Weapon>AltFire>FalloffStartRange", "Weapon>AltFire>FalloffEndRange",
     "Weapon>AltFire>ExplosionRadius", "Weapon>FalloffEndScale"})
     "Weapon>AltFire>ExplosionRadius", "Weapon>FalloffEndScale", "Weapon>AltFire>ExplosionDamageScaleAtMaxRadius",})
      
      
     local rows = {}
     local rows = {}
Line 392: Line 452:
             falloffStartRange = data["Weapon>FalloffStartRange"] or 0,
             falloffStartRange = data["Weapon>FalloffStartRange"] or 0,
             falloffEndRange = data["Weapon>FalloffEndRange"] or 0,
             falloffEndRange = data["Weapon>FalloffEndRange"] or 0,
             falloffEndScale = -100 + (100 * tonumber(data["Weapon>FalloffEndScale"] or 0)),
             falloffEndScale = -100 + (100 * (data["Weapon>FalloffEndScale"] or 0)),
             altFalloffStartRange = data["Weapon>AltFire>FalloffStartRange"] or 0,
             altFalloffStartRange = data["Weapon>AltFire>FalloffStartRange"] or 0,
             altFalloffEndRange = data["Weapon>AltFire>FalloffEndRange"] or 0,
             altFalloffEndRange = data["Weapon>AltFire>FalloffEndRange"] or 0,
             altExplosionRadius = data["Weapon>AltFire>ExplosionRadius"] or 0
             altExplosionRadius = 0,
            altExplosionEndScale = (100 * (data["Weapon>AltFire>ExplosionDamageScaleAtMaxRadius"] or 0))
         })
         })
   
        local altFalloffStartRange = data["Weapon>AltFire>FalloffStartRange"]
        local altFalloffEndRange = data["Weapon>AltFire>FalloffEndRange"]
        local altExplosionRadius = data["Weapon>AltFire>ExplosionRadius"] or 0
        local altExplosionEndScale = (100 * (data["Weapon>AltFire>ExplosionDamageScaleAtMaxRadius"] or 0))
        if altFalloffStartRange and altFalloffStartRange ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                falloffStartRange = altFalloffStartRange,
                falloffEndRange = altFalloffEndRange,
                altExplosionRadius = data["Weapon>AltFire>ExplosionRadius"] or 0,
                falloffEndScale = -100 + (100 * (data["Weapon>FalloffEndScale"] or 0))
            })
        end
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 406: Line 483:
                 { label = "Minimum Range (m)", field = "falloffStartRange"  },
                 { label = "Minimum Range (m)", field = "falloffStartRange"  },
                 { label = "Maximum Range (m)", field = "falloffEndRange"  },
                 { label = "Maximum Range (m)", field = "falloffEndRange"  },
                 { label = "Damage Dropoff (%)", field = "falloffEndScale"  },
                 { label = "Damage Falloff (%)", field = "falloffEndScale"  },
            }
        } ,
    {
            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"  },
                 { label = "Explosion Radius (m)", field = "altExplosionRadius"  },
             }
             }
Line 439: Line 509:
         table.insert(rows, {
         table.insert(rows, {
             name = name,
             name = name,
             bulletDamage = tonumber(data["Weapon>BulletDamage"]) or 0,
             bulletDamage = data["Weapon>BulletDamage"] or 0,
             bulletDamagePerShot = ((data["Weapon>BulletDamage"] or 0) * (data["Weapon>BulletsPerShot"] or 0) * (data["Weapon>BulletsPerBurst"] or 0)),
             bulletDamagePerShot = ((data["Weapon>BulletDamage"] or 0) * (data["Weapon>BulletsPerShot"] or 0) * (data["Weapon>BulletsPerBurst"] or 0)),
             scalingDamage = "+" .. tonumber(data["LevelScaling>BulletDamage"]) or 0,
             scalingDamage = "+" .. data["LevelScaling>BulletDamage"] or 0,
             maxScalingDamage =  tonumber(roundValue(data["Weapon>BulletDamage"] + data["LevelScaling>BulletDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
             maxScalingDamage =  (roundValue(data["Weapon>BulletDamage"] + data["LevelScaling>BulletDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
             altBulletDamage = tonumber(data["Weapon>AltFire>BulletDamage"]) or 0,
             altBulletDamage = data["Weapon>AltFire>BulletDamage"] or 0,
             spiritScalingDamage = "+" .. (tonumber(data["SpiritScaling>BulletDamage"]) or 0),
             spiritScalingDamage = "+" .. (data["SpiritScaling>BulletDamage"] or 0),
         })
         })
        -- Secondary fire is listed as its own row, right under the hero's main row
        local altDamage = data["Weapon>AltFire>BulletDamage"]
        if altDamage and altDamage ~= 0 then
            local altScaling = data["LevelScaling>BulletDamageAltFire"] or 0
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                bulletDamage = altDamage,
                scalingDamage = "+" .. altScaling,
                maxScalingDamage = roundValue(altDamage + altScaling * soul_unlock.get_max("PowerIncrease")),
                spiritScalingDamage = "+0",
            })
        end
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 461: Line 546:
     }
     }


    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.heroMeleeTable = function(frame)
    -- Pull data
    local heroMeleeData = heroDataArray({"Name", "LightMeleeDamage", "LevelScaling>LightMeleeDamage", "HeavyMeleeDamage", "LevelScaling>HeavyMeleeDamage", "SpiritScaling>HeavyMeleeDamage"})
    local rows = {}
    for name, data in pairs(heroMeleeData) do
        table.insert(rows, {
            name = name,
            lightMeleeDamage = data["LightMeleeDamage"] or 0,
            lightScalingDamage = "+" .. data["LevelScaling>LightMeleeDamage"] or 0,
            lightMaxScalingDamage = (roundValueFour(data["LightMeleeDamage"] + data["LevelScaling>LightMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
            heavyMeleeDamage = data["HeavyMeleeDamage"] or 0,
            heavyScalingDamage = "+" .. data["LevelScaling>HeavyMeleeDamage"] or 0,
            heavyMaxScalingDamage =  (roundValueFour(data["HeavyMeleeDamage"] + data["LevelScaling>HeavyMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
            heavySpiritScalingDamage = "+" .. (data["SpiritScaling>HeavyMeleeDamage"] or 0),
        })
    end
    sortHeroes(rows)
    local tableDefs = {
    {
            title  = "Base Light & Heavy Melee Stats",
            columns = {
                { label = "Starting Light", field = "lightMeleeDamage"  },
                { label = "Added per Boon (L)", field = "lightScalingDamage"  },
                { label = "At Max Boon (L)", field = "lightMaxScalingDamage"  },
                { label = "Starting Heavy", field = "heavyMeleeDamage"  },
                { label = "Added per Boon (H)", field = "heavyScalingDamage"  },
                { label = "At Max Boon (H)", field = "heavyMaxScalingDamage"  },
                { label = "Spirit Scaling (H)", field = "heavySpiritScalingDamage"  },
               
            }
        }
    }
   
     local output = {}
     local output = {}
     for _, def in ipairs(tableDefs) do
     for _, def in ipairs(tableDefs) do
Line 480: Line 611:
         table.insert(rows, {
         table.insert(rows, {
             name = name,
             name = name,
             lightMeleeDamage = tonumber(data["LightMeleeDamage"]) or 0,
             lightMeleeDamage = data["LightMeleeDamage"] or 0,
             scalingDamage = "+" .. tonumber(data["LevelScaling>LightMeleeDamage"]) or 0,
             scalingDamage = "+" .. data["LevelScaling>LightMeleeDamage"] or 0,
             maxScalingDamage =  tonumber(roundValue(data["LightMeleeDamage"] + data["LevelScaling>LightMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
             maxScalingDamage =  (roundValueFour(data["LightMeleeDamage"] + data["LevelScaling>LightMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 517: Line 648:
         table.insert(rows, {
         table.insert(rows, {
             name = name,
             name = name,
             heavyMeleeDamage = tonumber(data["HeavyMeleeDamage"]) or 0,
             heavyMeleeDamage = data["HeavyMeleeDamage"] or 0,
             scalingDamage = "+" .. tonumber(data["LevelScaling>HeavyMeleeDamage"]) or 0,
             scalingDamage = "+" .. data["LevelScaling>HeavyMeleeDamage"] or 0,
             maxScalingDamage = tonumber(roundValue(data["HeavyMeleeDamage"] + data["LevelScaling>HeavyMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
             maxScalingDamage = (roundValueFour(data["HeavyMeleeDamage"] + data["LevelScaling>HeavyMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
             spiritScalingDamage = "+" .. (tonumber(data["SpiritScaling>HeavyMeleeDamage"]) or 0),
             spiritScalingDamage = "+" .. (data["SpiritScaling>HeavyMeleeDamage"] or 0),
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 559: Line 690:
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 583: Line 714:


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


     local rows = {}
     local rows = {}
Line 589: Line 720:
         table.insert(rows, {
         table.insert(rows, {
             name    = name,
             name    = name,
             dashSpeed = data["GroundDashSpeed"] or 0
             dashSpeed = data["GroundDashSpeed"] or 0,
            dashDuration = data["GroundDashDuration"] or 0,
            airDashSpeed = data["AirDashSpeed"] or 0,
            airDashDuration = data["AirDashDuration"] or 0,
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)
      
      
     local tableDefs = {
     local tableDefs = {
Line 599: Line 733:
             columns = {
             columns = {
                 { label = "Dash Speed (m/s)", field = "dashSpeed"  },
                 { label = "Dash Speed (m/s)", field = "dashSpeed"  },
                { label = "Dash Duration (s)", field = "dashDuration"  },
                { label = "Air Dash Speed (m/s)", field = "airDashSpeed"  },
                { label = "Air Dash Duration (s)", field = "airDashDuration"  },
             }
             }
         }  
         }  
Line 621: Line 758:
         table.insert(rows, {
         table.insert(rows, {
             name      = name,
             name      = name,
             reloadTime = tonumber(data["Weapon>ReloadTime"]) or 0
             reloadTime = data["Weapon>ReloadTime"] or 0
         })
         })
     end
     end


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


     local tableDefs = {
     local tableDefs = {
Line 657: Line 794:
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 680: Line 817:


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


     local rows = {}
     local rows = {}
Line 686: Line 823:
         table.insert(rows, {
         table.insert(rows, {
             name = name,
             name = name,
             moveSpeed = tonumber(data["MaxMoveSpeed"]) or 0,
             moveSpeed = data["MaxMoveSpeed"] or 0,
             sprintSpeed = tonumber(data["SprintSpeed"]) or 0,
             sprintSpeed = data["SprintSpeed"] or 0,
             totalSpeed = tonumber(data["MaxMoveSpeed"] + data["SprintSpeed"]) or 0,
             totalSpeed = data["MaxMoveSpeed"] + data["SprintSpeed"] or 0,
             spiritMoveSpeed = "+" .. (tonumber(data["SpiritScaling>MaxMoveSpeed"]) or 0),
            shootSpeed = (data["Weapon>ShootMoveSpeed"] or 0) * 100,
             spiritSprintSpeed = "+" .. (tonumber(data["SpiritScaling>SprintSpeed"]) or 0),
            totalShootingSpeed = roundValue((data["MaxMoveSpeed"] + data["SprintSpeed"] or 0) * (data["Weapon>ShootMoveSpeed"] or 0)),
             spiritMoveSpeed = "+" .. (data["SpiritScaling>MaxMoveSpeed"] or 0),
             spiritSprintSpeed = "+" .. (data["SpiritScaling>SprintSpeed"] or 0),
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 702: Line 841:
                 { label = "Sprint Speed (m/s)", field = "sprintSpeed"  },
                 { label = "Sprint Speed (m/s)", field = "sprintSpeed"  },
                 { label = "Total Speed (m/s)", field = "totalSpeed"  },
                 { label = "Total Speed (m/s)", field = "totalSpeed"  },
                { label = "Shooting Speed (m/s)", field = "totalShootingSpeed"  },
                { label = "Shooting Mult (%)", field = "shootSpeed"  },
                 { label = "MS Spirit Scaling", field = "spiritMoveSpeed"  },
                 { label = "MS Spirit Scaling", field = "spiritMoveSpeed"  },
                 { label = "SS Spirit Scaling", field = "spiritSprintSpeed"  },
                 { label = "SS Spirit Scaling", field = "spiritSprintSpeed"  },
Line 730: Line 871:
             bulletResist  = data["BulletResist"] or 0,
             bulletResist  = data["BulletResist"] or 0,
             bulletResistScaling = "+".. (data["LevelScaling>BulletResist"] or 0),
             bulletResistScaling = "+".. (data["LevelScaling>BulletResist"] or 0),
             bulletResistScalingMax = (tonumber(roundValue((data["BulletResist"] or 0) + (data["LevelScaling>BulletResist"] or 0) * soul_unlock.get_max("PowerIncrease"))) or 0),
             bulletResistScalingMax = ((roundValue((data["BulletResist"] or 0) + (data["LevelScaling>BulletResist"] or 0) * soul_unlock.get_max("PowerIncrease"))) or 0),
             bulletResistSpiritScaling = "+".. (data["SpiritScaling>BulletResist"] or 0),
             bulletResistSpiritScaling = "+".. (data["SpiritScaling>BulletResist"] or 0),
             spiritResist  = data["TechResist"] or 0,
             spiritResist  = data["TechResist"] or 0,
             spiritResistScaling = "+".. (data["LevelScaling>TechResist"] or 0),
             spiritResistScaling = "+".. (data["LevelScaling>TechResist"] or 0),
             spiritResistScalingMax = (tonumber(roundValue((data["TechResist"] or 0) + (data["LevelScaling>TechResist"] or 0) * soul_unlock.get_max("PowerIncrease"))) or 0),
             spiritResistScalingMax = ((roundValue((data["TechResist"] or 0) + (data["LevelScaling>TechResist"] or 0) * soul_unlock.get_max("PowerIncrease"))) or 0),
             spiritResistSpiritScaling = "+".. (data["SpiritScaling>TechResist"] or 0),
             spiritResistSpiritScaling = "+".. (data["SpiritScaling>TechResist"] or 0),
             meleeResist  = data["MeleeResist"] or 0,
             meleeResist  = data["MeleeResist"] or 0,
Line 740: Line 881:
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 796: Line 937:
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 826: Line 967:
             name = name,
             name = name,
             spiritScaling  = "+".. data["LevelScaling>TechPower"] or 0,
             spiritScaling  = "+".. data["LevelScaling>TechPower"] or 0,
             maxSpiritScaling = tonumber(data["LevelScaling>TechPower"] * soul_unlock.get_max("PowerIncrease")) or 0,
             maxSpiritScaling = (data["LevelScaling>TechPower"] * soul_unlock.get_max("PowerIncrease")) or 0,
         })
         })
     end
     end
     table.sort(rows, function(a, b) return a.name:lower() < b.name:lower() end)
     sortHeroes(rows)


     local tableDefs = {
     local tableDefs = {
Line 839: Line 980:
             }
             }
         }     
         }     
    }
    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.heroGravityTable = function(frame)
    local heroGravityData = heroDataArray({"Name", "GravityChange"})
    local rows = {}
    for name, data in pairs(heroGravityData) do
        table.insert(rows, {
            name = name,
            gravityScale  = (data["GravityChange"] or 0),
        })
    end
    sortHeroes(rows)
    local tableDefs = {
    {
            title  = "Base Gravity Scale Stats",
            columns = {
                { label = "Gravity Scale (%)", field = "gravityScale"  },
            }
        }   
    }
    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.bulletGravityTable = function(frame)
    local heroGravityData = heroDataArray({"Name", "Weapon>BulletGravityScale", "Weapon>AltFire>BulletGravityScale"})
    local rows = {}
    for name, data in pairs(heroGravityData) do
        table.insert(rows, {
            name = name,
            gravityScale  = (data["Weapon>BulletGravityScale"] or 0),
            altGravityScale  = (data["Weapon>AltFire>BulletGravityScale"] or 0),
        })
    end
    sortHeroes(rows)
    local tableDefs = {
    {
            title  = "Base Bullet Gravity Scale Stats",
            columns = {
                { label = "Gravity Scale", field = "gravityScale"  },
            }
        },
        {
            title  = "Base Alt-fire Bullet Gravity Scale Stats",
            columns = {
                { label = "Gravity Scale", field = "altGravityScale"  },
            }
        }
    }
    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.bulletRadiusTable = function(frame)
    local heroBulletRadiusData = heroDataArray({"Name", "Weapon>BulletRadius", "Weapon>AltFire>BulletRadius"})
    local rows = {}
    for name, data in pairs(heroBulletRadiusData) do
        table.insert(rows, {
            name = name,
            bulletRadius  = (data["Weapon>BulletRadius"] or 0) * 100,
            bulletRadiusInches  = roundValueInteger((data["Weapon>BulletRadius"] or 0) * 100 * 0.394),
            altBulletRadius  = (data["Weapon>AltFire>BulletRadius"] or 0) * 100,
            altBulletRadiusInches  = roundValueInteger((data["Weapon>AltFire>BulletRadius"] or 0) * 100 * 0.394),
        })
       
        local altBulletRadius = (data["Weapon>AltFire>BulletRadius"] or 0) * 100
        local altBulletRadiusInches  = roundValueInteger((data["Weapon>AltFire>BulletRadius"] or 0) * 100 * 0.394)
        if altBulletRadius and altBulletRadius ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                bulletRadius = altBulletRadius,
                bulletRadiusInches = altBulletRadiusInches
            })
        end
    end
    sortHeroes(rows)
    local tableDefs = {
    {
            title  = "Base Bullet Radius Stats",
            columns = {
                { label = "Bullet Radius (cm)", field = "bulletRadius"  },
                { label = "Bullet Radius (in)", field = "bulletRadiusInches"  },
            }
        }
     }
     }



Latest revision as of 18:59, 20 August 2026

Overview

[edit source]

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

-- Use sort name for edge cases like "The Doorman"
local function sortHeroes(rows)
    local function getSortName(name)
        return (name:gsub("^The ", ""))
    end

    table.sort(rows, function(a, b)
        local nameA = getSortName(a.sortName or a.name or "")
        local nameB = getSortName(b.sortName or b.name or "")
        return nameA:lower() < nameB:lower()
    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

-- Round to 4 digits
local function roundValueFour(value)
    if type(value) == "number" then
        return util_module.round_to_sig_fig(value, 4)
    end
    return value
end

-- Round to an integer (up to 2 digits)
local function roundValueInteger(value)
    if type(value) == "number" then
        return util_module.round_to_sig_fig(value, 2)
    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
                	if r[col.field] ~= "+0" then
                    hasValue = true
                    break
                    end
                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

        -- A row may carry an alternate sort key and a label suffix, used by
        -- secondary fire rows so they stay underneath their hero's main row
        local nameCell = "| style=\"text-align:left;\""
        if r.sortName then
            nameCell = nameCell .. " data-sort-value=\"" .. r.sortName .. "\""
        end
        nameCell = nameCell .. " | " ..
            frame:expandTemplate{ title = "PageRef", args = { r.name } } ..
            (r.nameSuffix or "")

        t = t ..
            "|- \n" ..
            nameCell ..
            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 = data["MaxHealth"] or 0,
            scalingHealth = "+" .. data["LevelScaling>MaxHealth"] or 0,
            maxScalingHealth = (data["MaxHealth"] + data["LevelScaling>MaxHealth"] * soul_unlock.get_max("PowerIncrease")) or 0
        })
    end
    sortHeroes(rows)

    local tableDefs = {
        {
            title = "Base Health Stats",
            columns = {
                { label = "Starting", field = "maxHealth" },
                { label = "Added per Boon", field = "scalingHealth" },
                { label = "At Max Boon", field = "maxScalingHealth" },
            }
        }
    }
    
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.heroRegenTable = function(frame)
    local heroRegenData = heroDataArray({"Name", "BaseHealthRegen", "SpiritScaling>BaseHealthRegen"})
    
    local rows = {}
    for name, data in pairs(heroRegenData) do
        table.insert(rows, {
            name = name,
            baseHealthRegen = data["BaseHealthRegen"] or 0,
            spiritHealthRegen = "+".. (data["SpiritScaling>BaseHealthRegen"] or 0),
        })
    end
    sortHeroes(rows)

    local tableDefs = {
        {
            title = "Base Health Regen Stats",
            columns = {
                { label = "HP/s", field = "baseHealthRegen" },
                { label = "Spirit Scaling", field = "spiritHealthRegen" }
            }
        }
    }

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.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
        })
        
        local altBulletDamage = data["Weapon>AltFire>BulletDamage"]
        local altDPS = data["Weapon>AltFire>DPS"]
        local altRoundsPerSecond = data["Weapon>AltFire>RoundsPerSecond"]
        local altBulletsPerShot = data["Weapon>AltFire>BulletsPerShot"]
        if altBulletDamage and altBulletDamage ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                DPS = altDPS,
                reloadDelay = data["Weapon>ReloadDelay"] or 0,
                reloadTime = data["Weapon>ReloadTime"] or 0,
                clipSize = data["Weapon>ClipSize"] or 0,
                roundsPerSecond = altRoundsPerSecond,
                bulletDamage = altBulletDamage,
                bulletsPerShot = altBulletsPerShot
            })
        end
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Damage Stats",
            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"  },
            }
        }   
    }

    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
        })
        
        local altBulletSpeed = data["Weapon>AltFire>BulletSpeed"]
        if altBulletSpeed and altBulletSpeed ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                bulletSpeed = altBulletSpeed
            })
        end
    end

    sortHeroes(rows)

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

    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>BulletsPerBurst","Weapon>AltFire>AmmoConsumedPerShot","SpiritScaling>ClipSize"})
    
    local rows = {}
    for name, data in pairs(heroClipSizeData) do
        table.insert(rows, {
            name = name,
            clipSize = data["Weapon>ClipSize"] or 0,
            spiritClipSize = "+".. (data["SpiritScaling>ClipSize"] or 0),
            bulletsPerBurst = data["Weapon>BulletsPerBurst"] or 0,
            burstClipSize = data["Weapon>ClipSize"] or 0 / data["Weapon>BulletsPerBurst"] or 0,
            ammoConsumedPerShot = data["Weapon>AltFire>AmmoConsumedPerShot"] or 0
        })
        
        local ammoConsumedPerShot = data["Weapon>AltFire>AmmoConsumedPerShot"]
        if ammoConsumedPerShot and ammoConsumedPerShot ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                bulletsPerBurst = ammoConsumedPerShot,
                clipSize = data["Weapon>ClipSize"] or 0,
                spiritClipSize = "+".. (data["SpiritScaling>ClipSize"] or 0),
            })
        end
    end
 
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Ammo Count",
            columns = {
                { label = "Ammo", field = "clipSize"  },
                { label = "Ammo Per Shot", field = "bulletsPerBurst"  },
                { label = "Spirit Scaling", field = "spiritClipSize"  },
            }
        } 
    }

    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.heroRoundsPerSecondTable = function(frame)
    local heroRoundsPerSecondData = heroDataArray({"Name", "Weapon>RoundsPerSecond", "Weapon>AltFire>RoundsPerSecond", "SpiritScaling>RoundsPerSecond", "SpiritScaling>FireRate"})
    
    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,
            spiritRoundsPerSecond = "+".. (data["SpiritScaling>RoundsPerSecond"] or 0),
            spiritFireRate = "+".. (data["SpiritScaling>FireRate"] or 0),
        })
        
        local altRoundsPerSecond = data["Weapon>AltFire>RoundsPerSecond"]
        if altRoundsPerSecond and altRoundsPerSecond ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                roundsPerSecond = altRoundsPerSecond,
                spiritRoundsPerSecond = "+".. (data["SpiritScaling>RoundsPerSecond"] or 0),
            })
        end
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Fire Rate",
            columns = {
                { label = "Rounds/s", field = "roundsPerSecond"  },
                { label = "Spirit Scaling", field = "spiritRoundsPerSecond"  },
            }
        }  
    }

    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.heroFalloffRangeTable = function(frame)
    local heroFalloffRangeData = heroDataArray({"Name", "Weapon>FalloffStartRange", "Weapon>FalloffEndRange",  "Weapon>AltFire>FalloffStartRange", "Weapon>AltFire>FalloffEndRange",
    	"Weapon>AltFire>ExplosionRadius", "Weapon>FalloffEndScale", "Weapon>AltFire>ExplosionDamageScaleAtMaxRadius",})
    
    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,
            falloffEndScale = -100 + (100 * (data["Weapon>FalloffEndScale"] or 0)),
            altFalloffStartRange = data["Weapon>AltFire>FalloffStartRange"] or 0,
            altFalloffEndRange = data["Weapon>AltFire>FalloffEndRange"] or 0,
            altExplosionRadius = 0,
            altExplosionEndScale = (100 * (data["Weapon>AltFire>ExplosionDamageScaleAtMaxRadius"] or 0))
        })
    
        local altFalloffStartRange = data["Weapon>AltFire>FalloffStartRange"]
        local altFalloffEndRange = data["Weapon>AltFire>FalloffEndRange"]
        local altExplosionRadius = data["Weapon>AltFire>ExplosionRadius"] or 0
        local altExplosionEndScale = (100 * (data["Weapon>AltFire>ExplosionDamageScaleAtMaxRadius"] or 0))
        if altFalloffStartRange and altFalloffStartRange ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                falloffStartRange = altFalloffStartRange,
                falloffEndRange = altFalloffEndRange,
                altExplosionRadius = data["Weapon>AltFire>ExplosionRadius"] or 0,
                falloffEndScale = -100 + (100 * (data["Weapon>FalloffEndScale"] or 0))
            })
        end
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Falloff Range",
            columns = {
                { label = "Minimum Range (m)", field = "falloffStartRange"  },
                { label = "Maximum Range (m)", field = "falloffEndRange"  },
                { label = "Damage Falloff (%)", field = "falloffEndScale"  },
                { label = "Explosion Radius (m)", field = "altExplosionRadius"  },
            }
        }    
    }

    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.heroBulletDamageTable = function(frame)
    -- Pull data
    local heroBulletDamageData = heroDataArray({"Name", "Weapon>BulletDamage", "LevelScaling>BulletDamage", "Weapon>AltFire>BulletDamage", "LevelScaling>BulletDamageAltFire",
    	"SpiritScaling>BulletDamage", "Weapon>BulletsPerShot", "Weapon>BulletsPerBurst"})

    local rows = {}
    for name, data in pairs(heroBulletDamageData) do
        table.insert(rows, {
            name = name,
            bulletDamage = data["Weapon>BulletDamage"] or 0,
            bulletDamagePerShot = ((data["Weapon>BulletDamage"] or 0) * (data["Weapon>BulletsPerShot"] or 0) * (data["Weapon>BulletsPerBurst"] or 0)),
            scalingDamage = "+" .. data["LevelScaling>BulletDamage"] or 0,
            maxScalingDamage =  (roundValue(data["Weapon>BulletDamage"] + data["LevelScaling>BulletDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
            altBulletDamage = data["Weapon>AltFire>BulletDamage"] or 0,
            spiritScalingDamage = "+" .. (data["SpiritScaling>BulletDamage"] or 0),
        })

        -- Secondary fire is listed as its own row, right under the hero's main row
        local altDamage = data["Weapon>AltFire>BulletDamage"]
        if altDamage and altDamage ~= 0 then
            local altScaling = data["LevelScaling>BulletDamageAltFire"] or 0
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                bulletDamage = altDamage,
                scalingDamage = "+" .. altScaling,
                maxScalingDamage = roundValue(altDamage + altScaling * soul_unlock.get_max("PowerIncrease")),
                spiritScalingDamage = "+0",
            })
        end
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Bullet Damage Stats",
            columns = {
                { label = "Starting", field = "bulletDamage"  },
                { label = "Added per Boon", field = "scalingDamage"  },
                { label = "At Max Boon", field = "maxScalingDamage"  },
                { label = "Spirit Scaling", field = "spiritScalingDamage"  },
            }
        },
    }

    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.heroMeleeTable = function(frame)
    -- Pull data
    local heroMeleeData = heroDataArray({"Name", "LightMeleeDamage", "LevelScaling>LightMeleeDamage", "HeavyMeleeDamage", "LevelScaling>HeavyMeleeDamage", "SpiritScaling>HeavyMeleeDamage"})

    local rows = {}
    for name, data in pairs(heroMeleeData) do
        table.insert(rows, {
            name = name,
            lightMeleeDamage = data["LightMeleeDamage"] or 0,
            lightScalingDamage = "+" .. data["LevelScaling>LightMeleeDamage"] or 0,
            lightMaxScalingDamage = (roundValueFour(data["LightMeleeDamage"] + data["LevelScaling>LightMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
            heavyMeleeDamage = data["HeavyMeleeDamage"] or 0,
            heavyScalingDamage = "+" .. data["LevelScaling>HeavyMeleeDamage"] or 0,
            heavyMaxScalingDamage =  (roundValueFour(data["HeavyMeleeDamage"] + data["LevelScaling>HeavyMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
            heavySpiritScalingDamage = "+" .. (data["SpiritScaling>HeavyMeleeDamage"] or 0),
        })
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Light & Heavy Melee Stats",
            columns = {
                { label = "Starting Light", field = "lightMeleeDamage"  },
                { label = "Added per Boon (L)", field = "lightScalingDamage"  },
                { label = "At Max Boon (L)", field = "lightMaxScalingDamage"  },
                { label = "Starting Heavy", field = "heavyMeleeDamage"  },
                { label = "Added per Boon (H)", field = "heavyScalingDamage"  },
                { label = "At Max Boon (H)", field = "heavyMaxScalingDamage"  },
                { label = "Spirit Scaling (H)", field = "heavySpiritScalingDamage"  },
                
            }
        } 
    }
    
    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.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 = data["LightMeleeDamage"] or 0,
            scalingDamage = "+" .. data["LevelScaling>LightMeleeDamage"] or 0,
            maxScalingDamage =  (roundValueFour(data["LightMeleeDamage"] + data["LevelScaling>LightMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
        })
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Light Melee Stats",
            columns = {
                { label = "Starting", field = "lightMeleeDamage"  },
                { label = "Added per Boon", field = "scalingDamage"  },
                { label = "At Max Boon", field = "maxScalingDamage"  },
            }
        } 
    }
    
    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.heroHeavyMeleeTable = function(frame)
    -- Pull data
    local heroHeavyMeleeData = heroDataArray({"Name", "HeavyMeleeDamage", "LevelScaling>HeavyMeleeDamage", "SpiritScaling>HeavyMeleeDamage"})

    local rows = {}
    for name, data in pairs(heroHeavyMeleeData) do
        table.insert(rows, {
            name = name,
            heavyMeleeDamage = data["HeavyMeleeDamage"] or 0,
            scalingDamage = "+" .. data["LevelScaling>HeavyMeleeDamage"] or 0,
            maxScalingDamage = (roundValueFour(data["HeavyMeleeDamage"] + data["LevelScaling>HeavyMeleeDamage"] * soul_unlock.get_max("PowerIncrease"))) or 0,
            spiritScalingDamage = "+" .. (data["SpiritScaling>HeavyMeleeDamage"] or 0),
        })
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Heavy Melee Stats",
            columns = {
                { label = "Starting", field = "heavyMeleeDamage"  },
                { label = "Added per Boon", field = "scalingDamage"  },
                { label = "At Max Boon", field = "maxScalingDamage"  },
                { label = "Spirit Scaling", field = "spiritScalingDamage"  },
            }
        } 
    }
    
    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.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
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Stamina Stats",
            columns = {
                { label = "Stamina Bars", field = "stamina"  },
                { label = "Cooldown (s)", field = "staminaCooldown"  },
            }
        } 
    }
    
    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.heroDashTable = function(frame)
    local heroDashData = heroDataArray({"Name", "GroundDashSpeed", "GroundDashDuration", "AirDashSpeed", "AirDashDuration"})

    local rows = {}
    for name, data in pairs(heroDashData) do
        table.insert(rows, {
            name     = name,
            dashSpeed = data["GroundDashSpeed"] or 0,
            dashDuration = data["GroundDashDuration"] or 0,
            airDashSpeed = data["AirDashSpeed"] or 0,
            airDashDuration = data["AirDashDuration"] or 0,
        })
    end
    sortHeroes(rows)
    
    local tableDefs = {
    	{
            title   = "Base Dash Speed Stats",
            columns = {
                { label = "Dash Speed (m/s)", field = "dashSpeed"  },
                { label = "Dash Duration (s)", field = "dashDuration"  },
                { label = "Air Dash Speed (m/s)", field = "airDashSpeed"  },
                { label = "Air Dash Duration (s)", field = "airDashDuration"  },
            }
        } 
    }
    
    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.heroReloadTable = function(frame)
    local heroReloadData = heroDataArray({"Name", "Weapon>ReloadTime"})

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

    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Reload Time",
            columns = {
                { label = "Reload Time (s)", field = "reloadTime"  },
            }
        } 
    }
    
    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.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
    sortHeroes(rows)

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

    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.heroMoveSpeedTable = function(frame)
    local heroMoveSpeedData = heroDataArray({"Name", "MaxMoveSpeed", "SprintSpeed", "Weapon>ShootMoveSpeed", "SpiritScaling>MaxMoveSpeed", "SpiritScaling>SprintSpeed"})

    local rows = {}
    for name, data in pairs(heroMoveSpeedData) do
        table.insert(rows, {
            name = name,
            moveSpeed = data["MaxMoveSpeed"] or 0,
            sprintSpeed = data["SprintSpeed"] or 0,
            totalSpeed = data["MaxMoveSpeed"] + data["SprintSpeed"] or 0,
            shootSpeed = (data["Weapon>ShootMoveSpeed"] or 0) * 100,
            totalShootingSpeed = roundValue((data["MaxMoveSpeed"] + data["SprintSpeed"] or 0) * (data["Weapon>ShootMoveSpeed"] or 0)),
            spiritMoveSpeed = "+" .. (data["SpiritScaling>MaxMoveSpeed"] or 0),
            spiritSprintSpeed = "+" .. (data["SpiritScaling>SprintSpeed"] or 0),
        })
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Move Speed Stats",
            columns = {
                { label = "Move Speed (m/s)", field = "moveSpeed"  },
                { label = "Sprint Speed (m/s)", field = "sprintSpeed"  },
                { label = "Total Speed (m/s)", field = "totalSpeed"  },
                { label = "Shooting Speed (m/s)", field = "totalShootingSpeed"  },
                { label = "Shooting Mult (%)", field = "shootSpeed"  },
                { label = "MS Spirit Scaling", field = "spiritMoveSpeed"  },
                { label = "SS Spirit Scaling", field = "spiritSprintSpeed"  },
            }
        }    
    }

    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.heroDamageResistTable = function(frame)
    local heroDamageResistData = heroDataArray({"Name", "BulletResist", "LevelScaling>BulletResist", "TechResist", "LevelScaling>TechResist", "MeleeResist", "CritDamageReceivedPercent",
   "SpiritScaling>BulletResist", "SpiritScaling>TechResist",
    })

    local rows = {}
    for name, data in pairs(heroDamageResistData) do
        table.insert(rows, {
            name = name,
            bulletResist  = data["BulletResist"] or 0,
            bulletResistScaling = "+".. (data["LevelScaling>BulletResist"] or 0),
            bulletResistScalingMax = ((roundValue((data["BulletResist"] or 0) + (data["LevelScaling>BulletResist"] or 0) * soul_unlock.get_max("PowerIncrease"))) or 0),
            bulletResistSpiritScaling = "+".. (data["SpiritScaling>BulletResist"] or 0),
            spiritResist  = data["TechResist"] or 0,
            spiritResistScaling = "+".. (data["LevelScaling>TechResist"] or 0),
            spiritResistScalingMax = ((roundValue((data["TechResist"] or 0) + (data["LevelScaling>TechResist"] or 0) * soul_unlock.get_max("PowerIncrease"))) or 0),
            spiritResistSpiritScaling = "+".. (data["SpiritScaling>TechResist"] or 0),
            meleeResist   = data["MeleeResist"] or 0,
            critReduction = data["CritDamageReceivedPercent"] or 0
        })
    end
    sortHeroes(rows)

    local tableDefs = {
        {
            title   = "Base Bullet Resist Stats",
            columns = {
                { label = "Starting (%)", field = "bulletResist"  },
                { label = "Added per Boon (%)",   field = "bulletResistScaling" },
                { label = "At Max Boon (%)",   field = "bulletResistScalingMax" },
                { label = "Spirit Scaling (%)",   field = "bulletResistSpiritScaling" },
            }
        },
        {
            title   = "Base Spirit Resist Stats",
            columns = {
                { label = "Starting (%)", field = "spiritResist"  },
                { label = "Added per Boon (%)",   field = "spiritResistScaling" },
                { label = "At Max Boon (%)",   field = "spiritResistScalingMax" },
                { label = "Spirit Scaling (%)",   field = "spiritResistSpiritScaling" },
            }
        },
        {
            title   = "Base Melee Resist Stats",
            columns = {
                { label = "Melee Resist (%)", field = "meleeResist" },
            }
        },
        {
            title   = "Base Crit Reduction Stats",
            columns = {
                { label = "Crit Reduction (%)", field = "critReduction" },
            }
        },
    }

    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.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
    sortHeroes(rows)

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

    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.heroSpiritTable = function(frame)
    local heroSpiritData = heroDataArray({"Name", "LevelScaling>TechPower"})

    local rows = {}
    for name, data in pairs(heroSpiritData) do
        table.insert(rows, {
            name = name,
            spiritScaling  = "+".. data["LevelScaling>TechPower"] or 0,
            maxSpiritScaling = (data["LevelScaling>TechPower"] * soul_unlock.get_max("PowerIncrease")) or 0,
        })
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Spirit Scaling Stats",
            columns = {
                { label = "Spirit Per Boon", field = "spiritScaling"  },
                { label = "At Max Boon", field = "maxSpiritScaling"  },
            }
        }    
    }

    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.heroGravityTable = function(frame)
    local heroGravityData = heroDataArray({"Name", "GravityChange"})

    local rows = {}
    for name, data in pairs(heroGravityData) do
        table.insert(rows, {
            name = name,
            gravityScale  = (data["GravityChange"] or 0),
        })
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Gravity Scale Stats",
            columns = {
                { label = "Gravity Scale (%)", field = "gravityScale"  },
            }
        }    
    }

    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.bulletGravityTable = function(frame)
    local heroGravityData = heroDataArray({"Name", "Weapon>BulletGravityScale", "Weapon>AltFire>BulletGravityScale"})

    local rows = {}
    for name, data in pairs(heroGravityData) do
        table.insert(rows, {
            name = name,
            gravityScale  = (data["Weapon>BulletGravityScale"] or 0),
            altGravityScale  = (data["Weapon>AltFire>BulletGravityScale"] or 0),
        })
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Bullet Gravity Scale Stats",
            columns = {
                { label = "Gravity Scale", field = "gravityScale"  },
            }
        },
        {
            title   = "Base Alt-fire Bullet Gravity Scale Stats",
            columns = {
                { label = "Gravity Scale", field = "altGravityScale"  },
            }
        }
    }

    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.bulletRadiusTable = function(frame)
    local heroBulletRadiusData = heroDataArray({"Name", "Weapon>BulletRadius", "Weapon>AltFire>BulletRadius"})

    local rows = {}
    for name, data in pairs(heroBulletRadiusData) do
        table.insert(rows, {
            name = name,
            bulletRadius  = (data["Weapon>BulletRadius"] or 0) * 100,
            bulletRadiusInches  = roundValueInteger((data["Weapon>BulletRadius"] or 0) * 100 * 0.394),
            altBulletRadius  = (data["Weapon>AltFire>BulletRadius"] or 0) * 100,
            altBulletRadiusInches  = roundValueInteger((data["Weapon>AltFire>BulletRadius"] or 0) * 100 * 0.394),
        })
        
        local altBulletRadius = (data["Weapon>AltFire>BulletRadius"] or 0) * 100
        local altBulletRadiusInches  = roundValueInteger((data["Weapon>AltFire>BulletRadius"] or 0) * 100 * 0.394)
        if altBulletRadius and altBulletRadius ~= 0 then
            table.insert(rows, {
                name = name,
                sortName = name .. " (alt fire)",
                nameSuffix = " (alt fire)",
                bulletRadius = altBulletRadius,
                bulletRadiusInches = altBulletRadiusInches
            })
        end
    end
    sortHeroes(rows)

    local tableDefs = {
    	{
            title   = "Base Bullet Radius Stats",
            columns = {
                { label = "Bullet Radius (cm)", field = "bulletRadius"  },
                { label = "Bullet Radius (in)", field = "bulletRadiusInches"  },
            }
        }
    }

    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

return p