You aim it yourself now, and no two rounds are dealt alike

Oliver played it: "the auto aim just mean no skills involved at all. this
should just go away.. there is no penalty at the moment.. make some
variations of the position on the court and the color".

The first two notes are one finding. With the shot locked to the called body
it was unmissable, so the fault could never fire — there was no penalty
because there was no way to earn one. Both snaps are deleted. What replaced
them is one line: aimTie is 0 during a drill. That is not auto-aim, it is
removing the guess — the ordinary magnet lets a nearer body inside the same
cone steal a shot aimed past it, which is right for a crowd you are mowing
down and exactly wrong where hitting the wrong body IS the failure. An
unaimed tap now lands on grass.

And the variation:
- six colours (purple, white) with the court dealt three to five of them, so
  which colours turn up and how many decoys there are are live questions. At
  four out of four every round dealt every colour and only the order moved.
- PLACEMENTS: fan, cluster, column, stagger, wing — weighted, mirrored per
  round, lanes shuffled against the bib order. column stacks bodies in one
  lane, and the ball hits the first thing it touches, so feet matter.
- pips are dice faces now, shared by bib and HUD chip. A row of five dots has
  to be counted; a die is recognised.

Measured over 30 deals: all five shapes, counts even, all six colours, and
the first target screened by another body in ~10% of rounds. Verified end to
end in the browser that FAULT and DOUBLE FAULT both fire.

26 tests, including a named regression for the auto-aim staying gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-11 18:16:25 +02:00
1 parent 22df96cc1d
commit ef8a413031
10 files changed
+173 -64

No files matched your search

+40 -6
View File
@@ -61,7 +61,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const { G, P, score, ball, setupPoint } = await import('./src/state.js');
const { update, forceZombies, splatPoint, restartRun } = await import('./src/gameplay.js');
const { update, forceZombies, splatPoint, restartRun, flickTarget } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill } = await import('./src/beats.js');
const { zombies, spawnZombie, pellets } = await import('./src/zombies.js');
const { killPlayer } = await import('./src/zombies.js');
@@ -274,23 +274,31 @@ test('a burst fires through the beat that follows the kill that emptied the cour
// ---- the coaching drill: a new mode, so it owes the ownership contract a test ------------------
// Drive a drill round past its announcement: no serving and no targets while the umpire reads.
function drillPlaying() {
function drillPlaying({ bodies } = {}) {
// Pin the court size where a test needs a guaranteed decoy (bodies > the called sequence). The
// count is a RANGE in play, so an unpinned round can legitimately deal no decoy at all.
const min = TUNE.drillBodiesMin, max = TUNE.drillBodies;
if (bodies) { TUNE.drillBodiesMin = TUNE.drillBodies = bodies; }
startDrill();
TUNE.drillBodiesMin = min; TUNE.drillBodies = max;
for (let i = 0; i < TUNE.drillCallT * 60 + 4; i++) update(DT);
}
// kill a body the way every weapon does — through the shared funnel, via the ball's contact test
// A shambler soaks TUNE.shamblerHp splat-quality hits, so this re-arms the ball until the body is
// actually down — the drill keeps the roster's toughness on purpose (Oliver: "some are easier, some
// more difficult to kill"), which means a round is paced by hp as well as by order.
// Returns false when the body is already down — a ball carries through the lane, so a shot at one
// body can take the one standing behind it (see PROJECT.md's open question 1).
const drillKillBib = bib => {
const zb = zombies.find(z => z.bib === bib);
assert.ok(zb, 'bib ' + bib + ' should be on court');
if (!zb) return false;
for (let i = 0; i < 40 && zombies.includes(zb); i++) {
ball.active = true; ball.lastHit = 'P'; ball.y = 1; ball.pow = 1; ball.bounces = 0;
ball.x = zb.x; ball.z = zb.z; ball.vx = ball.vy = ball.vz = 0;
update(DT);
}
assert.ok(!zombies.includes(zb), 'the body should be down');
return true;
};
test('a drill owns the court: every other mode refuses while it runs', () => {
@@ -315,7 +323,8 @@ test('the call is not play: no target and no serving until the umpire finishes',
startDrill();
assert.ok(G.drill.callT > 0, 'the walk-on is running');
assert.equal(drillTarget(), null, 'nothing is a legal target during the call');
assert.equal(zombies.length, TUNE.drillBodies, 'the whole court is placed up front');
assert.ok(zombies.length >= TUNE.drillBodiesMin && zombies.length <= TUNE.drillBodies,
'the whole court is placed up front, ' + TUNE.drillBodiesMin + '..' + TUNE.drillBodies + ' of them');
assert.equal(new Set(zombies.map(z => z.bib)).size, zombies.length, 'every bib on court is distinct');
for (let i = 0; i < TUNE.drillCallT * 60 + 4; i++) update(DT);
assert.ok(drillTarget(), 'and the first target arms when the call ends');
@@ -335,7 +344,7 @@ test('killing in the called order wins the round; the tennis ladder stays out of
});
test('two wrong bodies is a double fault, and the round goes to them', () => {
drillPlaying();
drillPlaying({ bodies: 5 }); // a full court, so a decoy is guaranteed
// a colour the sequence never called is a DECOY — the body you must let walk at you
const decoy = zombies.find(z => !G.drill.seq.includes(z.bib));
assert.ok(decoy, 'setup: the court carries at least one decoy');
@@ -350,8 +359,33 @@ test('two wrong bodies is a double fault, and the round goes to them', () => {
assert.equal(G.drill.won, 0);
});
test('a wrong kill never leaves an un-winnable round', () => {
test('THE AUTO-AIM IS GONE: the shot goes where you point it', () => {
// It used to lock onto the called body wherever the pull pointed, which made the shot unmissable
// and the fault unreachable — Oliver: "no skills involved at all... there is no penalty".
drillPlaying();
const called = G.drill.seq[G.drill.step];
const wrong = zombies.find(z => z.bib !== called);
assert.ok(wrong, 'setup: something else is on court');
const ft = flickTarget(P, { fx: Math.max(-1, Math.min(1, (wrong.x - P.x) / 9)), fy: -0.95, mag: 140 });
assert.notEqual(ft.zt && ft.zt.bib, called, 'pointing away from the called body must not hit it');
});
test('rounds vary: which colours turn up, how many, and where they stand', () => {
startDrill();
const seen = { cols: new Set(), counts: new Set(), shapes: new Set() };
for (let r = 0; r < 40; r++) {
for (const z of zombies) seen.cols.add(z.bib);
seen.counts.add(zombies.length);
seen.shapes.add(G.drill.shape);
G.drill.beat = 0.001; update(DT); update(DT); // deal the next round
}
assert.ok(seen.cols.size > 4, 'the palette is bigger than any one court: ' + seen.cols.size + ' colours seen');
assert.ok(seen.counts.size > 1, 'the body count moves: ' + [...seen.counts]);
assert.ok(seen.shapes.size > 2, 'the placement shape moves: ' + [...seen.shapes]);
});
test('a wrong kill never leaves an un-winnable round', () => {
drillPlaying({ bodies: 5 });
const later = G.drill.seq[G.drill.seq.length - 1]; // kill the LAST call first
drillKillBib(later);
assert.equal(G.drill.faults, 1, 'out of turn is a fault');