Two phones, one match: the shot is the message, the receiver is the judge
VS A FRIEND on the mode screen (host a code, join a code), a ~110-line RFC6455 relay in server.py that holds no game state, and src/net.js: each phone plays itself near-side and the opponent's court is this one rotated half a turn about the net. The phone a ball flies toward makes every call on it; the striker's copy is picture. Six tests pin the rule from both seats. No reconnect yet — a dropped radio is a card and a reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
108ed695eb
commit
f302354bb2
11 files changed
+493
-21
No files matched your search
+31
-7
@@ -14,6 +14,7 @@ import { voice, zombieDuck, aww } from './audio.js';
|
||||
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
||||
import { cheer, aside, endBurst, deliverTin, showModal, logSwing, advancePoint, scoreKey } from './gameplay.js';
|
||||
import { startDrillStream } from './drill.js';
|
||||
import { netClose } from './net.js';
|
||||
|
||||
// the court is mopped up (or was already clear): play advances to whatever the won game earned —
|
||||
// a changeover breather, a plain even-game beat, or (on a set) the centered SET card then a horde.
|
||||
@@ -64,7 +65,7 @@ function startChangeover() {
|
||||
export function startDuel() {
|
||||
// Hard mutual exclusion, HERE, where every caller routes through: a duel must never begin while
|
||||
// another control mode owns the court. Each of these has ended a run when they overlapped.
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench) return;
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net) return;
|
||||
if (!zombies.some(z => z.variant === 'r')) spawnRacketZombie(); // the schedule guarantees an opponent
|
||||
for (const z of zombies) if (z.variant === 'r') z.dueled = true; // a veteran can be challenged again — this marks the fresh duel
|
||||
G.weapon = 0; // tennis rules for the duel: plain balls, no tins
|
||||
@@ -87,7 +88,7 @@ export function startDuel() {
|
||||
// It threatens BOTH channels: its volleys chip your hp, and letting it reach you is the ordinary
|
||||
// instant death. G.bossFight holds the chapter number while it runs.
|
||||
export function startBossFight(chapter) {
|
||||
if (G.bossFight || G.drill || G.match || G.bench) return;
|
||||
if (G.bossFight || G.drill || G.match || G.bench || G.net) return;
|
||||
G.bossFight = chapter;
|
||||
zombies.length = 0; pellets.length = 0; extras.length = 0; shots.length = 0; // a clean stage
|
||||
G.horde = null; G.mgT = 0; endBurst(); G.clearing = false;
|
||||
@@ -136,7 +137,7 @@ export function endBossFight() {
|
||||
// was walking around a moment ago is still walking around inside a drill round. The drill's own
|
||||
// clock and rules live in drill.js; only the transition is here, where the rule can be enforced.
|
||||
export function startDrill() {
|
||||
if (G.drill || G.interlude || G.horde || G.bossFight || G.match || G.bench) return false;
|
||||
if (G.drill || G.interlude || G.horde || G.bossFight || G.match || G.bench || G.net) return false;
|
||||
G.drill = { t: 0, spawnT: 0, score: 0, streak: 0, kills: 0, faults: 0, lost: 0, best: 0, msg: null };
|
||||
G.changeover = false; G.clearing = false; G.afterClear = null;
|
||||
// ...and DISARM the ordinary schedule. Taking the court is not enough on its own: a pending
|
||||
@@ -171,7 +172,7 @@ export function endDrill() {
|
||||
const BENCH_HP = 12; // a deep pool so a wound stage is set by FRACTION, cleanly
|
||||
|
||||
export function startBench() {
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench) return false;
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net) return false;
|
||||
G.bench = { i: 0, stage: 0, fam: 0, dir: 1 };
|
||||
zombies.length = 0; pellets.length = 0; extras.length = 0; shots.length = 0;
|
||||
// ...and DISARM the ordinary schedule, exactly as the drill does: a pending horde or an owed
|
||||
@@ -342,7 +343,7 @@ export function watchdog(dt) {
|
||||
// it, and killing it outright wins the game early (killZombie fires resolveInterlude('P'), which
|
||||
// lands in matchGameWon via the empty-court check).
|
||||
export function startMatch(cup = false) {
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench) return false;
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench || G.net) return false;
|
||||
G.match = { opp: 1, ending: false };
|
||||
// THE TOURNAMENT rides the whole match machinery and differs in exactly three places: who walks
|
||||
// on (a rung DRAWN from the round's band — cupDraw sets d.opp, so every ladder dial downstream
|
||||
@@ -827,12 +828,35 @@ export function resolveInterlude(side) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- NETPLAY: 1v1 classic tennis against another phone. The wire, the protocol and the judging
|
||||
// rule all live in src/net.js; ONLY the court ownership lives here, where the contract can be
|
||||
// enforced — same shape as every mode above. It runs in G.mode 'tennis' with G.classic set (the
|
||||
// caller does that via start('classic')), so the whole splatter schedule is structurally inert;
|
||||
// the disarm lines below are the same belt the drill and the bench wear anyway.
|
||||
export function startNetVs() {
|
||||
if (G.net || G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench) return false;
|
||||
G.net = { ready: false, dead: false, side: null, code: null, opp: null };
|
||||
G.pendingHorde = false; G.hordeWarn = 0; G.bossDue = 0; G.duelDue = false;
|
||||
G.changeover = false; G.clearing = false; G.afterClear = null;
|
||||
G.weapon = 0; G.mgT = 0; endBurst(); G.boy = null;
|
||||
zombies.length = 0; pellets.length = 0; extras.length = 0; shots.length = 0;
|
||||
ball.active = false; setupPoint();
|
||||
return true;
|
||||
}
|
||||
|
||||
export function endNetVs() {
|
||||
if (!G.net) return;
|
||||
netClose();
|
||||
G.net = null;
|
||||
ball.active = false; setupPoint();
|
||||
}
|
||||
|
||||
// THE BREATHER BEFORE THE WAVE. The horde used to begin on the same frame as its own announcement,
|
||||
// so the flash was already gone by the time the crowd was on you — "the horde is not announced
|
||||
// anymore". It is an ordinary 'over' beat (no serving, the world keeps walking, the RESUMING clock
|
||||
// counts it down under the flash), and startHorde still owns the transition itself.
|
||||
export function warnHorde() {
|
||||
if (G.horde || G.interlude || G.bossFight || G.drill || G.match || G.bench) return false; // same court-ownership contract as the rest
|
||||
if (G.horde || G.interlude || G.bossFight || G.drill || G.match || G.bench || G.net) return false; // same court-ownership contract as the rest
|
||||
G.hordeWarn = TUNE.hordeWarnT;
|
||||
G.flash = { text: 'THEY KEEP COMING', t: TUNE.hordeWarnT };
|
||||
ball.active = false; G.phase = 'over'; G.overT = TUNE.hordeWarnT;
|
||||
@@ -843,7 +867,7 @@ export function warnHorde() {
|
||||
// Returns false when another mode owns the court — the caller re-arms the latch rather than
|
||||
// silently eating the wave the set paid for.
|
||||
export function startHorde() {
|
||||
if (G.horde || G.interlude || G.bossFight || G.drill || G.match || G.bench) return false; // same contract as startDuel: one mode owns the court
|
||||
if (G.horde || G.interlude || G.bossFight || G.drill || G.match || G.bench || G.net) return false; // same contract as startDuel: one mode owns the court
|
||||
G.changeover = false; // a cheat-forced horde mid-changeover must not leave spawns locked
|
||||
const grow = 1 + (G.sets[0] + G.sets[1]) * TUNE.hordeGrow; // each set banked makes the next wave worse
|
||||
// the gun grows WITH the wave: spawn rate and length both scale by grow, so a flat gun fell
|
||||
|
||||
Reference in new issue
Block a user