| Latest revision |
Your text |
| Line 1: |
Line 1: |
| -- 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
| |
|
| |
| -- Reads a property both ways, returning its Street Brawl value and its normal one.
| |
| local function both_values(frame, ability, prop)
| |
| local sb = frame:expandTemplate{ title = "DataProp", args = { ability, prop, "streetbrawl" } }
| |
| local base = frame:expandTemplate{ title = "DataProp", args = { ability, prop } }
| |
| return tonumber(sb), tonumber(base)
| |
| 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> → '''" .. sb
| |
| .. "''' ''(default " .. base .. ")''"
| |
| end
| |
|
| |
| -- True when Street Brawl changes at least one of this hero's abilities. Accepts a
| |
| -- hero_* key or the hero's English name, so callers may pass whichever they hold.
| |
| function p.hero_has_changes(hero)
| |
| if hero == nil or hero == "" then return false end
| |
| local record = mw.loadJsonData(HERO_PAGE)[hero]
| |
| local name = (record and record.Name) or hero
| |
| local owners = build_owner_map()
| |
| local changes = mw.loadJsonData(SB_PAGE)["ability-changes"]
| |
| for key in pairs(changes) do
| |
| local owner = owners[key]
| |
| if owner and owner.hero == name then return true end
| |
| end
| |
| return false
| |
| end
| |
|
| |
| -- {{#invoke:StreetBrawlAbilities|hasChanges|HERO}} exposes the same check to wikitext,
| |
| -- answering "1" or the empty string so #if can branch on it.
| |
| function p.hasChanges(frame)
| |
| return p.hero_has_changes(mw.text.trim(frame.args[1] or "")) and "1" or ""
| |
| end
| |
|
| |
| -- {{#invoke:StreetBrawlAbilities|change|ABILITY|PROP}} describes how far a property moved
| |
| -- in Street Brawl, as "reduced by 45%" or "increased by ~40%".
| |
| function p.change(frame)
| |
| local ability = mw.text.trim(frame.args[1] or "")
| |
| local prop = mw.text.trim(frame.args[2] or "")
| |
| local sb, base = both_values(frame, ability, prop)
| |
|
| |
| -- DataProp answers a failed lookup with a red notice rather than a number.
| |
| if not sb or not base or base == 0 then
| |
| return '<span style="color:red;">No change for ' .. ability .. '/' .. prop .. '</span>'
| |
| end
| |
| -- Properties Street Brawl leaves alone fall back to the base value, so two equal
| |
| -- numbers mean this property was never in the overlay.
| |
| if sb == base then
| |
| return '<span style="color:red;">Street Brawl does not change ' .. ability .. '/' .. prop .. '</span>'
| |
| end
| |
|
| |
| -- Purely a comparison of the two numbers; whether that helps or hurts the hero is
| |
| -- for the surrounding prose to say.
| |
| local direction = sb > base and "increased" or "reduced"
| |
| local percent = math.abs(sb - base) / math.abs(base) * 100
| |
| -- Rounded to the nearest 5% so the prose reads as a round figure; when that rounding
| |
| -- moves the number by more than 1.5 points, "~" marks it as approximate.
| |
| local rounded = math.floor(percent / 5 + 0.5) * 5
| |
| local approx = math.abs(percent - rounded) > 1.5 and "~" or ""
| |
| return direction .. " by " .. approx .. string.format("%.0f", rounded) .. "%"
| |
| 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
| |