The finishing shot is spent, and the wound skins come back

"one-shot is bad - way too easy - also we're losing all the nice gore
animation sprites" and "I noticed the last kill of a game can carry to the
spawn of a new zombie of the next round".

drillOneShot is off. The second half of that note is the part worth
remembering: a 1 hp body never passes through a wound stage, so every skin in
ZWOUND — arm off, ribs out, still walking — is skipped and the roster's best
art never renders at all. Kept as a switch, never a default.

The carry-over was a real unfairness, not a curiosity. A ball keeps pace
through a kill and travels UP-COURT, which is exactly where the next batch is
born — so the shot that finished your round flew on into bodies that did not
exist when you took it, and faulted you on a round you had not been shown
yet. The shot that ends a round is spent now, and drillCaught does the same
since it also refills the court up-court. Pierce within a batch is untouched:
that is honest occlusion you could have read, this was not.

Verified with real aimed serves through a whole round: round banks, ball goes
inactive, the next batch walks on with 0 faults and a clean sequence, and the
gang carries roster HP again (brute 5, ball boy 3).

33 tests, 15 seeds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 20:43:26 +02:00
1 parent 0f524a08c9
commit 57e96d629e
5 files changed
+71 -22

No files matched your search

+15 -5
View File
@@ -739,11 +739,21 @@ Watch: **a roller in the middle of a sequence is seven aimed serves.** Oliver as
robustness to be kept, so it is — but if a round ever feels like grinding rather than reading, a robustness to be kept, so it is — but if a round ever feels like grinding rather than reading, a
drill-only HP scale is the knob, not a change to the roster. drill-only HP scale is the knob, not a change to the roster.
**TENTH PASS — two testing switches, and neither should be left on by drift.** **TENTH PASS — two switches, and the round-boundary bug.**
- **`TUNE.drillOneShot` (ON)**: every body on a drill court drops to 1 hp. Asked for to make testing - **`TUNE.drillOneShot` — tried ON for one playtest, now OFF.** *"one-shot is bad - way too easy -
quick, but be aware what it removes — **the roster's HP is what makes the called ORDER matter**. A also we're losing all the nice gore animation sprites"*. The second half is the one that is easy to
roller is normally seven serves, so being told to take it third means starting on it first; at one miss and worth remembering generally: **a 1 hp body never passes through a wound stage**, so every
hit that whole layer of planning is gone and the sequence is pure recall. Decide it on feel. skin in `ZWOUND` (arm off, ribs out, still walking) is skipped and the roster's best art never
renders at all. The HP is also what makes the called ORDER matter — a roller is seven serves, so
being told to take it third means starting on it first. Kept as a switch, never a default.
- **THE ROUND-BOUNDARY CARRY-OVER, and it was a real unfairness.** *"the last kill of a game can
carry to the spawn of a new zombie of the next round"*. A ball keeps pace through a kill
(`TUNE.zPierce`) and it travels UP-COURT — which is precisely where the next batch is born. So the
shot that finished your round flew on into bodies that did not exist when you took it and faulted
you on a round you had not been shown yet. **The shot that ends a round is spent** (`ball.active =
false` in drillKill's completion branch, and in `drillCaught` for the same reason: that also
refills the court up-court). Pierce WITHIN a batch is untouched — that is honest occlusion you
could have read; this was not.
- **`TUNE.drillEaterMax` 3 -> 0 (OFF)**: *"remove the fake zombies for now"*. The drill is the pure - **`TUNE.drillEaterMax` 3 -> 0 (OFF)**: *"remove the fake zombies for now"*. The drill is the pure
puzzle with no second threat at the moment. The mechanism is untouched — `spawnEater`, the wide puzzle with no second threat at the moment. The mechanism is untouched — `spawnEater`, the wide
entry away from the gang, the never-a-fault rule — so one number brings it back. entry away from the gang, the never-a-fault rule — so one number brings it back.
+1 -1
View File
@@ -399,6 +399,6 @@
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="src/main.js?v=161"></script> <script type="module" src="src/main.js?v=162"></script>
</body> </body>
</html> </html>
+7 -6
View File
@@ -374,12 +374,13 @@ export const TUNE = {
// brings it back. Past 3 the puzzle stops being readable. // brings it back. Past 3 the puzzle stops being readable.
drillEaterSpeed: 1.15,// they walk a touch faster than the batch: the batch is a list, this is a threat drillEaterSpeed: 1.15,// they walk a touch faster than the batch: the batch is a list, this is a threat
drillRampT: 210, // s to go from the bottom of every curve above to the top drillRampT: 210, // s to go from the bottom of every curve above to the top
// TESTING KNOB, currently ON. Every body in the drill dies to one splat-quality hit, whatever the // TESTING KNOB, and it is OFF again after one playtest: "one-shot is bad - way too easy - also
// roster says it should soak. Flip to 0 for the real thing — and note that this is not purely a // we're losing all the nice gore animation sprites". Both halves matter and the second is the one
// convenience: the roster's HP is what makes the called ORDER matter (a roller is seven serves, so // that is easy to miss — a 1 hp body never passes through any of its WOUND STAGES, so every skin
// being told to take it third means starting on it first), and one-shot bodies delete that whole // in ZWOUND (arm off, ribs out, still walking) is skipped entirely and the roster's best art never
// layer. Decide it on feel rather than leaving it on by drift. // renders. The HP is also what makes the called ORDER matter: a roller is seven serves, so being
drillOneShot: 1, // >= 0.5: batch bodies AND eaters all drop to 1 hp // told to take it third means starting on it first. Left here as a switch, not a default.
drillOneShot: 0, // >= 0.5: batch bodies AND eaters all drop to 1 hp
drillCaughtBack: 45, // s of ramp handed back when one reaches you. A real setback, not a reset drillCaughtBack: 45, // s of ramp handed back when one reaches you. A real setback, not a reset
fps: 60, // frame cap — 30 halves battery/heat but Oliver felt it; smoothness wins (0 = uncapped) fps: 60, // frame cap — 30 halves battery/heat but Oliver felt it; smoothness wins (0 = uncapped)
lowPower: 1, // >= 0.5 caps RES at 3 (−44% of pixels painted per frame) — the battery saver, lowPower: 1, // >= 0.5 caps RES at 3 (−44% of pixels painted per frame) — the battery saver,
+8 -1
View File
@@ -19,7 +19,7 @@
// render.js reads DRILL_COLS and draws. // render.js reads DRILL_COLS and draws.
import { TUNE } from './config.js'; import { TUNE } from './config.js';
import { G } from './state.js'; import { G, ball } from './state.js';
import { zombies, spawnZombie } from './zombies.js'; import { zombies, spawnZombie } from './zombies.js';
import { voice } from './audio.js'; import { voice } from './audio.js';
@@ -176,6 +176,12 @@ export function drillKill(zb) {
if (d.step >= d.seq.length) { // round complete — and the next one is already here if (d.step >= d.seq.length) { // round complete — and the next one is already here
d.rounds++; d.rounds++;
d.msg = { text: 'GOOD', t: 0.9 }; d.msg = { text: 'GOOD', t: 0.9 };
// THE SHOT THAT ENDS A ROUND IS SPENT. A ball keeps pace through a kill (TUNE.zPierce) and it
// is travelling UP-COURT — which is exactly where the next batch is about to be born. So the
// ball that finished your round flew on into bodies that did not exist when you took the shot
// and faulted you on a round you had not been shown yet. Pierce WITHIN a batch stays: that is
// honest occlusion you could have read. This is not.
ball.active = false;
newRound(d); newRound(d);
} }
return; return;
@@ -206,6 +212,7 @@ export function drillCaught(text = 'CAUGHT') {
voice('game_opponent', 0.1); voice('game_opponent', 0.1);
d.msg = { text, t: 1.8, bad: true }; d.msg = { text, t: 1.8, bad: true };
zombies.length = 0; zombies.length = 0;
ball.active = false; // same reason as the round-complete case: the court refills up-court
d.seq = []; d.step = 0; d.faults = 0; d.spawnT = 0; d.seq = []; d.step = 0; d.faults = 0; d.spawnT = 0;
d.t = Math.max(0, d.t - TUNE.drillCaughtBack); // ...and the ramp backs off a little d.t = Math.max(0, d.t - TUNE.drillCaughtBack); // ...and the ramp backs off a little
} }
+40 -9
View File
@@ -490,17 +490,48 @@ test('a wrong kill never leaves an un-winnable round', () => {
} }
}); });
test('drillOneShot drops every body on the drill court to a single hit', () => { test('the shot that ends a round cannot carry into the next batch', () => {
// Oliver: "the last kill of a game can carry to the spawn of a new zombie of the next round". A
// ball keeps pace through a kill and travels UP-COURT, which is exactly where the next batch is
// born — so it faulted you on a round you had not been shown yet.
drillPlaying();
const seq = G.drill.seq.slice();
for (const v of seq) drillKillType(v);
assert.ok(G.drill.rounds >= 1, 'setup: the round completed');
assert.equal(ball.active, false, 'the finishing shot is spent, not left in flight');
// let the next batch walk on and confirm it arrives untouched
for (let i = 0; i < 30; i++) update(DT);
const gang = zombies.filter(z => z.drill);
assert.ok(gang.length >= 2, 'the next batch is on court');
assert.equal(G.drill.faults, 0, 'and it did not inherit a fault from the previous round');
assert.equal(G.drill.step, 0, 'nor a half-finished sequence');
});
test('being caught also spends the ball, for the same reason', () => {
drillPlaying();
ball.active = true; ball.lastHit = 'P';
killPlayer();
assert.equal(ball.active, false, 'the court refills up-court; a live ball would meet it');
});
test('drillOneShot is a switch, and OFF means the roster HP is intact', () => {
// Off by default: one-shot bodies were "way too easy" AND skipped every wound skin in ZWOUND,
// because a 1 hp body never passes through a damage stage. Asserts whichever way the knob is set
// rather than being deleted when it was turned off.
startDrill(); startDrill();
assert.ok(TUNE.drillOneShot >= 0.5, 'setup: the testing knob is on');
for (const z of zombies) assert.equal(z.hp, 1, z.variant + ' should be one-shot, saw hp ' + z.hp);
// and it really dies to ONE splat-quality ball, roster HP notwithstanding
const tank = zombies.find(z => z.drill && z.variant === 'k') const tank = zombies.find(z => z.drill && z.variant === 'k')
|| zombies.find(z => z.drill && z.variant === 'g') || zombies.find(z => z.drill); || zombies.find(z => z.drill && z.variant === 'g');
ball.active = true; ball.lastHit = 'P'; ball.y = 1; ball.pow = 1; ball.bounces = 0; if (TUNE.drillOneShot >= 0.5) {
ball.x = tank.x; ball.z = tank.z; ball.vx = ball.vy = ball.vz = 0; for (const z of zombies) assert.equal(z.hp, 1, z.variant + ' should be one-shot, saw hp ' + z.hp);
update(DT); } else if (tank) {
assert.ok(!zombies.includes(tank), 'one hit put down a ' + tank.variant); assert.ok(tank.hp > 1, 'the heavy bodies keep the roster HP: ' + tank.variant + ' at ' + tank.hp);
// ...and that is what lets it show a wound stage on the way down
ball.active = true; ball.lastHit = 'P'; ball.y = 1; ball.pow = 1; ball.bounces = 0;
ball.x = tank.x; ball.z = tank.z; ball.vx = ball.vy = ball.vz = 0;
update(DT);
assert.ok(zombies.includes(tank), 'one hit does not put down a ' + tank.variant);
assert.ok(tank.hp < tank.hpMax, 'but it is wounded: ' + tank.hp + '/' + tank.hpMax);
}
}); });
test('a batch body that walks past you counts as caught, never as a free escape', () => { test('a batch body that walks past you counts as caught, never as a free escape', () => {