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
+50
-9
@@ -1,7 +1,7 @@
|
||||
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
|
||||
|
||||
import { cv, ctx } from './dom.js';
|
||||
import { TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS } from './config.js';
|
||||
import { BENCH, BENCH_STAGES, TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, DONE_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour } from './state.js';
|
||||
import { predictLanding, previewServe, slotId, slotStock, serveApexY } from './gameplay.js';
|
||||
import { drillQueue, drillLegal, drillMult } from './drill.js';
|
||||
@@ -195,7 +195,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 <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.
|
||||
export const SPRV = 96;
|
||||
export const SPRV = 98;
|
||||
// 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".
|
||||
@@ -243,6 +243,10 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
|
||||
// (…s is the swing). Its own family, so the turned CPU above keeps its shamble.
|
||||
'z_r1','z_r2','z_r3','z_r4',
|
||||
'z_rw1','z_rw1b','z_rw1s','z_rw2','z_rw2b','z_rw2s',
|
||||
// ...and the states the MODE still needs before it is a real tennis match: the
|
||||
// serve (slice 7, alternating service) and the fall between points (slice 4).
|
||||
// Loaded now so the sprite bench can show them the day they are drawn.
|
||||
'z_rbounce','z_rtoss','z_rserve','z_rfall1','z_rfall2','z_rfall3','z_rrise',
|
||||
'w_ball','w_bomb','w_triple','w_fire','w_gun', // weapon-rack icons
|
||||
'w_beacon','w_laser','w_rocket','w_flamer', // ...and the four that come out of a barrel
|
||||
'z_barrel1','z_barrel2', // the weapon barrel, two frames so it ROLLS
|
||||
@@ -680,6 +684,10 @@ function drawZombie(zb) {
|
||||
// project's oldest trap) is enough to cause it. Hold frame 1 instead: a body that stops animating
|
||||
// is a bug you can see and diagnose; one that flickers in and out reads as the game breaking.
|
||||
if (!SPR[name] && SPR['z_' + v + '1']) name = 'z_' + v + '1';
|
||||
// THE BENCH OVERRIDE, last of all: it names a frame outright, so states the game cannot reach
|
||||
// yet (the serve, the fall) can be looked at the day they are generated rather than the day they
|
||||
// are wired up. Only the back room's sprite bench ever sets this.
|
||||
if (zb.showFrame && SPR[zb.showFrame]) name = zb.showFrame;
|
||||
// ...and only NOW the tour swap, so the height read below comes off the frame actually drawn. The
|
||||
// branches above all test the men's names (which always exist), so the wound logic is untouched.
|
||||
name = SK(name);
|
||||
@@ -695,6 +703,8 @@ function drawZombie(zb) {
|
||||
// Every side-on body is DRAWN facing left, so one walking right was moonwalking — obvious the
|
||||
// moment they close on you across your own half. The front-facing ones (the turned CPU, the
|
||||
// runner, the machine, a chapter boss) have no left or right to turn and are left alone.
|
||||
zb.drawn = name; // WHICH FRAME THIS ACTUALLY IS. The sprite bench prints it, so a frame that looks
|
||||
// wrong on the court names its own file instead of being hunted for afterwards.
|
||||
drawStatic(name, zb.x, zb.z, h, bob, zb.face > 0 && !'cfmB'.includes(v));
|
||||
if (zb.burnT > 0) drawFlames(zb, h);
|
||||
if (zb.windT > 0 && zb.variant === 'B') { // the boss telegraphs too: it rears up, glowing, then throws
|
||||
@@ -763,6 +773,33 @@ function drawApexBand() {
|
||||
// THE TRIAGE READOUT: score, multiplier, best — and the calibration marker. There is no strip and
|
||||
// no called list any more; the court is the puzzle, so the HUD only carries what the court cannot
|
||||
// say: how much the run is worth and how hot the streak is.
|
||||
// THE SPRITE BENCH'S READOUT. The one thing a contact sheet cannot give you: what the game is
|
||||
// ACTUALLY drawing, right now, at the size it plays at — including the filename, so a frame that
|
||||
// looks wrong on the lawn names itself instead of being hunted for afterwards.
|
||||
function drawBenchHud() {
|
||||
const b = G.bench; if (!b) return;
|
||||
const zb = zombies.find(z => z.variant === 'r');
|
||||
const y = CARD_TOP + 4;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.font = B(20);
|
||||
shadowText(BENCH[b.i].name, W / 2, y, UI.cream);
|
||||
ctx.font = F(8);
|
||||
// ...and say so when the stop has no wound variant. The serve and the fall exist for the HEALTHY
|
||||
// body only so far, so a stage label there would be claiming art that has not been drawn — and the
|
||||
// whole reason this screen exists is to stop the art and the wiring lying to each other.
|
||||
const named = !!BENCH[b.i].spr;
|
||||
shadowText(named ? 'HEALTHY ONLY · NO WOUND VARIANT YET'
|
||||
: BENCH_STAGES[b.stage] + ' · ' + (b.i + 1) + '/' + BENCH.length,
|
||||
W / 2, y + 24, named ? UI.dim : UI.warm);
|
||||
// the file on screen, and the hp fraction the wound stage was read from — the two numbers you
|
||||
// need to tell "the art is wrong" from "the wiring picked the wrong art"
|
||||
ctx.font = F(8);
|
||||
shadowText(zb ? zb.drawn || '—' : 'NO BODY', W / 2, y + 40, UI.pick);
|
||||
if (zb) shadowText('hp ' + zb.hp + '/' + zb.hpMax, W / 2, y + 54, UI.dim);
|
||||
ctx.font = F(7, 0);
|
||||
shadowText(IS_TOUCH ? 'TAP: NEXT STATE' : 'SPACE: NEXT STATE ←/→: WOUND STAGE', W / 2, H - 26, UI.dim);
|
||||
}
|
||||
|
||||
function drawDrillHud() {
|
||||
const d = G.drill; if (!d) return;
|
||||
const y = CARD_TOP + 4;
|
||||
@@ -2059,6 +2096,8 @@ export function render() {
|
||||
// top/left, dark bottom/right), a purple club spine, cream serif names, and the LIVE game
|
||||
// sunk into a recessed slot — purple for the server, gold digits. Splatter adds a recessed
|
||||
// status well (kills-to-next-point pips + weapon stock). Hard edges, no radius = retro.
|
||||
const benchOn = !!G.bench; // an art test has no score; the board would be the biggest thing on
|
||||
// screen arguing with the one frame you came here to look at
|
||||
const splat = G.mode !== 'tennis';
|
||||
// ZOMBIE TENNIS HAS ITS OWN BOARD. The club's bottle-green plate is the club's; the match is
|
||||
// played on a board nobody has cleaned in a long time (assets/_board.py), and its ink has to
|
||||
@@ -2077,7 +2116,8 @@ export function render() {
|
||||
ctx.save(); ctx.translate(6, 6);
|
||||
const bW = 150, bH = 42; // fixed: keeps the ornate PNG undistorted (art is 300x84 = 150x42)
|
||||
const plate = zBoard || SPR.scoreboard;
|
||||
if (plate) ctx.drawImage(plate, 0, 0, bW, bH); // generated ornate board backdrop
|
||||
if (benchOn) { /* an art test has no score — not even the empty plate */ }
|
||||
else if (plate) ctx.drawImage(plate, 0, 0, bW, bH); // generated ornate board backdrop
|
||||
else { // code-drawn fallback until the PNG loads
|
||||
R(0, 0, bW, bH, bd.face);
|
||||
R(0, 0, bW, 1, bd.hi); R(0, 0, 1, bH, bd.hi);
|
||||
@@ -2092,7 +2132,7 @@ export function render() {
|
||||
const rung = G.match && zombies.find(z => z.variant === 'r' && z.rung);
|
||||
const names = G.match ? ['YOU', (rung && rung.rung.short) || 'THEM']
|
||||
: splat ? ['YOU', 'THEM'] : ['YOU', 'CPU'];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
for (let i = 0; i < 2 && !benchOn; i++) {
|
||||
const cy = 12 + i * 18, svr = (score.server === 'P') === (i === 0); // rows aligned to the art's two bands
|
||||
if (svr) { ctx.fillStyle = bd.gold; ctx.beginPath(); ctx.arc(11, cy, 1.8, 0, 7); ctx.fill(); } // server pip on the purple spine
|
||||
ctx.textAlign = 'left'; ctx.font = F(8, svr); ctx.fillStyle = svr ? bd.cream : bd.dim;
|
||||
@@ -2106,7 +2146,7 @@ export function render() {
|
||||
ctx.font = F(8); ctx.fillStyle = bd.cream; ctx.fillText(String(score.games[i]), 90, cy); // games
|
||||
ctx.font = F(8); ctx.fillStyle = svr ? bd.gold : bd.cream; ctx.fillText(ptTxt(i), 126, cy); // points, in the art's slot
|
||||
}
|
||||
if (splat || G.classic) {
|
||||
if ((splat || G.classic) && !benchOn) {
|
||||
// This row used to be bare text on the court. That was fine over one dark green lawn and is
|
||||
// unreadable over a sunlit clay crowd or a floodlit blue one, so it gets its own panel — the
|
||||
// same trick the scoreboard art above it already relies on.
|
||||
@@ -2129,7 +2169,7 @@ export function render() {
|
||||
}
|
||||
// ...and NOT during a match: nothing on that court is killed for points, so the pips were both a
|
||||
// lie and a collision — 'NEXT ○○' ran straight into the OPPONENT line sharing the panel.
|
||||
if (splat && !G.match && !G.classic && TUNE.killPips && TUNE.killsPerPoint > 1) { // kills-to-next-point pips, on the panel above
|
||||
if (splat && !G.match && !G.classic && !benchOn && TUNE.killPips && TUNE.killsPerPoint > 1) { // kills-to-next-point pips, on the panel above
|
||||
ctx.textAlign = 'left'; ctx.font = F(7, 0); ctx.fillStyle = bd.dim; ctx.fillText('NEXT', 4, bH + 8);
|
||||
for (let k = 0; k < TUNE.killsPerPoint; k++) {
|
||||
ctx.beginPath(); ctx.arc(34 + k * 8, bH + 8, 2.4, 0, 7);
|
||||
@@ -2139,9 +2179,10 @@ export function render() {
|
||||
}
|
||||
ctx.restore();
|
||||
ctx.textBaseline = 'top'; ctx.textAlign = 'center';
|
||||
drawBlood(); // how many more hits you have left, beside the board — the vignette says "worse", not "how many"
|
||||
if (G.mode === 'splatter' && !G.interlude && !G.drill) drawRack(); // courtside weapon rack (hidden during the tennis-rules duel AND the drill, which is played with plain balls)
|
||||
if (!G.bench) drawBlood(); // how many more hits you have left, beside the board — the vignette says "worse", not "how many"
|
||||
if (G.mode === 'splatter' && !G.interlude && !G.drill && !G.bench) drawRack(); // courtside weapon rack (hidden during the tennis-rules duel AND the drill, which is played with plain balls)
|
||||
drawDrillHud(); // the triage readout — no-op unless a drill is running
|
||||
drawBenchHud(); // ...and the sprite bench's, same rule
|
||||
drawApproachRing(); drawApexBand(); // the match's timing, drawn — no-ops unless a match is running
|
||||
// THE END OF A RUN, whichever way it ended. Three modes end three different ways and they share
|
||||
// one screen — a wash, a headline, the run's own two numbers, the mark to beat and the gated tap
|
||||
@@ -2195,7 +2236,7 @@ export function render() {
|
||||
: Math.max(G.changeover > 0 ? G.changeover : 0,
|
||||
G.phase === 'over' ? G.overT : 0,
|
||||
G.interlude ? G.interlude.introT : 0);
|
||||
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !(G.mgT > 0) && !G.aim && waitT <= 0.5) { // while pulling, the draw meter owns this slot
|
||||
if (G.phase === 'pretoss' && score.server === 'P' && G.mode !== 'eaten' && !G.horde && !G.bench && !(G.mgT > 0) && !G.aim && waitT <= 0.5) { // while pulling, the draw meter owns this slot
|
||||
ctx.font = F(8);
|
||||
const touch = IS_TOUCH, switchHint = touch ? 'TAP THE RACK TO SWITCH' : '1-0 TO SWITCH';
|
||||
const selW = WEAPONS[slotId(G.weapon)], sel = selW ? selW.label : 'PLAIN';
|
||||
|
||||
Reference in new issue
Block a user