Module:Sandbox/Vergir: Difference between revisions

Jump to navigation Jump to search
Vergir (talk | contribs)
Testing merged alt-fire rows for heroBulletDamageTable (with help from vergir-bot LLM)
Vergir (talk | contribs)
Audit script: compare stat label link targets under current vs proposed priority in Module:Infobox item (with help from vergir-bot LLM)
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