A match opponent is a FAMILY now, so a generated variant is one row and not dead art
--variant produces 31 frames; nothing could draw them. drawZombie hardcoded the `z_r` prefix, the preloader spelled all 31 filenames out by hand, and the sprite bench built its names from the same literal. A new zombie would have sat on disk. config.ZFAMS = ['r'] the families that exist config.famFrames(f) the 31 names derived from a key Everything that needs those names now derives them: the preloader spreads ZFAMS.flatMap(famFrames), drawZombie reads `zb.fam` (unset = 'r', so the club pro is untouched) and builds its wound lookups off it, and the bench walks families on up/down. Adding an opponent is ONE ROW — which also retires 31 hand-typed filenames from the preload list, each of which was a chance to typo one and have a body silently vanish for a frame. That is the oldest bug in render.js and it is now impossible in this corner of it. And the link that makes it worth anything: a LADDER RUNG can name a family. `fam: 'br'` on a row in LADDER_TRAITS and that rung walks on as a different body, all the way down its own wound ladder. So the whole path is now: _master.py --variant br "…" -> generate 31 frames ZFAMS += 'br' -> the game loads them fam: 'br' on a ladder row -> that opponent is in the match Verified: the bench still walks all 33 cells pulling the right derived name at every wound stage, and a real match still opens on THE GROUNDSKEEPER wearing family 'r' and drawing z_r1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4bec0cb09c
commit
e2487b784a
4 files changed
+52
-24
No files matched your search
+1
-1
@@ -431,6 +431,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=189"></script>
|
||||
<script type="module" src="src/main.js?v=190"></script>
|
||||
</body>
|
||||
</html>
|
||||
+9
-3
@@ -7,7 +7,7 @@
|
||||
// (beats <-> gameplay and beats <-> zombies are runtime-only import cycles, same pattern as
|
||||
// input <-> gameplay: the imported bindings are only touched inside functions, never at load.)
|
||||
|
||||
import { TUNE, UI, bossArt, matchRung, BENCH, COURT, HOME } from './config.js';
|
||||
import { TUNE, UI, bossArt, matchRung, BENCH, ZFAMS, COURT, HOME } from './config.js';
|
||||
import { G, P, O, ball, score, setupPoint, sendBoy } from './state.js';
|
||||
import { voice, zombieDuck, aww } from './audio.js';
|
||||
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
||||
@@ -171,7 +171,7 @@ const BENCH_HP = 12; // a deep pool so a wound stage is se
|
||||
|
||||
export function startBench() {
|
||||
if (G.interlude || G.horde || G.bossFight || G.drill || G.match || G.bench) return false;
|
||||
G.bench = { i: 0, stage: 0, dir: 1 };
|
||||
G.bench = { i: 0, stage: 0, fam: 0, dir: 1 };
|
||||
zombies.length = 0; pellets.length = 0; extras.length = 0; shots.length = 0;
|
||||
// ...and DISARM the ordinary schedule, exactly as the drill does: a pending horde or an owed
|
||||
// chapter is a latch that keeps trying to fire behind a mode that has no bodies for it.
|
||||
@@ -203,8 +203,10 @@ function benchApply() {
|
||||
// ...and the named states take the wound stage with them: z_r<suffix> at full health,
|
||||
// z_rw1<suffix> / z_rw2<suffix> below it. render falls back to the healthy frame if a stage has
|
||||
// not been drawn yet, and the HUD says so — a bench must never imply art that does not exist.
|
||||
const f = ZFAMS[G.bench.fam] || 'r';
|
||||
zb.fam = f;
|
||||
const suf = BENCH[G.bench.i].spr;
|
||||
zb.showFrame = suf ? (G.bench.stage ? 'z_rw' + G.bench.stage : 'z_r') + suf : null;
|
||||
zb.showFrame = suf ? 'z_' + f + (G.bench.stage ? 'w' + G.bench.stage : '') + suf : null;
|
||||
}
|
||||
|
||||
// the bench's own controls, taken before anything else reads the key: SPACE / a tap steps to the
|
||||
@@ -214,6 +216,9 @@ export function benchKey(code) {
|
||||
const b = G.bench;
|
||||
if (code === 'ArrowLeft' || code === 'KeyA') b.stage = (b.stage + 2) % 3;
|
||||
else if (code === 'ArrowRight' || code === 'KeyD') b.stage = (b.stage + 1) % 3;
|
||||
// up/down walks the OPPONENT FAMILIES — one today, one row in config.ZFAMS per variant after that
|
||||
else if (code === 'ArrowUp' || code === 'KeyW') b.fam = (b.fam + ZFAMS.length - 1) % ZFAMS.length;
|
||||
else if (code === 'ArrowDown' || code === 'KeyS') b.fam = (b.fam + 1) % ZFAMS.length;
|
||||
else b.i = (b.i + 1) % BENCH.length;
|
||||
benchApply();
|
||||
}
|
||||
@@ -343,6 +348,7 @@ function matchOpponent() {
|
||||
// pure data, read where the dials are already read (see matchRung in config). It rides on the
|
||||
// BODY so that killZombie can file the kill under it without asking G what rung we were on.
|
||||
zb.rung = matchRung(G.match.opp);
|
||||
zb.fam = zb.rung.fam; // ...and the body that rung wears (config.ZFAMS)
|
||||
G.card = { title: zb.rung.name, sub1: zb.rung.line, color: UI.hot, t: 2.4 };
|
||||
zb.hp = zb.hpMax = TUNE.matchOppHp; // a FLAT pool: with regicide gone the body is pure
|
||||
// scoreboard (a quarter per point), so a deeper pool would
|
||||
|
||||
+21
-1
@@ -677,6 +677,9 @@ export const bossArt = ch => ch % 10 === 0 ? BOSS_ART[BOSS_ART.length - 1]
|
||||
// pace scale on its returns' speed wide scale on how far off centre they may land
|
||||
// flub scale on its whiff chance speed scale on how fast it covers the ground
|
||||
// line the personality, said once, on the name plate as it walks on
|
||||
// fam which SPRITE FAMILY this rung wears (config.ZFAMS). Absent = the club pro's body, 'r'.
|
||||
// This is the payoff of _master.py --variant: generate an opponent, add its key to ZFAMS,
|
||||
// name it on a rung, and that rung is a different body all the way down its wound ladder.
|
||||
// short 8 characters, because that is the board's name slot — the match board names
|
||||
// the thing you are playing instead of calling it THEM
|
||||
const LADDER_TRAITS = [
|
||||
@@ -695,7 +698,7 @@ const LADDER_TRAITS = [
|
||||
// lands on rung 10 exactly as he lands on chapter 10 and the two rosters can never drift apart.
|
||||
export const matchRung = n => {
|
||||
const art = bossArt(n), i = BOSS_ART.indexOf(art);
|
||||
return { name: art.name, key: 'L' + i, ...LADDER_TRAITS[i] };
|
||||
return { name: art.name, key: 'L' + i, fam: 'r', ...LADDER_TRAITS[i] };
|
||||
};
|
||||
|
||||
// ---- THE SPRITE BENCH's stops (back room only, see beats.startBench). Each is either an ANIMATED
|
||||
@@ -718,6 +721,23 @@ export const BENCH = [
|
||||
];
|
||||
export const BENCH_STAGES = ['HEALTHY', 'ARM OFF', 'HEADLESS'];
|
||||
|
||||
// ---- MATCH-OPPONENT SPRITE FAMILIES. A family is a complete opponent: the eleven states at three
|
||||
// wound stages, 31 frames, all named off one key (`r` -> z_r1, z_rtoss, z_rw1fall2 …). Generating a
|
||||
// new one is `assets/_master.py --variant <key>`; USING it is one row here, because everything that
|
||||
// needs the names — the preloader, drawZombie's wound lookup, the sprite bench — derives them from
|
||||
// this table rather than spelling them out. That is the whole point: 31 hand-written filenames in a
|
||||
// preload list is 31 chances to typo one and have a body silently vanish for a frame.
|
||||
export const ZFAMS = ['r'];
|
||||
const POSES = ['bounce', 'toss', 'serve', 'fall1', 'fall2', 'fall3', 'rise'];
|
||||
export const famFrames = f => [
|
||||
`z_${f}1`, `z_${f}2`, `z_${f}3`, `z_${f}4`, // stance, shuffle A, shuffle B, forehand
|
||||
...POSES.map(s => `z_${f}${s}`),
|
||||
// the wound rows carry no separate stance — the shuffle doubles as one, which is how drawZombie
|
||||
// reads them (…w1 / …w1b are the stride pair, …w1s the forehand)
|
||||
...[1, 2].flatMap(st => [`z_${f}w${st}`, `z_${f}w${st}b`, `z_${f}w${st}s`,
|
||||
...POSES.map(s => `z_${f}w${st}${s}`)]),
|
||||
];
|
||||
|
||||
// The roster, as the BODY COUNT screen reads it. Until now a variant's NAME lived only in the
|
||||
// comment above its hp value — fine while nothing ever had to print one. The key is the `variant`
|
||||
// char the zombie already carries, except that 'a' and 'b' fold together (they are one shambler in
|
||||
|
||||
+21
-19
@@ -1,7 +1,7 @@
|
||||
// Camera, viewport, sprites and all drawing. Owns the crowd dots (visual-only state).
|
||||
|
||||
import { cv, ctx } from './dom.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 { BENCH, BENCH_STAGES, ZFAMS, famFrames, 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 = 100;
|
||||
export const SPRV = 101;
|
||||
// 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".
|
||||
@@ -241,17 +241,11 @@ for (const n of ['p_idle1','p_idle2','p_bounce','p_bounce2','p_toss','p_smash','
|
||||
// THE MATCH OPPONENT, drawn to play tennis rather than to shamble: ready stance,
|
||||
// shuffle pair, forehand — and the same three poses again at each wound stage
|
||||
// (…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',
|
||||
// ...and the same seven at each wound stage: a one-armed opponent still has to serve
|
||||
// and still has to go down, and reverting to the healthy body to do it would undo the
|
||||
// damage the score just did
|
||||
'z_rw1bounce','z_rw1toss','z_rw1serve','z_rw1fall1','z_rw1fall2','z_rw1fall3','z_rw1rise',
|
||||
'z_rw2bounce','z_rw2toss','z_rw2serve','z_rw2fall1','z_rw2fall2','z_rw2fall3','z_rw2rise',
|
||||
// EVERY MATCH-OPPONENT FAMILY, derived from config.ZFAMS — 31 names each, so a new
|
||||
// opponent generated by _master.py --variant is ONE row in that table and not 31
|
||||
// filenames typed out here (each of which is a chance to typo one and have a body
|
||||
// vanish for a frame — the oldest bug in this file).
|
||||
...ZFAMS.flatMap(famFrames),
|
||||
'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
|
||||
@@ -582,8 +576,13 @@ function drawZombie(zb) {
|
||||
// tennis points against you. The 'r' family is drawn off the LIVING opponent's poses instead (a
|
||||
// ready stance, a shuffle pair and a real forehand, at every wound stage). 'c' keeps its shamble
|
||||
// untouched, because in the gore mode shambling is exactly what the turned CPU should be doing.
|
||||
const v = rk ? (SPR.z_r1 ? 'r' : 'c') : zb.variant; // 'c' until the new sheets have landed
|
||||
const rNew = rk && v === 'r';
|
||||
// WHICH FAMILY THIS BODY WEARS. `fam` rides on the zombie (spawnRacketZombie leaves it unset, so
|
||||
// the club pro is 'r'), which is what lets a generated variant be a different opponent rather than
|
||||
// a different game: every name below is built from it. Falls back to the turned CPU's shamble
|
||||
// while a family's sheets have not landed.
|
||||
const fam = zb.fam || 'r';
|
||||
const v = rk ? (SPR['z_' + fam + '1'] ? fam : 'c') : zb.variant;
|
||||
const rNew = rk && v !== 'c';
|
||||
let N = ZFRAMES[v] || 2;
|
||||
if (!SPR['z_' + v + N]) N = 2; // frames still loading (or sheet rejected): 2-frame fallback
|
||||
// same CYCLE frequency as before — more frames subdivide the stride, they don't speed it up
|
||||
@@ -643,7 +642,7 @@ function drawZombie(zb) {
|
||||
// ...stepped off GROUND COVERED, the same clock the healthy cycle two blocks up uses. It was
|
||||
// walkT — a wall clock — so a wounded opponent's legs cycled at a fixed cadence however fast
|
||||
// it was actually moving, which is the foot-slide this project banned for the heavy bodies.
|
||||
const base = 'z_rw' + st;
|
||||
const base = 'z_' + fam + 'w' + st;
|
||||
const want = zb.pose === 'swing' ? base + 's'
|
||||
: zb.pose === 'run' && SPR[base + 'b'] && stepFrame((zb.dist || 0) / TUNE.racketStride, 2) ? base + 'b'
|
||||
: base;
|
||||
@@ -695,7 +694,7 @@ function drawZombie(zb) {
|
||||
if (zb.showFrame) {
|
||||
// a wound stage that has not been drawn yet degrades to the HEALTHY frame rather than to the
|
||||
// walk cycle — the bench prints what it actually drew, so the substitution is visible
|
||||
const heal = zb.showFrame.replace(/^z_rw\d/, 'z_r');
|
||||
const heal = zb.showFrame.replace(new RegExp('^z_' + fam + 'w\\d'), 'z_' + fam);
|
||||
if (SPR[zb.showFrame]) name = zb.showFrame;
|
||||
else if (SPR[heal]) name = heal;
|
||||
}
|
||||
@@ -793,7 +792,8 @@ function drawBenchHud() {
|
||||
const y = CARD_TOP + 4;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.font = B(20);
|
||||
shadowText(BENCH[b.i].name, W / 2, y, UI.cream);
|
||||
shadowText((ZFAMS.length > 1 ? ZFAMS[b.fam].toUpperCase() + ' · ' : '') + 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
|
||||
@@ -809,7 +809,9 @@ function drawBenchHud() {
|
||||
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);
|
||||
shadowText(IS_TOUCH ? 'TAP: NEXT STATE'
|
||||
: 'SPACE: NEXT STATE ←/→: WOUND STAGE' + (ZFAMS.length > 1 ? ' ↑/↓: OPPONENT' : ''),
|
||||
W / 2, H - 26, UI.dim);
|
||||
}
|
||||
|
||||
function drawDrillHud() {
|
||||
|
||||
Reference in new issue
Block a user