A room you can share by link, a code you can type, and a name to be called
Multiplayer setup moves out of a canvas modal and a prompt() dialog into one menu screen: your name, the two ways to host, and the friend's code. The socket opens from the menu and the game stays parked behind the overlay until the room fills — netHooks.paired takes the court one line before netStart writes to it — so the room code can carry a real SHARE button and a wrong code is a typo you fix rather than a CONNECTION LOST card over a match that never began. The name is the world board's name (stats.saveName), crosses on the existing look message, and the scoreboard finally names the person instead of CPU. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ad258ace97
commit
425c18dcb6
7 files changed
+255
-75
No files matched your search
+78
-42
@@ -6,7 +6,7 @@
|
||||
import { TUNE, COURTS, TOURS, KITS, SKINS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt, CUP_ROUNDS, matchFoe } from './config.js';
|
||||
import { P, O, ball, score, G, setupPoint, selectCourt, selectTour, selectKit, selectSkin } from './state.js';
|
||||
import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, startGore, 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';
|
||||
import { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall, startNetVs, endNetVs } from './beats.js';
|
||||
import { netConnect, netHooks } from './net.js';
|
||||
import { fitViewport, render, proj, RES, SPRV, LOAD, SPR, drawLook } from './render.js';
|
||||
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
|
||||
@@ -100,39 +100,11 @@ const banner = (card, cls, last) => {
|
||||
const pickMode = id => { pickedMode = id; try { localStorage.setItem('mode', id); } catch {} };
|
||||
// TAP ADVANCES. Each screen's tap both answers and moves on — the wizard's whole promise is one
|
||||
// decision per screen, and a NEXT button under a single row of choices is a second tap for nothing.
|
||||
// NETPLAY's second step: two chips, and either one IS the start — a room needs a socket, not a
|
||||
// setup screen (court and tour ride whatever was last picked). HOST shows the code to share in a
|
||||
// modal over the frozen court; JOIN asks for a friend's code.
|
||||
// ponytail: prompt() is the join UI — replace with a drawn code entry if the mode ships.
|
||||
// The host's one decision — classic or the death match — rides the room, so the joiner never picks
|
||||
// a mode: the start message carries it to both phones and both courts are rebuilt on pairing.
|
||||
function netChip(label, join, death = false) {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'chip';
|
||||
b.textContent = label;
|
||||
b.addEventListener('click', () => {
|
||||
let code = null;
|
||||
if (join) {
|
||||
code = (prompt('ROOM CODE FROM YOUR OPPONENT') || '').trim().toUpperCase();
|
||||
if (!code) return;
|
||||
}
|
||||
start('classic'); // the waiting court is classic either way; a death room swaps it on pairing
|
||||
// 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;
|
||||
}
|
||||
|
||||
// (NETPLAY is the exception and has a screen of its own — see THE LOBBY below.)
|
||||
function buildSub(top) {
|
||||
modeSub.innerHTML = '';
|
||||
const t = top || topOf(pickedMode);
|
||||
document.getElementById('subTitle').textContent = t.diffs ? 'DIFFICULTY' : (t.name || '');
|
||||
if (t.net) {
|
||||
modeSub.appendChild(netChip('HOST — CLASSIC TENNIS', false));
|
||||
modeSub.appendChild(netChip('HOST — DEATH MATCH', false, true));
|
||||
modeSub.appendChild(netChip('JOIN WITH A CODE', true));
|
||||
return;
|
||||
}
|
||||
if (t.diffs) for (const d of DIFFS) {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'chip';
|
||||
@@ -161,9 +133,9 @@ function buildTops() {
|
||||
for (const t of MENU) {
|
||||
const b = banner(t.mode ? modeOf(t.mode) : t, 'mode', topOf(pickedMode) === t);
|
||||
b.addEventListener('click', () => {
|
||||
// the netplay poster deals HOST/JOIN and never touches pickedMode — it is not a run mode
|
||||
// the stats screen or the summary could name, and either chip starts the game itself
|
||||
if (t.net) { buildSub(t); go('modesub'); return; }
|
||||
// the netplay poster opens THE LOBBY and never touches pickedMode — it is not a run mode
|
||||
// the stats screen or the summary could name, and the lobby starts the game itself
|
||||
if (t.net) { vsOpen(); return; }
|
||||
// a group remembers which child you were on; entering it fresh lands on its first card
|
||||
pickMode(t.mode || (t.kids.includes(pickedMode) ? pickedMode : t.kids[0]));
|
||||
buildSub();
|
||||
@@ -176,6 +148,75 @@ function buildTops() {
|
||||
buildTops();
|
||||
buildSub();
|
||||
|
||||
// ---- THE LOBBY. Oliver, 2026-08-15: *"multiplayer should be easier to setup — a share with link
|
||||
// next to the code; entering the code is done in some plain js confirm dialog thingy; somewhere
|
||||
// when hosting or also when joining the player should name themselves."* All three are one screen.
|
||||
//
|
||||
// WHY IT IS A MENU SCREEN AND NOT A COURT. Hosting used to drop you straight onto an empty court
|
||||
// and show the code in a canvas modal — which cannot carry a SHARE button, cannot be typed into,
|
||||
// and turns a mistyped code into a CONNECTION LOST card over a match that never started. The
|
||||
// socket is now opened from the menu and the game stays parked behind the overlay until the room
|
||||
// actually fills: `netHooks.paired` is what takes the court, one frame before netStart writes to
|
||||
// it. Everything else about the protocol is untouched.
|
||||
//
|
||||
// THE NAME IS THE WORLD BOARD'S NAME (stats.saveName). One name per player, wherever it shows.
|
||||
const vsPick = document.getElementById('vsPick'), vsWait = document.getElementById('vsWait');
|
||||
const vsName = document.getElementById('vsName'), vsCode = document.getElementById('vsCode');
|
||||
const vsSay = (id, text) => { document.getElementById(id).textContent = text; };
|
||||
// the link IS the invitation: the joiner's phone opens the lobby with the code already in the field
|
||||
const vsLink = code => location.origin + location.pathname + '#net-' + code;
|
||||
|
||||
function vsOpen(code = '') {
|
||||
vsName.value = stats.savedName();
|
||||
vsCode.value = code;
|
||||
vsPick.hidden = false; vsWait.hidden = true;
|
||||
vsSay('vsErr', '');
|
||||
go('vs');
|
||||
}
|
||||
// join === null hosts; `death` is the host's one decision and rides the room to both phones
|
||||
function vsGo(join, death = false) {
|
||||
stats.saveName(vsName.value); // whatever is in the field is who the other phone sees
|
||||
const code = join ? stats.tidyName(vsCode.value).replace(/[^A-Z0-9]/g, '') : null;
|
||||
if (join && code.length < 4) { vsSay('vsErr', 'THE CODE IS FOUR LETTERS'); return; }
|
||||
if (!startNetVs()) return; // the court-ownership contract, same as every other mode
|
||||
netConnect(code, death, picked); // the host's court rides the room (server.py)
|
||||
vsPick.hidden = true; vsWait.hidden = false;
|
||||
vsSay('vsWHdr', join ? 'JOINING' : 'ROOM CODE');
|
||||
vsSay('vsBig', join ? code : '····');
|
||||
vsSay('vsMsg', join ? 'KNOCKING ON THEIR DOOR' : 'WAITING FOR THEM TO JOIN');
|
||||
document.getElementById('vsShare').hidden = !!join; // nothing to share on the way in
|
||||
}
|
||||
document.getElementById('vsHostC').addEventListener('click', () => vsGo(null));
|
||||
document.getElementById('vsHostD').addEventListener('click', () => vsGo(null, true));
|
||||
document.getElementById('vsJoin').addEventListener('click', () => vsGo(true));
|
||||
vsCode.addEventListener('keydown', e => { if (e.key === 'Enter') vsGo(true); });
|
||||
// SHARE, then COPY. navigator.share is the phone's own sheet (WhatsApp, Signal, wherever the
|
||||
// friend actually is) and is exactly the "easier setup" — the clipboard is the desktop fallback,
|
||||
// and the code stays on screen for the two of them in the same room reading it aloud.
|
||||
document.getElementById('vsShare').addEventListener('click', async () => {
|
||||
const url = vsLink(G.net && G.net.code || '');
|
||||
try {
|
||||
if (navigator.share) await navigator.share({ title: 'SPLATTER TENNIS', text: 'COME AND PLAY ME', url });
|
||||
else { await navigator.clipboard.writeText(url); vsSay('vsMsg', 'LINK COPIED — SEND IT TO THEM'); }
|
||||
} catch { /* a cancelled share sheet is not an error */ }
|
||||
});
|
||||
const vsQuit = () => { endNetVs(); vsOpen(); }; // a room you walked out of must give its socket back
|
||||
document.getElementById('vsCancel').addEventListener('click', vsQuit);
|
||||
document.getElementById('vsBack').addEventListener('click', () => {
|
||||
if (!vsWait.hidden) return vsQuit(); // waiting: the chevron cancels rather than leaves
|
||||
endNetVs(); goMode();
|
||||
});
|
||||
// the three hooks net.js calls back through — all of them no-ops once the court has been taken
|
||||
netHooks.paired = () => start('classic'); // start() refuses when the menu is already down (URL entry)
|
||||
netHooks.room = code => { vsSay('vsBig', code); vsSay('vsMsg', 'WAITING FOR THEM TO JOIN'); };
|
||||
netHooks.lost = text => {
|
||||
if (!menuUp()) return false; // mid-match: the court's own card says it
|
||||
endNetVs();
|
||||
vsPick.hidden = false; vsWait.hidden = true;
|
||||
vsSay('vsErr', text);
|
||||
return true;
|
||||
};
|
||||
|
||||
// --- tour tiles, built from the table. Same "tap selects, SERVE UP starts" rule as the court rows —
|
||||
// the setup screen picks WHO and WHERE, and nothing on it commits you.
|
||||
const tourList = document.getElementById('tours');
|
||||
@@ -733,15 +774,10 @@ if (location.hash === '#drill') setTimeout(() => { start(null); intoSplatter();
|
||||
if (location.hash === '#match') setTimeout(() => { start(null); intoSplatter(); startMatch(); }, 700);
|
||||
if (location.hash === '#cup') setTimeout(() => { start(null); intoSplatter(); startMatch(true); }, 700);
|
||||
if (location.hash === '#bench') setTimeout(() => { start(null); intoSplatter(); startBench(); }, 700);
|
||||
// #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, picked);
|
||||
}, 700);
|
||||
else if (location.hash.startsWith('#net')) setTimeout(() => {
|
||||
start('classic');
|
||||
if (startNetVs()) netConnect(location.hash.slice(5).toUpperCase() || null, false, picked);
|
||||
}, 700);
|
||||
// #net opens the lobby, #net-ABCD opens it with a friend's code already in the field — that link
|
||||
// IS what the SHARE button sends, so the invited phone lands one tap from the court (the tap is
|
||||
// not ceremony: it is where they see, and can change, the name the other phone will call them).
|
||||
if (location.hash.startsWith('#net')) setTimeout(() => vsOpen(location.hash.slice(5).toUpperCase()), 700);
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
|
||||
Reference in new issue
Block a user