The cup board: the league ranks, the level breaks the tie

Oliver wanted to rank the level a league was taken at. The challenge held: xp
comes only from playing, so a board that rewards a low level rewards not
playing — it would pay you to avoid the three rooms the season leans on. It
also inverts every other number in the game, freezes after five entries, and
makes the player who played more look worse.

So the league ranks and the level decides between everyone who got equally far,
where "lower is better" reads naturally. BOARDS.u carries subLow; server
rank_key is the other half, and the same key decides whether a submission beats
the row already standing. The level is written once at the promotion — before
the league moves, because it is the level that WON the cup — mirrored like the
rest of the profile, and inscribed under the shield it belongs to. Earned
shields smoulder while they are on screen: five CSS embers each, no loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-31 15:51:38 +02:00
1 parent e96b4374d8
commit 80eb60d181
8 files changed
+226 -40

No files matched your search

+39 -13
View File
@@ -598,6 +598,15 @@ const countUp = (write, target, dur = 700) => {
if (p < 1) cardTimers.push(setTimeout(tick, 30));
})();
};
// EMBERS off an earned shield (2026-08-31, Oliver: "particles die random nach oben wegfliegen, wie
// leichtes feuer"). CSS keyframes, not a loop: the menu deliberately parks the game loop, and six
// two-pixel sparks per badge have no business waking it. The randomness is baked into each spark's
// own delay, duration and lane at build time, so no two shields burn in step.
const embers = won => !won ? '' : Array.from({ length: 5 }, () =>
`<em style="left:${(14 + Math.random() * 62).toFixed(0)}%;` +
`animation-delay:${(Math.random() * 2.6).toFixed(2)}s;` +
`animation-duration:${(1.9 + Math.random() * 1.5).toFixed(2)}s"></em>`).join('');
function buildCard() {
for (const t of cardTimers) clearTimeout(t);
cardTimers = [];
@@ -605,10 +614,12 @@ function buildCard() {
document.getElementById('cdName').textContent = stats.savedName() || 'THE WALK-ON';
document.getElementById('cdBadge').src = `assets/badge_l${lg.id}.png?v=${SPRV}`;
const el = document.getElementById('cdLeague');
el.textContent = `LEAGUE ${lg.id} OF ${LEAGUES.length} — ${lg.name}`;
// the crest stands beside this line now, and the column is narrow: two short lines beat one
// that wraps three times (the league's NAME is the second, in its own colour)
el.innerHTML = `LEAGUE ${lg.id} OF ${LEAGUES.length}<i>${lg.name}</i>`;
el.style.color = lg.color;
document.getElementById('cdLevel').textContent =
'LEVEL ' + pr.level + ' OF ' + profile.LEVEL_CAP;
// ...and the cap belongs to the bar under it, which already counts toward the next level
document.getElementById('cdLevel').textContent = 'LEVEL ' + pr.level;
document.getElementById('cdFrom').textContent = pr.level;
document.getElementById('cdTo').textContent = pr.need ? pr.level + 1 : 'MAX';
// both bars sweep from empty to their mark (the 0.7s eased transition in the CSS)
@@ -628,18 +639,24 @@ function buildCard() {
const row = document.getElementById('cdBadges');
row.innerHTML = '';
for (const l of LEAGUES) {
const img = document.createElement('img');
img.src = `assets/badge_l${l.id}.png?v=${SPRV}`;
img.title = l.name;
// a league's badge is EARNED by finishing it — winning its cup. Being past it means exactly
// that; league five has no "past", so its shield lights with the champion's crown.
img.className = l.id < profile.prof.lg || (l.id === LEAGUES.length && profile.prof.ch) ? '' : 'dark';
row.appendChild(img);
const won = l.id < profile.prof.lg || (l.id === LEAGUES.length && profile.prof.ch);
const at = profile.cupAt(l.id);
const cell = document.createElement('div');
cell.className = 'badge' + (won ? '' : ' dark');
// THE INSCRIPTION (2026-08-31, the cup board): a trophy says when it was won. Here that is the
// LEVEL it was won at — the number the cup board breaks its ties on, in the one place where a
// figure set once and never again belongs.
// an unearned slot says nothing: the shield's own art already says which league it is
cell.innerHTML = `<img src="assets/badge_l${l.id}.png?v=${SPRV}" alt="" title="${l.name}">`
+ `<u>${won ? (at ? 'AT ' + at : 'WON') : '—'}</u>` + embers(won);
row.appendChild(cell);
}
const champ = document.createElement('img');
champ.src = `assets/badge_champ.png?v=${SPRV}`;
champ.title = 'CHAMPION OF THE DEAD CLUB';
champ.className = profile.prof.ch ? '' : 'dark';
const champ = document.createElement('div');
champ.className = 'badge' + (profile.prof.ch ? '' : ' dark');
champ.innerHTML = `<img src="assets/badge_champ.png?v=${SPRV}" alt="" title="CHAMPION OF THE DEAD CLUB">`
+ `<u>${profile.prof.ch ? 'CHAMPION' : '—'}</u>` + embers(profile.prof.ch);
row.appendChild(champ);
const g = stats.read('all', undefined, stats.MODE_GORE),
z = stats.read('all', undefined, stats.MODE_MATCH),
@@ -697,6 +714,15 @@ function buildPromo(pr, run) {
? `THE TITLE IS YOURS TO HOLD<u>EVERY CUP FROM HERE IS A DEFENCE</u>`
: `NEXT: ${fx.title}<u>${fx.line}</u>`;
}
// THE CUP BOARD (2026-08-31): a promotion is the row, and it posts itself. There is no name
// prompt in the way — you have had a name since the identity screen — and no personal-best gate:
// the server keeps one row per name per board and the league you just took either beats what
// stands there or does not (rank_key, server.py).
function postCup(pr) {
const took = pr.champion ? LEAGUES.length : pr.took;
if (!took || !pr.at || !stats.savedName()) return;
stats.submit(stats.savedName(), took, pr.at, stats.MODE_CUP, profile.prof.lg).catch(() => {});
}
document.getElementById('prGo').addEventListener('click', () => {
const run = promoRun; promoRun = null;
if (run && run.newKillBest && run.score > 0) askName(run);
@@ -1091,7 +1117,7 @@ const maybeRestart = e => {
// THE PROMOTION plays first (the leagues): the ceremony the cup win parked, then the name
// prompt it may also have earned (prGo hands the run on), then the title.
const promo = profile.takePromo();
if (promo) { buildPromo(promo, run); go('promo'); return; }
if (promo) { postCup(promo); buildPromo(promo, run); go('promo'); return; }
// ...on the BOARD's number, not on kills: a classic match has none, so the gore-shaped test here
// silently barred two of the three modes from ever reaching the prompt
// ...on a board that still EXISTS: classic tennis left the world board with the stats streamline