The length of the flick is the strength of the aim, and you can see it leave
The rally flick was read as three lanes: sideways past 0.38 of its own length took the whole corner, a hair under it took dead centre, and nothing lived in between — "sometimes applied very strong, sometimes it barely does anything", with no way to tell which one you had asked for. swayOf reads the swipe's LENGTH as a continuous strength, exactly as the serve already did, and doHit spends that instead of the lane vote. Keys keep the full lane; the opponent's aim has no length and is untouched. That is also what hands the OUT back: going out was never a choice, because every aimed shot took the full width and then the scatter on top. Below about 0.7 of a full swipe — the line minus aimError, over the width timing can earn — the worst mistiming in the game cannot sail it now, and above it the old risk curve is exactly as tested. And the aim is drawn while the thumb is still moving: the placement ring is a receipt that arrives with the ball already gone, so the same ring is shown live with a ray from the player's feet, cream while the ask is safe and hot once the sway crosses that derived threshold. The receipt goes red when the shot really is out, so the two rings together are the lesson. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99b2dec3bf
commit
f3d6f71144
8 files changed
+144
-14
No files matched your search
@@ -84,7 +84,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
|
||||
// ---- game modules (real code) ----------------------------------------------
|
||||
const state = await import('./src/state.js');
|
||||
const { G, P, O, score, ball, setupPoint } = state;
|
||||
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, flatCount, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe } = await import('./src/gameplay.js');
|
||||
const { update, forceZombies, startGore, splatPoint, restartRun, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe } = await import('./src/gameplay.js');
|
||||
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs } = await import('./src/beats.js');
|
||||
const { mirror, netMsg, netTest, netTick } = await import('./src/net.js');
|
||||
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
|
||||
@@ -814,6 +814,41 @@ test('an aimed return can be played OUT, but only by mistiming it', () => {
|
||||
assert.ok(aimW > line * 0.9, 'a perfectly timed flick still reaches the corner');
|
||||
});
|
||||
|
||||
test("a flick's LENGTH is its width: the three-lane cliff is gone", () => {
|
||||
// The gesture used to be read as three LANES off a 0.38 ratio, so two neighbouring swipes differed
|
||||
// by the whole court — "sometimes applied very strong, sometimes barely does anything", with no
|
||||
// way to tell which one you had asked for. Length is the strength now, as it always was on serve.
|
||||
const swipe = (m, fx = 1) => ({ ax: 1, fx, fy: 0, mag: m });
|
||||
assert.equal(swayOf({ ax: -1 }), -1, 'keys have no length — a held direction is the whole lane');
|
||||
assert.equal(swayOf(swipe(TUNE.flickMin)), 0, 'a swipe at the flick threshold aims nothing yet');
|
||||
assert.equal(swayOf(swipe(TUNE.flickFull)), 1, '...and a committed one takes the whole width');
|
||||
assert.equal(swayOf(swipe(999)), 1, 'over-swiping cannot aim past the corner');
|
||||
let prev = 0;
|
||||
for (let m = TUNE.flickMin; m <= TUNE.flickFull; m += 2) {
|
||||
const v = swayOf(swipe(m));
|
||||
assert.ok(v >= prev - 1e-9 && v - prev < 0.05, `the sway climbs smoothly at ${m}px, no cliff`);
|
||||
prev = v;
|
||||
}
|
||||
// ...and a diagonal is a diagonal, not a lane vote: under the old ratio this aimed dead centre
|
||||
assert.ok(Math.abs(swayOf(swipe(TUNE.flickFull, 0.35)) - 0.35) < 1e-9, 'fx carries the aim');
|
||||
assert.equal(flatCount(3, swipe(TUNE.flickMin + 4)), 4, 'a swipe too short to bend it counts as flat');
|
||||
assert.equal(flatCount(3, swipe(TUNE.flickFull)), 0, '...and a real one ends the lesson');
|
||||
});
|
||||
|
||||
test('a gentle flick cannot be played out — only a committed one can', () => {
|
||||
// Oliver: "the number one reason i die is that i myself keep firing balls into the out." Every
|
||||
// aimed shot took the FULL lane and then +-aimError on top, so whether it sailed was the dice's
|
||||
// call at any width the player picked. Width follows the swipe now, so the risk band has an edge
|
||||
// the player can stop short of — and going out becomes a thing you chose, not a thing rolled.
|
||||
const line = COURT.W / 2;
|
||||
const worst = s => s * TUNE.flickWidth * TUNE.flickReach + TUNE.aimError; // widest it can land
|
||||
assert.ok(worst(0.5) < line, 'half a swipe is in at ANY timing');
|
||||
assert.ok(worst(1) > line, '...and a full one can still sail: the risk is kept, and it is chosen');
|
||||
// the threshold render colours the live aim ring with is that same arithmetic, not a magic number
|
||||
const safe = (line - TUNE.aimError) / (TUNE.flickWidth * TUNE.flickReach);
|
||||
assert.ok(safe > 0.4 && safe < 1, 'the ring turns hot inside the swipe range, not at either end');
|
||||
});
|
||||
|
||||
test('a wounding point knocks the body down and it gets up MINUS the limb', () => {
|
||||
// The beat exists because the wound stage used to POP between points — an arm stopped being there
|
||||
// and nothing said why. The contract: the collapse wears the stage it STILL WAS, the rise wears
|
||||
|
||||
Reference in new issue
Block a user