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
+26
-5
@@ -9,6 +9,7 @@ import { updateDrill } from './drill.js';
|
||||
import { endRun, beginRun, MODE_CLASSIC } from './stats.js';
|
||||
import { updateZombies, updateParts, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, zombies, gore, explode, meleeSwipe, extras, parts, pellets, shots, playerGore, eatenBleed, resetZombies , firePellet , spawnBarrel , fireBeam , pops, popText } from './zombies.js';
|
||||
import { proceedAfterClear, startDuel, startHorde, warnHorde, resolveInterlude, watchdog, updateBench, benchKey, tickMatchFall } from './beats.js'; // runtime-only cycle: beats calls back into cheer/aside/endBurst/deliverTin/showModal/logSwing
|
||||
import { netSendShot, netTick, netJudging, netSendPoint, netSendFault, netClose } from './net.js'; // runtime-only cycle: net calls back into pointTo/fault/showModal
|
||||
|
||||
// ------------------------------------------------------------- ballistics ----
|
||||
// Launch ball from (x0,y0,z0) so it arrives at (tx, tz) with horizontal speed s under gravity g.
|
||||
@@ -227,6 +228,7 @@ function doHit(ch, quality, aim) {
|
||||
if (w) popText(ball.x, ball.y, ball.z, w);
|
||||
}
|
||||
ch.pose = 'swing'; ch.poseT = 0;
|
||||
if (G.net && ch.side === 'P') netSendShot(); // netplay: the shot IS the sync unit
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- serve ----
|
||||
@@ -460,6 +462,7 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
// bounce. Praising at release meant the HUD spoke while the ball was still mid-air.
|
||||
ball.praise = praiseWord();
|
||||
}
|
||||
if (G.net && sv.side === 'P') netSendShot(); // netplay: a serve is a shot event like any other
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- scoring ----
|
||||
@@ -539,7 +542,11 @@ export function advancePoint(side) { // shared scoring math; returns whether a g
|
||||
return false;
|
||||
}
|
||||
|
||||
function pointTo(side, reason) {
|
||||
export function pointTo(side, reason) { // exported for net.js: a remote verdict lands here
|
||||
// netplay: this phone judged the live flight — the verdict goes over the wire before it is
|
||||
// applied, so both scoreboards move on the same call. netJudging() is false while a REMOTE
|
||||
// verdict is being applied, which is what stops the two phones echoing a point back and forth.
|
||||
if (netJudging()) netSendPoint(side, reason);
|
||||
if (G.mode === 'tennis' && side === 'O' && ball.lastHit === 'O' && G.rallyNear && !G.swung) { // their ball died on your side untouched — and no whiff already explains it
|
||||
const n = G.rallyNear;
|
||||
logSwing({ result: 'PASSED', reason: `never swung in range — ball got ${r2(n.d)}m away at y=${r2(n.y)}m`,
|
||||
@@ -859,6 +866,7 @@ const xLimit = () => Math.max(xVisible(), COURT.W / 2);
|
||||
// bag has a pristine copy (state.resetState) and the two modules with timers of their own put them
|
||||
// back themselves, so nothing here has to know the shape of any of it.
|
||||
export function restartRun() {
|
||||
netClose(); // netplay: resetState wipes G.net below; the socket must not outlive the run it carried
|
||||
resetZombies(); resetInput(); resetState(); setupPoint();
|
||||
beginRun(); // ...and the stats bag, which is the only state that does not live on G
|
||||
}
|
||||
@@ -917,8 +925,12 @@ function transition(dt) {
|
||||
});
|
||||
}
|
||||
|
||||
function fault() {
|
||||
if (G.serveNum === 1) { G.serveNum = 2; voice('fault'); G.phase = 'over'; G.overT = 1.4; sendBoy(); }
|
||||
export function fault() { // exported for net.js: the receiver's fault call re-arms the server's second serve
|
||||
if (G.serveNum === 1) {
|
||||
if (netJudging()) netSendFault(); // netplay: only the first-serve branch wires — the double
|
||||
// fault below goes out through pointTo as the point it is
|
||||
G.serveNum = 2; voice('fault'); G.phase = 'over'; G.overT = 1.4; sendBoy();
|
||||
}
|
||||
else pointTo(other(score.server), 'double_fault');
|
||||
}
|
||||
|
||||
@@ -1031,6 +1043,11 @@ function onBounce() {
|
||||
return;
|
||||
}
|
||||
if (G.mode !== 'tennis') return; // no lines, no faults — the umpire has other concerns
|
||||
// NETPLAY: THE RECEIVING PHONE IS THE ONLY JUDGE OF A FLIGHT. Our outbound ball is picture, not
|
||||
// evidence — the two sims are ~a latency apart, and this one would call a double bounce before
|
||||
// the other one's player had swung. Every call on this ball (box, line, second bounce) arrives
|
||||
// as a message instead; the ball rolls on harmlessly until it does. (See net.js.)
|
||||
if (G.net && ball.lastHit === 'P') return;
|
||||
if (ball.bounces === 1) {
|
||||
const wasServe = G.wasServe; G.wasServe = false;
|
||||
if (wasServe) {
|
||||
@@ -1365,6 +1382,7 @@ export function keyPressed(code) {
|
||||
if (G.horde || G.mgT > 0) return; // auto-fire modes: a tap only swishes, never serves
|
||||
if (G.changeover) return; // the breather is mandatory — the melee swish above still works
|
||||
if (G.interlude && G.interlude.introT > 0) return; // let the umpire finish announcing the duel
|
||||
if (G.net && (!G.net.ready || G.net.dead)) return; // netplay: no serving into an empty room
|
||||
// splatter: a tap/flick during the brief 'over' beat re-arms the next serve immediately
|
||||
if (G.mode === 'splatter' && G.phase === 'over') { ball.active = false; setupPoint(); }
|
||||
// splatter serve stays a TWO-STAGE ritual — toss up, THEN fire. It can only start from
|
||||
@@ -1693,6 +1711,7 @@ export function update(dt) {
|
||||
if (G.meleeT > 0) G.meleeT -= dt;
|
||||
if (G.rackGlow && (G.rackGlow.t -= dt) <= 0) G.rackGlow = null; // the fresh-pickup pulse fades out
|
||||
watchdog(dt);
|
||||
if (G.net) netTick(dt); // netplay: the 10 Hz position stream out, the opponent's body in
|
||||
// !G.horde: a horde mows the court down to an EMPTY FRAME between spawns, and a clear latched
|
||||
// before the wave began (a game won during the set card) then advanced the beat mid-horde —
|
||||
// which is how a sudden-death duel once started INSIDE a horde. The clear waits for endHorde.
|
||||
@@ -1717,7 +1736,8 @@ export function update(dt) {
|
||||
// own baseline and arms aiServeT; this is the one condition that was keeping it switched off.
|
||||
// The intro beat holds it: a body still walking on must not open with a serve.
|
||||
const oServes = (G.mode === 'tennis' || G.match) && score.server === 'O'
|
||||
&& !(G.interlude && G.interlude.introT > 0);
|
||||
&& !(G.interlude && G.interlude.introT > 0)
|
||||
&& !G.net; // netplay: the far side serves on the OTHER phone — the serve arrives as a shot event
|
||||
if (oServes && G.phase === 'pretoss') {
|
||||
G.aiServeT -= dt;
|
||||
if (G.aiServeT <= 0) tossBall(O);
|
||||
@@ -1728,7 +1748,8 @@ export function update(dt) {
|
||||
if (G.match) { const rz = zombies.find(z => z.variant === 'r'); if (rz) rz.serveHold = 0.4; }
|
||||
}
|
||||
}
|
||||
if (G.mode === 'tennis' && G.phase === 'rally') {
|
||||
if (G.mode === 'tennis' && G.phase === 'rally' && !G.net) { // netplay: O is a real person on
|
||||
// another phone — netTick drives the body
|
||||
G.aiReactT += dt;
|
||||
const incoming = ball.active && ball.lastHit === 'P'
|
||||
&& G.aiReactT >= (G.diff ? G.diff.reaction : TUNE.aiReaction);
|
||||
|
||||
Reference in new issue
Block a user