|
|
| Line 1: |
Line 1: |
| local p = {}; | | -- Audit: for every item stat that goes through Infobox item's append_label |
| local util_module = require('Module:Utilities') | | -- with attribute styling enabled (Passive/Active Main stats), compute the |
| local allHeroData = mw.loadJsonData("Data:HeroData.json") | | -- label link target under the CURRENT priority (attr link > StatLinks) |
| local soul_unlock = require('Module:SoulUnlock') | | -- and the PROPOSED priority (StatLinks > attr link) and list the results. |
| local inGameHeroData = {}
| | local p = {} |
| | local lang = require "Module:Lang" |
| | local data = mw.loadJsonData("Data:ItemCards.json") |
| | local stat_links = mw.loadJsonData("Data:StatLinks.json") |
| | local ATTR = require("Module:Icon/attr") |
|
| |
|
| -- Just get heroes that are playable and have a "name" in HeroData
| | local LABEL_KEY_REMAP = { DotDuration = "DOTDuration" } |
| for i, heroData in pairs(allHeroData) do
| | local IGNORED = { BuildUpPerShot = true } |
| 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 get_pair(base, s1, s2) |
| local function sortHeroes(rows) | | local v = lang.get_string(base .. s1) |
| local function getSortName(name) | | if v and v ~= "" then return v end |
| return (name:gsub("^The ", ""))
| | v = lang.get_string(base .. s2) |
| end
| | if v and v ~= "" then return v end |
| | | return "" |
| 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 |
|
| |
|
| -- Helper function to round numeric values
| | local function resolve_label_key(prop_key, loc_override) |
| local function roundValue(value) | | local base = (loc_override or prop_key):gsub("^#", "") |
| if type(value) == "number" then | | return LABEL_KEY_REMAP[base] or base |
| return util_module.round_to_sig_fig(value, 3)
| |
| end
| |
| return value | |
| end | | end |
|
| |
|
| -- Round to an integer (up to 2 digits)
| | local function esc(s) |
| local function roundValueInteger(value) | | if s == nil or s == "" then return "''(none)''" end |
| if type(value) == "number" then | | return "<nowiki>" .. tostring(s) .. "</nowiki>" |
| return util_module.round_to_sig_fig(value, 2)
| |
| end
| |
| return value | |
| end | | end |
|
| |
|
| -- Generate tables from given properties
| | function p.audit(frame) |
| local function buildTable(frame, rows, tableDef, options)
| | local mode = frame.args[1] or "diff" -- "diff" | "all" |
| options = options or {} | | local rows = {} |
| -- option to ignore heroes who don't have any of the given properties. true by default
| | local names = {} |
| local filterZero = options.filterZero or true | | for key, item in pairs(data) do |
| | | if not item.IsDisabled then |
| local filteredRows = {} | | table.insert(names, key) |
| | |
| 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 |
| end | | end |
| | table.sort(names, function(a, b) |
| | return (data[a].Name or a) < (data[b].Name or b) |
| | end) |
|
| |
|
| if #filteredRows == 0 then | | local n_items, n_stats, n_diff = 0, 0, 0 |
| return ""
| | for _, key in ipairs(names) do |
| end
| | local item = data[key] |
| | | n_items = n_items + 1 |
| -- Build header
| | local i = 1 |
| local header = ""
| | while item["Info" .. i] do |
| for _, col in ipairs(tableDef.columns) do | | local sec = item["Info" .. i] |
| header = header .. " !! " .. col.label | | if sec.Type ~= "Innate" then |
| end
| | local seen = {} |
| | | for _, obj in ipairs(sec.Main or {}) do |
| -- Start table
| | local pk = obj.Key |
| local t = "{| class=\"wikitable sortable mw-collapsible\" style=\"text-align:center\" \n" ..
| | if not seen[pk] and not IGNORED[pk] and not pk:match("^StatusEffect") then |
| "|+ " .. tableDef.title .. " \n" .. | | seen[pk] = true |
| "! Hero" .. header .. " \n"
| | local label_key = resolve_label_key(pk, obj.LocTokenOverride) |
| | | local label = get_pair(label_key, "_label", "_Label") |
| -- Rows
| | if label ~= "" then |
| for _, r in ipairs(filteredRows) do
| | n_stats = n_stats + 1 |
| local cells = ""
| | local attr = obj.Type and ATTR[obj.Type] or nil |
| for _, col in ipairs(tableDef.columns) do
| | local attr_link = attr and attr.link or nil |
| cells = cells .. " || " .. (r[col.field] or 0)
| | if attr_link == "" then attr_link = nil end |
| end
| | local sl = stat_links[label_key] |
| | | if sl == "" then sl = nil end |
| -- Rows may carry an alternate sort key and a label suffix, used for
| | local cur = attr_link or sl |
| -- secondary fire rows listed underneath their hero's main row
| | local new = sl or attr_link |
| local nameCell = "| style=\"text-align:left;\""
| | local differs = (cur or "") ~= (new or "") |
| if r.sortName then
| | if differs then n_diff = n_diff + 1 end |
| nameCell = nameCell .. " data-sort-value=\"" .. r.sortName .. "\""
| | if mode == "all" or differs then |
| end
| | table.insert(rows, string.format( |
| nameCell = nameCell .. " | " ..
| | "|-%s\n| [[%s]] || %s || %s || <nowiki>%s</nowiki> || %s || %s || %s", |
| frame:expandTemplate{ title = "PageRef", args = { r.name } } ..
| | differs and ' style="background:#4a3a1a"' or "", |
| (r.nameSuffix or "")
| | item.Name or key, sec.Type, esc(label_key), tostring(obj.Type), |
| | | esc(label), esc(cur), esc(new))) |
| t = t ..
| | end |
| "|- \n" ..
| | end |
| nameCell ..
| | end |
| 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 | | 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 |
| end
| | i = i + 1 |
| 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 |
| end | | end |
|
| |
|
| return table.concat(output, "\n\n") | | local out = {} |
| | table.insert(out, string.format("Items scanned: %d. Passive/Active main stats with a label: %d. Rows whose link changes: %d.\n", |
| | n_items, n_stats, n_diff)) |
| | table.insert(out, '{| class="wikitable sortable"\n! Item !! Section !! Stat key !! Type !! Label !! Current link !! Proposed link') |
| | for _, r in ipairs(rows) do table.insert(out, r) end |
| | table.insert(out, "|}") |
| | return table.concat(out, "\n") |
| end | | end |
|
| |
|
| return p | | return p |