A charge that passes, two strides that actually stride, a door in the corner
THE LEGS. Measured as lower-third silhouette difference between the two stride frames: the groundskeeper, whose walk has always been right, is 0.80. The seller was 0.017 — the same legs twice, which is why he hopped along on one of them. Both sheets regenerated with the stride as the entire subject (one foot planted ahead, the other trailing, and they SWAP): seller 0.42, champion 0.67. Three things the champion's re-rolls proved, all of them the same lesson — THE REFERENCE IS WHAT IT COPIES. Anchored on his own one-legged frame it drew that pose again. Given a working walk from another boss alongside him "for the legs only" it blended the two and returned a slim side-on walker, then a pot-bellied man in cream. One character in the reference, one character out — and pick the keeper by EYE, because the metric happily crowned the side view. THE CHARGE. It rushed, then withdrew a fraction of what it covered, so it ended the fight parked on top of you: dodge or die every two seconds with no window to hit it, and nothing but a hitscan laser quick enough to use the window there was — Oliver, "they come too close, they seem unbeatable". A rush now RETREATS to its line afterwards, and to keep it from being toothless the other way (from the band it ran out of clock 8 m short) the rush is TIMED rather than paced: it covers whatever ground the retreat left in bossChargeT, so every charge arrives and every charge has the same readable run-up. Measured over 30 s: five rushes, each one reaching you, 13% of the fight in your face instead of all of it. And the back room moved off the whole splash onto an invisible box in the TOP- LEFT CORNER. Five taps in the middle of the screen — which is where your thumb goes on the way to PLAY MATCH — now do nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No files matched your search
@@ -1089,5 +1089,31 @@
|
|||||||
"check": "any",
|
"check": "any",
|
||||||
"state": true,
|
"state": true,
|
||||||
"subject": "The reference shows the SAME boss in four states: undamaged, then three worsening stages of damage. Draw all FOUR of those exact states again, in the same order, same size, same costume, same wounds — but each one caught MID-THROW: the whole body wound up and twisted, the surviving arm drawn back high over the shoulder about to hurl a tennis ball straight at the viewer, weight on the back foot. The wounds must stay clearly visible in every frame. Output exactly four figures in one row."
|
"subject": "The reference shows the SAME boss in four states: undamaged, then three worsening stages of damage. Draw all FOUR of those exact states again, in the same order, same size, same costume, same wounds — but each one caught MID-THROW: the whole body wound up and twisted, the surviving arm drawn back high over the shoulder about to hurl a tennis ball straight at the viewer, weight on the back foot. The wounds must stay clearly visible in every frame. Output exactly four figures in one row."
|
||||||
|
},
|
||||||
|
"boss_berry_stride": {
|
||||||
|
"sprites": [
|
||||||
|
"z_berry1",
|
||||||
|
"z_berry2"
|
||||||
|
],
|
||||||
|
"ref_only": [
|
||||||
|
"z_berry1"
|
||||||
|
],
|
||||||
|
"anchor": 0,
|
||||||
|
"check": "any",
|
||||||
|
"state": true,
|
||||||
|
"subject": "The reference is THE STRAWBERRY SELLER — a towering dead tournament vendor facing the viewer, in a white apron soaked through with deep red and a folded paper cap, a wooden vendor's tray slung from a neck strap across his belly loaded with punnets of DEEP RED strawberries and white jugs of cream, sleeves shoved up over blood-smeared forearms, a serving ladle hanging from his belt. Green-grey dead skin, hollow eyes. He grips a tennis ball in one huge fist. No pink, no magenta, no violet, no purple. Draw TWO frames of that SAME boss walking straight toward the viewer. THE LEGS ARE THE ENTIRE POINT OF THIS SHEET AND THE TWO FRAMES MUST BE OPPOSITE HALVES OF ONE STRIDE. FRAME ONE: his LEFT leg is striding forward with the foot flat on the ground well ahead of him and his RIGHT leg is trailing far behind, that back heel lifted off the ground — the feet are wide apart with clear empty space between the two shoes. FRAME TWO: exactly the other way round — his RIGHT leg strides forward with the foot flat on the ground well ahead of him and the LEFT leg trails far behind with the heel lifted, again with the feet wide apart. In frame two the arms swing the opposite way to frame one as well. Do NOT draw the same leg forward in both frames and do NOT draw him standing on one leg with a knee raised — these are two moments of a heavy walk, so in each frame one foot is planted ahead and the other is behind. Same size, same costume, same face, chest and face squarely toward the viewer in both. Output exactly two figures in one row."
|
||||||
|
},
|
||||||
|
"boss_champ_stride": {
|
||||||
|
"sprites": [
|
||||||
|
"z_champ1",
|
||||||
|
"z_champ2"
|
||||||
|
],
|
||||||
|
"ref_only": [
|
||||||
|
"z_champ1"
|
||||||
|
],
|
||||||
|
"anchor": 0,
|
||||||
|
"check": "any",
|
||||||
|
"state": true,
|
||||||
|
"subject": "The FIRST figure in the reference is THE CHAMPION — the towering dead fourteen-time champion facing the viewer, in a torn all-white tennis kit gone grey and yellow with age, gold trim at the collar, a laurel of white strapping tape round his head, a sweatband on each wrist, a huge silver trophy gripped in one fist and a tennis ball in the other. Green-grey dead skin, heavy jaw, sunken eyes. The SECOND and THIRD figures are a DIFFERENT boss and they are in the reference for ONE reason: their LEGS. Draw TWO frames of THE CHAMPION — the first figure's face, body, kit, trophy and ball, nobody else's — walking straight toward the viewer, and copy the LEG AND FOOT POSITIONS of the second and third figures exactly: in each frame one leg strides far forward with the foot flat on the ground and the other trails far behind with the heel lifted, the two shoes WIDE APART with a big empty gap between them, and the forward leg swaps from one frame to the other. Do not put both feet together and do not stand him on one leg. Do not give him the second figure's clothes, tools or face. BOTH FRAMES MUST FACE THE VIEWER HEAD ON: his chest, both shoulders and his whole face square to the camera, both arms visible on either side of his body. Never a side view, never a three-quarter view, never walking across the picture. He is an ENORMOUS heavy-set figure — do not slim him down. Output exactly two figures in one row."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
@@ -148,7 +148,10 @@ export const TUNE = {
|
|||||||
bossZMax: 5.6, // you (~57 s at bossSpeed) rather than by you killing it — and every extra hp
|
bossZMax: 5.6, // you (~57 s at bossSpeed) rather than by you killing it — and every extra hp
|
||||||
// above just made that MORE likely, not less. Only 'charge' breaks the band.
|
// above just made that MORE likely, not less. Only 'charge' breaks the band.
|
||||||
bossChargeV: 4.2, // m/s the physio rushes at once it commits
|
bossChargeV: 4.2, // m/s the physio rushes at once it commits
|
||||||
bossChargeT: 1.5, // s a charge lasts before it withdraws
|
bossChargeT: 2.2, // s ONE RUSH takes, start to arrival — the charge speed is derived from it and
|
||||||
|
// from how far away it is (bossAI), so the run-up reads the same every time and
|
||||||
|
// is long enough to see coming. Was 1.5 s at a flat 4.2 m/s, which only reached
|
||||||
|
// you at all because the boss crept nearer every cycle until it was on top of you.
|
||||||
bossChargeRest: 2.2, // s of held ground between charges — the window you get to hit it
|
bossChargeRest: 2.2, // s of held ground between charges — the window you get to hit it
|
||||||
bossHopT: 0.55, // s per mascot hop (it moves in arcs, not in a slide)
|
bossHopT: 0.55, // s per mascot hop (it moves in arcs, not in a slide)
|
||||||
bossHopX: 2.0, // m of ground one hop covers (the eased arc lands it at about 2/pi of this)
|
bossHopX: 2.0, // m of ground one hop covers (the eased arc lands it at about 2/pi of this)
|
||||||
@@ -500,12 +503,13 @@ export const BOSS_ART = [
|
|||||||
{ name: 'THE PHYSIO', walk: ['z_physio1', 'z_physio2'],
|
{ name: 'THE PHYSIO', walk: ['z_physio1', 'z_physio2'],
|
||||||
wounds: ['z_physiow1', 'z_physiow2', 'z_physiow3'],
|
wounds: ['z_physiow1', 'z_physiow2', 'z_physiow3'],
|
||||||
throws: ['z_physiot0', 'z_physiot1', 'z_physiot2', 'z_physiot3'],
|
throws: ['z_physiot0', 'z_physiot1', 'z_physiot2', 'z_physiot3'],
|
||||||
// THE BODY IS THE WEAPON — which it never once was. One ball every 3.4 s and a withdrawal that
|
// THE BODY IS THE WEAPON — which it never once was. One ball every 3.4 s and a charge that gave
|
||||||
// gave back 4.2 of every 6.3 m it charged meant the fight was over long before it arrived:
|
// back most of its own ground meant the fight was over long before it arrived: Oliver, "the
|
||||||
// Oliver, "the physio does basically nothing but move towards the net, too easy". It now keeps
|
// physio does basically nothing but move towards the net, too easy". It covers the ground half
|
||||||
// three quarters of every rush (withdraw) and covers the ground half again as fast (speedMul),
|
// again as fast now (speedMul) and drills you on the way in, and since the charge style itself
|
||||||
// so it is on top of you inside two charges, and it drills you on the way in.
|
// retreats to the front of the band after every rush (bossAI), it arrives OVER AND OVER rather
|
||||||
move: 'charge', hpMul: 0.9, speedMul: 1.45, withdraw: 0.2,
|
// than either never or once and for good.
|
||||||
|
move: 'charge', hpMul: 0.9, speedMul: 1.45,
|
||||||
volley: 3, gap: 0.26, cycle: 2.4, wind: 0.3, shot: 22, spread: 0.5 },
|
volley: 3, gap: 0.26, cycle: 2.4, wind: 0.3, shot: 22, spread: 0.5 },
|
||||||
{ name: 'THE SPONSOR MASCOT', walk: ['z_mascot1', 'z_mascot2'],
|
{ name: 'THE SPONSOR MASCOT', walk: ['z_mascot1', 'z_mascot2'],
|
||||||
wounds: ['z_mascotw1', 'z_mascotw2', 'z_mascotw3'],
|
wounds: ['z_mascotw1', 'z_mascotw2', 'z_mascotw3'],
|
||||||
@@ -526,7 +530,7 @@ export const BOSS_ART = [
|
|||||||
{ name: 'THE STRAWBERRY SELLER', walk: ['z_berry1', 'z_berry2'],
|
{ name: 'THE STRAWBERRY SELLER', walk: ['z_berry1', 'z_berry2'],
|
||||||
wounds: ['z_berryw1', 'z_berryw2', 'z_berryw3'],
|
wounds: ['z_berryw1', 'z_berryw2', 'z_berryw3'],
|
||||||
throws: ['z_berryt0', 'z_berryt1', 'z_berryt2', 'z_berryt3'],
|
throws: ['z_berryt0', 'z_berryt1', 'z_berryt2', 'z_berryt3'],
|
||||||
move: 'charge', hpMul: 1.4, speedMul: 1.15, withdraw: 0.3, // a tray of cream and a ladle: he
|
move: 'charge', hpMul: 1.4, speedMul: 1.15, // a tray of cream and a ladle: he
|
||||||
volley: 4, gap: 0.12, spread: 1.4, cycle: 3.0, wind: 0.55, shot: 15 }, // sprays, then comes for you
|
volley: 4, gap: 0.12, spread: 1.4, cycle: 3.0, wind: 0.55, shot: 15 }, // sprays, then comes for you
|
||||||
// THE FINAL BOSS, and the only row that is not in the chapter rotation: bossArt() hands him out
|
// THE FINAL BOSS, and the only row that is not in the chapter rotation: bossArt() hands him out
|
||||||
// every TENTH chapter (game 100, then 200). Two things make him the wall the run ends on.
|
// every TENTH chapter (game 100, then 200). Two things make him the wall the run ends on.
|
||||||
@@ -544,7 +548,7 @@ export const BOSS_ART = [
|
|||||||
{ move: 'pace', paceMul: 0.9, volley: 1, cycle: 3.2, wind: 0.6, shot: 17, spread: 0.3 },
|
{ move: 'pace', paceMul: 0.9, volley: 1, cycle: 3.2, wind: 0.6, shot: 17, spread: 0.3 },
|
||||||
{ move: 'shadow', paceMul: 1.8, volley: 4, gap: 0.22, cycle: 2.2, wind: 0.4, shot: 20, spread: 0.5,
|
{ move: 'shadow', paceMul: 1.8, volley: 4, gap: 0.22, cycle: 2.2, wind: 0.4, shot: 20, spread: 0.5,
|
||||||
cry: 'SECOND SET.' },
|
cry: 'SECOND SET.' },
|
||||||
{ move: 'charge', speedMul: 1.5, withdraw: 0.15, volley: 3, gap: 0.1, cycle: 1.9, wind: 0.3,
|
{ move: 'charge', speedMul: 1.5, volley: 3, gap: 0.1, cycle: 1.9, wind: 0.3,
|
||||||
shot: 24, spread: 1.1, cry: 'MATCH POINT.' },
|
shot: 24, spread: 1.1, cry: 'MATCH POINT.' },
|
||||||
] },
|
] },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -94,6 +94,12 @@
|
|||||||
rgba(12,20,17,0.58) 100%),
|
rgba(12,20,17,0.58) 100%),
|
||||||
url('assets/menu_bg.png?v=86') 50% 42% / cover no-repeat var(--ink);
|
url('assets/menu_bg.png?v=86') 50% 42% / cover no-repeat var(--ink);
|
||||||
image-rendering: pixelated; }
|
image-rendering: pixelated; }
|
||||||
|
/* THE BACK-ROOM HOTSPOT: five taps here open it (main.js). Top-left CORNER, not the logo and not
|
||||||
|
the screen — a hidden door has to be somewhere you do not put your thumb by accident, and the
|
||||||
|
whole splash was reachable by every stray tap on the way to PLAY MATCH. Invisible on purpose,
|
||||||
|
and a child of #splash so it exists on no other stage. */
|
||||||
|
#hot { position: fixed; top: 0; left: 0; width: 26vw; max-width: 150px; height: 15vh;
|
||||||
|
max-height: 130px; z-index: 4; }
|
||||||
#intro .card { font: 400 12px Silkscreen, monospace; color: var(--cream); text-align: center;
|
#intro .card { font: 400 12px Silkscreen, monospace; color: var(--cream); text-align: center;
|
||||||
line-height: 1.9; padding: 16px 14px 20px; width: min(370px, 94vw);
|
line-height: 1.9; padding: 16px 14px 20px; width: min(370px, 94vw);
|
||||||
animation: drop 0.5s ease-out; }
|
animation: drop 0.5s ease-out; }
|
||||||
@@ -307,6 +313,7 @@
|
|||||||
<!-- three bands: the figures at the top, the title floating in the middle, the buttons under
|
<!-- three bands: the figures at the top, the title floating in the middle, the buttons under
|
||||||
your thumb. Same anchoring as every other screen. -->
|
your thumb. Same anchoring as every other screen. -->
|
||||||
<div id="splash">
|
<div id="splash">
|
||||||
|
<div id="hot"></div>
|
||||||
<div class="mid">
|
<div class="mid">
|
||||||
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
|
<img class="logo" src="assets/logo.png" alt="Splatter Tennis">
|
||||||
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
|
<p class="slogan">Fifteen. Thirty. Forty. <b>Dead.</b></p>
|
||||||
@@ -391,6 +398,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="main.js?v=140"></script>
|
<script type="module" src="main.js?v=141"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -190,11 +190,13 @@ for (const id of ['sBack', 'stBack', 'rkBack', 'scBack'])
|
|||||||
document.getElementById(id).addEventListener('click', () => go('splash'));
|
document.getElementById(id).addEventListener('click', () => go('splash'));
|
||||||
document.getElementById('bsBack').addEventListener('click', () => go('secret'));
|
document.getElementById('bsBack').addEventListener('click', () => go('secret'));
|
||||||
|
|
||||||
// ---- THE BACK ROOM: five taps on the logo, and nothing on the screen says so. It replaces the
|
// ---- THE BACK ROOM: five taps in the TOP-LEFT CORNER of the splash (#hot, an invisible box in
|
||||||
// triple-tap on the in-play scoreboard, which sat on top of the miss-log tap and which the
|
// index.html), and nothing on the screen says so. It replaces the triple-tap on the in-play
|
||||||
// testteam kept opening by accident — a cheat you can trip mid-run is a bug.
|
// scoreboard, which sat on top of the miss-log tap and which the testteam kept opening by accident.
|
||||||
|
// The corner rather than the logo: on the way to PLAY MATCH your thumb crosses the middle of the
|
||||||
|
// splash all the time, and a hidden door you can open by accident is just a bug with a secret.
|
||||||
let logoTaps = [];
|
let logoTaps = [];
|
||||||
document.querySelector('#splash .mid').addEventListener('pointerdown', () => {
|
document.getElementById('hot').addEventListener('pointerdown', () => {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
logoTaps = logoTaps.filter(t => now - t < 2000);
|
logoTaps = logoTaps.filter(t => now - t < 2000);
|
||||||
logoTaps.push(now);
|
logoTaps.push(now);
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export function proj(x, y, z) {
|
|||||||
// Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the
|
// Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the
|
||||||
// menu screens hang <img> tags on the same files, and two copies of this number is one copy too
|
// menu screens hang <img> tags on the same files, and two copies of this number is one copy too
|
||||||
// many: the one that does not get bumped is the one that serves a cached 404.
|
// many: the one that does not get bumped is the one that serves a cached 404.
|
||||||
export const SPRV = 88;
|
export const SPRV = 89;
|
||||||
// The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was
|
// The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was
|
||||||
// already requested at import — it just makes the wait visible and holds the menu back until the
|
// already requested at import — it just makes the wait visible and holds the menu back until the
|
||||||
// last sheet has landed. See the onerror note below for the other half of "sometimes missing".
|
// last sheet has landed. See the onerror note below for the other half of "sometimes missing".
|
||||||
|
|||||||
@@ -175,16 +175,26 @@ function bossAI(zb, dt) {
|
|||||||
const v = TUNE.bossChargeV * (A.speedMul ?? 1);
|
const v = TUNE.bossChargeV * (A.speedMul ?? 1);
|
||||||
if (zb.charging) {
|
if (zb.charging) {
|
||||||
const dx = P.x - zb.x;
|
const dx = P.x - zb.x;
|
||||||
zb.x += Math.sign(dx) * Math.min(Math.abs(dx), TUNE.bossStrafe * dt);
|
zb.x += Math.sign(dx) * Math.min(Math.abs(dx), TUNE.bossStrafe * dt); // it tracks your lane SLOWLY: a sidestep beats it
|
||||||
zb.z = Math.min(P.z, zb.z + v * dt); // AT you and NO FURTHER: unclamped it ran
|
zb.z = Math.min(P.z, zb.z + zb.chargeV * dt); // AT you and NO FURTHER: unclamped it ran
|
||||||
if (zb.moveT <= 0) { zb.charging = false; zb.moveT = TUNE.bossChargeRest; } // off the back of the court
|
if (zb.moveT <= 0) { zb.charging = false; zb.moveT = TUNE.bossChargeRest; } // off the back of the court
|
||||||
} else {
|
} else {
|
||||||
// It withdraws LESS than it charged, so the fight has a visible clock: it is nearer after every
|
// AND IT FALLS BACK. A rush that ended where it landed left the boss parked on top of you for
|
||||||
// rush and arrives while you are still fighting it. A withdrawal that undid the whole charge
|
// the rest of the fight: dodge or die every two seconds, with no window to hit it and nothing
|
||||||
// made "the body is the weapon" untrue — it never once got near you, which is exactly what the
|
// but a hitscan laser fast enough to use what window there was — Oliver, "they come too close,
|
||||||
// physio's old 0.45 still did over a whole fight (`withdraw` is per design now).
|
// they seem unbeatable". So the retreat OUTRUNS the rush (retreat > 1) and stops at the front
|
||||||
zb.z = Math.max(TUNE.bossZMin, zb.z - v * (A.withdraw ?? 0.45) * dt);
|
// of the band rather than crawling a fraction of the way back. A charge is a threat that
|
||||||
if (zb.moveT <= 0) { zb.charging = true; zb.moveT = TUNE.bossChargeT; }
|
// PASSES: you dodge it, and the ground it gives up again is your turn to hit it.
|
||||||
|
const home = TUNE.bossZMax, sp = v * (A.retreat ?? 1.3) * dt; // toward its LINE, from either side:
|
||||||
|
zb.z += Math.sign(home - zb.z) * Math.min(Math.abs(home - zb.z), sp); // backward off a rush, forward on the way in
|
||||||
|
// THE RUSH IS TIMED, NOT PACED. Whatever ground the retreat left between you, it covers it in
|
||||||
|
// bossChargeT — so every charge arrives, and every charge takes the same readable run-up to do
|
||||||
|
// it. Paced at a flat speed instead, the same fix that stopped it camping on you also stopped
|
||||||
|
// it ever reaching you: it started each rush from the band and ran out of clock 8 m short.
|
||||||
|
if (zb.moveT <= 0) {
|
||||||
|
zb.charging = true; zb.moveT = TUNE.bossChargeT;
|
||||||
|
zb.chargeV = Math.max(v, (P.z - zb.z) / TUNE.bossChargeT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (style === 'shadow') {
|
} else if (style === 'shadow') {
|
||||||
// paces — but the ground it paces over is centred on YOU, so it never stops moving and it takes
|
// paces — but the ground it paces over is centred on YOU, so it never stops moving and it takes
|
||||||
|
|||||||