Module:Sandbox/VergirGive feedback
Documentation for this module may be created at Module:Sandbox/Vergir/doc
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 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
-- Rows may carry an alternate sort key and a label suffix, used for
-- secondary fire rows listed 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.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
-- Kept for regression testing: an untouched table that also goes through
-- buildTable() and sortHeroes()
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 = roundValue(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
return p