Add opt-in phone gameplay recording and JSON export
This commit is contained in:
1 parent
ede1f48152
commit
fe64242090
8 files changed
+394
-12
No files matched your search
+22
-9
@@ -9,6 +9,7 @@ import { W } from './render.js'; // runtime-only cycle (render imports gameplay)
|
||||
import { updateDrill } from './drill.js';
|
||||
import { endRun, beginRun, MODE_CLASSIC } from './stats.js';
|
||||
import { award, beginRunXp, prof, bonus, levelAt } from './profile.js';
|
||||
import { recordEvent, recordFrame } from './recorder.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, placeReceiver } from './beats.js'; // runtime-only cycle: beats calls back into cheer/aside/endBurst/deliverTin/showModal/logSwing
|
||||
import { netSendShot, netSendToss, netTick, netJudging, netSendPoint, netSendFault, netClose } from './net.js'; // runtime-only cycle: net calls back into pointTo/fault/tossBall/showModal
|
||||
@@ -320,6 +321,8 @@ function doHit(ch, quality, aim) {
|
||||
ball.kills = 0; // fresh shot: multi-kill streak restarts
|
||||
ball.bomb = false; ball.flame = false; ball.wid = 'ball'; // only a consuming serve arms these; any other hit clears them
|
||||
ball.chain = 0; // ...and the slice dies with the shot that earned it
|
||||
recordEvent('shot', { kind: 'return', side: ch.side, quality, aim: aim ? { ...aim } : null,
|
||||
target: [tx, tz] });
|
||||
ball.praise = null; // a re-hit is a new shot: a stale armed word must not pop on ITS impact
|
||||
// (the landing ring that used to be stamped here confused testers — the umpire calls the ball,
|
||||
// the court stays clean)
|
||||
@@ -571,6 +574,7 @@ export function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
}
|
||||
// (spendSlot already hands you back plain balls when the last one goes)
|
||||
G.phase = 'rally'; sfx(sv.side === 'P' ? 'tennis-player' : 'tennis-opponent', 0.9, { reverb: true, rate: strikeRate() }); // recorded serve strike, per side
|
||||
recordEvent('shot', { kind: 'serve', side: sv.side, quality, perfect, aim: aim ? { ...aim } : null });
|
||||
if (sv.side === 'P') strikeKick();
|
||||
if (sv.side === 'P') haptic(perfect ? 'medium' : 'light'); // the strike, same tick as a rally hit
|
||||
sv.pose = 'serve'; sv.poseT = 0;
|
||||
@@ -1028,6 +1032,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() {
|
||||
recordEvent('run_end', { outcome: G.done?.won ? 'won' : G.mode === 'eaten' ? 'lost' : 'abandoned' });
|
||||
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
|
||||
@@ -1114,6 +1119,7 @@ export function startGore() {
|
||||
}
|
||||
|
||||
export function fault() { // exported for net.js: the receiver's fault call re-arms the server's second serve
|
||||
recordEvent('fault', { serveNumber: G.serveNum });
|
||||
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
|
||||
@@ -1132,7 +1138,7 @@ export function fault() { // exported for net.js: the receiver's fault call re-a
|
||||
// the ladder) and a raw pointTo there would bank a number and skip all of it — the same seam the
|
||||
// interlude branch of onBounce is written around. Nothing reached this line from an interlude
|
||||
// before, because those modes had no fault in them at all.
|
||||
else if (G.interlude) { voice('double_fault'); resolveInterlude(other(score.server)); }
|
||||
else if (G.interlude) { voice('double_fault'); resolveInterlude(other(score.server), 'double_fault'); }
|
||||
else pointTo(other(score.server), 'double_fault');
|
||||
}
|
||||
|
||||
@@ -1256,7 +1262,7 @@ function onBounce() {
|
||||
// serve stays legal in the match, which is the concession that mode was tuned around.
|
||||
if (!inCourt(ball.x, ball.z) || !okSide) {
|
||||
endFlight();
|
||||
if (G.wasServe) fault(); else { resolveInterlude(other(ball.lastHit)); voice('out'); }
|
||||
if (G.wasServe) fault(); else { resolveInterlude(other(ball.lastHit), 'out'); voice('out'); }
|
||||
}
|
||||
// THE FALL BEATS THE BALL (Oliver: "the zombie falls after the ball is lost although we know
|
||||
// this earlier — it could play out as ball lost BECAUSE of the falling"). A winner that just
|
||||
@@ -1279,10 +1285,10 @@ function onBounce() {
|
||||
&& matchFallWillPlay() && matchOutOfReach()) {
|
||||
G.match.decided = true; // scope: the REMAINING ball events of this rally
|
||||
G.matchEarly = true; // startFall leaves the ball rolling; the swallow exempts us
|
||||
resolveInterlude('P');
|
||||
resolveInterlude('P', 'unreachable_after_bounce');
|
||||
G.matchEarly = false;
|
||||
}
|
||||
} else if (ball.bounces === 2) { endFlight(); resolveInterlude(ball.lastHit); }
|
||||
} else if (ball.bounces === 2) { endFlight(); resolveInterlude(ball.lastHit, 'double_bounce'); }
|
||||
return;
|
||||
}
|
||||
if (G.mode !== 'tennis') return; // no lines, no faults — the umpire has other concerns
|
||||
@@ -1552,7 +1558,10 @@ function racketAI(rz, dt) {
|
||||
// the surprise ball ADDS whiff on top of the rung's own rate, scaled by how much this
|
||||
// body had actually learned. The champion's near-zero flub is exactly what this opens.
|
||||
+ (G.match && G.match.surprised ? TUNE.surpFlub * learnK(rz) : 0);
|
||||
if (Math.random() < flub) rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
||||
if (Math.random() < flub) {
|
||||
recordEvent('opponent_whiff', { chance: flub, chaseSpeed: rzChaseSpeed(rz), reach: TUNE.aiReach * wnd.reach });
|
||||
rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
||||
}
|
||||
else {
|
||||
const qCap = G.match && !G.net ? (rz.foe?.qMax ?? TUNE.racketQMax)
|
||||
: TUNE.racketQMax + TUNE.duelQBonus * duelSkill();
|
||||
@@ -1742,7 +1751,10 @@ export function keyPressed(code) {
|
||||
hitServe(P, q, sway || hard ? { sx: sway, hard } : null);
|
||||
return;
|
||||
}
|
||||
if (G.phase === 'rally' && P.swingT < 0) { P.swingT = 0; P.pose = 'swing'; P.poseT = 0; G.swung = true; }
|
||||
if (G.phase === 'rally' && P.swingT < 0) {
|
||||
recordEvent('swing', { window: TUNE.swingWindow * winLevel(), assist: assistK() });
|
||||
P.swingT = 0; P.pose = 'swing'; P.poseT = 0; G.swung = true;
|
||||
}
|
||||
}
|
||||
|
||||
function expirePose(ch, dt) {
|
||||
@@ -1813,6 +1825,7 @@ function mopRound(b, dt) {
|
||||
|
||||
// ---------------------------------------------------------------- update ----
|
||||
export function update(dt) {
|
||||
recordFrame(dt);
|
||||
if (G.hitstop > 0) { G.hitstop -= dt; return; }
|
||||
// THE OTHER ENDING: a classic match played out. The world holds still — the man is standing, the
|
||||
// crowd is still clapping it down — and the only thing running is the clock the summary and its
|
||||
@@ -2175,7 +2188,7 @@ export function update(dt) {
|
||||
if (pred && inCourt(pred.x, pred.z) && matchOutOfReachFlight(arz, pred)) {
|
||||
G.match.decided = true; // same latch discipline as the bounce-1 hook below
|
||||
G.matchEarly = true;
|
||||
resolveInterlude('P');
|
||||
resolveInterlude('P', 'unreachable_in_flight');
|
||||
G.matchEarly = false;
|
||||
}
|
||||
}
|
||||
@@ -2200,7 +2213,7 @@ export function update(dt) {
|
||||
if (ball.scored) { /* already called: this IS the natural halt it was left rolling for */ }
|
||||
else if (G.mode === 'splatter' && G.interlude) { // a roller nobody returned: point to the hitter
|
||||
if (ball.lastHit === 'X') spentBall(); // spent on the boss's body — no point, re-serve the duel
|
||||
else resolveInterlude(ball.lastHit);
|
||||
else resolveInterlude(ball.lastHit, 'dead_roller');
|
||||
} else if (G.mode === 'splatter') { // hand the server a fresh one, fast — they're coming
|
||||
spentBall(); sendBoy();
|
||||
if (Math.random() < 0.12) voice('new_balls', 0.3);
|
||||
@@ -2210,7 +2223,7 @@ export function update(dt) {
|
||||
// a racket zombie's return that gets past you (crosses your baseline) is a point for THEM
|
||||
if (G.mode === 'splatter' && ball.active && !ball.scored && ball.lastHit === 'O' && ball.z > COURT.L + 0.3) {
|
||||
ball.active = false;
|
||||
if (G.interlude) resolveInterlude('O'); // sudden death: its passing shot ends the duel
|
||||
if (G.interlude) resolveInterlude('O', 'passed_player'); // sudden death: its passing shot ends the duel
|
||||
else { if (!G.horde && !G.clearing) splatPoint('O'); spentBall(); } // clearing = mop-up, no scoring either way
|
||||
}
|
||||
// the far backstop: a LIVE ball doesn't leave the field, it rebounds off the fence and comes
|
||||
|
||||
Reference in new issue
Block a user