Module:StreetBrawlAbilities

Revision as of 20:32, 28 July 2026 by ~2026-CreepActivePhantom802 (talk) (Fix hero lookup: ability slot keys decode as numbers, not strings (with help from vergir-bot LLM))

Documentation for this module may be created at Module:StreetBrawlAbilities/doc

-- Lists the Street Brawl ability overrides from [[Data:StreetBrawlData.json]] with the
-- {{DataProp}} call that cites each one and the base value it replaces.

local p = {}

local SB_PAGE    = "Data:StreetBrawlData.json"
local CARDS_PAGE = "Data:AbilityCards.json"
local HERO_PAGE  = "Data:HeroData.json"
local UNMATCHED  = "Unmatched abilities"

-- Maps each ability key to the hero who owns it.
local function build_owner_map()
	local cards  = mw.loadJsonData(CARDS_PAGE)
	local heroes = mw.loadJsonData(HERO_PAGE)
	local map = {}
	for hero_key, card in pairs(cards) do
		local hero = heroes[hero_key]
		-- Unreleased hero stubs reuse released heroes' ability keys (hero_gunslinger
		-- carries Paradox's Pulse Grenade), so only live hero records may claim one.
		if type(card) == "table" and hero and hero.Name and hero.IsDisabled ~= true then
			for slot = 1, 8 do
				-- The card's slot keys decode as numbers, so index with one.
				local ability = card[slot] or card[tostring(slot)]
				if type(ability) == "table" and ability.Key then
					map[ability.Key] = { hero = hero.Name, slot = slot, name = ability.Name }
				end
			end
		end
	end
	return map
end

-- Orders one node's keys: plain props before Upgrades, Value before Scale, tiers numerically.
local function sorted_keys(node, top)
	local keys = {}
	for k in pairs(node) do
		-- Name labels the record; it is not one of the changed properties.
		if not (top and k == "Name") then keys[#keys + 1] = k end
	end
	table.sort(keys, function(a, b)
		if top then
			if a == "Upgrades" then return false end
			if b == "Upgrades" then return true end
		end
		if a == "Value" then return true end
		if b == "Value" then return false end
		local na, nb = tonumber(a), tonumber(b)
		if na and nb then return na < nb end
		-- Upgrade tiers arrive as numbers, so never compare a number with a string.
		return tostring(a) < tostring(b)
	end)
	return keys
end

-- Collects the changed properties as dot-notation paths, the form {{DataProp}} accepts.
local function flatten(node, prefix, out, top)
	for _, key in ipairs(sorted_keys(node, top)) do
		local value = node[key]
		local path = prefix and (prefix .. "." .. key) or tostring(key)
		if type(value) == "table" then
			flatten(value, path, out, false)
		else
			out[#out + 1] = path
		end
	end
end

-- Builds one property line: the wikitext to copy, its Street Brawl value, and the default.
local function render_line(frame, ability, path)
	local call = "{{DataProp|" .. ability .. "|" .. path .. "|streetbrawl}}"
	-- Expanded rather than read from the JSON, so this shows what a copied call really gives.
	local sb   = frame:expandTemplate{ title = "DataProp", args = { ability, path, "streetbrawl" } }
	local base = frame:expandTemplate{ title = "DataProp", args = { ability, path } }
	return "* <code>" .. mw.text.nowiki(call) .. "</code> &rarr; '''" .. sb
		.. "''' ''(default " .. base .. ")''"
end

-- {{#invoke:StreetBrawlAbilities|report}} renders every change, grouped by hero.
function p.report(frame)
	local changes = mw.loadJsonData(SB_PAGE)["ability-changes"]
	local owners  = build_owner_map()

	local by_hero, hero_order = {}, {}
	for key, record in pairs(changes) do
		local owner = owners[key]
		local hero  = owner and owner.hero or UNMATCHED
		if not by_hero[hero] then
			by_hero[hero] = {}
			hero_order[#hero_order + 1] = hero
		end
		local paths = {}
		flatten(record, nil, paths, true)
		table.insert(by_hero[hero], {
			slot  = owner and owner.slot or 99,
			name  = record.Name or (owner and owner.name) or key,
			paths = paths,
		})
	end

	-- Anything the data adds that no live hero claims is listed last rather than dropped.
	table.sort(hero_order, function(a, b)
		if a == UNMATCHED then return false end
		if b == UNMATCHED then return true end
		return a < b
	end)

	local out = {}
	for _, hero in ipairs(hero_order) do
		local abilities = by_hero[hero]
		table.sort(abilities, function(a, b) return a.slot < b.slot end)
		local matched = hero ~= UNMATCHED
		out[#out + 1] = "\n== "
			.. (matched and frame:expandTemplate{ title = "HeroIcon", args = { hero } } or hero)
			.. " =="
		for _, ability in ipairs(abilities) do
			out[#out + 1] = "=== "
				.. (matched and frame:expandTemplate{ title = "AbilityIcon", args = { ability.name } } or ability.name)
				.. " ==="
			for _, path in ipairs(ability.paths) do
				out[#out + 1] = render_line(frame, ability.name, path)
			end
		end
	end
	return table.concat(out, "\n")
end

return p