The slice stops steering: each kill strikes the ball once, then physics
Oliver, watching a sliced ball circle a shambler it had only wounded: "an orbiting tennis ball midair, nobody knows what is going on." He is right, and not only about that shot. Every ball in this game flies a parabola you can read, so the one that thinks reads as broken even on the shots where it works. The hook was the wrong primitive, not a hook with two bugs in it. So there is no hook. At the instant of a kill the ball is struck again — ONE velocity out of solveShot, the same solver the serve uses, aimed at the next body's chest — and from the next frame it is an ordinary ball under ordinary gravity. It cannot orbit, cannot hover and cannot outlive its shot, because after the strike nothing about the slice is still running. That also deletes the wound bug rather than fixing it. Six sites spend the ball (lastHit = 'X') and not one of them cleared the hook, so a ball that merely WOUNDED its target was left steering at 30% pace toward a point that body had already walked off — the loop he saw. With no persistent state there is nothing to clear: a ball spent on a body it failed to kill just drops, like every other spent ball. The hop draws its own band, white, so a chain leaves the same chalk streak every other shot leaves. sliceTurn and sliceHookT are gone with the steering that needed them; sliceArc stays, no longer as a turn-radius limit but to keep a carom from doubling back down-court, which reads as a stray deflection rather than as a cut. Net 24 lines lighter than the thing it replaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e11ad30a63
commit
5de42648ec
3 files changed
+50
-74
No files matched your search
+23
-34
@@ -161,7 +161,7 @@ function doHit(ch, quality, aim) {
|
||||
ball.pow = quality; // carried pace: zombies splatter only above zSplatQ
|
||||
ball.kills = 0; // fresh shot: multi-kill streak restarts
|
||||
ball.bomb = false; ball.flame = false; ball.wid = 'ball'; // only a consuming serve arms these; any other hit clears them
|
||||
ball.chain = 0; ball.hook = null; // ...and the slice dies with the shot that earned it
|
||||
ball.chain = 0; // ...and the slice dies with the shot that earned it
|
||||
ball.praise = null; // a re-hit is a new shot: a stale armed word must not pop on ITS impact
|
||||
if (ch.side === 'P') { // shot marker: where this will land — red means it's going out
|
||||
// TENNIS ONLY. It answers "did that land in?", and splatter does not ask: `out` could never be
|
||||
@@ -266,7 +266,7 @@ export function previewServe(aim) {
|
||||
// ...and WHETHER THIS ONE SLICES. The perfect window was invisible until now: quality saturates
|
||||
// at pullFull, so an overpull and a perfect pull drew an identical band and you were being asked
|
||||
// to hit a target the game never showed you. Now the band goes chalk-white inside it. Plain balls
|
||||
// only, because only a plain ball hooks (see hitServe).
|
||||
// only, because only a plain ball slices (see hitServe).
|
||||
return shotBand(c.x, c.y, c.z, v, g, st.tx, st.tz, st.ty, !!st.ft.zt, q,
|
||||
pullPerfect(aim.mag) && G.weapon === 0 && !G.temp);
|
||||
}
|
||||
@@ -279,6 +279,26 @@ function shotBand(x0, y0, z0, v, g, tx, tz, ty, zt, q, pf = false) {
|
||||
return { x0, y0, z0, v, g, tx, tz, ty, zt, q, pf, f: qualityFrac(q), T, t: 0 };
|
||||
}
|
||||
|
||||
// ---- THE SLICE CAROM. A sliced ball does not steer. At the instant it kills something it is
|
||||
// STRUCK AGAIN — one new velocity out of the same solver a serve uses, aimed at the next body's
|
||||
// chest — and from the next frame it is an ordinary ball under ordinary gravity.
|
||||
// This replaces a hook that turned toward its target a little more every frame, and the reason is
|
||||
// not that the hook was buggy twice (it was) but that it was the wrong thing to build: EVERY other
|
||||
// ball in this game flies a parabola you can read, so the one that thinks reads as a glitch even on
|
||||
// the shots where it works. Oliver, on watching one circle a wounded shambler: "an orbiting tennis
|
||||
// ball midair, nobody knows what is going on." A carom cannot orbit, cannot hover and cannot
|
||||
// outlive its shot, because after this function returns there is no slice left running anywhere —
|
||||
// which is also why the wound case needed no fix of its own. A ball spent on a body it failed to
|
||||
// kill simply drops, like every other spent ball.
|
||||
// The hop draws its own band, white, so the chain leaves a chalk arc from the corpse to the next
|
||||
// one — the same object the aim preview and the serve already use, given a target of its own.
|
||||
export function sliceCarom(tgt, speed) {
|
||||
const g = ball.g || TUNE.gravity, ty = zombieH(tgt) * Z_CHEST;
|
||||
const v = solveShot(ball.x, ball.y, ball.z, tgt.x, tgt.z, speed, g, ty);
|
||||
ball.vx = v.vx; ball.vy = v.vy; ball.vz = v.vz;
|
||||
G.trail = shotBand(ball.x, ball.y, ball.z, v, g, tgt.x, tgt.z, ty, true, 1, true);
|
||||
}
|
||||
|
||||
// `perfect` is passed SEPARATELY from `quality` because the two serves earn it differently: the toss
|
||||
// ritual earns it on a 45 ms timing window, while the slingshot's quality saturates at pullFull and
|
||||
// so can never fail the old `quality > 0.999` test once you have pulled far enough.
|
||||
@@ -319,10 +339,9 @@ function hitServe(sv, quality, aim = null, perfect = quality > 0.999) {
|
||||
const wid = G.interlude || (wsel === 0 && !G.temp) ? 'ball' : slotId(wsel), armed = slotStock(wsel) > 0;
|
||||
ball.wid = armed ? wid : 'ball'; // an unstocked tin throws a plain ball — file the kills that way
|
||||
// THE SLICE. A perfect serve with a PLAIN ball is cut: it hooks body to body (see splatter).
|
||||
// Plain only — a bomb, a flame ball and a laser already have an identity, and stacking a hook
|
||||
// Plain only — a bomb, a flame ball and a laser already have an identity, and stacking a carom
|
||||
// on top of one would read as the tin misbehaving rather than as something you did.
|
||||
ball.chain = perfect && ball.wid === 'ball' ? TUNE.sliceHops : 0;
|
||||
ball.hook = null;
|
||||
// THE LASER fires instead of the ball, down the lane this serve was aimed at. Everything else
|
||||
// about the gesture is untouched — the pull, the aim, the release — which is the whole point of it.
|
||||
if (wid === 'laser' && armed) {
|
||||
@@ -1573,36 +1592,6 @@ export function update(dt) {
|
||||
t: 0.5 + Math.random() * 0.4, smoke: true });
|
||||
}
|
||||
ball.vy -= (ball.g || TUNE.gravity) * dt;
|
||||
// THE HOOK. splatter() points a sliced ball at its next body; this is the swing onto it, and it
|
||||
// is the whole look of the weapon — a ball that BENDS through a crowd instead of ricocheting off
|
||||
// it at an angle. It flies flat while hooking (a hop of a few metres would otherwise drop half a
|
||||
// metre and pass under the next chest), and it sheds a one-pixel chalk thread so the curve is
|
||||
// still on screen a beat after the ball has gone. The thread is the beacon's `fine` particle:
|
||||
// 1 px, so the ball stays the bright thing and the line reads as the cut, not as a second ball.
|
||||
if (ball.hook) {
|
||||
const dx = ball.hook.x - ball.x, dz = ball.hook.z - ball.z;
|
||||
ball.hook.t -= dt;
|
||||
// arrived, or out of time. The timer is not a polish detail: without it a ball that cannot
|
||||
// close on a walking body steers forever, and with vy pinned at 0 nothing else ever ends it.
|
||||
if (dx * dx + dz * dz < 0.09 || ball.hook.t <= 0) ball.hook = null; // gravity has it back
|
||||
else {
|
||||
const sp = Math.hypot(ball.vx, ball.vz) || 1;
|
||||
const cur = Math.atan2(ball.vx, ball.vz), want = Math.atan2(dx, dz);
|
||||
const da = ((want - cur + Math.PI * 3) % (Math.PI * 2)) - Math.PI; // shortest way round
|
||||
// THE RATE COMES FROM THE GEOMETRY. An arc that starts along the current heading and passes
|
||||
// through a body at distance d, bearing da, has radius d / (2 sin da) — so the ball must be
|
||||
// able to turn at 2 v sin(da) / d or it can never arrive, however long it steers. sliceTurn
|
||||
// is only the floor (turning FASTER than needed is safe: the aim is re-solved every frame,
|
||||
// so it simply finishes the turn early and flies straight in). Solved live rather than
|
||||
// frozen at the kill, because the body it is hooking to is walking while the ball travels.
|
||||
const d = Math.max(0.6, Math.hypot(dx, dz)); // floored: d -> 0 would demand an infinite rate
|
||||
const w = Math.max(TUNE.sliceTurn, 2 * sp * Math.abs(Math.sin(da)) / d);
|
||||
const a = cur + Math.sign(da) * Math.min(Math.abs(da), w * dt);
|
||||
ball.vx = Math.sin(a) * sp; ball.vz = Math.cos(a) * sp; ball.vy = 0;
|
||||
parts.push({ x: ball.x, y: ball.y, z: ball.z, vx: 0, vy: 0, vz: 0,
|
||||
t: 0.28, fine: true, spark: '#f2f2e6' });
|
||||
}
|
||||
}
|
||||
const oldZ = ball.z;
|
||||
ball.x += ball.vx * dt; ball.y += ball.vy * dt; ball.z += ball.vz * dt;
|
||||
// net: plane at netZ, only during rally flight
|
||||
|
||||
Reference in new issue
Block a user