Fill the screen, and let the swish keep up with a late horde
The portrait canvas is adaptive now. A fixed 270x480 is 9:16 while a modern iPhone is taller, so width bound the fit and ~150 CSS px of screen went unused at the bottom. Height is derived from the device aspect, and TUNE.horizon rides with it — proj() places the court in absolute pixels, so leaving horizon fixed just grew a dead green band between the court and the rack instead of moving the court down. RES 3 to 4 for the extra pixels: an iPhone gives ~1125 device px across and we were rendering 810. That is the first knob to turn back down if the phone stutters. The swish cooldown scales with how swarmed you are — 1.00s at one body, 0.47 at four, 0.22 from fourteen. Oliver died on every game-20 horde because the wave grows with hordeGrow while the panic button stayed a flat second; he chose scaling the melee over shrinking the horde. The first version measured the crowd after meleeSwipe had already killed it, so it counted survivors and barely moved. Winning a duel while wounded lost the weapon popup entirely: showModal never clobbers an open modal, so the heal modal opened first and the tin's was dropped. One popup carries both now. And the mop boy stays off the court unless a stain actually passes the test mopRound uses to pick a target. He was walking on, finding nothing reachable, and walking straight off again. Tennis untouched: 50 hits / 0 whiffs. 45-game soak clean, all four chapters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
49d391dc64
commit
ae64daa0fb
6 files changed
+108
-19
No files matched your search
+48
-1
@@ -5,7 +5,7 @@ everything they specified is shipped, and what still matters from them is record
|
|||||||
long history, `~/.claude/projects/.../memory/splatter-tennis-history.md` is auto-loaded; for the
|
long history, `~/.claude/projects/.../memory/splatter-tennis-history.md` is auto-loaded; for the
|
||||||
detail of any individual change, read the git log.
|
detail of any individual change, read the git log.
|
||||||
|
|
||||||
Last updated 2026-07-30, after Oliver's THIRD playtest round (the readability pass). All playtest
|
Last updated 2026-07-30, after Oliver's FOURTH playtest round (viewport, horde survivability). All playtest
|
||||||
rounds are closed at the bottom. Step 6 (crowd turning, ball-boy catcher) stays PARKED by his
|
rounds are closed at the bottom. Step 6 (crowd turning, ball-boy catcher) stays PARKED by his
|
||||||
decision. **Awaiting his next phone playtest.**
|
decision. **Awaiting his next phone playtest.**
|
||||||
|
|
||||||
@@ -166,6 +166,22 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
|
|||||||
"too much and not situation aware.. they seem random". Each line is now tied to the thing that
|
"too much and not situation aware.. they seem random". Each line is now tied to the thing that
|
||||||
justifies it — a new variant appearing, the court filling up, one crossing your baseline, a game lost,
|
justifies it — a new variant appearing, the court filling up, one crossing your baseline, a game lost,
|
||||||
a tin delivered, a horde, a changeover, a machine, a boss, the trainer, taking a hit.
|
a tin delivered, a horde, a changeover, a machine, a boss, the trainer, taking a hit.
|
||||||
|
- **The portrait viewport ADAPTS to the device, and `TUNE.horizon` must ride with it.** `fitViewport`
|
||||||
|
picks `h = 270 * clamp(innerHeight/innerWidth, 1.6, 2.3)` — a fixed 270x480 is 9:16 while a modern
|
||||||
|
iPhone is ~0.46, so width bound the fit and ~150 CSS px of screen went unused. `proj()` places the
|
||||||
|
court in ABSOLUTE logical px off `horizon`, and the rack/meter hang off `H - ...`, so extra height
|
||||||
|
lands entirely in the apron between them: leaving `horizon` fixed just grew a dead green band.
|
||||||
|
It is computed as `45 + (h - 480) * 0.72` for that reason. `RES` is 4 (was 3) so an iPhone's ~1125
|
||||||
|
device px across is nearly 1:1 with our 1080 — **`RES` is the first knob to turn down if the phone
|
||||||
|
ever stutters**, it is the whole fill-rate budget.
|
||||||
|
Note `#hint` (the desktop key legend) is already `display:none` on `pointer: coarse`; it only
|
||||||
|
overlaps the rack in the desktop test pane, which is not a real bug.
|
||||||
|
- **The swish cooldown scales with how swarmed you are** (`meleeCooldown()` in gameplay.js):
|
||||||
|
`meleeCool / (1 + (near-1) * meleeCrowd)`, floored at `meleeCoolMin`, counting bodies within
|
||||||
|
`meleeCrowdR`. 1 body 1.00 s, 4 → 0.47, 8 → 0.27, 14+ → 0.22. Oliver died on every game-20 horde
|
||||||
|
because the wave grows with `hordeGrow` while the panic button was a flat 1 s; he chose scaling the
|
||||||
|
melee over shrinking the horde. **Measure the crowd BEFORE calling `meleeSwipe`** — it kills
|
||||||
|
everything in reach, so counting after counts the survivors and the cooldown barely moves.
|
||||||
- **`UI` in config.js is the accent palette** (hot/warm/pick/cream/dim/bad/ball). Every HUD colour
|
- **`UI` in config.js is the accent palette** (hot/warm/pick/cream/dim/bad/ball). Every HUD colour
|
||||||
goes through it. Gore reds are deliberately NOT in it — the splatter stays loud.
|
goes through it. Gore reds are deliberately NOT in it — the splatter stays loud.
|
||||||
- **`F(size, bold)` in render.js builds every canvas font string.** Bare `monospace` resolves to
|
- **`F(size, bold)` in render.js builds every canvas font string.** Bare `monospace` resolves to
|
||||||
@@ -329,6 +345,37 @@ Known generator quirks, all still true:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# CLOSED — Oliver's fourth playtest (2026-07-30)
|
||||||
|
|
||||||
|
### The game did not use the full height of his iPhone
|
||||||
|
Fixed by making the portrait logical height adaptive and letting `horizon` ride with it — see the
|
||||||
|
invariant above. On a 375x812 pane: logical 270x585 (was 270x480), canvas CSS 375x812, **100% of both
|
||||||
|
axes**. `RES` 3 -> 4 for the "extra pixels" he asked about.
|
||||||
|
|
||||||
|
### Dying on every game-20 horde
|
||||||
|
The swish cooldown now scales with crowding — see the invariant. Note the ordering trap recorded
|
||||||
|
there; the first implementation measured the crowd after the swipe had already killed it, so the
|
||||||
|
cooldown only moved once ~14 bodies were present.
|
||||||
|
|
||||||
|
### The weapon message was lost when the heal popup fired
|
||||||
|
`showModal` deliberately never clobbers an open modal, so winning a duel while wounded opened the
|
||||||
|
heal modal and the weapon modal was silently dropped. `deliverTin(healLine)` now folds the trainer
|
||||||
|
into the WEAPON popup as an extra line, so there is one popup carrying both. An unwounded duel win
|
||||||
|
still shows the plain weapon popup.
|
||||||
|
|
||||||
|
### The mop boy walked out with nothing to mop
|
||||||
|
`sendBoy` now returns early when `mop` is set and no stain passes the same test `mopRound` uses to
|
||||||
|
pick a target (`a >= 0.15 && z >= COURT.netZ - 2`) — he was walking on, finding nothing reachable,
|
||||||
|
and walking straight off. This needs `state.js` to read `gore`, which adds a **third runtime-only
|
||||||
|
import cycle** (state <-> zombies) alongside input<->gameplay and zombies<->gameplay; it is safe for
|
||||||
|
the same reason, `gore` is only touched inside `sendBoy` at runtime, never during module evaluation.
|
||||||
|
|
||||||
|
### Regression net
|
||||||
|
30 tapped tennis points = 50 hits / 0 whiffs. 45-game soak: no exceptions, all four chapters,
|
||||||
|
`mgT` drains, gore 40 / piles 30 / bodies capped at 34.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# CLOSED — Oliver's third playtest: audio, hit timing, readability (2026-07-30)
|
# CLOSED — Oliver's third playtest: audio, hit timing, readability (2026-07-30)
|
||||||
|
|
||||||
### The umpire talked too much and at random
|
### The umpire talked too much and at random
|
||||||
|
|||||||
@@ -163,6 +163,13 @@ export const TUNE = {
|
|||||||
backstopKeep: 0.55, // pace a ball keeps after the rebound — still lethal, slower
|
backstopKeep: 0.55, // pace a ball keeps after the rebound — still lethal, slower
|
||||||
backstopPop: 1.8, // m/s of lift off the fence, so the rebound travels back instead of dying at the net posts
|
backstopPop: 1.8, // m/s of lift off the fence, so the rebound travels back instead of dying at the net posts
|
||||||
meleeCool: 1.0, // s between racket swishes — melee is the panic button, not the default
|
meleeCool: 1.0, // s between racket swishes — melee is the panic button, not the default
|
||||||
|
meleeCrowd: 0.38, // ...but the panic button PANICS. The cooldown shortens with how many bodies are
|
||||||
|
// actually on top of you, so a late horde stops outrunning your only close-range
|
||||||
|
// answer: cool = meleeCool / (1 + (near-1)*meleeCrowd), floored at meleeCoolMin.
|
||||||
|
// 1 body 1.00s, 4 bodies 0.47s, 8 bodies 0.28s.
|
||||||
|
meleeCoolMin: 0.22,
|
||||||
|
meleeCrowdR: 3.4, // m — the radius that counts as "on top of you" for that scaling (> meleeRange,
|
||||||
|
// so the swish speeds up as they close in, not only once they are already on you)
|
||||||
zPierce: 0.75, // pace a ball keeps after a splatter — hot balls plow through lined-up pairs
|
zPierce: 0.75, // pace a ball keeps after a splatter — hot balls plow through lined-up pairs
|
||||||
// The schedule calls the DUEL, not the tin: weapons are only ever won, but a duel comes round
|
// The schedule calls the DUEL, not the tin: weapons are only ever won, but a duel comes round
|
||||||
// on its own so there is always a chance at one. Fires off the next hand-cleared wave.
|
// on its own so there is always a chance at one. Fires off the next hand-cleared wave.
|
||||||
|
|||||||
+24
-10
@@ -411,7 +411,7 @@ const WDESC = { // what the tin you just won actually does — one line, shown o
|
|||||||
FIRE: ['KILLS ON ANY TOUCH', 'AND SETS THE OTHERS ALIGHT'],
|
FIRE: ['KILLS ON ANY TOUCH', 'AND SETS THE OTHERS ALIGHT'],
|
||||||
GUN: ['HOLD THE LINE AND SPRAY', 'A BURST, NOT A SERVE'],
|
GUN: ['HOLD THE LINE AND SPRAY', 'A BURST, NOT A SERVE'],
|
||||||
};
|
};
|
||||||
export function deliverTin() {
|
export function deliverTin(healLine = null) {
|
||||||
const w = G.deliveries++ % 4, add = k => Math.min(TUNE.wCap, k);
|
const w = G.deliveries++ % 4, add = k => Math.min(TUNE.wCap, k);
|
||||||
if (w === 0) G.bombs = add(G.bombs + TUNE.bombMax);
|
if (w === 0) G.bombs = add(G.bombs + TUNE.bombMax);
|
||||||
else if (w === 1) G.triples = add(G.triples + TUNE.tinTriples);
|
else if (w === 1) G.triples = add(G.triples + TUNE.tinTriples);
|
||||||
@@ -422,8 +422,11 @@ export function deliverTin() {
|
|||||||
aside('ump_new_balls_dark', 2.0); // "...and a new opponent, apparently."
|
aside('ump_new_balls_dark', 2.0); // "...and a new opponent, apparently."
|
||||||
|
|
||||||
const name = SLOTS[w + 1];
|
const name = SLOTS[w + 1];
|
||||||
showModal({ title: name, body: WDESC[name], icon: ['w_bomb', 'w_triple', 'w_fire', 'w_gun'][w],
|
// healLine folds the trainer's visit INTO this popup. `showModal` deliberately never clobbers an
|
||||||
iconH: 40, color: UI.warm });
|
// open modal (a kill cascade can try to open two in one frame), so when both happened the heal
|
||||||
|
// modal opened first and the weapon one was lost entirely — Oliver: "the weapon message is lost".
|
||||||
|
showModal({ title: name, body: healLine ? [...WDESC[name], '', healLine] : WDESC[name],
|
||||||
|
icon: ['w_bomb', 'w_triple', 'w_fire', 'w_gun'][w], iconH: 40, color: UI.warm });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- the interlude: order is restored, one sudden-death point vs the racket zombie.
|
// ---- the interlude: order is restored, one sudden-death point vs the racket zombie.
|
||||||
@@ -496,17 +499,14 @@ export function resolveInterlude(side) {
|
|||||||
cheer(3, 0.7);
|
cheer(3, 0.7);
|
||||||
// winning the duel patches you up. THE reason to want a duel rather than merely tolerate it —
|
// winning the duel patches you up. THE reason to want a duel rather than merely tolerate it —
|
||||||
// it is the only healing in the game, and blood is the only thing you cannot buy back otherwise.
|
// it is the only healing in the game, and blood is the only thing you cannot buy back otherwise.
|
||||||
|
let healLine = null;
|
||||||
if (G.hp < G.hpMax) {
|
if (G.hp < G.hpMax) {
|
||||||
const before = G.hp;
|
const before = G.hp;
|
||||||
G.hp = Math.min(G.hpMax, G.hp + TUNE.healPerDuel);
|
G.hp = Math.min(G.hpMax, G.hp + TUNE.healPerDuel);
|
||||||
// a real popup, not a flash: getting patched up is the only way blood comes back, so it deserves
|
|
||||||
// to stop the world for a beat and say so plainly. The trainer came on. It's still tennis.
|
|
||||||
voice('ump_trainer', 0.2, 0.95);
|
voice('ump_trainer', 0.2, 0.95);
|
||||||
showModal({ title: 'WOUNDS TREATED', color: UI.pick, icon: 'x_cross', iconH: 34,
|
healLine = '+ WOUNDS TREATED · NOW ' + G.hp + '/' + G.hpMax;
|
||||||
body: ['THE TRAINER PATCHED YOU UP',
|
|
||||||
(G.hp - before) + ' OF ' + G.hpMax + ' BACK · NOW ' + G.hp + '/' + G.hpMax] });
|
|
||||||
}
|
}
|
||||||
deliverTin(); // win the point, win the next tin — on the spot
|
deliverTin(healLine); // win the point, win the next tin — and the trainer, in ONE popup
|
||||||
} else {
|
} else {
|
||||||
voice('game_opponent', 0.4); aww(0.85); // lost the sudden-death duel
|
voice('game_opponent', 0.4); aww(0.85); // lost the sudden-death duel
|
||||||
score.games[1]++; G.runGames++; // it goes on the scoreboard
|
score.games[1]++; G.runGames++; // it goes on the scoreboard
|
||||||
@@ -793,12 +793,26 @@ export function dragServe(aim) {
|
|||||||
hitServe(P, pullQuality(aim.mag), aim); // a full pull is quality 1.0 — PERFECT, hitstop and all
|
hitServe(P, pullQuality(aim.mag), aim); // a full pull is quality 1.0 — PERFECT, hitstop and all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The swish cooldown, scaled by how crowded you are. Oliver died on every game-20 horde: the wave
|
||||||
|
// grows with hordeGrow but the panic button was a flat 1 s, so past a certain density it simply could
|
||||||
|
// not keep up. He picked "melee fires more frequently with horde density" over shrinking the horde.
|
||||||
|
function meleeCooldown() {
|
||||||
|
let near = 0;
|
||||||
|
for (const zb of zombies) if (zb && Math.hypot(zb.x - P.x, zb.z - P.z) < TUNE.meleeCrowdR) near++;
|
||||||
|
return Math.max(TUNE.meleeCoolMin, TUNE.meleeCool / (1 + Math.max(0, near - 1) * TUNE.meleeCrowd));
|
||||||
|
}
|
||||||
|
|
||||||
export function keyPressed(code) {
|
export function keyPressed(code) {
|
||||||
if (code !== 'Space') return;
|
if (code !== 'Space') return;
|
||||||
audio(); // unlock on first gesture
|
audio(); // unlock on first gesture
|
||||||
// a swish cleaves adjacent shamblers — but it's on a cooldown now: the panic button, not the
|
// a swish cleaves adjacent shamblers — but it's on a cooldown now: the panic button, not the
|
||||||
// default attack. A whiff costs nothing (nothing was in range), a connect locks it for meleeCool.
|
// default attack. A whiff costs nothing (nothing was in range), a connect locks it for meleeCool.
|
||||||
if (G.mode === 'splatter' && G.meleeT <= 0 && meleeSwipe(P.x, P.z, TUNE.meleeRange)) G.meleeT = TUNE.meleeCool;
|
if (G.mode === 'splatter' && G.meleeT <= 0) {
|
||||||
|
// measure the crowd BEFORE the swipe — it kills everything in reach, so counting afterwards
|
||||||
|
// counts the SURVIVORS and the cooldown barely moved no matter how swarmed you were
|
||||||
|
const cool = meleeCooldown();
|
||||||
|
if (meleeSwipe(P.x, P.z, TUNE.meleeRange)) G.meleeT = cool;
|
||||||
|
}
|
||||||
// AN INCOMING SHOT OWNS THE TAP. The parry used to live behind the `phase === 'rally'` branch at the
|
// AN INCOMING SHOT OWNS THE TAP. The parry used to live behind the `phase === 'rally'` branch at the
|
||||||
// bottom of this function — and in splatter you are in `pretoss` almost all of the time, so a tap
|
// bottom of this function — and in splatter you are in `pretoss` almost all of the time, so a tap
|
||||||
// aimed at a machine's shot tossed the serve ball instead and the return was, in practice,
|
// aimed at a machine's shot tossed the serve ball instead and the return was, in practice,
|
||||||
|
|||||||
+1
-1
@@ -60,6 +60,6 @@
|
|||||||
<p class="start">TAP TO START</p>
|
<p class="start">TAP TO START</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js?v=43"></script>
|
<script type="module" src="main.js?v=44"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -39,13 +39,22 @@ const hexA = (h, a) => `rgba(${parseInt(h.slice(1, 3), 16)},${parseInt(h.slice(3
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- camera ----
|
// ---------------------------------------------------------------- camera ----
|
||||||
const IS_TOUCH = matchMedia('(pointer: coarse)').matches; // hoisted: a matchMedia call per frame churns GC
|
const IS_TOUCH = matchMedia('(pointer: coarse)').matches; // hoisted: a matchMedia call per frame churns GC
|
||||||
export const RES = 3; // physical pixels per logical pixel — 3x so phone screens downscale (crisp) instead of upscaling (soft)
|
// Physical pixels per logical pixel. On an iPhone the canvas ends up ~390 CSS px wide = 1170 DEVICE
|
||||||
|
// px, so RES 3 (810) was still being upscaled 1.44x by the browser. 4 gets us to 1080, near 1:1.
|
||||||
|
// THIS IS THE FIRST KNOB TO TURN DOWN if the phone ever stutters: it is the whole fill-rate budget.
|
||||||
|
export const RES = 4;
|
||||||
export let W = 480, H = 270;
|
export let W = 480, H = 270;
|
||||||
|
|
||||||
export function fitViewport() { // landscape 480x270, portrait 270x480 with a longer lens
|
export function fitViewport() { // landscape 480x270; portrait 270 wide and as TALL as the phone is
|
||||||
const portrait = innerHeight > innerWidth;
|
const portrait = innerHeight > innerWidth;
|
||||||
const [w, h] = portrait ? [270, 480] : [480, 270];
|
// Portrait height ADAPTS to the device. A fixed 270x480 is 9:16, but a modern iPhone is taller
|
||||||
if (cv.width !== w * RES) {
|
// (~390x844 = 0.46), so width bound the fit and ~150 CSS px of screen went unused at the bottom.
|
||||||
|
// The court is positioned by proj() off TUNE.horizon in ABSOLUTE logical px, and the rack/meter hang
|
||||||
|
// off `H - ...`, so extra height simply widens the apron between the court and the rack — nothing
|
||||||
|
// has to be re-laid-out. Clamped so a freak aspect can't stretch the apron absurdly.
|
||||||
|
const tall = Math.round(270 * Math.max(1.6, Math.min(2.3, innerHeight / Math.max(1, innerWidth))));
|
||||||
|
const [w, h] = portrait ? [270, tall] : [480, 270];
|
||||||
|
if (cv.width !== w * RES || cv.height !== h * RES) {
|
||||||
W = w; H = h;
|
W = w; H = h;
|
||||||
cv.width = w * RES; cv.height = h * RES;
|
cv.width = w * RES; cv.height = h * RES;
|
||||||
// flatter lens: camZ pushed back (config 42, was 36) with camF scaled to keep the PLAYER
|
// flatter lens: camZ pushed back (config 42, was 36) with camF scaled to keep the PLAYER
|
||||||
@@ -60,7 +69,11 @@ export function fitViewport() { // landscape 480x270, portrait 270x480 with a lo
|
|||||||
TUNE.camZ = portrait ? 48 : 42;
|
TUNE.camZ = portrait ? 48 : 42;
|
||||||
TUNE.camY = portrait ? 11 : 9;
|
TUNE.camY = portrait ? 11 : 9;
|
||||||
TUNE.camF = portrait ? 760 : 406;
|
TUNE.camF = portrait ? 760 : 406;
|
||||||
TUNE.horizon = portrait ? 45 : 40;
|
// horizon RIDES the adaptive height. proj() places the court in ABSOLUTE logical px off this, so
|
||||||
|
// leaving it fixed while h grew just dumped every extra pixel into a dead green apron between the
|
||||||
|
// court and the rack. 0.72 of the extra height goes to pushing the court down (the rest becomes
|
||||||
|
// more stand above it), which keeps the apron about where it was at the old fixed 480.
|
||||||
|
TUNE.horizon = portrait ? Math.round(45 + (h - 480) * 0.72) : 40;
|
||||||
buildCrowd();
|
buildCrowd();
|
||||||
}
|
}
|
||||||
ctx.imageSmoothingEnabled = false; // resets on canvas resize
|
ctx.imageSmoothingEnabled = false; // resets on canvas resize
|
||||||
@@ -81,7 +94,7 @@ export function proj(x, y, z) {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- sprites ----
|
// ---------------------------------------------------------------- sprites ----
|
||||||
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
|
// assets/<name>.png; game falls back to code-drawn figures until sprites load.
|
||||||
const SPRV = 43; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
|
const SPRV = 44; // asset cache-buster — bump with main.js?v=N (see the img.src note below)
|
||||||
const SPR = {};
|
const SPR = {};
|
||||||
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_swing1','p_swing2',
|
for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','p_run1','p_run2','p_swing1','p_swing2',
|
||||||
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2',
|
'o_idle1','o_idle2','o_bounce','o_bounce2','o_toss','o_smash','o_run1','o_run2','o_swing1','o_swing2',
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// All mutable game state in one place, plus the point-reset that re-arms it.
|
// All mutable game state in one place, plus the point-reset that re-arms it.
|
||||||
|
|
||||||
import { HOME, BOY_ENTER, TUNE } from './config.js';
|
import { HOME, BOY_ENTER, TUNE, COURT } from './config.js';
|
||||||
|
import { gore } from './zombies.js'; // runtime-only cycle (zombies imports state): sendBoy reads gore
|
||||||
|
// to decide whether a mopping round is worth walking out for
|
||||||
|
|
||||||
export const P = { x: 1.3, z: 22.8, side: 'P', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
|
export const P = { x: 1.3, z: 22.8, side: 'P', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
|
||||||
export const O = { x: -1.3, z: 1.0, side: 'O', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
|
export const O = { x: -1.3, z: 1.0, side: 'O', pose: 'idle', poseT: 0, swingT: -1, facing: 1, runT: 0 };
|
||||||
@@ -112,8 +114,14 @@ export function setupPoint() {
|
|||||||
// fetch a dead ball — or, in splatter, come out to CLEAN: a different sprite sheet and a slower
|
// fetch a dead ball — or, in splatter, come out to CLEAN: a different sprite sheet and a slower
|
||||||
// walk, because mopping is not sprinting. Mopping is the DEFAULT once the gore starts; the plain
|
// walk, because mopping is not sprinting. Mopping is the DEFAULT once the gore starts; the plain
|
||||||
// running fetch-boy survives for tennis mode, where there is nothing on the grass but a ball.
|
// running fetch-boy survives for tennis mode, where there is nothing on the grass but a ball.
|
||||||
|
// Is there a stain worth walking out for? Must match mopRound's own target test, or he walks on,
|
||||||
|
// finds nothing he can reach, and walks straight off again — which is exactly what Oliver saw:
|
||||||
|
// "the mopboy sometimes moves over the court without anything to mop".
|
||||||
|
const mopWorthIt = () => gore.some(d => d.a >= 0.15 && d.z >= COURT.netZ - 2);
|
||||||
|
|
||||||
export function sendBoy(tx = ball.x, tz = ball.z, mop = G.mode === 'splatter') {
|
export function sendBoy(tx = ball.x, tz = ball.z, mop = G.mode === 'splatter') {
|
||||||
if (G.interlude) return; // the sudden-death duel is clean tennis: nobody mops through it
|
if (G.interlude) return; // the sudden-death duel is clean tennis: nobody mops through it
|
||||||
|
if (mop && !mopWorthIt()) return; // no job, no boy
|
||||||
if (tz < -3 || tz > 27) return; // target left the field lengthwise, let it go
|
if (tz < -3 || tz > 27) return; // target left the field lengthwise, let it go
|
||||||
// ONE boy, one slot. Every kill used to call this and teleport him back to the wing mid-stride,
|
// ONE boy, one slot. Every kill used to call this and teleport him back to the wing mid-stride,
|
||||||
// which is why he flickered in and out: a kill streak restarted him several times a second.
|
// which is why he flickered in and out: a kill streak restarted him several times a second.
|
||||||
|
|||||||
Reference in new issue
Block a user