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
@@ -3,9 +3,10 @@
|
||||
// functions at runtime, never during module evaluation.
|
||||
|
||||
import { cv } from './dom.js';
|
||||
import { audio } from './audio.js';
|
||||
import { audio, pock } from './audio.js';
|
||||
import { TUNE } from './config.js';
|
||||
import { G, score } from './state.js';
|
||||
import { keyPressed, dragServe, selectWeapon, serveToss } from './gameplay.js';
|
||||
import { keyPressed, dragServe, selectWeapon, serveToss, pullQuality } from './gameplay.js';
|
||||
import { rackHit } from './render.js';
|
||||
import { togglePanel } from './panel.js';
|
||||
|
||||
@@ -41,10 +42,8 @@ export function takeFlickAim(maxAgeMs = 450) {
|
||||
|
||||
// the two-stage serve: a drag that STARTS at a serveable pretoss aims a visible reticle
|
||||
// (G.aim, drawn by render); release fires the serve there, dragging back home cancels.
|
||||
// With no ball in hand this is false, so the same drag steers your legs instead — which is
|
||||
// exactly what you need then: walking your loose ball down.
|
||||
const canAimServe = () =>
|
||||
G.mode === 'splatter' && G.phase === 'pretoss' && !G.loose && !(G.fetchT > 0)
|
||||
G.mode === 'splatter' && G.phase === 'pretoss'
|
||||
&& score.server === 'P' && G.weapon !== 4 // GUN pops on tap, no reticle
|
||||
&& !G.interlude && !G.horde && !(G.mgT > 0) && !G.changeover; // the duel serves like tennis: no reticle
|
||||
|
||||
@@ -62,7 +61,7 @@ cv.addEventListener('touchstart', e => {
|
||||
if (!stick) {
|
||||
stick = { id: t.identifier, x0: t.clientX, y0: t.clientY, t0: performance.now(),
|
||||
moved: false, aimable: canAimServe() }; // context locked at touch start — a movement drag never morphs into a serve
|
||||
// the reticle appears the moment the thumb lands: mag 0 = auto-target the most advanced
|
||||
// the reticle appears the moment the thumb lands, but mag 0 aims at nothing — you have to pull
|
||||
if (stick.aimable) G.aim = { fx: 0, fy: 0, mag: 0 };
|
||||
else steerPX = (t.clientX - r.left) / r.width * 2 - 1;
|
||||
}
|
||||
@@ -77,10 +76,14 @@ cv.addEventListener('touchmove', e => {
|
||||
if (t.identifier !== stick.id) continue;
|
||||
const dx = t.clientX - stick.x0, dy = t.clientY - stick.y0, dist = Math.hypot(dx, dy);
|
||||
if (dist > DEAD) stick.moved = true;
|
||||
if (stick.aimable) { // slingshot: PULL BACK, launch forward — aim is the inverse of the drag (home = auto-target)
|
||||
if (stick.aimable) { // slingshot: PULL BACK, launch forward — the shot is the inverse of the drag
|
||||
if (dist > DEAD) {
|
||||
if (!stick.tossed) { serveToss(); stick.tossed = true; } // the pull kicks off the toss animation
|
||||
G.aim = { fx: -dx / dist, fy: -dy / dist, mag: dist };
|
||||
// the moment the pull is strong enough to actually splatter, it clicks — you can feel
|
||||
// where the threshold is without watching the reticle
|
||||
const hot = pullQuality(dist) >= TUNE.zSplatQ;
|
||||
if (hot !== !!stick.hot) { stick.hot = hot; if (hot) pock(0.45, 1250); }
|
||||
} else G.aim = { fx: 0, fy: 0, mag: 0 };
|
||||
continue;
|
||||
}
|
||||
@@ -98,10 +101,10 @@ const endTouch = e => {
|
||||
const dt = performance.now() - stick.t0;
|
||||
const dx = t.clientX - stick.x0, dy = t.clientY - stick.y0, dist = Math.hypot(dx, dy);
|
||||
const aim = G.aim; G.aim = null; steerPX = null;
|
||||
if (stick.aimable && (stick.moved || dt >= 250)) { // two-stage release
|
||||
// a pure HOLD (never moved) fires at the auto-target; a drag fires at the reticle;
|
||||
// a drag walked back home cancels — the tin stays in stock
|
||||
if (aim && (!stick.moved || dist > DEAD * 1.5)) dragServe(aim);
|
||||
if (stick.aimable && stick.moved) { // slingshot release
|
||||
// only a real pull fires — walked back under pullMin, it cancels and nothing is spent.
|
||||
// (a bare hold used to fire at a free auto-target; that was the whole exploit)
|
||||
if (aim && dist >= TUNE.pullMin) dragServe(aim);
|
||||
}
|
||||
else if (!stick.moved && dt < 250) keyPressed('Space'); // tap
|
||||
else if (dt < 300 && dist > 28) { // flick = aimed swing
|
||||
|
||||
Reference in new issue
Block a user