The dead club doubles its staff, and the draw is made where you can see it
Six new opponent families (head coach, stringer, strawberry seller, chair umpire, spectator, roller man) through the famroll/zsheet route, placed by hierarchy into an 18-rung ladder; the champion steps up behind them. THE DRAW: picking the tournament now rolls its own bracket in front of you — only the final was ever settled — and what lands is what walks on. Netplay, off Oliver's cellular report: the mid-flight early-decide was missing its !G.net gate, so the striker judged its own outbound flight and the two scoreboards drifted a point apart into a court nobody could serve on. The host's court now rides the room, its scoreboard rides the pretoss stream, and a called ball rolls to its own halt instead of freezing where the call caught it. Plus the night court, and somebody in the stand is taking pictures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c47295a73
commit
9cc3ac54b3
207 files changed
+632
-81
No files matched your search
+80
-7
@@ -3,7 +3,7 @@
|
||||
// Module map: config (tuning/constants) · state (mutable game state) · gameplay (logic/update)
|
||||
// render (camera/drawing) · input (keys/touch) · audio (sfx/umpire) · panel (T tuning UI)
|
||||
|
||||
import { TUNE, COURTS, TOURS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt } from './config.js';
|
||||
import { TUNE, COURTS, TOURS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt, CUP_ROUNDS, matchFoe } from './config.js';
|
||||
import { P, O, ball, score, G, setupPoint, selectCourt, selectTour } from './state.js';
|
||||
import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, flickTarget, dragServe, toggleTestMode } from './gameplay.js';
|
||||
import { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall, startNetVs } from './beats.js';
|
||||
@@ -117,7 +117,8 @@ function netChip(label, join, death = false) {
|
||||
if (!code) return;
|
||||
}
|
||||
start('classic'); // the waiting court is classic either way; a death room swaps it on pairing
|
||||
if (startNetVs()) netConnect(code, death);
|
||||
// the host's court rides the room too (server.py) — the guest plays where the host plays
|
||||
if (startNetVs()) netConnect(code, death, picked);
|
||||
});
|
||||
return b;
|
||||
}
|
||||
@@ -219,6 +220,69 @@ for (const c of COURTS) {
|
||||
courtList.appendChild(b);
|
||||
}
|
||||
|
||||
// ---- 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 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
|
||||
const foeArt = f => `assets/z_${f.fam}head.png?v=${SPRV}`;
|
||||
|
||||
function buildBracket() {
|
||||
for (const t of cupSpins) clearTimeout(t);
|
||||
cupSpins = [];
|
||||
cupPicks = CUP_ROUNDS.map(r => r.band[Math.floor(Math.random() * r.band.length)]);
|
||||
cupRows.innerHTML = '';
|
||||
cupGo.disabled = true;
|
||||
document.getElementById('brState').textContent = 'DRAWING…';
|
||||
document.getElementById('brSum').innerHTML =
|
||||
`<span>ONLY THE FINAL WAS EVER SETTLED</span><span>${
|
||||
(COURTS.find(c => c.id === picked) || COURTS[0]).name}</span>`;
|
||||
CUP_ROUNDS.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 === CUP_ROUNDS.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'));
|
||||
|
||||
// what the setup footer says you are about to play, and what it is worth beating
|
||||
function summary() {
|
||||
const t = TOURS.find(x => x.id === pickedTour) || TOURS[0];
|
||||
@@ -267,7 +331,9 @@ function start(modeId = pickedMode) {
|
||||
// the difficulty rides on G like G.classic does, so a mid-run restart keeps the CPU you chose
|
||||
G.diff = G.classic ? (DIFFS.find(d => d.id === pickedDiff) || DIFFS[1]) : null;
|
||||
if (modeId === 'zombie') { intoSplatter(); startMatch(); }
|
||||
if (modeId === 'cup') { intoSplatter(); startMatch(true); }
|
||||
// ...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); }
|
||||
}
|
||||
let stPeriod = 'all', stMode = stats.MODE_GORE;
|
||||
// the stats screen opens on the mode you last PICKED to play — that is the one whose numbers you
|
||||
@@ -328,7 +394,12 @@ nbName.addEventListener('keydown', e => { if (e.key === 'Enter') document.getEle
|
||||
// SERVE UP dropped you into the gore mode whatever the setup screen said, and picking CLASSIC or
|
||||
// ZOMBIE TENNIS did nothing at all. A default parameter only applies to `undefined`, and a click
|
||||
// handler always passes something.
|
||||
document.getElementById('cStart').addEventListener('click', () => start());
|
||||
// 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; }
|
||||
start();
|
||||
});
|
||||
for (const id of ['mBack', 'stBack', 'rkBack', 'scBack'])
|
||||
document.getElementById(id).addEventListener('click', () => go('splash'));
|
||||
// the wizard walks back the way it came: who-and-where returns to the mode's own second step when
|
||||
@@ -476,7 +547,9 @@ addEventListener('keydown', e => {
|
||||
if (stage !== 'setup') return; // stats and ranks are read-only: Escape is the whole interface
|
||||
const n = parseInt(e.key, 10);
|
||||
if (n >= 1 && n <= COURTS.length) courtList.children[n - 1].click();
|
||||
else if (e.key === 'Enter' || e.key === ' ') start();
|
||||
// through the BUTTON, not start() — the cup's draw screen sits between the two and a keyboard
|
||||
// must not be the one way to walk past it
|
||||
else if (e.key === 'Enter' || e.key === ' ') document.getElementById('cStart').click();
|
||||
});
|
||||
|
||||
// A run the player walks out on is still a run. killPlayer banks a death; this banks everything
|
||||
@@ -590,11 +663,11 @@ if (location.hash === '#bench') setTimeout(() => { start(null); intoSplatter();
|
||||
// #net hosts classic, #netd hosts a death match, #net-ABCD joins — the netplay doors from a URL
|
||||
if (location.hash === '#netd') setTimeout(() => {
|
||||
start('classic');
|
||||
if (startNetVs()) netConnect(null, true);
|
||||
if (startNetVs()) netConnect(null, true, picked);
|
||||
}, 700);
|
||||
else if (location.hash.startsWith('#net')) setTimeout(() => {
|
||||
start('classic');
|
||||
if (startNetVs()) netConnect(location.hash.slice(5).toUpperCase() || null);
|
||||
if (startNetVs()) netConnect(location.hash.slice(5).toUpperCase() || null, false, picked);
|
||||
}, 700);
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
Reference in new issue
Block a user