Module:Sandbox/Vergir: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Final test draft of StatScaling before publishing (with help from vergir-bot LLM) |
Testing merged alt-fire rows for heroBulletDamageTable (with help from vergir-bot LLM) |
||
| Line 1: | Line 1: | ||
local p = {}; | |||
local util_module = require('Module:Utilities') | |||
local allHeroData = mw.loadJsonData("Data:HeroData.json") | |||
local soul_unlock = require('Module:SoulUnlock') | |||
local inGameHeroData = {} | |||
local | |||
-- 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 | 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 "") | |||
local | 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) | |||
local function | if type(value) == "number" then | ||
return util_module.round_to_sig_fig(value, 2) | |||
end | |||
return value | |||
end | 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 | 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 | |||
end | 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 | |||
end | |||
t = t .. "|}" | |||
return t | |||
end | end | ||
-- | -- heroDataArray takes as argument a table of any length containing keys or | ||
local function | -- 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 | 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 | 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 | end | ||
return p | return p | ||
Revision as of 21:08, 11 August 2026
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