Module:ABVRS Points: Difference between revisions

From zunagi

mNo edit summary
mNo edit summary
Β 
(2 intermediate revisions by the same user not shown)
Line 86: Line 86:
return "" -- outside the ladder entirely -- matches the blank data cells below it
return "" -- outside the ladder entirely -- matches the blank data cells below it
end
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
end


Line 93: 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 165: Line 211:
end
end


-- Subheading reads "Division 1 is a 3β˜… ABVRS event with a 12-team draw" -- using the
-- Whether this event's resolved org is NBVA -- gates the ladder-floor-doubling
-- same star notation as Division 1's own column header (tierLabel(start), the tier
-- exception in tierForDivision above, and thereby also affects tab2Text below (via
-- Division 1 starts at) rather than the plain rating name, so it's consistent with
-- tierForDivision(start, 4, isNBVAChildPage)). Added 2026-09-18. org_code/region_code
-- both the points table's headers and the info table's "ABVRS Rating" row above it.
-- weren't previously passed to this module at all -- Template:Beach Event and
-- "Int'l" is the only label this produces that takes "an" rather than "a".
-- 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 topLabel = tierLabel(start)
local article = "a"
local article = "a"
if topLabel:match("^[AEIOUaeiou]") then
if topLabel:match("^[AEIOUaeiou]") then
article = "an"
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 185: Line 258:
:wikitext("ABVRS Points on Offer")
:wikitext("ABVRS Points on Offer")


out:tag("div")
local subheading = out:tag("div")
:addClass("zunagi-points-subheading")
:addClass("zunagi-points-subheading")
:css("font-size", "0.8em")
:css("font-size", "0.8em")
:css("color", "#666")
:css("color", "#666")
:css("margin", "4px 0 12px 0")
:css("margin", "4px 0 12px 0")
:wikitext("Division 1 is " .. article .. " " .. topLabel .. " ABVRS event with a " .. drawSize .. "-team draw")
: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
-- Explicit width:100% (not just "auto") on the wrapper. overflow-x:auto puts this div
Line 236: Line 314:
:wikitext("Place")
:wikitext("Place")
for d = 1, divisions do
for d = 1, divisions do
local tier = start + d - 1
local tier = tierForDivision(start, d, isNBVAChildPage)
local th = headerRow:tag("th")
local th = headerRow:tag("th")
:css("padding", "6px 14px")
:css("padding", "6px 14px")
Line 267: Line 345:
:wikitext(ordinal[place] or (place .. "th"))
:wikitext(ordinal[place] or (place .. "th"))
for d = 1, divisions do
for d = 1, divisions do
local tier = start + d - 1
local tier = tierForDivision(start, d, isNBVAChildPage)
local mult = multiplier[tier]
local mult = multiplier[tier]
local cell = row:tag("td")
local cell = row:tag("td")

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