The ladder has names, the bodies play tennis, and the stats screen splits
THE SPRITES. The match opponent borrowed the turned CPU's art — a body drawn to SHAMBLE at the camera with its racket hanging at its side, which is what was playing tennis points against you. It has its own family now, 'r', drawn off the LIVING opponent's poses: a ready stance, a shuffle pair and a real forehand, at every wound stage, men's and women's — 20 frames, six batches. 'c' keeps its shamble untouched, because in the gore mode shambling is exactly what the turned CPU should be doing. The pose comes from racketAI's own `pose`, so the picture cannot drift from the behaviour, and a one-armed opponent still plays tennis instead of reverting to a walk. Two pipeline bugs found doing it, both fixed at the root in assets/: - _fine.py keyed EVERY magenta pixel, not just the background, and the generator paints racket strings in the backdrop colour more often than not — so frame after frame installed a man holding an empty hoop. Background is now the region connected to the border; magenta left INSIDE a body is repainted pale grey rather than punched out. - the flood fill silently did nothing, because Image.fromarray images are read-only and PIL's floodfill swallows the write error. .copy() plus a real error if it ever fills zero pixels. THE LADDER (handoff slice 2). Rung N takes design N's name off the chapter roster — the club's dead staff play you at tennis here and come for you in gore, same joke twice — plus a personality that is DIALS, not new mechanics: pace, width, flub and speed scalings multiplying numbers racketAI already reads. A rung is a data row. Name plate on the walk-on, and the kill files under the rung rather than under the generic body. THE STATS SCREEN follows the mode split: BODY COUNT / THE LADDER / THE RECORD behind a mode strip, each with its own three figures, its own roster heading and its own footer. The weapon shelf and the roster hide in the modes that have neither, rather than printing a line about something the mode cannot do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c89d4a3ff9
commit
c629ae1108
16 files changed
+138
-33
No files matched your search
+8
-4
@@ -152,7 +152,9 @@ function doHit(ch, quality, aim) {
|
||||
// at ~27 m/s (baseShotSpeed + 0.55 quality) — crossing to your baseline in ~0.6 s, which is less
|
||||
// time than auto-run needs from the far corner. Opponent one gives you the flight time; the
|
||||
// rungs take it back.
|
||||
s *= Math.min(1, TUNE.matchOppPace + (G.match.opp - 1) * TUNE.matchOppPacePer);
|
||||
// ...and the RUNG's own pace trait rides on top, inside the cap: full pace is still the ceiling,
|
||||
// a rung just gets there sooner or later than the flat ladder would (see matchRung in config).
|
||||
s *= Math.min(1, (TUNE.matchOppPace + (G.match.opp - 1) * TUNE.matchOppPacePer) * (ch.rung?.pace || 1));
|
||||
}
|
||||
if (G.match) {
|
||||
// THE OPPONENT'S WIDTH IS THE LADDER'S THIRD DIAL. Its returns scatter off aimError x (1 - 0.55
|
||||
@@ -160,7 +162,7 @@ function doHit(ch, quality, aim) {
|
||||
// balls". Opponent one keeps its returns inside matchWideBase of the centre line; each rung
|
||||
// earns matchWidePer more width, so the line-painting is the LATE ladder, not the opening one.
|
||||
if (ch.side !== 'P') {
|
||||
const wide = TUNE.matchWideBase + (G.match.opp - 1) * TUNE.matchWidePer;
|
||||
const wide = (TUNE.matchWideBase + (G.match.opp - 1) * TUNE.matchWidePer) * (ch.rung?.wide || 1);
|
||||
tx = Math.max(-wide, Math.min(wide, tx));
|
||||
}
|
||||
// THE PLACEMENT, MADE VISIBLE: your aimed rally hit stamps its landing point, and render draws a
|
||||
@@ -1047,7 +1049,8 @@ function racketAI(rz, dt) {
|
||||
// so a harder opponent is a FASTER one that flubs less. The BASE is boosted too — the duel's
|
||||
// 2.8 m/s lumber was tuned for one sudden-death point against a fresh player, and across a
|
||||
// whole game it read as "very weak and slow": opponent one already moves at matchSpeedBase.
|
||||
const spd = TUNE.racketSpeed * (G.match ? TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer : 1);
|
||||
const spd = TUNE.racketSpeed * (G.match ? (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer)
|
||||
* (rz.rung?.speed || 1) : 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.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt;
|
||||
@@ -1059,7 +1062,8 @@ function racketAI(rz, dt) {
|
||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
|
||||
// ...and the match has its OWN flub base: the duel's 0.2 hands a fifth of all returns away,
|
||||
// which across a game is an opponent that beats itself.
|
||||
const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) : TUNE.racketFlub;
|
||||
const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) * (rz.rung?.flub || 1)
|
||||
: TUNE.racketFlub;
|
||||
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; }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user