The match plays fair: deeper pool, no regicide, serves from the line
"the player is too weak in this mode. also the killing by smashing occurs too often. i cant really control it. maybe obsolete? also the serving after the first serve does not happen from behind the groundline". matchPlayerHp 8 — the match brings its own pool. The gore mode's 4 was an inheritance, not a decision, and a real game has rallies you deserve to lose without the run ending. Regicide is out of the match (kept in the duel, where any-weapon-counts is the point of sudden death). In a full game every hard serve that happened to meet the body was a free kill nobody aimed. A match ball never damages the opponent — it passes through like a soft ball and racketAI plays it — so the wound stages are exactly the scoreboard. That also made hp-per-opponent meaningless, so the ladder escalates in racketAI instead: +12% speed per body felled, whiff chance x0.82 per rung. And serving resets to behind the baseline with real deuce/ad mirroring: setupPoint's "serve from wherever you're standing" branch now excludes the match, which falls through to the tennis positioning. Verified live: second serve from z 24.3, x mirrored to the ad side, opponent unwounded by anything but the score. 38 tests, 15 seeds clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0620e97d6c
commit
ea6e50f8f8
8 files changed
+72
-16
No files matched your search
+17
@@ -853,6 +853,23 @@ because the deepest system in the repo (swings, volleys, lanes, the AI) was alre
|
|||||||
**The drill stays in the back room beside it** — triage is a decent idea that failed the
|
**The drill stays in the back room beside it** — triage is a decent idea that failed the
|
||||||
grasp-in-five-seconds test, and the match is now the lead candidate for the middle mode.
|
grasp-in-five-seconds test, and the match is now the lead candidate for the middle mode.
|
||||||
|
|
||||||
|
**FOURTEENTH PASS — the match plays fair.** Oliver: *"the player is too weak in this mode. also the
|
||||||
|
killing by smashing occurs too often. i cant really control it. maybe obsolete? also the serving
|
||||||
|
after the first serve does not happen from behind the groundline"*.
|
||||||
|
- **`matchPlayerHp` 8** — the match brings its own pool. The gore mode's 4 was never a decision
|
||||||
|
here, just an inheritance, and a real game has rallies you deserve to lose without the run ending.
|
||||||
|
- **REGICIDE IS OUT OF THE MATCH** (kept in the duel, where "any weapon counts" is the point of a
|
||||||
|
sudden-death). In a full game, every hard serve that happened to meet the body was a free kill
|
||||||
|
nobody aimed. A match ball NEVER damages the opponent now — it passes through like a soft ball and
|
||||||
|
racketAI plays it — so the wound stages are exactly the scoreboard with no side-channel smearing
|
||||||
|
them. This also deleted the hp-per-opponent escalation (a deeper pool changes nothing when the
|
||||||
|
body is pure scoreboard): **the ladder escalates in racketAI instead** — `matchSpeedPer` (+12%
|
||||||
|
speed per body felled) and `matchFlubKeep` (whiff chance × 0.82 per rung).
|
||||||
|
- **Serving resets to BEHIND THE BASELINE with real deuce/ad mirroring** — `setupPoint`'s splatter
|
||||||
|
branch ("serve from wherever you're standing") now excludes the match, which falls through to the
|
||||||
|
tennis branch. Serving from mid-court where the last rally left you read as a bug because it was
|
||||||
|
one. Verified: second serve from z 24.3, x mirrored to the ad side.
|
||||||
|
|
||||||
**Open, and every one of them is Oliver's call after he plays it:**
|
**Open, and every one of them is Oliver's call after he plays it:**
|
||||||
1. ~~**A ball that carries through the lane can complete two steps at once.**~~ Largely CLOSED by
|
1. ~~**A ball that carries through the lane can complete two steps at once.**~~ Largely CLOSED by
|
||||||
the slice removal (seventh pass) — a cut ball picking its own next victim was the main way this
|
the slice removal (seventh pass) — a cut ball picking its own next victim was the main way this
|
||||||
|
|||||||
+1
-1
@@ -400,6 +400,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="src/main.js?v=165"></script>
|
<script type="module" src="src/main.js?v=166"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+7
-1
@@ -219,6 +219,9 @@ export function watchdog(dt) {
|
|||||||
export function startMatch() {
|
export function startMatch() {
|
||||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match) return false;
|
if (G.interlude || G.horde || G.bossFight || G.drill || G.match) return false;
|
||||||
G.match = { opp: 1, ending: false };
|
G.match = { opp: 1, ending: false };
|
||||||
|
G.hp = G.hpMax = TUNE.matchPlayerHp; // a real match is longer than four mistakes — the gore
|
||||||
|
// mode's 4 was "too weak" here, felt not calculated
|
||||||
|
score.server = 'P'; // you serve every point (the corpse never learned to toss)
|
||||||
G.weapon = 0; G.mgT = 0; endBurst(); pellets.length = 0; extras.length = 0; shots.length = 0;
|
G.weapon = 0; G.mgT = 0; endBurst(); pellets.length = 0; extras.length = 0; shots.length = 0;
|
||||||
zombies.length = 0;
|
zombies.length = 0;
|
||||||
G.boy = null;
|
G.boy = null;
|
||||||
@@ -239,7 +242,10 @@ function matchOpponent() {
|
|||||||
spawnRacketZombie();
|
spawnRacketZombie();
|
||||||
const zb = zombies.find(z => z.variant === 'r');
|
const zb = zombies.find(z => z.variant === 'r');
|
||||||
zb.dueled = true; // wakes racketAI, same as a fresh duel
|
zb.dueled = true; // wakes racketAI, same as a fresh duel
|
||||||
zb.hp = zb.hpMax = TUNE.matchOppHp + (G.match.opp - 1) * TUNE.matchOppHpPer;
|
zb.hp = zb.hpMax = TUNE.matchOppHp; // a FLAT pool: with regicide gone the body is pure
|
||||||
|
// scoreboard (a quarter per point), so a deeper pool would
|
||||||
|
// change nothing — the ladder escalates in racketAI instead
|
||||||
|
// (matchSpeedPer / matchFlubKeep)
|
||||||
G.interlude = { introT: 1.6 }; // the duel machinery IS the match machinery
|
G.interlude = { introT: 1.6 }; // the duel machinery IS the match machinery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -383,9 +383,12 @@ export const TUNE = {
|
|||||||
matchOppHp: 4, // the first opponent's pool. FOUR, deliberately: a quarter comes off per point
|
matchOppHp: 4, // the first opponent's pool. FOUR, deliberately: a quarter comes off per point
|
||||||
// won, so a love game walks it through every wound stage and the game point
|
// won, so a love game walks it through every wound stage and the game point
|
||||||
// fells it — the scoreboard and the body agree all the way down
|
// fells it — the scoreboard and the body agree all the way down
|
||||||
matchOppHpPer: 2, // extra pool per opponent felled — later corpses soak more regicide punishment
|
matchPlayerHp: 8, // your pool for the MATCH — richer than the gore mode's 4, because a real game
|
||||||
matchHeal: 2, // hp back for a game won (the duel's healPerDuel rate) — a clean game is how
|
// has rallies you deserve to lose without the run ending ("the player is too
|
||||||
// you last; their every point costs 1 of your playerHp 4
|
// weak in this mode"). Their point still costs 1; matchHeal is how you last
|
||||||
|
matchSpeedPer: 0.12, // racketAI speed grows this much per opponent felled...
|
||||||
|
matchFlubKeep: 0.82, // ...and its whiff chance keeps only this much — the ladder now that hp is flat
|
||||||
|
matchHeal: 2, // hp back for a game won (the duel's healPerDuel rate)
|
||||||
// The timing windows, DRAWN (the match's whole ask: "what is the possible tap window, what is the
|
// The timing windows, DRAWN (the match's whole ask: "what is the possible tap window, what is the
|
||||||
// sweet spot"). Numbers shared with the mechanics they visualise, so the picture cannot lie:
|
// sweet spot"). Numbers shared with the mechanics they visualise, so the picture cannot lie:
|
||||||
matchRingFar: 9, // m — the approach ring fades in once the incoming ball is this close
|
matchRingFar: 9, // m — the approach ring fades in once the incoming ball is this close
|
||||||
|
|||||||
+6
-2
@@ -949,7 +949,10 @@ function racketAI(rz, dt) {
|
|||||||
const tz = Math.max(1.5, Math.min(COURT.svcFar, ball.z)); // chase laterally, hold deep on the far court
|
const tz = Math.max(1.5, Math.min(COURT.svcFar, ball.z)); // chase laterally, hold deep on the far court
|
||||||
const dx = ball.x - rz.x, dz = tz - rz.z, d = Math.hypot(dx, dz);
|
const dx = ball.x - rz.x, dz = tz - rz.z, d = Math.hypot(dx, dz);
|
||||||
if (d > 0.15) {
|
if (d > 0.15) {
|
||||||
const step = Math.min(TUNE.racketSpeed * dt, d);
|
// THE MATCH LADDER ESCALATES HERE, not in hp: with regicide gone the pool is pure scoreboard,
|
||||||
|
// so a harder opponent is a FASTER one that flubs less — the same knobs, scaled per body felled.
|
||||||
|
const spd = TUNE.racketSpeed * (G.match ? 1 + (G.match.opp - 1) * TUNE.matchSpeedPer : 1);
|
||||||
|
const step = Math.min(spd * dt, d);
|
||||||
rz.dist = (rz.dist || 0) + step; // ground covered — his walk cycle steps off this, not the clock
|
rz.dist = (rz.dist || 0) + step; // ground covered — his walk cycle steps off this, not the clock
|
||||||
rz.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt;
|
rz.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt;
|
||||||
if (rz.pose !== 'swing') rz.pose = 'run';
|
if (rz.pose !== 'swing') rz.pose = 'run';
|
||||||
@@ -958,7 +961,8 @@ function racketAI(rz, dt) {
|
|||||||
if (rz.swingCool <= 0 && rz.swingT < 0
|
if (rz.swingCool <= 0 && rz.swingT < 0
|
||||||
&& Math.hypot(ball.x - rz.x, (ball.z - rz.z) * 0.8) < TUNE.aiReach
|
&& Math.hypot(ball.x - rz.x, (ball.z - rz.z) * 0.8) < TUNE.aiReach
|
||||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
|
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
|
||||||
if (Math.random() < TUNE.racketFlub) rz.swingCool = 0.5; // whiff — it hesitates
|
const flub = TUNE.racketFlub * (G.match ? Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) : 1);
|
||||||
|
if (Math.random() < flub) rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
||||||
else { rz.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; trySwing(rz, true, TUNE.racketQMax); rz.swingT = -1; rz.swingCool = 0.3; }
|
else { rz.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; trySwing(rz, true, TUNE.racketQMax); rz.swingT = -1; rz.swingCool = 0.3; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -161,7 +161,11 @@ export const deuceSide = () => (score.pts[0] + score.pts[1]) % 2 === 0; // even
|
|||||||
export function setupPoint() {
|
export function setupPoint() {
|
||||||
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0;
|
G.phase = 'pretoss'; ball.active = false; ball.bounces = 0;
|
||||||
G.swingSnaps = null; G.rallyNear = null; G.swung = false;
|
G.swingSnaps = null; G.rallyNear = null; G.swung = false;
|
||||||
if (G.mode === 'splatter') { // always your serve, from wherever you're standing
|
if (G.mode === 'splatter' && !G.match) { // always your serve, from wherever you're standing
|
||||||
|
// (the MATCH falls through to the tennis branch below:
|
||||||
|
// real tennis serves from behind the baseline, deuce side
|
||||||
|
// mirroring and all — serving from mid-court where the
|
||||||
|
// last rally left you read as a bug, because it was one)
|
||||||
score.server = 'P';
|
score.server = 'P';
|
||||||
P.pose = 'idle'; P.swingT = -1; P.facing = 1;
|
P.pose = 'idle'; P.swingT = -1; P.facing = 1;
|
||||||
return;
|
return;
|
||||||
|
|||||||
+7
-1
@@ -1139,7 +1139,13 @@ export function updateZombies(dt) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isBoss(zb)) { // boss: splat-quality wounds it; soft balls the racket one rallies (racketAI)
|
if (isBoss(zb)) { // boss: splat-quality wounds it; soft balls the racket one rallies (racketAI)
|
||||||
if ((ball.pow || 0) >= TUNE.zSplatQ) {
|
// NOT IN THE MATCH. Regicide made sense in the duel (one sudden-death point, any weapon
|
||||||
|
// counts) but in a full game every hard serve that happened to meet the body was a free
|
||||||
|
// kill nobody aimed — Oliver: "the killing by smashing occurs too often. i cant really
|
||||||
|
// control it". A match ball NEVER damages the opponent; it passes through like any soft
|
||||||
|
// ball and racketAI plays it. The score is the only way through — which also makes the
|
||||||
|
// wound stages exactly the scoreboard, with no side-channel smearing them.
|
||||||
|
if ((ball.pow || 0) >= TUNE.zSplatQ && !G.match) {
|
||||||
zb.hitT = TUNE.bossHitCool; // belt-and-braces: this ball is spent below, but the cooldown
|
zb.hitT = TUNE.bossHitCool; // belt-and-braces: this ball is spent below, but the cooldown
|
||||||
const dead = wound(zb, TUNE.dmgBall); // is what stops ANY lingering contact multi-wounding
|
const dead = wound(zb, TUNE.dmgBall); // is what stops ANY lingering contact multi-wounding
|
||||||
pock(0.6, 300);
|
pock(0.6, 300);
|
||||||
|
|||||||
@@ -531,23 +531,39 @@ test('a point won tears a wound stage off the body; only the game point fells it
|
|||||||
assert.ok(!zombies.includes(zb), 'the game-winning point IS the kill');
|
assert.ok(!zombies.includes(zb), 'the game-winning point IS the kill');
|
||||||
const next = oppOf();
|
const next = oppOf();
|
||||||
assert.ok(next && next !== zb, 'a fresh opponent shambled on');
|
assert.ok(next && next !== zb, 'a fresh opponent shambled on');
|
||||||
assert.equal(next.hpMax, TUNE.matchOppHp + TUNE.matchOppHpPer, 'with a deeper pool');
|
assert.equal(next.hpMax, TUNE.matchOppHp, 'same flat pool — the ladder escalates in speed/flub, not hp');
|
||||||
assert.equal(G.runGames, 1, 'the ladder banked the game');
|
assert.equal(G.runGames, 1, 'the ladder banked the game');
|
||||||
assert.deepEqual(score.pts, [0, 0], 'and the points reset');
|
assert.deepEqual(score.pts, [0, 0], 'and the points reset');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('their points draw blood, and the fourth is the run', () => {
|
test('their points draw blood from the MATCH pool, and running dry is the run', () => {
|
||||||
startMatch();
|
startMatch();
|
||||||
G.hp = TUNE.playerHp;
|
assert.equal(G.hp, TUNE.matchPlayerHp, 'the match brings its own, deeper pool');
|
||||||
for (let k = 1; k <= 3; k++) {
|
|
||||||
resolveInterlude('O');
|
resolveInterlude('O');
|
||||||
assert.equal(G.hp, TUNE.playerHp - k, 'point ' + k + ' cost a wound');
|
assert.equal(G.hp, TUNE.matchPlayerHp - 1, 'a point costs one');
|
||||||
assert.notEqual(G.mode, 'eaten');
|
G.hp = 1;
|
||||||
}
|
|
||||||
resolveInterlude('O');
|
resolveInterlude('O');
|
||||||
assert.equal(G.mode, 'eaten', 'blood only — and it runs out the way it always does');
|
assert.equal(G.mode, 'eaten', 'blood only — and it runs out the way it always does');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a match ball never wounds the body: the score is the only way through', () => {
|
||||||
|
startMatch();
|
||||||
|
const zb = oppOf();
|
||||||
|
// a full-power ball straight into the opponent — the regicide path, now closed for matches
|
||||||
|
ball.active = true; ball.lastHit = 'P'; ball.y = 1.2; ball.pow = 1; ball.bounces = 0;
|
||||||
|
ball.x = zb.x; ball.z = zb.z; ball.vx = 0; ball.vy = 0; ball.vz = -1;
|
||||||
|
for (let i = 0; i < 5; i++) update(DT);
|
||||||
|
assert.ok(zombies.includes(zb), 'still standing');
|
||||||
|
assert.equal(zb.hp, zb.hpMax, 'and unwounded — "killing by smashing" is out of the match');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the ladder escalates in speed and reliability, not in hp', () => {
|
||||||
|
// pure arithmetic on the knobs racketAI reads — pinned so a future tuning pass cannot silently
|
||||||
|
// flatten the ladder again
|
||||||
|
assert.ok(TUNE.matchSpeedPer > 0, 'each opponent is faster');
|
||||||
|
assert.ok(TUNE.matchFlubKeep < 1, 'and whiffs less');
|
||||||
|
});
|
||||||
|
|
||||||
test('a game won heals, so a clean player can go the distance', () => {
|
test('a game won heals, so a clean player can go the distance', () => {
|
||||||
startMatch();
|
startMatch();
|
||||||
G.hp = 1;
|
G.hp = 1;
|
||||||
|
|||||||
Reference in new issue
Block a user