Module:StreetBrawlItems: Difference between revisions

Add p.tier: three-column High/Normal/Low chance layout for a tier (prototype) (with help from vergir-bot LLM)
Vergir (talk | contribs)
m make item cards on the Street Brawl section sliiiightly bigger (+5%)
 
(18 intermediate revisions by 4 users not shown)
Line 7: Line 7:
-- Slot display order: Weapon -> Vitality (Armor) -> Spirit (Tech)
-- Slot display order: Weapon -> Vitality (Armor) -> Spirit (Tech)
local SLOT_RANK = { Weapon = 1, Armor = 2, Tech = 3 }
local SLOT_RANK = { Weapon = 1, Armor = 2, Tech = 3 }
-- Card scale passed to ItemBox for favored/unfavored/counter cards.
-- Card scale passed to ItemBox.
local CARD_SIZE = "0.8"
local CARD_SIZE = "0.7"


local NAME_TO_KEY, KEY_TO_NAME, RELEASED = {}, {}, {}
local NAME_TO_KEY = {}
for key, h in pairs(heroes) do
for key, h in pairs(heroes) do
if type(h) == "table" and h.Name then
if type(h) == "table" and h.Name then
KEY_TO_NAME[key] = h.Name
NAME_TO_KEY[mw.ustring.lower(h.Name)] = key
NAME_TO_KEY[mw.ustring.lower(h.Name)] = key
if not h.InDevelopment and not h.IsDisabled then RELEASED[key] = true end
end
end
end
end
Line 54: Line 52:
end
end


local function render_item(frame, code, style)
local function render_item(frame, code)
local name = iname(code); if not name then return nil end
local name = iname(code); if not name then return nil end
if style == "icon" then return frame:expandTemplate{title = "ItemIcon", args = {name}} end
-- These hero pages place hundreds of cards at once, so the cards render in
return frame:expandTemplate{title = "ItemBox", args = {name, showTierTag = "true", size = CARD_SIZE}}
-- lowGraphics mode: ItemBox swaps its paper texture for a single cacheable
end
-- overlay, which keeps scrolling smooth. See [[Template:ItemBox]].
local function render_hero(frame, key)
local args = { name, size = CARD_SIZE, lowGraphics = "true" }
local name = KEY_TO_NAME[key]; if not name then return nil end
-- Legendary (tier 5) cards omit the tier badge; it is redundant in the Legendary tier.
return frame:expandTemplate{title = "HeroIcon", args = {name}}
if itier(code) ~= 5 then args.showTierTag = "true" end
return frame:expandTemplate{title = "ItemBox", args = args}
end
end


Line 68: Line 67:
if #parts == 0 then return "" end
if #parts == 0 then return "" end
return '<div class="' .. class .. '">' .. table.concat(parts) .. '</div>'
return '<div class="' .. class .. '">' .. table.concat(parts) .. '</div>'
end
-- A tier/section heading: a plain div carrying the skin's own heading wrapper
-- classes, so it inherits real heading styling while staying out of the table
-- of contents (and can be hidden inside the gate).
local function heading_div(label)
return '<div class="mw-heading mw-heading4 sb-tier-head">' .. label .. '</div>'
end
end


