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:
1 parent
22df96cc1d
commit
ef8a413031
10 files changed
+173
-64
No files matched your search
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user