Two testing switches: one-shot bodies, and the eaters off

"please make the zombie killable with a single hit for testing" and "remove
the fake zombies for now".

drillOneShot drops every body on a drill court to 1 hp, applied at spawn to
the batch and the eaters alike. Worth knowing what it removes: the roster's
HP is what makes the called ORDER matter — a roller is normally seven serves,
so being told to take it third means starting on it first. At one hit that
planning layer is gone and the sequence is pure recall. Flagged in PROJECT.md
so it gets decided on feel rather than left on by drift.

drillEaterMax 3 -> 0 switches the plain shamblers off. The mechanism is
untouched — spawnEater, the wide entry away from the gang, the never-a-fault
rule — so one number brings them back.

The eater test now asserts whichever contract the knob describes (at 0:
nothing but the puzzle ever appears; above 0: they arrive, capped, always
plain shamblers) instead of being deleted and rotting. And the never-a-fault
rule gets its own test with a hand-placed body, so the rule the eaters depend
on stays covered while they are switched off.

31 tests, 15 seeds clean. Verified live: brute/steward/roller all at 1 hp
where the roster says 5/5/7, zero strays over 40s, one aimed serve puts down
the roller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 20:37:01 +02:00
1 parent d34683bc68
commit 0f524a08c9
5 files changed
+73 -23

No files matched your search

+11 -1
View File
@@ -367,9 +367,19 @@ export const TUNE = {
// no business standing in front of the puzzle.
drillEaterEvery: 12.0, // s between plain-shambler eaters at the bottom of the ramp...
drillEaterMin: 5.0, // ...and at the top. Still THE difficulty dial, just a slower one
drillEaterMax: 3, // eaters allowed on court at once — past this the puzzle is unreadable
drillEaterMax: 0, // eaters allowed on court at once. **0 = OFF, which is where it is now** —
// Oliver: "remove the fake zombies for now", so the drill is currently the
// pure puzzle with no second threat. The whole mechanism is intact
// (spawnEater, the wide entry, the never-a-fault rule); this one number
// 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
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
// roster says it should soak. Flip to 0 for the real thing — and note that this is not purely a
// convenience: the roster's HP is what makes the called ORDER matter (a roller is seven serves, so
// being told to take it third means starting on it first), and one-shot bodies delete that whole
// layer. Decide it on feel rather than leaving it on by drift.
drillOneShot: 1, // >= 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
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,
+6 -2
View File
@@ -66,6 +66,10 @@ export const drillSpeed = d => lerp(TUNE.drillSpeed, TUNE.drillSpeedMax, ramp(d)
const batchSize = d => Math.round(lerp(TUNE.drillBatch, TUNE.drillBatchMax, ramp(d)));
const eaterGap = d => lerp(TUNE.drillEaterEvery, TUNE.drillEaterMin, ramp(d));
// TUNE.drillOneShot: strip a body down to a single hit. Applied at spawn to the batch AND the
// eaters, so nothing on a drill court ever needs a second serve.
const oneShot = zb => { if (TUNE.drillOneShot >= 0.5) zb.hp = zb.hpMax = 1; return zb; };
const shuffle = a => { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; };
const gang = () => zombies.filter(z => z.drill); // the batch: a member's identity IS its variant
@@ -93,7 +97,7 @@ function spawnBatch(d) {
// rank walking in abreast — but every one still starts on the far side, which is the promise.
spawnZombie(lanes[i] + (Math.random() - 0.5) * 0.4,
TUNE.drillSpawnZ + Math.random() * TUNE.drillBatchDepth, t.v);
const zb = zombies[zombies.length - 1];
const zb = oneShot(zombies[zombies.length - 1]);
zb.drill = true; // marks it as the puzzle AND holds its lane (see updateZombies)
zb.v = drillSpeed(d) * t.vMul * (0.92 + Math.random() * 0.16);
});
@@ -125,7 +129,7 @@ function spawnEater(d) {
// ALWAYS A PLAIN SHAMBLER — rule 1 above doing its work: the alphabet never contains 'a'/'b', so
// an eater can never be mistaken for a body the sequence is asking for, with no marking on either.
spawnZombie(bx, TUNE.drillSpawnZ + Math.random() * 0.8, Math.random() < 0.3 ? 'b' : 'a');
const zb = zombies[zombies.length - 1];
const zb = oneShot(zombies[zombies.length - 1]);
zb.v = drillSpeed(d) * TUNE.drillEaterSpeed * (0.9 + Math.random() * 0.2);
}