Module:ABVRS Points: Difference between revisions
From zunagi
Created page with "local p = {} -- Base ranking points by finishing place (1st-16th), from the 2024/25 ABVRS Points Table -- (https://zunagi.com/images/2024-25-ABVRS-Points-Table.pdf). local base = { [1] = 30, [2] = 28, [3] = 25, [4] = 23, [5] = 21, [6] = 20, [7] = 19, [8] = 18, [9] = 17, [10] = 16.5, [11] = 16, [12] = 15.5, [13] = 15, [14] = 14.5, [15] = 14, [16] = 13.5, } -- Ladder of competition tiers, highest (-4) to lowest (6), with each tier's points -- multiplier. -4 = FIVB Wor..." Β |
mNo edit summary Β |
||
| (26 intermediate revisions by the same user not shown) | |||
| Line 40: | Line 40: | ||
-- Draw size -> number of "Place" rows to show. | -- Draw size -> number of "Place" rows to show. | ||
local drawSizes = { ["12"] = 12, ["12 teams"] = 12, ["16"] = 16, ["16 teams"] = 16 } | local drawSizes = { ["12"] = 12, ["12 teams"] = 12, ["16"] = 16, ["16 teams"] = 16 } | ||
-- Event rating -> a clean display label. No longer used by the subheading (which now uses | |||
-- tierLabel/startIndex instead, for star notation), but still used as p.ratingLabel's | |||
-- fallback for a rating with no startIndex entry. Keys must stay in sync with startIndex. | |||
local ratingLabel = { | |||
["4 star"] = "4-Star", ["4"] = "4-Star", | |||
["3 star"] = "3-Star", ["3"] = "3-Star", | |||
["2 star"] = "2-Star", ["2"] = "2-Star", | |||
["1 star"] = "1-Star", ["1"] = "1-Star", | |||
["no star"] = "No Star", ["none"] = "No Star", ["0"] = "No Star", | |||
["no star international"] = "No Star - International", ["international"] = "No Star - International", | |||
} | |||
local ordinal = { | local ordinal = { | ||
| Line 46: | Line 58: | ||
[13] = "13th", [14] = "14th", [15] = "15th", [16] = "16th", | [13] = "13th", [14] = "14th", [15] = "15th", [16] = "16th", | ||
} | } | ||
-- Column headers show the star rating each division's tier corresponds to, instead of a | |||
-- generic "Div N" label -- e.g. a 3-Star event's 3 divisions read "3β
/ 2β
/ 1β
" rather | |||
-- than "Div 1 / Div 2 / Div 3". Tiers 5 and 6 ("Local Competitive" and "Local Club Entry") | |||
-- are both genuinely zero-star, so both display the same "0β
" -- and the international | |||
-- tiers aren't star-rated at all, so those fall back to "Int'l" instead of a star count. | |||
-- | |||
-- Uses the plain "β
" text glyph (U+2605), not the "βοΈ" colour emoji -- the emoji forces | |||
-- its own yellow rendering and an oversized glyph that doesn't match the surrounding | |||
-- text, where "β
" just renders in the header's own font colour and size. | |||
local tierStars = { | |||
[0] = "5β
", -- ABVT Elite | |||
[1] = "4β
", -- ABVT Challenger / Pacific Games | |||
[2] = "3β
", -- Highest State | |||
[3] = "2β
", -- Competitive State | |||
[4] = "1β
", -- Entry Level State | |||
[5] = "0β
", -- Local Competitive | |||
[6] = "0β
", -- Local Club Entry level | |||
} | |||
local function tierLabel(tier) | |||
if tierStars[tier] then | |||
return tierStars[tier] | |||
elseif tier < 0 then | |||
return "Int'l" | |||
else | |||
return "" -- outside the ladder entirely -- matches the blank data cells below it | |||
end | |||
end | |||
-- Division -> ladder tier. Normally one rung per division (tier = start + d - 1) -- but on | |||
-- an NBVA child event page, NBVA's own scheme has more divisions (up to 6: AAA/AA/A/BBB/ | |||
-- BB/B) than the general ABVRS ladder has distinct tiers below a typical rating, so the | |||
-- straight formula runs off the bottom of the ladder into undefined tiers, printing blank | |||
-- headers/cells for the last one or more divisions. On an NBVA page, once the naive tier | |||
-- reaches the "0 star" floor (tiers 5 and 6, the two lowest tiers this module defines), | |||
-- tiers instead advance one rung per *two* divisions, and never advance past tier 6 -- | |||
-- i.e. each of the two zero-star tiers is shared by two divisions instead of one, and any | |||
-- further divisions beyond that just repeat tier 6, rather than the table ever going | |||
-- blank. Added 2026-09-18, confirmed against a worked 2-Star/6-division example: divisions | |||
-- 1-2 step normally (2β
, 1β
), divisions 3-4 both land on the first 0β
tier, and divisions | |||
-- 5-6 both land on the second (lowest) 0β
tier. Off (the plain "one rung per division" | |||
-- rule, unchanged from before) on every non-NBVA page. | |||
local ZERO_STAR_START_TIER = 5 | |||
local LADDER_FLOOR_TIER = 6 | |||
local function tierForDivision(start, d, applyNBVAException) | |||
local naiveTier = start + d - 1 | |||
if not applyNBVAException or naiveTier < ZERO_STAR_START_TIER then | |||
return naiveTier | |||
end | |||
-- d0 is the division number at which the *naive* tier first reaches the zero-star | |||
-- floor (ZERO_STAR_START_TIER) -- from there on, tiers advance one rung per two | |||
-- divisions instead of one per division, and never advance past the ladder floor. | |||
local d0 = ZERO_STAR_START_TIER - start + 1 | |||
local offset = d - d0 | |||
local tier = ZERO_STAR_START_TIER + math.floor(offset / 2) | |||
if tier > LADDER_FLOOR_TIER then | |||
tier = LADDER_FLOOR_TIER | |||
end | |||
return tier | |||
end | |||
local function trim(s) | local function trim(s) | ||
| Line 52: | Line 130: | ||
if s == "" then return nil end | if s == "" then return nil end | ||
return s | return s | ||
end | |||
-- Prefers org_code, falls back to the deprecated region_code -- mirrors Module:NBVA | |||
-- Points' own resolveCode (and, in turn, Module:Beach Org's) rather than requiring either | |||
-- of those modules, so this module keeps no load-order dependency on them. Keep the three | |||
-- in sync by hand if the org_code/region_code fallback rule ever changes. Added 2026-09-18 | |||
-- so this module can detect the NBVA-child-page exception above. | |||
local function resolveCode(args) | |||
return trim(args.org_code) or trim(args.region_code) or "" | |||
end | end | ||
| Line 69: | Line 156: | ||
end | end | ||
return tostring(n) | return tostring(n) | ||
end | |||
-- {{#invoke:ABVRS Points|ratingLabel|rating=3 Star}} -> "3β
(highest division rating)". | |||
-- Used by Template:Beach Event to display the event's own ABVRS rating on the page's info | |||
-- table. Rather than printing the plain rating name (e.g. "3-Star"), this shows the same | |||
-- star notation as the points table's own Division 1 column header, plus a short label | |||
-- clarifying that it's the event's highest division -- since divisions below it step down | |||
-- the ladder to lower star ratings, "3-Star" alone read as if the whole event were 3-Star, | |||
-- when only its top division is. | |||
-- | |||
-- Reuses startIndex/tierLabel rather than duplicating a separate lookup table, so this | |||
-- always matches whatever the points table's own top-division header shows -- including | |||
-- "Int'l" for a "No Star - International" rating, which has no star count of its own. | |||
function p.ratingLabel(frame) | |||
local rating = trim(frame.args.rating) | |||
if not rating then | |||
return "" | |||
end | |||
local key = normalizeRating(rating) | |||
local start = startIndex[key] | |||
if start then | |||
local label = tierLabel(start) | |||
if label ~= "" then | |||
return label .. " (highest division rating)" | |||
end | |||
end | |||
return ratingLabel[key] or rating | |||
end | end | ||
| Line 95: | Line 209: | ||
if divisions < 1 then | if divisions < 1 then | ||
return "" | return "" | ||
end | |||
-- Whether this event's resolved org is NBVA -- gates the ladder-floor-doubling | |||
-- exception in tierForDivision above, and thereby also affects tab2Text below (via | |||
-- tierForDivision(start, 4, isNBVAChildPage)). Added 2026-09-18. org_code/region_code | |||
-- weren't previously passed to this module at all -- Template:Beach Event and | |||
-- Template:ABVRS Points Table both needed a one-line addition to pass them through | |||
-- (see claude/nbva-points-table.md and the template sections below). | |||
local code = resolveCode(args) | |||
local isNBVAChildPage = string.lower(code) == "nbva" | |||
-- Subheading reads "Division 1 is a 3β
ABVRS event with a 12-team draw" on tab 1 (using | |||
-- the same star notation as Division 1's own column header, tierLabel(start)) and, when | |||
-- there are at least 4 divisions (i.e. the mobile tab-2 view has something to show), | |||
-- "Division 4 is a [tier] ABVRS event with a 12-team draw" on tab 2 -- using | |||
-- tierForDivision(start, 4, isNBVAChildPage), the exact same tier the Division 4 | |||
-- column header below already uses, so the two always agree (added 2026-09-18, | |||
-- confirmed live -- see claude/nbva-points-table.md, "Tab-2 subheading now reflects | |||
-- Division 4"). Both strings are stashed as data attributes for Common.js's tab script | |||
-- to swap between; "Int'l" is the only label either produces that takes "an" rather | |||
-- than "a". | |||
local topLabel = tierLabel(start) | |||
local article = "a" | |||
if topLabel:match("^[AEIOUaeiou]") then | |||
article = "an" | |||
end | |||
local tab1Text = "Division 1 is " .. article .. " " .. topLabel .. " ABVRS event with a " .. drawSize .. "-team draw" | |||
local tab2Text = nil | |||
if divisions >= 4 then | |||
local tab2Label = tierLabel(tierForDivision(start, 4, isNBVAChildPage)) | |||
if tab2Label ~= "" then | |||
local tab2Article = "a" | |||
if tab2Label:match("^[AEIOUaeiou]") then | |||
tab2Article = "an" | |||
end | |||
tab2Text = "Division 4 is " .. tab2Article .. " " .. tab2Label .. " ABVRS event with a " .. drawSize .. "-team draw" | |||
end | |||
end | end | ||
| Line 103: | Line 255: | ||
:css("font-size", "1.1em") | :css("font-size", "1.1em") | ||
:css("font-weight", "bold") | :css("font-weight", "bold") | ||
:css("margin", "1em 0 0. | :css("margin", "1em 0 0") | ||
:wikitext(" | :wikitext("ABVRS Points on Offer") | ||
Β | |||
local subheading = out:tag("div") | |||
:addClass("zunagi-points-subheading") | |||
:css("font-size", "0.8em") | |||
:css("color", "#666") | |||
:css("margin", "4px 0 12px 0") | |||
:attr("data-tab1-text", tab1Text) | |||
:wikitext(tab1Text) | |||
Β | |||
if tab2Text then | |||
subheading:attr("data-tab2-text", tab2Text) | |||
end | |||
Β | |||
-- Explicit width:100% (not just "auto") on the wrapper. overflow-x:auto puts this div | |||
-- into its own block formatting context, and on some mobile browsers a BFC root with | |||
-- no explicit width shrinks to fit its content instead of filling the space available | |||
-- -- fine when there are enough columns that the table's natural width already fills | |||
-- the screen, but visible as unwanted empty space next to a narrower table (e.g. only | |||
-- 2 divisions). Giving the wrapper itself width:100% removes that ambiguity so the | |||
-- table's own width:100% (below) resolves against the full available width. | |||
local wrapper = out:tag("div") | |||
:addClass("zunagi-points-table-wrapper") | |||
:css("overflow-x", "auto") | |||
:css("width", "100%") | |||
:css("box-sizing", "border-box") | |||
Β | |||
-- Border/shading colours match .zunagi-event-record (the info table above this one on | |||
-- the event page), for a consistent look: #f8f9fa header shading, #eaecf0 cell | |||
-- dividers, #c8ccd1 outer border. | |||
-- | |||
-- Columns use a percentage width (100 / number of columns) rather than a fixed pixel | |||
-- width. Fixed pixel widths looked right on desktop, but on a narrow phone screen the | |||
-- site's own responsive CSS still squeezes the table down to fit -- and since that | |||
-- squeeze isn't proportional across fixed-width columns, they end up uneven instead | |||
-- of just uniformly smaller. A percentage of the table's own width shrinks (or grows) | |||
-- every column by the same proportion no matter how wide the table itself ends up. | |||
local columns = divisions + 1 | |||
local colWidth = string.format("%.4f%%", 100 / columns) | |||
local tbl = | local tbl = wrapper:tag("table") | ||
:addClass("wikitable zunagi-points-table") | |||
:css("width", "100%") | |||
:css("margin-top", "0") | |||
:css("border-collapse", "collapse") | |||
:css("table-layout", "fixed") | |||
:css("font-size", "1em") | |||
:css("border-top", "1px solid #c8ccd1") | |||
:css("border-bottom", "1px solid #c8ccd1") | |||
local headerRow = tbl:tag("tr") | local headerRow = tbl:tag("tr") | ||
headerRow:tag("th"):wikitext("Place") | headerRow:tag("th") | ||
:css("padding", "6px 10px") | |||
:css("background-color", "#f8f9fa") | |||
:css("border", "1px solid #eaecf0") | |||
:css("width", colWidth) | |||
:wikitext("Place") | |||
for d = 1, divisions do | for d = 1, divisions do | ||
headerRow:tag("th"):wikitext(" | local tier = tierForDivision(start, d, isNBVAChildPage) | ||
local th = headerRow:tag("th") | |||
:css("padding", "6px 14px") | |||
:css("background-color", "#f8f9fa") | |||
:css("border", "1px solid #eaecf0") | |||
:css("width", colWidth) | |||
:css("text-align", "center") | |||
:css("white-space", "nowrap") | |||
if tierStars[tier] then | |||
-- Same font-size as the rest of the table (no more 1.15em bump) now that | |||
-- the table's own base size has been raised to match the rest of the page. | |||
th:tag("span") | |||
:wikitext(tierStars[tier]) | |||
else | |||
-- "Int'l" (the only tier without a star-notation label) stays bold like | |||
-- the rest of the header row. | |||
th:tag("span") | |||
:css("font-weight", "bold") | |||
:wikitext(tierLabel(tier)) | |||
end | |||
end | end | ||
for place = 1, drawSize do | for place = 1, drawSize do | ||
local row = tbl:tag("tr") | local row = tbl:tag("tr") | ||
row:tag("th"):wikitext(ordinal[place] or (place .. "th")) | row:tag("th") | ||
:css("padding", "6px 10px") | |||
:css("background-color", "#f8f9fa") | |||
:css("border", "1px solid #eaecf0") | |||
:css("width", colWidth) | |||
:wikitext(ordinal[place] or (place .. "th")) | |||
for d = 1, divisions do | for d = 1, divisions do | ||
local tier = start | local tier = tierForDivision(start, d, isNBVAChildPage) | ||
local mult = multiplier[tier] | local mult = multiplier[tier] | ||
local cell = row:tag("td") | local cell = row:tag("td") | ||
:css("text-align", "center") | |||
:css("padding", "6px 14px") | |||
:css("background-color", "#fff") | |||
:css("border", "1px solid #eaecf0") | |||
:css("width", colWidth) | |||
if mult and base[place] then | if mult and base[place] then | ||
cell:wikitext(formatNumber(base[place] * mult)) | cell:wikitext(formatNumber(base[place] * mult)) | ||
Latest revision as of 14:19, 18 September 2026
Documentation for this module may be created at Module:ABVRS Points/doc
local p = {}
-- Base ranking points by finishing place (1st-16th), from the 2024/25 ABVRS Points Table
-- (https://zunagi.com/images/2024-25-ABVRS-Points-Table.pdf).
local base = {
[1] = 30, [2] = 28, [3] = 25, [4] = 23, [5] = 21, [6] = 20,
[7] = 19, [8] = 18, [9] = 17, [10] = 16.5, [11] = 16, [12] = 15.5,
[13] = 15, [14] = 14.5, [15] = 14, [16] = 13.5,
}
-- Ladder of competition tiers, highest (-4) to lowest (6), with each tier's points
-- multiplier. -4 = FIVB World Championships, -3 = Beach Pro Tour Elite 16, -2 = Beach Pro
-- Tour Challenge/Asian Championships, -1 = Beach Pro Tour Futures/AVC Open/Australian
-- Championships (these four are all "No Star - International" in the PDF, differentiated
-- from each other by tournament name rather than star count), 0 = ABVT Elite (5 star),
-- 1 = ABVT Challenger/Pacific Games (4 star), 2 = Highest State (3 star), 3 = Competitive
-- State (2 star), 4 = Entry Level State (1 star), 5 = Local Competitive (no star),
-- 6 = Local Club Entry level (no star).
local multiplier = {
[-4] = 80, [-3] = 70, [-2] = 50, [-1] = 30,
[0] = 20, [1] = 12, [2] = 8, [3] = 5, [4] = 3, [5] = 2, [6] = 1,
}
-- Event rating -> ladder index that Division 1 starts at. Division 2 is one tier lower,
-- Division 3 two tiers lower, and so on.
--
-- "No Star - International" is only wired up to the Australian Championships tier (-1) for
-- now, since that's the only one of the four international tiers this calendar currently
-- needs (ABVC Australian Championships). If a future event needs one of the other three,
-- add it here the same way, e.g. ["no star international asian championships"] = -2.
local startIndex = {
["4 star"] = 1, ["4"] = 1,
["3 star"] = 2, ["3"] = 2,
["2 star"] = 3, ["2"] = 3,
["1 star"] = 4, ["1"] = 4,
["no star"] = 5, ["none"] = 5, ["0"] = 5,
["no star international"] = -1, ["international"] = -1,
}
-- Draw size -> number of "Place" rows to show.
local drawSizes = { ["12"] = 12, ["12 teams"] = 12, ["16"] = 16, ["16 teams"] = 16 }
-- Event rating -> a clean display label. No longer used by the subheading (which now uses
-- tierLabel/startIndex instead, for star notation), but still used as p.ratingLabel's
-- fallback for a rating with no startIndex entry. Keys must stay in sync with startIndex.
local ratingLabel = {
["4 star"] = "4-Star", ["4"] = "4-Star",
["3 star"] = "3-Star", ["3"] = "3-Star",
["2 star"] = "2-Star", ["2"] = "2-Star",
["1 star"] = "1-Star", ["1"] = "1-Star",
["no star"] = "No Star", ["none"] = "No Star", ["0"] = "No Star",
["no star international"] = "No Star - International", ["international"] = "No Star - International",
}
local ordinal = {
[1] = "1st", [2] = "2nd", [3] = "3rd", [4] = "4th", [5] = "5th", [6] = "6th",
[7] = "7th", [8] = "8th", [9] = "9th", [10] = "10th", [11] = "11th", [12] = "12th",
[13] = "13th", [14] = "14th", [15] = "15th", [16] = "16th",
}
-- Column headers show the star rating each division's tier corresponds to, instead of a
-- generic "Div N" label -- e.g. a 3-Star event's 3 divisions read "3β
/ 2β
/ 1β
" rather
-- than "Div 1 / Div 2 / Div 3". Tiers 5 and 6 ("Local Competitive" and "Local Club Entry")
-- are both genuinely zero-star, so both display the same "0β
" -- and the international
-- tiers aren't star-rated at all, so those fall back to "Int'l" instead of a star count.
--
-- Uses the plain "β
" text glyph (U+2605), not the "βοΈ" colour emoji -- the emoji forces
-- its own yellow rendering and an oversized glyph that doesn't match the surrounding
-- text, where "β
" just renders in the header's own font colour and size.
local tierStars = {
[0] = "5β
", -- ABVT Elite
[1] = "4β
", -- ABVT Challenger / Pacific Games
[2] = "3β
", -- Highest State
[3] = "2β
", -- Competitive State
[4] = "1β
", -- Entry Level State
[5] = "0β
", -- Local Competitive
[6] = "0β
", -- Local Club Entry level
}
local function tierLabel(tier)
if tierStars[tier] then
return tierStars[tier]
elseif tier < 0 then
return "Int'l"
else
return "" -- outside the ladder entirely -- matches the blank data cells below it
end
end
-- Division -> ladder tier. Normally one rung per division (tier = start + d - 1) -- but on
-- an NBVA child event page, NBVA's own scheme has more divisions (up to 6: AAA/AA/A/BBB/
-- BB/B) than the general ABVRS ladder has distinct tiers below a typical rating, so the
-- straight formula runs off the bottom of the ladder into undefined tiers, printing blank
-- headers/cells for the last one or more divisions. On an NBVA page, once the naive tier
-- reaches the "0 star" floor (tiers 5 and 6, the two lowest tiers this module defines),
-- tiers instead advance one rung per *two* divisions, and never advance past tier 6 --
-- i.e. each of the two zero-star tiers is shared by two divisions instead of one, and any
-- further divisions beyond that just repeat tier 6, rather than the table ever going
-- blank. Added 2026-09-18, confirmed against a worked 2-Star/6-division example: divisions
-- 1-2 step normally (2β
, 1β
), divisions 3-4 both land on the first 0β
tier, and divisions
-- 5-6 both land on the second (lowest) 0β
tier. Off (the plain "one rung per division"
-- rule, unchanged from before) on every non-NBVA page.
local ZERO_STAR_START_TIER = 5
local LADDER_FLOOR_TIER = 6
local function tierForDivision(start, d, applyNBVAException)
local naiveTier = start + d - 1
if not applyNBVAException or naiveTier < ZERO_STAR_START_TIER then
return naiveTier
end
-- d0 is the division number at which the *naive* tier first reaches the zero-star
-- floor (ZERO_STAR_START_TIER) -- from there on, tiers advance one rung per two
-- divisions instead of one per division, and never advance past the ladder floor.
local d0 = ZERO_STAR_START_TIER - start + 1
local offset = d - d0
local tier = ZERO_STAR_START_TIER + math.floor(offset / 2)
if tier > LADDER_FLOOR_TIER then
tier = LADDER_FLOOR_TIER
end
return tier
end
local function trim(s)
if not s then return nil end
s = mw.text.trim(s)
if s == "" then return nil end
return s
end
-- Prefers org_code, falls back to the deprecated region_code -- mirrors Module:NBVA
-- Points' own resolveCode (and, in turn, Module:Beach Org's) rather than requiring either
-- of those modules, so this module keeps no load-order dependency on them. Keep the three
-- in sync by hand if the org_code/region_code fallback rule ever changes. Added 2026-09-18
-- so this module can detect the NBVA-child-page exception above.
local function resolveCode(args)
return trim(args.org_code) or trim(args.region_code) or ""
end
-- Lowercase, and treat hyphens as spaces so "No Star - International" and "No Star
-- International" (and similar minor punctuation variants) match the same lookup key.
local function normalizeRating(s)
s = string.lower(s)
s = s:gsub("%-", " ")
s = s:gsub("%s+", " ")
return mw.text.trim(s)
end
-- Show "240" rather than "240.0", but keep the ".5" values (e.g. "82.5") as-is.
local function formatNumber(n)
if n == math.floor(n) then
return tostring(math.floor(n))
end
return tostring(n)
end
-- {{#invoke:ABVRS Points|ratingLabel|rating=3 Star}} -> "3β
(highest division rating)".
-- Used by Template:Beach Event to display the event's own ABVRS rating on the page's info
-- table. Rather than printing the plain rating name (e.g. "3-Star"), this shows the same
-- star notation as the points table's own Division 1 column header, plus a short label
-- clarifying that it's the event's highest division -- since divisions below it step down
-- the ladder to lower star ratings, "3-Star" alone read as if the whole event were 3-Star,
-- when only its top division is.
--
-- Reuses startIndex/tierLabel rather than duplicating a separate lookup table, so this
-- always matches whatever the points table's own top-division header shows -- including
-- "Int'l" for a "No Star - International" rating, which has no star count of its own.
function p.ratingLabel(frame)
local rating = trim(frame.args.rating)
if not rating then
return ""
end
local key = normalizeRating(rating)
local start = startIndex[key]
if start then
local label = tierLabel(start)
if label ~= "" then
return label .. " (highest division rating)"
end
end
return ratingLabel[key] or rating
end
-- {{#invoke:ABVRS Points|table|rating=3 Star|draw_size=12|divisions=3}}
function p.table(frame)
local args = frame.args
local rating = trim(args.rating)
local drawSizeRaw = trim(args.draw_size)
local divisionsRaw = trim(args.divisions)
-- All three hidden fields must be filled in, or nothing is shown.
if not rating or not drawSizeRaw or not divisionsRaw then
return ""
end
local start = startIndex[normalizeRating(rating)]
local drawSize = drawSizes[string.lower(drawSizeRaw)]
local divisions = tonumber(divisionsRaw)
if not start or not drawSize or not divisions then
return ""
end
divisions = math.floor(divisions)
if divisions < 1 then
return ""
end
-- Whether this event's resolved org is NBVA -- gates the ladder-floor-doubling
-- exception in tierForDivision above, and thereby also affects tab2Text below (via
-- tierForDivision(start, 4, isNBVAChildPage)). Added 2026-09-18. org_code/region_code
-- weren't previously passed to this module at all -- Template:Beach Event and
-- Template:ABVRS Points Table both needed a one-line addition to pass them through
-- (see claude/nbva-points-table.md and the template sections below).
local code = resolveCode(args)
local isNBVAChildPage = string.lower(code) == "nbva"
-- Subheading reads "Division 1 is a 3β
ABVRS event with a 12-team draw" on tab 1 (using
-- the same star notation as Division 1's own column header, tierLabel(start)) and, when
-- there are at least 4 divisions (i.e. the mobile tab-2 view has something to show),
-- "Division 4 is a [tier] ABVRS event with a 12-team draw" on tab 2 -- using
-- tierForDivision(start, 4, isNBVAChildPage), the exact same tier the Division 4
-- column header below already uses, so the two always agree (added 2026-09-18,
-- confirmed live -- see claude/nbva-points-table.md, "Tab-2 subheading now reflects
-- Division 4"). Both strings are stashed as data attributes for Common.js's tab script
-- to swap between; "Int'l" is the only label either produces that takes "an" rather
-- than "a".
local topLabel = tierLabel(start)
local article = "a"
if topLabel:match("^[AEIOUaeiou]") then
article = "an"
end
local tab1Text = "Division 1 is " .. article .. " " .. topLabel .. " ABVRS event with a " .. drawSize .. "-team draw"
local tab2Text = nil
if divisions >= 4 then
local tab2Label = tierLabel(tierForDivision(start, 4, isNBVAChildPage))
if tab2Label ~= "" then
local tab2Article = "a"
if tab2Label:match("^[AEIOUaeiou]") then
tab2Article = "an"
end
tab2Text = "Division 4 is " .. tab2Article .. " " .. tab2Label .. " ABVRS event with a " .. drawSize .. "-team draw"
end
end
local out = mw.html.create("")
out:tag("div")
:addClass("zunagi-points-heading")
:css("font-size", "1.1em")
:css("font-weight", "bold")
:css("margin", "1em 0 0")
:wikitext("ABVRS Points on Offer")
local subheading = out:tag("div")
:addClass("zunagi-points-subheading")
:css("font-size", "0.8em")
:css("color", "#666")
:css("margin", "4px 0 12px 0")
:attr("data-tab1-text", tab1Text)
:wikitext(tab1Text)
if tab2Text then
subheading:attr("data-tab2-text", tab2Text)
end
-- Explicit width:100% (not just "auto") on the wrapper. overflow-x:auto puts this div
-- into its own block formatting context, and on some mobile browsers a BFC root with
-- no explicit width shrinks to fit its content instead of filling the space available
-- -- fine when there are enough columns that the table's natural width already fills
-- the screen, but visible as unwanted empty space next to a narrower table (e.g. only
-- 2 divisions). Giving the wrapper itself width:100% removes that ambiguity so the
-- table's own width:100% (below) resolves against the full available width.
local wrapper = out:tag("div")
:addClass("zunagi-points-table-wrapper")
:css("overflow-x", "auto")
:css("width", "100%")
:css("box-sizing", "border-box")
-- Border/shading colours match .zunagi-event-record (the info table above this one on
-- the event page), for a consistent look: #f8f9fa header shading, #eaecf0 cell
-- dividers, #c8ccd1 outer border.
--
-- Columns use a percentage width (100 / number of columns) rather than a fixed pixel
-- width. Fixed pixel widths looked right on desktop, but on a narrow phone screen the
-- site's own responsive CSS still squeezes the table down to fit -- and since that
-- squeeze isn't proportional across fixed-width columns, they end up uneven instead
-- of just uniformly smaller. A percentage of the table's own width shrinks (or grows)
-- every column by the same proportion no matter how wide the table itself ends up.
local columns = divisions + 1
local colWidth = string.format("%.4f%%", 100 / columns)
local tbl = wrapper:tag("table")
:addClass("wikitable zunagi-points-table")
:css("width", "100%")
:css("margin-top", "0")
:css("border-collapse", "collapse")
:css("table-layout", "fixed")
:css("font-size", "1em")
:css("border-top", "1px solid #c8ccd1")
:css("border-bottom", "1px solid #c8ccd1")
local headerRow = tbl:tag("tr")
headerRow:tag("th")
:css("padding", "6px 10px")
:css("background-color", "#f8f9fa")
:css("border", "1px solid #eaecf0")
:css("width", colWidth)
:wikitext("Place")
for d = 1, divisions do
local tier = tierForDivision(start, d, isNBVAChildPage)
local th = headerRow:tag("th")
:css("padding", "6px 14px")
:css("background-color", "#f8f9fa")
:css("border", "1px solid #eaecf0")
:css("width", colWidth)
:css("text-align", "center")
:css("white-space", "nowrap")
if tierStars[tier] then
-- Same font-size as the rest of the table (no more 1.15em bump) now that
-- the table's own base size has been raised to match the rest of the page.
th:tag("span")
:wikitext(tierStars[tier])
else
-- "Int'l" (the only tier without a star-notation label) stays bold like
-- the rest of the header row.
th:tag("span")
:css("font-weight", "bold")
:wikitext(tierLabel(tier))
end
end
for place = 1, drawSize do
local row = tbl:tag("tr")
row:tag("th")
:css("padding", "6px 10px")
:css("background-color", "#f8f9fa")
:css("border", "1px solid #eaecf0")
:css("width", colWidth)
:wikitext(ordinal[place] or (place .. "th"))
for d = 1, divisions do
local tier = tierForDivision(start, d, isNBVAChildPage)
local mult = multiplier[tier]
local cell = row:tag("td")
:css("text-align", "center")
:css("padding", "6px 14px")
:css("background-color", "#fff")
:css("border", "1px solid #eaecf0")
:css("width", colWidth)
if mult and base[place] then
cell:wikitext(formatNumber(base[place] * mult))
end
end
end
return tostring(out)
end
return p