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
+33
-16
@@ -19,14 +19,19 @@
|
||||
// plan calls reconnection "the bulk of the real work" and a prototype must not start there.
|
||||
import { COURT, UI, KITS, SKINS } from './config.js';
|
||||
import { G, P, O, ball, score, setupPoint, selectCourt, court, kit, skin } from './state.js';
|
||||
import { pointTo, fault, showModal, tossBall } from './gameplay.js';
|
||||
import { pointTo, fault, tossBall } from './gameplay.js';
|
||||
import { startMatch, matchReady, resolveInterlude, netMatchWon } from './beats.js'; // runtime-only cycle: beats sends verdicts back through this module
|
||||
import { zombies } from './zombies.js';
|
||||
import { savedName, tidyName } from './stats.js';
|
||||
|
||||
// main.js hangs its court builders here, so this module never has to know how a splatter court is
|
||||
// entered (the transition hack lives in main). Headless tests leave it empty and net falls back to
|
||||
// the minimal direct entry, which is exactly what the suite's own freshSplatter does.
|
||||
export const netHooks = { intoSplatter: null };
|
||||
// main.js hangs its court builders and its LOBBY here, so this module never has to know how a
|
||||
// splatter court is entered or what the menu looks like. Headless tests leave them empty and net
|
||||
// falls back to the minimal direct entry, which is exactly what the suite's own freshSplatter does.
|
||||
// intoSplatter the death match's court rebuild
|
||||
// paired the room filled: leave the menu and take the court, BEFORE netStart touches it
|
||||
// room the code the relay minted (the lobby shows it and shares the link)
|
||||
// lost the line died; returns true if the lobby said so itself and needs no card
|
||||
export const netHooks = { intoSplatter: null, paired: null, room: null, lost: null };
|
||||
|
||||
let ws = null, hb = 0, posT = 0;
|
||||
let applying = false; // a remote verdict being applied must not echo back out
|
||||
@@ -115,10 +120,16 @@ function netSync(m) {
|
||||
}
|
||||
|
||||
function netStart(side, death, courtId) {
|
||||
// THE LOBBY HANDS THE COURT OVER FIRST. Hosting and joining both happen on the menu now (the
|
||||
// code and its share link are DOM, not a canvas modal), so the game is still parked behind the
|
||||
// overlay when the room fills — and everything below writes to a court that must already exist,
|
||||
// with the kit and tour this phone picked already selected (the look message reads them).
|
||||
if (netHooks.paired) netHooks.paired();
|
||||
G.net.ready = true; G.net.side = side;
|
||||
// tell them how we look, the moment there is somebody to tell. Sent once, on pairing: neither
|
||||
// phone can change its pick mid-match, because the setup screen is only reachable between runs.
|
||||
send({ t: 'look', k: kit.id, s: skin.id });
|
||||
// tell them how we look AND WHO WE ARE, the moment there is somebody to tell. Sent once, on
|
||||
// pairing: neither phone can change its pick mid-match, because the setup screen is only
|
||||
// reachable between runs.
|
||||
send({ t: 'look', k: kit.id, s: skin.id, n: savedName() });
|
||||
G.modal = null; G.modalQ.length = 0; // the waiting card has done its job
|
||||
// THE HOST'S COURT RIDES THE ROOM, exactly as the death-match pick does — two phones playing the
|
||||
// same rally on two different surfaces disagree about the bounce as well as the picture, and the
|
||||
@@ -159,6 +170,9 @@ function netDead(text) {
|
||||
G.net.ready = false; G.net.dead = true;
|
||||
ball.active = false;
|
||||
G.modal = null; G.modalQ.length = 0;
|
||||
// the lobby is a menu screen: while it is up there is no court to lay a card on, and it says the
|
||||
// bad news in its own words (a wrong code is a typo to fix, not a match that ended)
|
||||
if (netHooks.lost && netHooks.lost(text)) return;
|
||||
G.card = { title: 'CONNECTION LOST', sub1: text, color: UI.bad, t: 30 };
|
||||
}
|
||||
|
||||
@@ -166,12 +180,12 @@ export function netMsg(m) {
|
||||
if (!G.net) return;
|
||||
if (m.t === 'room') {
|
||||
G.net.code = m.code;
|
||||
// a modal is legal here: an empty court waiting for a stranger is the calmest court there is
|
||||
showModal({ title: m.code, color: UI.pick,
|
||||
body: ['THE ROOM CODE — TELL YOUR OPPONENT', 'WAITING FOR THEM TO JOIN'] });
|
||||
if (netHooks.room) netHooks.room(m.code); // the lobby: the code big, and a link to send them
|
||||
}
|
||||
else if (m.t === 'start') netStart(m.side, m.death, m.court);
|
||||
else if (m.t === 'gone') netDead('NO SUCH ROOM — RELOAD AND HOST ONE');
|
||||
// 'gone' can only ever reach a phone that is JOINING, and joining happens in the lobby — so it is
|
||||
// a typo to fix on the screen you are already looking at, not a match that ended
|
||||
else if (m.t === 'gone') netDead('NO SUCH ROOM — CHECK THE CODE');
|
||||
else if (m.t === 'bye') netDead('YOUR OPPONENT LEFT — RELOAD TO PLAY AGAIN');
|
||||
else if (m.t === 'toss') netApplyToss();
|
||||
else if (m.t === 'shot') netApplyShot(m);
|
||||
@@ -179,13 +193,16 @@ export function netMsg(m) {
|
||||
else if (m.t === 'fault') netApplyFault();
|
||||
else if (m.t === 'dead') netMatchWon(); // their blood ran out: this side's win ending
|
||||
else if (m.t === 'pos') { G.net.opp = m; if (m.srv) netSync(m); }
|
||||
// WHAT THEY LOOK LIKE. Their phone's kit and skin, resolved against our own tables so a build
|
||||
// that has never heard of a colour falls back to the drawn one rather than drawing nothing.
|
||||
// Unknown message types are ignored above, so an older build simply never sends this and their
|
||||
// friend sees the opponent as drawn — the match still plays.
|
||||
// WHAT THEY LOOK LIKE AND WHAT THEY ARE CALLED. Their phone's kit and skin, resolved against our
|
||||
// own tables so a build that has never heard of a colour falls back to the drawn one rather than
|
||||
// drawing nothing. Unknown message types are ignored above, so an older build simply never sends
|
||||
// this and their friend sees the opponent as drawn, called FRIEND — the match still plays.
|
||||
// The name is a stranger's string on OUR scoreboard: it goes through the same tidyName the world
|
||||
// board uses, then into the board's 8-character slot.
|
||||
else if (m.t === 'look') G.net.look = {
|
||||
kit: KITS.find(k => k.id === m.k) || KITS[0],
|
||||
skin: SKINS.find(s => s.id === m.s) || SKINS[0],
|
||||
name: tidyName(m.n || '').slice(0, 8) || 'FRIEND',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user