local function hero_sets(hkey)
local function hero_sets(hkey)
local favored, unfavored, special, counter = {}, {}, {}, {}
local favored, unfavored, counter = {}, {}, {}
for _, o in ipairs(sb.Outliers) do
for _, o in ipairs(sb.Outliers) do
if o.Hero == hkey then
if o.Hero == hkey then
if o.Bucket == "Good" then favored[o.Item] = true; special[o.Item] = true
if o.Bucket == "Good" then favored[o.Item] = true
elseif type(o.Weight) == "number" and o.Weight < 1 then unfavored[o.Item] = true; special[o.Item] = true end
elseif type(o.Weight) == "number" and o.Weight < 1 then unfavored[o.Item] = true end
if o.Counter ~= nil then counter[#counter + 1] = o.Item end
if o.Counter ~= nil then counter[#counter + 1] = o.Item end
end
end
end
end
return favored, unfavored, special, counter
return favored, unfavored, counter
end
 
local function item_code(input)
input = mw.text.trim(input or "")
if input == "" then return nil end
if items[input] then return input end
for k, v in pairs(items) do
if type(v) == "table" and v.Name == input and v.IsDisabled ~= true then return k end
end
for k, v in pairs(items) do
if type(v) == "table" and v.Name == input then return k end
end
return nil
end
 
-- {{#invoke:StreetBrawlItems|hero|HERO|GROUP|TIER|label=..|collapse=yes|style=card|icon}}
-- GROUP = favored | unfavored | common  (use hero_counter for counters)
function p.hero(frame)
local hkey = hero_key(frame.args[1]); if not hkey then return "" end
local group = mw.text.trim(frame.args[2] or "")
local tier = tonumber(frame.args[3])
local style = frame.args.style
if not style or style == "" then style = (group == "common") and "icon" or "card" end
local collapse = (frame.args.collapse == "yes" or frame.args.collapse == "true")
local label = frame.args.label or ""
 
local favored, unfavored, special = hero_sets(hkey)
local pred
if group == "favored" then pred = function(c) return favored[c] end
elseif group == "unfavored" then pred = function(c) return unfavored[c] end
elseif group == "common" then pred = function(c) return not special[c] end
else return "" end
 
local codes = {}
for _, c in ipairs(sb.AvailableItems) do
if iok(c) and (tier == nil or itier(c) == tier) and pred(c) then codes[#codes + 1] = c end
end
table.sort(codes, item_less)
 
local parts = {}
for _, c in ipairs(codes) do local r = render_item(frame, c, style); if r then parts[#parts + 1] = r end end
if #parts == 0 then return "" end
 
if collapse then
-- Common: a div collapsible with the label inside as a custom toggle (no native
-- toggle -> no brackets). The caret flips off the container's mw-collapsed state.
local id = "sbcommon-" .. hkey .. "-" .. tostring(tier or 0)
local head = (label ~= "" and label) or "Common"
return '<div class="sb-common mw-collapsible mw-collapsed" id="mw-customcollapsible-' .. id .. '">'
.. '<div class="sb-common-head mw-customtoggle-' .. id .. '">' .. head .. '</div>'
.. '<div class="sb-common-body mw-collapsible-content">' .. table.concat(parts, " · ") .. '</div>'
.. '</div>'
end
 
local row_class = (style == "icon") and "sb-icons" or "sb-cards"
local out = '<div class="sb-group">'
if label ~= "" then out = out .. '<div class="sb-label">' .. label .. '</div>' end
return out .. classdiv(parts, row_class) .. '</div>'
end
end


-- {{#invoke:StreetBrawlItems|tier|HERO|TIER|high=..|normal=..|low=..}}
-- Returns sorted high (favored), average (pool), low (unfavored) item-code lists for a tier.
-- Renders one tier as three columns: High chance (favored), Normal chance
local function collect(hkey, tier)
-- (the leftover pool), Low chance (unfavored). Empty columns still render.
function p.tier(frame)
local hkey = hero_key(frame.args[1]); if not hkey then return "" end
local tier = tonumber(frame.args[2])
local favored, unfavored = hero_sets(hkey)
local favored, unfavored = hero_sets(hkey)
 
local high, average, low = {}, {}, {}
local high, normal, low = {}, {}, {}
for _, c in ipairs(sb.AvailableItems) do
for _, c in ipairs(sb.AvailableItems) do
if iok(c) and (tier == nil or itier(c) == tier) then
if iok(c) and (tier == nil or itier(c) == tier) then
if favored[c] then high[#high + 1] = c
if favored[c] then high[#high + 1] = c
elseif unfavored[c] then low[#low + 1] = c
elseif unfavored[c] then low[#low + 1] = c
else normal[#normal + 1] = c end
else average[#average + 1] = c end
end
end
end
end
table.sort(high, item_less)
table.sort(high, item_less)
table.sort(normal, item_less)
table.sort(average, item_less)
table.sort(low, item_less)
table.sort(low, item_less)
return high, average, low
end
-- Builds one tier as conditional columns:
--  High chance (favored) takes 35%, the average column fills the rest.
--  Low chance (unfavored) is dropped when empty; a narrow column on the right when
--  High is present; a fit-content column on the right when High is absent.
local function tier_columns(frame, hkey, tier, hi, avg, lo)
local high, average, low = collect(hkey, tier)


local function col(codes, cls, head)
local function col(codes, cls, head)
local parts = {}
local parts = {}
for _, c in ipairs(codes) do local r = render_item(frame, c, "icon"); if r then parts[#parts + 1] = r end end
for _, c in ipairs(codes) do local r = render_item(frame, c); if r then parts[#parts + 1] = r end end
return '<div class="sb-col ' .. cls .. '">'
return '<div class="sb-col ' .. cls .. '">'
.. '<div class="sb-col-head">' .. head .. '</div>'
.. '<div class="sb-col-head">' .. head .. '</div>'
.. '<div class="sb-col-body">' .. table.concat(parts, "<br>") .. '</div>'
.. '<div class="sb-col-body sb-col-cards">' .. table.concat(parts) .. '</div>'
.. '</div>'
.. '</div>'
end
end


local hi = frame.args.high or "High chance"
local out = {}
local no = frame.args.normal or "Normal chance"
if #high > 0 then
local lo = frame.args.low or "Low chance"
out[#out + 1] = col(high, "sb-col-high sb-w-high", hi)
return '<div class="sb-tier">'
end
.. col(high, "sb-col-high", hi)
out[#out + 1] = col(average, "sb-col-average sb-w-fill", avg)
.. col(normal, "sb-col-normal", no)
if #low > 0 then
.. col(low, "sb-col-low", lo)
local low_w = (#high > 0) and "sb-low-narrow" or "sb-low-wide"
.. '</div>'
out[#out + 1] = col(low, "sb-col-low " .. low_w, lo)
end
return '<div class="sb-tier">' .. table.concat(out) .. '</div>'
end
 
-- {{#invoke:StreetBrawlItems|tiers|HERO|gate=ID|high=..|average=..|low=..|tier1=..|...|tier5=..}}
-- All five tiers under styled (non-TOC) headings. All display text is supplied by the
-- caller. With gate=ID, the Tier 1 heading and everything after it is wrapped in an open
-- <div id=ID class="sb-gate">, whose inner <div class="sb-gate-body"> the caller must
-- close after any trailing content. The id sits on the outer wrapper so following the
-- anchor lands on the Tier 1 heading rather than the columns below it.
function p.tiers(frame)
local hkey = hero_key(frame.args[1]); if not hkey then return "" end
local hi = frame.args.high or ""
local avg = frame.args.average or ""
local lo = frame.args.low or ""
local gate = frame.args.gate
if gate == "" then gate = nil end
 
local out = {}
for t = 1, 5 do
local head = heading_div(frame.args["tier" .. t] or "")
local cols = tier_columns(frame, hkey, t, hi, avg, lo)
if t == 1 and gate then
out[#out + 1] = '<div id="' .. gate .. '" class="sb-gate">\n'
.. head .. "\n"
.. '<div class="sb-gate-body">\n'
.. cols
else
out[#out + 1] = head .. "\n" .. cols
end
end
return table.concat(out, "\n\n")
end
end


-- {{#invoke:StreetBrawlItems|hero_counter|HERO|heading=..|note=..}}
-- {{#invoke:StreetBrawlItems|hero_counter|HERO|heading=..|note=..}}
-- Returns the whole counter block (heading + note + cards), or "" when empty.
-- The whole counter block (heading + note + cards), or "" when the hero has none.
function p.hero_counter(frame)
function p.hero_counter(frame)
local hkey = hero_key(frame.args[1]); if not hkey then return "" end
local hkey = hero_key(frame.args[1]); if not hkey then return "" end
local _, _, _, counter = hero_sets(hkey)
local _, _, counter = hero_sets(hkey)


local codes = {}
local codes = {}
Line 191: Line 175:


local parts = {}
local parts = {}
for _, c in ipairs(codes) do local r = render_item(frame, c, "card"); if r then parts[#parts + 1] = r end end
for _, c in ipairs(codes) do local r = render_item(frame, c); if r then parts[#parts + 1] = r end end


local out = {}
local out = {}
local heading = frame.args.heading
local heading = frame.args.heading
if heading and heading ~= "" then out[#out + 1] = "=== " .. heading .. " ===" end
if heading and heading ~= "" then out[#out + 1] = heading_div(heading) end
local note = frame.args.note
local note = frame.args.note
if note and note ~= "" then out[#out + 1] = "''" .. note .. "''" end
if note and note ~= "" then out[#out + 1] = "''" .. note .. "''" end
Line 202: Line 186:
end
end


-- {{#invoke:StreetBrawlItems|item|ITEM|GROUP|label=..|note=..}}
-- {{#invoke:StreetBrawlItems|counters_table}}
-- GROUP = favored | unfavored | counter
-- Every hero that has counter items, one row each, sorted by hero name. Items
function p.item(frame)
-- within a row are listed alphabetically. Built as HTML rather than pipe-table
local code = item_code(frame.args[1]); if not code then return "" end
-- markup so the invoke does not have to sit at the start of a line.
local group = mw.text.trim(frame.args[2] or "")
function p.counters_table(frame)
 
local by_hero, rows = {}, {}
local fav, unf, ctr = {}, {}, {}
for _, o in ipairs(sb.Outliers) do
for _, o in ipairs(sb.Outliers) do
if o.Item == code and RELEASED[o.Hero] then
if o.Counter ~= nil and iok(o.Item) then
if o.Bucket == "Good" then fav[#fav + 1] = o.Hero
local hero = heroes[o.Hero]
elseif type(o.Weight) == "number" and o.Weight < 1 then unf[#unf + 1] = o.Hero end
if hero and hero.Name then
if o.Counter ~= nil then ctr[#ctr + 1] = o.Hero end
local row = by_hero[o.Hero]
if not row then
row = { name = hero.Name, items = {}, seen = {} }
by_hero[o.Hero] = row
rows[#rows + 1] = row
end
-- Guard against a hero/item pair being listed twice.
if not row.seen[o.Item] then
row.seen[o.Item] = true
row.items[#row.items + 1] = iname(o.Item)
end
end
end
end
end
end
local list = (group == "favored" and fav) or (group == "unfavored" and unf) or (group == "counter" and ctr)
if #rows == 0 then return "" end
if not list then return "" end
table.sort(rows, function(a, b) return a.name < b.name end)
table.sort(list, function(a, b) return (KEY_TO_NAME[a] or a) < (KEY_TO_NAME[b] or b) end)


local parts = {}
local tbl = mw.html.create("table"):addClass("wikitable")
for _, k in ipairs(list) do local r = render_hero(frame, k); if r then parts[#parts + 1] = r end end
local head = tbl:tag("tr")
if #parts == 0 then return "" end
head:tag("th"):wikitext("Hero")
head:tag("th"):wikitext("Items")


local pre = ""
for _, row in ipairs(rows) do
local label = frame.args.label
table.sort(row.items)
if label and label ~= "" then pre = pre .. "'''" .. label .. "'''<br>" end
local icons = {}
local note = frame.args.note
for _, n in ipairs(row.items) do
if note and note ~= "" then pre = pre .. "''" .. note .. "''<br>" end
icons[#icons + 1] = frame:expandTemplate{title = "ItemIcon", args = {n}}
return pre .. classdiv(parts, "sb-icons")
end
end
local tr = tbl:tag("tr")
 
tr:tag("td"):wikitext(frame:expandTemplate{title = "HeroIcon", args = {row.name}})
-- {{#invoke:StreetBrawlItems|item_in_pool|ITEM}} -> "1" if in the Street Brawl pool, else ""
tr:tag("td"):wikitext(table.concat(icons, ", "))
function p.item_in_pool(frame)
end
local code = item_code(frame.args[1]); if not code then return "" end
return tostring(tbl)
for _, c in ipairs(sb.AvailableItems) do if c == code then return "1" end end
return ""
end
end


return p
return p