Splatter: the pull IS the shot — direction aims, length is power
The ball economy made it harder in the wrong way: waiting for a ballboy is friction, not difficulty, and walking with the finger was one gesture too many for anyone to discover. Both are out — a spent ball hands you a fresh one again, and the thumb only ever does the slingshot. The brake moves into the gesture instead. The pull's LENGTH is now the shot's power and nothing else: under TUNE.zSplatQ it merely staggers, at full draw it's quality 1.0 with the PERFECT flash and hitstop. A lazy rhythm of short pulls can't kill anything, so tempo stops being a strategy. The pull's DIRECTION is a ray, not a landing point, which is the ergonomic fix for having to drag off the bottom of the phone: distance to the target no longer costs drag length. The first shambler standing in the lane eats the ball, whether it's 5 or 21 metres away, and the cone widens with range so distant lanes stay pickable. Full power now sits at 95px of travel. Also gone: the free predictive auto-lock on a directionless aim (zombieTarget returns null with no direction, and a bare hold no longer fires at all). Melee goes back to cleaving the cluster — it's the panic button — with the 1s cooldown left in as the brake. The reticle reads power throughout: cool blue only staggers, hot red kills, the drawn band grows behind you, and the pull clicks the moment it crosses the kill threshold. Verified by simulated touch gestures: 10px cancels, 35px = 0.31 (stagger), 60px = 0.60, 95px = 1.00; the same 60px pull reaches shamblers at 5m, 21m and diagonally; 22 serves in 30s, so the pace is back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2787778ad2
commit
ff973cf6bb
6 files changed
+82
-97
No files matched your search
@@ -396,18 +396,24 @@ export function render() {
|
||||
if (l) drawRing(l.x, l.z, 'rgba(255,255,255,0.85)');
|
||||
}
|
||||
if (G.outMarker) drawRing(G.outMarker.x, G.outMarker.z, G.outMarker.out ? '#ff5050' : '#f8e048');
|
||||
if (G.loose) drawRing(ball.x, ball.z, '#f8e048'); // your ball is down there — go and get it
|
||||
// slingshot serve aim: an angry-birds dotted trajectory from the racket to the aim point;
|
||||
// red = the magnet has a zombie locked, yellow = a free spot on the court
|
||||
// slingshot aim: the pull's direction is the lane, its length is POWER. Everything on screen
|
||||
// reads power, because that's the decision — a dim thin line only staggers, a hot fat one kills.
|
||||
if (G.aim && G.mode === 'splatter' && !G.interlude && (G.phase === 'pretoss' || G.phase === 'toss' || G.phase === 'rally')) {
|
||||
const ft = flickTarget(P, G.aim);
|
||||
const tx = ft.zt ? ft.zt.x : ft.tx, tz = ft.zt ? ft.zt.z : ft.tz;
|
||||
if (tx !== undefined) {
|
||||
const col = ft.zt ? '#ff5050' : '#f8e048';
|
||||
const kills = ft.q >= TUNE.zSplatQ;
|
||||
// cool blue-white = this pull only staggers. It has to stay READABLE on the dark lawn —
|
||||
// the player still needs to see where they're aiming, they just need to know it's weak.
|
||||
const col = !kills ? '#9db4d0' : ft.zt ? '#ff5050' : '#f8e048';
|
||||
const a = proj(P.x, 1.4, P.z), b = proj(tx, 0, tz);
|
||||
ctx.strokeStyle = col; ctx.lineWidth = 2; ctx.setLineDash([3, 5]);
|
||||
// the drawn band: it grows BEHIND you as you pull, so the gesture has a visible cost
|
||||
const bl = proj(P.x - G.aim.fx * ft.f * 3, 1.2, P.z - G.aim.fy * ft.f * 3);
|
||||
ctx.strokeStyle = col; ctx.lineWidth = 1 + ft.f * 2;
|
||||
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(bl.x, bl.y); ctx.stroke();
|
||||
ctx.setLineDash([3, Math.max(2, 7 - ft.f * 5)]); // dashes tighten up as the shot gets hot
|
||||
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
ctx.setLineDash([]); ctx.lineWidth = 1;
|
||||
drawRing(tx, tz, col);
|
||||
if (ft.zt) { const hp = proj(ft.zt.x, 2.2, ft.zt.z); ctx.fillStyle = col; ctx.fillRect(hp.x - 1, hp.y - 4, 3, 3); }
|
||||
}
|
||||
@@ -435,14 +441,14 @@ export function render() {
|
||||
zombies.filter(zb => zb.z > COURT.netZ).sort((a, b) => a.z - b.z).forEach(drawZombie);
|
||||
|
||||
// server idly bouncing the ball while waiting
|
||||
if (G.phase === 'pretoss' && !(G.mode === 'splatter' && (G.loose || G.fetchT > 0))) { // empty-handed = no ball to bounce
|
||||
if (G.phase === 'pretoss') {
|
||||
const sv = score.server === 'P' ? P : O;
|
||||
const bx = sv.x - BOUNCE_IDLE.dx * (sv.side === 'P' ? 1 : -1);
|
||||
const by = Math.abs(Math.sin(G.idleT * BOUNCE_IDLE.tempo)) * BOUNCE_IDLE.apex;
|
||||
drawBall(bx, by, sv.z);
|
||||
}
|
||||
// the match ball (and any triple-serve side balls in flight)
|
||||
if ((ball.active || G.phase === 'over' || G.loose) && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb, ball.flame);
|
||||
if ((ball.active || G.phase === 'over') && !ball.collected) drawBall(ball.x, ball.y, ball.z, true, ball.bomb, ball.flame);
|
||||
for (const e of extras) drawBall(e.x, e.y, e.z);
|
||||
// splatter particles (fire embers ride the same array)
|
||||
for (const pt of parts) {
|
||||
@@ -530,10 +536,8 @@ export function render() {
|
||||
const touch = IS_TOUCH, switchHint = touch ? 'TAP THE RACK TO SWITCH' : '1-5 TO SWITCH';
|
||||
const sel = ['PLAIN', 'BOMB', 'TRIPLE', 'FIRE', 'GUN'][G.weapon];
|
||||
const txt = G.mode === 'splatter'
|
||||
? (G.loose ? 'RUN YOUR BALL DOWN'
|
||||
: G.fetchT > 0 ? 'NEW BALLS PLEASE...'
|
||||
: G.weapon === 4 ? (touch ? 'TAP TO UNLOAD THE GUN' : 'SPACE UNLOADS THE GUN')
|
||||
: G.aim ? 'PULL BACK TO AIM · RELEASE TO FIRE'
|
||||
? (G.weapon === 4 ? (touch ? 'TAP TO UNLOAD THE GUN' : 'SPACE UNLOADS THE GUN')
|
||||
: G.aim ? 'PULL BACK: FURTHER = HARDER'
|
||||
: G.weapon > 0 ? `SERVE FIRES: ${sel} · ${switchHint}`
|
||||
: touch ? 'TAP TO SERVE · PULL BACK TO AIM' : 'SPACE SERVES · ARROWS AIM')
|
||||
: G.serveNum === 2 ? (touch ? 'SECOND SERVE — TAP' : 'SECOND SERVE — SPACE')
|
||||
|
||||
Reference in new issue
Block a user