The opponent can serve and fall, and there is a bench to judge it on
THE MISSING ART, generated by the whole-sheet restyle method: bouncing the ball at the baseline, the toss with the ball leaving the fingertips, the strike at full stretch — and the fall, which slice 4 has been waiting for: buckling, down on the knees, flat on the court face down, then dragging itself back up. One zombie is now complete as a tennis player rather than as a body that shuffles. (Healthy only so far; the wound variants are the next sheet.) THE SPRITE BENCH, in the back room. No ball, no scoring, no AI — one opponent on an empty court, walking, at the size and speed it is actually played at, because that is the only place a walk cycle can be judged. A sheet that looks fine at 224 px on a contact sheet is a different animal at 30 px crossing a lawn, and every sprite argument this project has had was really about that gap. It steps through every state including the ones the game cannot reach yet, so the serve and the fall could be looked at the day they were drawn instead of the day they get wired up. It prints the FILENAME it is drawing and the hp the wound stage was read from — the two numbers that separate "the art is wrong" from "the wiring picked the wrong art" — and it says HEALTHY ONLY where a wound variant does not exist rather than implying one does. The stride steps off ground covered, like the real thing: a bench animating on a clock would flatter exactly the sprites that foot-slide in play. It owns the court like every other mode and every start* refuses while it runs. FOUND BY IT, ten seconds in: `start()` applies the mode the setup screen picked, so a remembered ZOMBIE TENNIS was taking the court before the back room could — every back-room entry (weapons, bosses, drill, match, bench) had been silently refusing since the mode select landed this morning. The back room passes null. And a third calibration lesson: POSE MOVES A GLOBAL COLOUR STATISTIC. A body face down shows a fraction of the shirt it shows standing, so `redness` fell 14 points across the fall sheet without a colour changing. The measure that survives a pose change is the shorts HUE — mid-tone neutrals, present in every frame in the same proportion — which is why it is the one that caught the drift Oliver actually saw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ffc9cd3a60
commit
4bbdf6357a
16 files changed
+234
-30
No files matched your search
+22
-10
@@ -6,7 +6,7 @@
|
||||
import { TUNE, COURTS, TOURS, MODES, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt } from './config.js';
|
||||
import { P, O, ball, score, G, setupPoint, selectCourt, selectTour } from './state.js';
|
||||
import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, flickTarget, dragServe, toggleTestMode } from './gameplay.js';
|
||||
import { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch } from './beats.js';
|
||||
import { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench } from './beats.js';
|
||||
import { fitViewport, render, proj, RES, SPRV, LOAD } from './render.js';
|
||||
import { voice, ambient, menuMusic, menuDuck } from './audio.js';
|
||||
import { zombies, spawnZombie, spawnRacketZombie, spawnMachine, spawnChapterBoss, piles, gore, parts, pellets, extras, shots, pops, explode, hurtPlayer, killPlayer , spawnBarrel } from './zombies.js';
|
||||
@@ -161,7 +161,11 @@ function refreshSplash() {
|
||||
summary();
|
||||
}
|
||||
|
||||
function start() {
|
||||
// `modeId` is the mode to drop into. It defaults to what the setup screen picked — but the BACK
|
||||
// ROOM passes null, because those entries bring their own mode and a remembered ZOMBIE TENNIS was
|
||||
// taking the court before startDrill/startBench/toggleTestMode could, so every one of them silently
|
||||
// refused. The sprite bench found this the first time it was opened.
|
||||
function start(modeId = pickedMode) {
|
||||
if (!menuUp()) return;
|
||||
selectTour(pickedTour);
|
||||
selectCourt(picked);
|
||||
@@ -171,8 +175,8 @@ function start() {
|
||||
// differ by at this point: gore is the game as it always was, classic is the same tennis with the
|
||||
// turn switched off, and zombie tennis is the match taking the court. Everything else about a mode
|
||||
// lives in the code that reads G.classic / G.match, never here.
|
||||
G.classic = pickedMode === 'classic';
|
||||
if (pickedMode === 'zombie') { intoSplatter(); startMatch(); }
|
||||
G.classic = modeId === 'classic';
|
||||
if (modeId === 'zombie') { intoSplatter(); startMatch(); }
|
||||
}
|
||||
let stPeriod = 'all', stMode = stats.MODE_GORE;
|
||||
// the stats screen opens on the mode you last PICKED to play — that is the one whose numbers you
|
||||
@@ -255,20 +259,27 @@ function intoSplatter() {
|
||||
score.server = 'P'; setupPoint();
|
||||
}
|
||||
document.getElementById('scWeapons').addEventListener('click', () => {
|
||||
start(); intoSplatter(); toggleTestMode();
|
||||
start(null); intoSplatter(); toggleTestMode();
|
||||
});
|
||||
document.getElementById('scBosses').addEventListener('click', () => go('bosses'));
|
||||
// THE DRILL, from the back room — the cognitive mode's feel-test. Like the two above it drops
|
||||
// into a real run first (same loop, same contact rules) and then hands the court to startDrill,
|
||||
// which refuses if anything else still owns it.
|
||||
document.getElementById('scDrill').addEventListener('click', () => {
|
||||
start(); intoSplatter();
|
||||
start(null); intoSplatter();
|
||||
startDrill();
|
||||
});
|
||||
document.getElementById('scMatch').addEventListener('click', () => {
|
||||
start(); intoSplatter();
|
||||
start(null); intoSplatter();
|
||||
startMatch();
|
||||
});
|
||||
// THE SPRITE BENCH — the art test. Same "drop into a real run first" trick as its neighbours, so the
|
||||
// body is drawn by the real renderer at the real size, on the real court, under the real palette.
|
||||
// Judging a walk cycle anywhere else is judging a different picture.
|
||||
document.getElementById('scBench').addEventListener('click', () => {
|
||||
start(null); intoSplatter();
|
||||
startBench();
|
||||
});
|
||||
|
||||
// The list is built by ASKING bossArt which design each chapter gets, so it can never drift from
|
||||
// what the run actually spawns — including the champion sitting on chapter 10 rather than 7.
|
||||
@@ -278,7 +289,7 @@ for (let ch = 1; ch <= 10; ch++) {
|
||||
b.innerHTML = `<img src="assets/${art.walk[0]}.png?v=${SPRV}" alt="">`
|
||||
+ `<b>${art.name}</b><i>CHAPTER ${ch} · GAME ${ch * TUNE.bossEvery}</i>`;
|
||||
b.addEventListener('click', () => {
|
||||
start(); intoSplatter();
|
||||
start(null); intoSplatter();
|
||||
G.runGames = ch * TUNE.bossEvery; // the roster, the HUD and the spawn ramp all read this one
|
||||
G.bossDue = 0; // ...and the schedule must not queue a SECOND fight behind it
|
||||
for (const id of Object.keys(WEAPONS)) grantWeapon(id, TUNE.wCap); // full arsenal, every tin
|
||||
@@ -440,8 +451,9 @@ function laserFire() {
|
||||
// a plain timer, not the load event: this module runs as a module, so `load` may already have gone
|
||||
if (location.hash === '#laser') setTimeout(laserScene, 600);
|
||||
// #drill — the same door from a URL, so a phone can be sent straight into the mode
|
||||
if (location.hash === '#drill') setTimeout(() => { start(); intoSplatter(); startDrill(); }, 700);
|
||||
if (location.hash === '#match') setTimeout(() => { start(); intoSplatter(); startMatch(); }, 700);
|
||||
if (location.hash === '#drill') setTimeout(() => { start(null); intoSplatter(); startDrill(); }, 700);
|
||||
if (location.hash === '#match') setTimeout(() => { start(null); intoSplatter(); startMatch(); }, 700);
|
||||
if (location.hash === '#bench') setTimeout(() => { start(null); intoSplatter(); startBench(); }, 700);
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
|
||||
Reference in new issue
Block a user