A facing flip must be earned: the racket stops flashing across the body
Oliver, women's tour: 'the racket she is holding flashing left and right very fast' during auto-run. The rolled cycles carry a real racket, and the mirror flip teleports it across the body — the per-frame steering flutter the old racketless art could hide. earnFlip() requires an auto-steered direction to persist TUNE.faceFlip (0.13 s) before the body mirrors, at all three steering sites (player assist, tennis CPU, match opponent); manual input stays instant. Measured over an 8 s live rally: 4 player flips, 2 opponent flips — every one an actual turn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e8b5c1905f
commit
35671c689b
3 files changed
+19
-5
No files matched your search
+1
-1
@@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=216"></script>
|
||||
<script type="module" src="src/main.js?v=217"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -43,6 +43,9 @@ export const TUNE = {
|
||||
// faults, which is what makes tap = safe second serve a real decision
|
||||
runHz: 2.4, // stride cycles/s for the rolled 6/8-frame run sets. The old hardcoded 4.5 was
|
||||
// tuned on 2-4 near-duplicate frames; on eight real phases it strobed
|
||||
faceFlip: 0.13, // s an auto-steered direction must persist before the body mirrors (earnFlip).
|
||||
// The rolled cycles hold a real racket, and an un-earned flip teleports it
|
||||
// across the body — the "racket flashing left and right" report
|
||||
serveSpin: 1.3, // extra gravity a full-power serve carries (was a fixed 0.35). Without it the
|
||||
// net-clearance loop ate the whole smash: a flat fast ball can't get over the
|
||||
// tape AND come down on a target, so it got throttled back to walking pace.
|
||||
|
||||
+15
-4
@@ -1115,7 +1115,7 @@ function racketAI(rz, dt) {
|
||||
// facing RIGHT and the bodies are drawn facing LEFT). This line wrote a field nothing reads for a
|
||||
// zombie, so the match opponent's turn was left to the generic sticky-face line in updateZombies.
|
||||
// vx is kept so a body that is knocked down mid-chase can carry the run into its collapse.
|
||||
if (Math.abs(dx) > 0.2) rz.face = dx > 0 ? 1 : -1;
|
||||
earnFlip(rz, 'face', Math.abs(dx) > 0.2 ? (dx > 0 ? 1 : -1) : 0, dt);
|
||||
rz.vx = dx / d * step / dt;
|
||||
} else { rz.vx = 0; if (rz.pose === 'run') rz.pose = 'idle'; }
|
||||
if (rz.swingCool <= 0 && rz.swingT < 0
|
||||
@@ -1309,6 +1309,17 @@ function expirePose(ch, dt) {
|
||||
if (ch.pose === 'swing' || ch.pose === 'serve') { ch.poseT += dt; if (ch.poseT > POSE_TIME) ch.pose = 'idle'; }
|
||||
}
|
||||
|
||||
// A FACING FLIP MUST BE EARNED. The rolled run cycles carry a real racket, and a mirror flip
|
||||
// teleports it across the body — so the per-frame steering flutter the old racketless art could
|
||||
// hide reads as "the racket she is holding flashing left and right very fast" (Oliver, women's
|
||||
// tour, where the drawn racket is biggest). A flip now needs the wanted direction to PERSIST for
|
||||
// TUNE.faceFlip seconds. Every auto-steered body routes through here (the player's assist, the
|
||||
// tennis CPU, the match opponent); MANUAL input stays instant — a thumb reversal is intent.
|
||||
function earnFlip(ch, field, want, dt) {
|
||||
if (!want || want === ch[field]) { ch.flipT = 0; return; }
|
||||
if ((ch.flipT = (ch.flipT || 0) + dt) >= TUNE.faceFlip) { ch[field] = want; ch.flipT = 0; }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- ball boys ----
|
||||
// The fetch boy is unchanged: sprint the width of the court, scoop the dead ball in passing.
|
||||
function fetchRound(b, dt) {
|
||||
@@ -1522,8 +1533,8 @@ export function update(dt) {
|
||||
// run animation" whether he was moving or not. The pose follows MOVEMENT in every mode now, so
|
||||
// auto-fire uses the ordinary run animation and standing still is standing still.
|
||||
if (P.pose !== 'swing' && P.pose !== 'serve') P.pose = moving ? 'run' : 'idle';
|
||||
if (mx) P.facing = mx > 0 ? 1 : -1;
|
||||
else if (Math.abs(assist) > 0.25) P.facing = assist > 0 ? 1 : -1; // hysteresis: small corrections don't flip facing
|
||||
if (mx) { P.facing = mx > 0 ? 1 : -1; P.flipT = 0; } // manual: instant, and it resets the earn clock
|
||||
else earnFlip(P, 'facing', Math.abs(assist) > 0.25 ? (assist > 0 ? 1 : -1) : 0, dt);
|
||||
}
|
||||
// ...and out of 'rally' he STANDS. 'run' is the one pose nothing else expires (expirePose only
|
||||
// handles swing/serve), and P.runT stops advancing outside the block, so a beat that began
|
||||
@@ -1616,7 +1627,7 @@ export function update(dt) {
|
||||
O.x += dx / d * Math.min(spd * dt, d);
|
||||
O.z += dz / d * Math.min(spd * dt, d);
|
||||
O.pose = O.pose === 'swing' ? 'swing' : 'run'; O.runT += dt;
|
||||
if (Math.abs(dx) > 0.25) O.facing = dx > 0 ? 1 : -1;
|
||||
earnFlip(O, 'facing', Math.abs(dx) > 0.25 ? (dx > 0 ? 1 : -1) : 0, dt);
|
||||
} else if (O.pose === 'run') O.pose = 'idle';
|
||||
G.aiSwingCool -= dt;
|
||||
if (incoming && G.aiSwingCool <= 0 && O.swingT < 0) {
|
||||
|
||||
Reference in new issue
Block a user