The draw screen retires: the league cup rolls inside the run, ENTER THE DRAW moves onto the setup button

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-29 16:46:34 +02:00
1 parent 812bea3234
commit dbfd085493
5 files changed
+34 -130

No files matched your search

+13
View File
@@ -491,6 +491,19 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
`RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
## Session log: THE DRAW SCREEN RETIRES (2026-08-29, same day, STAGING ONLY)
Oliver: "the draw ist doch jetzt eigentlich obsolet, oder?" — yes. The screen was built for the
18-rung ladder (his own 2026-08-15 ask), where four bands rolling out of a deep hat was a
ceremony; a league cup shuffles 2-3 faces the setup's league panel already shows face-up. Deleted:
the #bracket stage (DOM, CSS, buildBracket, its timers and routing). What survives: the shuffle
itself lives INSIDE the run (cupDraw, without replacement, the final settled — startMatch's `draw`
parameter stays for the suite's validation test, renamed accordingly), each round still announces
itself on the walk-on modal, and the cup's big setup button now says **ENTER THE DRAW** — all the
ritual the moment needs. The rules modal's copy went league-neutral ("UP YOUR LEAGUE'S DEAD
HIERARCHY — THE FINAL IS SET"; a 3-rung league plays three rounds, not four). The screen's
mechanics live in git history (like the lob) if the ceremony is ever asked back.
## Session log: THE LEAGUES — the plaque moves mid-court, and the 94vw scrollbar (2026-08-29, same day, STAGING ONLY)
Live review with Oliver on the pane, three finds:
+2 -45
View File
@@ -177,7 +177,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
/* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame
alive across every screen is the point — a screen that unmounts the whole overlay flashes the
court through between menus. */
#splash, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #stsub, #bracket, #vs, #train, #player, #promo, #card { display: none; }
#splash, #setup, #stats, #ranks, #newbest, #secret, #bosses, #stmode, #stsub, #vs, #train, #player, #promo, #card { display: none; }
#intro[data-stage="splash"] #splash { display: flex; flex-direction: column; flex: 1; }
/* flex COLUMN, not block: it is what lets the footer's margin-top:auto reach the bottom of a
screen whose content does not fill it */
@@ -188,7 +188,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
#intro[data-stage="ranks"] #ranks,
#intro[data-stage="secret"] #secret,
#intro[data-stage="bosses"] #bosses,
#intro[data-stage="bracket"] #bracket,
#intro[data-stage="train"] #train,
#intro[data-stage="vs"] #vs,
#intro[data-stage="player"] #player,
@@ -411,35 +410,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
/* the training picker is 18 rungs deep — the tile carries the HEAD (the same z_*head crop the
draw and the walk-on wear), which reads at a third of the boss portrait's height */
#trainList img { height: 44px; width: auto; display: block; image-rendering: pixelated; }
/* THE DRAW. Four rungs read top-down as a ladder, because that is what the mode is — the bracket
shape a real draw is printed in needs width a phone has not got, and half of it would be blank
anyway (you only ever play one side of it). The rows spin their portrait and their name while
the draw is being made, and .set is what a landed one wears: the placeholder look is on the
ROW, so a slot that has stopped is obvious at a glance and START stays out of reach until the
last of them lands. The final wears .set from the first frame — it was never in doubt. */
#cupRows { display: flex; flex-direction: column; gap: 8px; }
.cupRow { display: grid; grid-template-columns: 54px 1fr auto; grid-template-rows: auto auto;
align-items: center; column-gap: 10px; row-gap: 2px;
padding: 7px 10px; background: var(--panel); border: 2px solid var(--line);
/* the connector: every round but the first hangs off the one above it */
position: relative; }
.cupRow + .cupRow::before { content: ''; position: absolute; left: 35px; top: -10px; width: 2px;
height: 10px; background: var(--line); }
.cupRow img { grid-column: 1; grid-row: 1 / span 2; width: 54px; height: 54px;
object-fit: contain; object-position: center; image-rendering: pixelated;
/* mid-spin the faces flicker past unreadable; dimming them says "still rolling"
without a second element to keep in sync */
opacity: 0.5; filter: grayscale(0.7); }
.cupRow u { grid-column: 2; grid-row: 1; font: 400 8px Silkscreen, monospace; color: var(--dim);
letter-spacing: 1.5px; text-decoration: none; text-align: left; }
.cupRow b { grid-column: 2; grid-row: 2; font: 400 11px Silkscreen, monospace; color: var(--dim);
letter-spacing: 0.5px; line-height: 1.35; text-align: left; }
.cupRow i { grid-column: 3; grid-row: 1 / span 2; font: 400 8px Silkscreen, monospace;
font-style: normal; color: var(--dim); letter-spacing: 1px; white-space: nowrap; }
.cupRow.set img { opacity: 1; filter: none; }
.cupRow.set b { color: var(--cream); }
.cupRow.fin { border-color: var(--red); }
.cupRow.fin.set b { color: var(--acid); }
/* START is genuinely unreachable until the draw has landed — the wait IS the ceremony */
.btn[disabled] { opacity: 0.32; cursor: default; }
/* a row, not a stacked tile: three 3:2 tiles stacked fill a phone twice over, and the art still
@@ -774,19 +744,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
<button class="btn sec" id="cdEdit">EDIT PLAYER<i>NAME · TOUR · COLOURS</i></button>
</div>
</div>
<!-- THE DRAW (cup mode only). SERVE UP lands here instead of on a court: the four rounds roll
through their own band of the ladder and stop, and what you watched land is what walks on
(main.js hands the settled rungs to startMatch). The rows are built in main.js from
config.CUP_ROUNDS, so a round added there needs no markup. -->
<div id="bracket">
<div class="hdr"><button class="chev" id="brBack">&lt;</button><h2>THE DRAW</h2></div>
<h3>FOUR ROUNDS<span id="brState">DRAWING…</span></h3>
<div id="cupRows"></div>
<div class="foot">
<p id="brSum"></p>
<button class="btn big" id="cupGo" disabled>ENTER THE DRAW</button>
</div>
</div>
<!-- THE TRAINING COURT's opponent picker: the whole ladder face-up, tap a body to drill it.
Built in main.js from config's roster (matchFoe/LADDER), so a new rung needs no markup. -->
<div id="train">
@@ -897,6 +854,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div>
</div>
</div>
<script type="module" src="src/main.js?v=328"></script>
<script type="module" src="src/main.js?v=329"></script>
</body>
</html>
+6 -7
View File
@@ -354,9 +354,9 @@ export function watchdog(dt) {
// the match mode ends. Regicide is inherited for free: smashing the ball into the body still wounds
// it, and killing it outright wins the game early (killZombie fires resolveInterlude('P'), which
// lands in matchGameWon via the empty-court check).
// `draw` is the bracket screen's settled rungs, one per cup round (main.buildBracket). It is
// optional and null means "roll each round as it comes", which is what the mode did before the
// screen existed and is still what the #cup back door gets.
// `draw` is an optional pre-settled list of rungs, one per cup round — null (the normal case
// since the bracket screen was deleted, 2026-08-29) means "roll each round as it comes". The
// parameter stays: the suite uses it to pin the validation below.
// `train` (2026-08-28) is a RUNG NUMBER and turns the run into the TRAINING COURT: that one
// opponent, one game at a time, nothing banked. The ladder is parked — d.opp never advances, a
// felled body walks straight back on, and bleeding out costs a modal instead of the run. All of
@@ -395,7 +395,7 @@ export function startMatch(cup = false, draw = null, train = 0) {
} else if (cup && !G.seen.cup) {
G.seen.cup = true;
showModal({ title: 'THE TOURNAMENT', color: UI.hot,
body: ['FOUR ROUNDS UP THE DEAD HIERARCHY',
body: ["UP YOUR LEAGUE'S DEAD HIERARCHY — THE FINAL IS SET",
'ITS POINTS DRAW BLOOD — ' + TUNE.matchPlayerHp + ' AND YOU ARE EATEN'] });
} else if (!cup && !train && !G.seen.match) {
G.seen.match = true;
@@ -414,9 +414,8 @@ export function startMatch(cup = false, draw = null, train = 0) {
// The round's rung, drawn from its band. d.opp carries it so matchFoe, the ladder speed dials and
// the felled arithmetic all read the drawn rung with no cup-awareness of their own.
// A DRAW MADE ON SCREEN IS THE DRAW THAT IS PLAYED. When the bracket screen settled the rungs they
// arrive here as cup.draw and this simply reads them off — rolling again would mean the faces the
// player watched land had nothing to do with what walks on, which is worse than showing nothing.
// A PASSED DRAW IS THE DRAW THAT IS PLAYED (it once arrived from the bracket screen; the suite
// still passes one) — this reads it off rather than rolling again.
// Guarded on the band all the same: a saved or hand-passed draw that names a rung this round does
// not contain falls back to an honest roll rather than dealing the champion in round one.
function cupDraw() {
+12 -77
View File
@@ -352,6 +352,7 @@ function buildLeaguePanel() {
// on the card now — the setup should show the league and who of it you have already beaten).
function syncSetup() {
const cupMode = pickedMode === 'cup', climbMode = pickedMode === 'zombie';
document.getElementById('cStart').textContent = cupMode ? 'ENTER THE DRAW' : 'SERVE UP';
document.getElementById('who').style.display = cupMode || climbMode ? 'none' : 'flex';
document.getElementById('lgPanel').style.display = cupMode || climbMode ? 'block' : 'none';
document.getElementById('s01').textContent =
@@ -466,82 +467,17 @@ for (const c of COURTS) {
// to the league's own court rather than serving up a court the player has not earned
if (!courtUnlocked(picked)) picked = leagueOf(profile.prof.lg).court;
// ---- THE DRAW (Oliver, 2026-08-15: "when starting the tournament there should be a view of the
// tournament hierarchy but only the finale is settled — the game against the champion... random
// browsing through the roster in the individual spots which begins fast, gets slower and then
// settles on a random character, so that every tournament is new").
//
// THE SCREEN IS THE DRAW, NOT A TEASER. `cupPicks` is what walks on: startMatch takes it and
// cupDraw reads it instead of rolling its own, so the face you watched land in ROUND ONE is the
// body across the net. A screen that showed a different draw from the one being played would be
// worse than no screen — it would teach the player that the ceremony is a lie.
//
// The roll is one recursive setTimeout per row whose gap grows with p^2 — fast, then visibly
// slowing, then stopped. No rAF: the menu deliberately parks the game loop (see menuUp), and three
// staggered timers are the whole animation.
const cupRows = document.getElementById('cupRows'), cupGo = document.getElementById('cupGo');
let cupPicks = null, cupSpins = [];
// THE DRAW SCREEN IS GONE (2026-08-29, Oliver: "das ist doch jetzt eigentlich obsolet"). It was
// built for the 18-rung ladder, where four bands rolling out of a deep hat was a ceremony worth a
// screen; a league cup shuffles 2-3 faces the setup's league panel already shows face-up. The
// shuffle itself survives INSIDE the run (cupDraw rolls without replacement, the final settled)
// and each round still announces itself on the walk-on modal — only the screen is deleted. The
// cup's big button says ENTER THE DRAW (syncSetup), which is all the ritual the moment needs.
// The screen's mechanics live in git history (like the lob) if the ceremony is ever asked back.
// the portrait: every family's own head crop (assets/_heads.py), the club pro's included — the
// same file the in-play walk-on card uses, so the draw and the court show the same face
// same file the in-play walk-on card uses, so the picker and the court show the same face
const foeArt = f => `assets/z_${f.fam}head.png?v=${SPRV}`;
function buildBracket() {
for (const t of cupSpins) clearTimeout(t);
cupSpins = [];
const R = cupRoundsFor(profile.prof.lg);
// WITHOUT REPLACEMENT, like cupDraw: a league band is 3-4 rungs, so the non-final rounds are a
// shuffle of the sub-band — every tournament a new order, no body dealt twice, the final settled
cupPicks = [];
const used = new Set();
for (const r of R) {
const pool = r.band.filter(x => !used.has(x));
const pick = (pool.length ? pool : r.band)[Math.floor(Math.random() * (pool.length ? pool : r.band).length)];
used.add(pick); cupPicks.push(pick);
}
// the header counts the league's own rounds (a 3-rung league plays three)
document.querySelector('#bracket h3').firstChild.nodeValue =
R.length === 4 ? 'FOUR ROUNDS' : 'THREE ROUNDS';
cupRows.innerHTML = '';
cupGo.disabled = true;
document.getElementById('brState').textContent = 'DRAWING…';
const lg = leagueOf(profile.prof.lg);
document.getElementById('brSum').innerHTML =
`<span>ONLY THE FINAL WAS EVER SETTLED</span><span>${lg.name} · ${
(COURTS.find(c => c.id === lg.court) || COURTS[0]).name}</span>`;
R.forEach((r, i) => {
const fixed = r.band.length === 1; // the final: one rung, nothing to roll
const row = document.createElement('div');
row.className = 'cupRow' + (fixed ? ' fin set' : '');
row.innerHTML = `<u>${r.name}</u><img alt=""><b></b><i>${
r.need === 1 ? 'ONE GAME' : r.need + ' GAMES'}</i>`;
cupRows.appendChild(row);
const img = row.querySelector('img'), nm = row.querySelector('b');
const show = n => { const f = matchFoe(n); img.src = foeArt(f); nm.textContent = f.name; };
if (fixed) return show(cupPicks[i]);
const t0 = performance.now(), len = 1500 + i * 550; // 1.5 / 2.05 / 2.6 s — they land in order
(function roll() {
const p = Math.min(1, (performance.now() - t0) / len);
if (p >= 1) {
show(cupPicks[i]);
row.classList.add('set');
if (cupRows.querySelectorAll('.set').length === R.length) {
cupGo.disabled = false;
document.getElementById('brState').textContent = 'THE DRAW IS MADE';
}
return;
}
show(r.band[Math.floor(Math.random() * r.band.length)]);
cupSpins.push(setTimeout(roll, 45 + 330 * p * p));
})();
});
}
document.getElementById('brBack').addEventListener('click', () => {
for (const t of cupSpins) clearTimeout(t); // a screen you walked out of must stop spinning
cupSpins = [];
go('setup');
});
document.getElementById('cupGo').addEventListener('click', () => start('cup'));
// ---- THE TRAINING PICKER (Oliver, 2026-08-28: "ein Trainingsbereich — Auswahl des Gegners, ein
// Spiel, tap to replay, bis man keine Lust mehr hat"). The whole ladder face-up, one tile per rung,
// wearing the same z_*head crop the draw and the walk-on wear. Tap = start — the wizard's own
@@ -747,9 +683,9 @@ function start(modeId = pickedMode) {
G.diff = G.classic ? (DIFFS.find(d => d.id === pickedDiff) || DIFFS[1]) : null;
if (modeId === 'gore') startGore(); // no tennis prologue: the mode you picked is the mode you get
if (modeId === 'zombie') { intoSplatter(); startMatch(); }
// ...and the cup carries the draw the bracket screen just made. Null (the #cup back door) still
// means "roll it yourself", which is what the mode did before the screen existed.
if (modeId === 'cup') { intoSplatter(); startMatch(true, cupPicks); }
// the cup rolls its own draw per round (cupDraw, without replacement) — the bracket screen
// that used to settle it on-screen is gone (2026-08-29, see the foeArt note above).
if (modeId === 'cup') { intoSplatter(); startMatch(true); } // cupDraw shuffles per round
// training: the picked rung rides the match machinery with the ladder parked (beats.startMatch)
if (modeId === 'train') { intoSplatter(); startMatch(false, null, trainRung); }
}
@@ -840,7 +776,6 @@ nbName.addEventListener('keydown', e => { if (e.key === 'Enter') document.getEle
// SERVE UP starts the run — except in the cup, where the draw has to be made first. That screen
// carries its own start button, so this stays the one place a run begins from the setup screen.
document.getElementById('cStart').addEventListener('click', () => {
if (pickedMode === 'cup') { buildBracket(); go('bracket'); return; }
if (pickedMode === 'train') { go('train'); return; } // the picker starts it — tap advances
start();
});
+1 -1
View File
@@ -1233,7 +1233,7 @@ test('THE TOURNAMENT: bands draw, bodies deepen, rounds hold their body, the cha
assert.ok(pr && pr.to === 2, 'the ceremony is parked for the menu to play');
});
test('THE DRAW: a bracket settled on screen is the bracket that is played', () => {
test('THE DRAW: a passed draw is validated per round, honoured, or honestly re-rolled', () => {
// The screen would be worse than nothing if it dealt a different tournament from the one it just
// showed you settling (main.buildBracket hands its rungs straight to startMatch). The guard is
// the other half: a rung that is not in the round's band falls back to an honest roll rather