diff --git a/assets/_regen_batches.json b/assets/_regen_batches.json index 3395c74..dee1528 100644 --- a/assets/_regen_batches.json +++ b/assets/_regen_batches.json @@ -1089,5 +1089,31 @@ "check": "any", "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." + }, + "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." } } \ No newline at end of file diff --git a/assets/z_berry1.png b/assets/z_berry1.png index deb0731..bec7691 100644 Binary files a/assets/z_berry1.png and b/assets/z_berry1.png differ diff --git a/assets/z_berry2.png b/assets/z_berry2.png index 83272a0..468113e 100644 Binary files a/assets/z_berry2.png and b/assets/z_berry2.png differ diff --git a/assets/z_champ1.png b/assets/z_champ1.png index 33ad8a9..3742f0e 100644 Binary files a/assets/z_champ1.png and b/assets/z_champ1.png differ diff --git a/assets/z_champ2.png b/assets/z_champ2.png index b540708..b2eee7b 100644 Binary files a/assets/z_champ2.png and b/assets/z_champ2.png differ diff --git a/config.js b/config.js index 469842c..f36cb97 100644 --- a/config.js +++ b/config.js @@ -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 // 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 - 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 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) @@ -500,12 +503,13 @@ export const BOSS_ART = [ { name: 'THE PHYSIO', walk: ['z_physio1', 'z_physio2'], wounds: ['z_physiow1', 'z_physiow2', 'z_physiow3'], 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 - // gave back 4.2 of every 6.3 m it charged meant the fight was over long before it arrived: - // Oliver, "the physio does basically nothing but move towards the net, too easy". It now keeps - // three quarters of every rush (withdraw) and covers the ground half again as fast (speedMul), - // so it is on top of you inside two charges, and it drills you on the way in. - move: 'charge', hpMul: 0.9, speedMul: 1.45, withdraw: 0.2, + // THE BODY IS THE WEAPON — which it never once was. One ball every 3.4 s and a charge that gave + // back most of its own ground meant the fight was over long before it arrived: Oliver, "the + // physio does basically nothing but move towards the net, too easy". It covers the ground half + // again as fast now (speedMul) and drills you on the way in, and since the charge style itself + // retreats to the front of the band after every rush (bossAI), it arrives OVER AND OVER rather + // 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 }, { name: 'THE SPONSOR MASCOT', walk: ['z_mascot1', 'z_mascot2'], wounds: ['z_mascotw1', 'z_mascotw2', 'z_mascotw3'], @@ -526,7 +530,7 @@ export const BOSS_ART = [ { name: 'THE STRAWBERRY SELLER', walk: ['z_berry1', 'z_berry2'], wounds: ['z_berryw1', 'z_berryw2', 'z_berryw3'], 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 // 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. @@ -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: 'shadow', paceMul: 1.8, volley: 4, gap: 0.22, cycle: 2.2, wind: 0.4, shot: 20, spread: 0.5, 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.' }, ] }, ]; diff --git a/index.html b/index.html index df2f806..902b637 100644 --- a/index.html +++ b/index.html @@ -94,6 +94,12 @@ rgba(12,20,17,0.58) 100%), url('assets/menu_bg.png?v=86') 50% 42% / cover no-repeat var(--ink); 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; line-height: 1.9; padding: 16px 14px 20px; width: min(370px, 94vw); animation: drop 0.5s ease-out; } @@ -307,6 +313,7 @@
+

Fifteen. Thirty. Forty. Dead.

@@ -391,6 +398,6 @@
- + diff --git a/main.js b/main.js index 28176ed..7f0e749 100644 --- a/main.js +++ b/main.js @@ -190,11 +190,13 @@ for (const id of ['sBack', 'stBack', 'rkBack', 'scBack']) document.getElementById(id).addEventListener('click', () => go('splash')); 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 -// triple-tap on the in-play scoreboard, which sat on top of the miss-log tap and which the -// testteam kept opening by accident — a cheat you can trip mid-run is a bug. +// ---- THE BACK ROOM: five taps in the TOP-LEFT CORNER of the splash (#hot, an invisible box in +// index.html), and nothing on the screen says so. It replaces the triple-tap on the in-play +// 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 = []; -document.querySelector('#splash .mid').addEventListener('pointerdown', () => { +document.getElementById('hot').addEventListener('pointerdown', () => { const now = performance.now(); logoTaps = logoTaps.filter(t => now - t < 2000); logoTaps.push(now); diff --git a/render.js b/render.js index 7ec6da5..71e91ae 100644 --- a/render.js +++ b/render.js @@ -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 // menu screens hang 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. -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 // 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". diff --git a/zombies.js b/zombies.js index 90407b3..8d6a267 100644 --- a/zombies.js +++ b/zombies.js @@ -175,16 +175,26 @@ function bossAI(zb, dt) { const v = TUNE.bossChargeV * (A.speedMul ?? 1); if (zb.charging) { const dx = P.x - zb.x; - zb.x += Math.sign(dx) * Math.min(Math.abs(dx), TUNE.bossStrafe * dt); - zb.z = Math.min(P.z, zb.z + v * dt); // AT you and NO FURTHER: unclamped it ran + 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 + 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 } else { - // It withdraws LESS than it charged, so the fight has a visible clock: it is nearer after every - // rush and arrives while you are still fighting it. A withdrawal that undid the whole charge - // made "the body is the weapon" untrue — it never once got near you, which is exactly what the - // physio's old 0.45 still did over a whole fight (`withdraw` is per design now). - zb.z = Math.max(TUNE.bossZMin, zb.z - v * (A.withdraw ?? 0.45) * dt); - if (zb.moveT <= 0) { zb.charging = true; zb.moveT = TUNE.bossChargeT; } + // AND IT FALLS BACK. A rush that ended where it landed left the boss parked on top of you for + // the rest of the fight: dodge or die every two seconds, with no window to hit it and nothing + // but a hitscan laser fast enough to use what window there was — Oliver, "they come too close, + // they seem unbeatable". So the retreat OUTRUNS the rush (retreat > 1) and stops at the front + // of the band rather than crawling a fraction of the way back. A charge is a threat that + // 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') { // paces — but the ground it paces over is centred on YOU, so it never stops moving and it takes