Stop calling three different things "the ladder"
Oliver: "the talk about ladder and rung is extremely confusing." It was, and it
was my vocabulary, not his reading. "Ladder" had come to mean three unrelated
things in this codebase — the sequence of opponents in zombie tennis, the
sequence of WOUND STAGES on one body, and the name of a stats screen — and
"rung" meant a position in whichever of those you happened to be reading about.
One word per thing now:
the OPPONENTS who you play, in order, one game each (config.OPPONENTS,
matchFoe(n), zb.foe). Never "rungs".
WOUND STAGES healthy -> arm off -> headless, on one body. Unchanged, and
now the only thing allowed to be called a progression.
a FAMILY the sprite set a body wears (config.ZFAMS).
Renames: LADDER_TRAITS -> OPPONENTS, matchRung -> matchFoe, zb.rung -> zb.foe,
and the stats screen THE LADDER -> OPPONENTS FELLED, which is what it lists.
The 'L0'..'L9' record keys are deliberately NOT renamed: they are written into
saved run records, and renaming them would orphan every match anyone has
played. A comment says so where they are built.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2487b784a
commit
df454c5d92
6 files changed
+40
-37
No files matched your search
+7
-7
@@ -7,7 +7,7 @@
|
|||||||
// (beats <-> gameplay and beats <-> zombies are runtime-only import cycles, same pattern as
|
// (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.)
|
// input <-> gameplay: the imported bindings are only touched inside functions, never at load.)
|
||||||
|
|
||||||
import { TUNE, UI, bossArt, matchRung, BENCH, ZFAMS, COURT, HOME } from './config.js';
|
import { TUNE, UI, bossArt, matchFoe, BENCH, ZFAMS, COURT, HOME } from './config.js';
|
||||||
import { G, P, O, ball, score, setupPoint, sendBoy } from './state.js';
|
import { G, P, O, ball, score, setupPoint, sendBoy } from './state.js';
|
||||||
import { voice, zombieDuck, aww } from './audio.js';
|
import { voice, zombieDuck, aww } from './audio.js';
|
||||||
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
import { zombies, pellets, extras, shots, spawnRacketZombie, spawnChapterBoss, spawnZombie, explode, killPlayer } from './zombies.js';
|
||||||
@@ -344,12 +344,12 @@ function matchOpponent() {
|
|||||||
spawnRacketZombie();
|
spawnRacketZombie();
|
||||||
const zb = zombies.find(z => z.variant === 'r');
|
const zb = zombies.find(z => z.variant === 'r');
|
||||||
zb.dueled = true; // wakes racketAI, same as a fresh duel
|
zb.dueled = true; // wakes racketAI, same as a fresh duel
|
||||||
// WHO THIS ONE IS. The rung carries a name off the chapter roster and a set of dial scalings —
|
// WHO THIS ONE IS. The opponent carries a name off the chapter roster and a set of dial scalings —
|
||||||
// pure data, read where the dials are already read (see matchRung in config). It rides on the
|
// pure data, read where the dials are already read (see matchFoe in config). It rides on the BODY
|
||||||
// BODY so that killZombie can file the kill under it without asking G what rung we were on.
|
// so killZombie can file the kill under it without asking G which opponent we were on.
|
||||||
zb.rung = matchRung(G.match.opp);
|
zb.foe = matchFoe(G.match.opp);
|
||||||
zb.fam = zb.rung.fam; // ...and the body that rung wears (config.ZFAMS)
|
zb.fam = zb.foe.fam; // ...and the body it wears (config.ZFAMS)
|
||||||
G.card = { title: zb.rung.name, sub1: zb.rung.line, color: UI.hot, t: 2.4 };
|
G.card = { title: zb.foe.name, sub1: zb.foe.line, color: UI.hot, t: 2.4 };
|
||||||
zb.hp = zb.hpMax = TUNE.matchOppHp; // a FLAT pool: with regicide gone the body is pure
|
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
|
// scoreboard (a quarter per point), so a deeper pool would
|
||||||
// change nothing — the ladder escalates in racketAI instead
|
// change nothing — the ladder escalates in racketAI instead
|
||||||
|
|||||||
+22
-19
@@ -667,22 +667,23 @@ export const BOSS_ART = [
|
|||||||
export const bossArt = ch => ch % 10 === 0 ? BOSS_ART[BOSS_ART.length - 1]
|
export const bossArt = ch => ch % 10 === 0 ? BOSS_ART[BOSS_ART.length - 1]
|
||||||
: BOSS_ART[(ch - 1 - Math.floor(ch / 10)) % (BOSS_ART.length - 1)];
|
: BOSS_ART[(ch - 1 - Math.floor(ch / 10)) % (BOSS_ART.length - 1)];
|
||||||
|
|
||||||
// ---- THE MATCH LADDER. Zombie tennis walks the SAME dead club staff onto the court as the gore
|
// ---- THE MATCH OPPONENTS. In zombie tennis you play a SEQUENCE of opponents, one game each:
|
||||||
// mode's chapter bosses — the roster is the joke, and it is worth telling twice: in gore they come
|
// opponent 1, opponent 2, and so on. This is that list. It walks the SAME dead club staff onto the
|
||||||
// for you, here they play you at tennis. So rung N takes design N's NAME (bossArt, champion on
|
// court as the gore mode's chapter bosses — the roster is the joke and it is worth telling twice:
|
||||||
// ten) and a PERSONALITY, and the body stays the racket zombie's, which is the one drawn to play.
|
// in gore they come for you, here they play you at tennis. So opponent N takes design N's NAME
|
||||||
|
// (bossArt, the champion on ten) and a PERSONALITY.
|
||||||
//
|
//
|
||||||
// A personality is DIALS, not new mechanics: every field below multiplies a number racketAI and the
|
// A personality is DIALS, not new mechanics: every field below multiplies a number racketAI and the
|
||||||
// return shaping already read, so a rung is a data row and never a branch. All optional.
|
// return shaping already read, so an opponent is a data row and never a branch. All optional.
|
||||||
// pace scale on its returns' speed wide scale on how far off centre they may land
|
// 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
|
// 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
|
// 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'.
|
// fam which SPRITE FAMILY this opponent wears (config.ZFAMS). Absent = the club pro's body.
|
||||||
// This is the payoff of _master.py --variant: generate an opponent, add its key to ZFAMS,
|
// This is the payoff of _master.py --variant: generate a body, add its key to ZFAMS,
|
||||||
// name it on a rung, and that rung is a different body all the way down its wound ladder.
|
// name it here, and that opponent walks on as it — through all three wound stages.
|
||||||
// short 8 characters, because that is the board's name slot — the match board names
|
// 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
|
// the thing you are playing instead of calling it THEM
|
||||||
const LADDER_TRAITS = [
|
const OPPONENTS = [
|
||||||
{ short: 'KEEPER', line: 'HE MOWED THIS LAWN FOR THIRTY YEARS.', speed: 0.85, pace: 0.9, wide: 0.85 },
|
{ short: 'KEEPER', line: 'HE MOWED THIS LAWN FOR THIRTY YEARS.', speed: 0.85, pace: 0.9, wide: 0.85 },
|
||||||
{ short: 'JUDGE', line: 'EVERY BALL OF YOURS IS OUT.', wide: 1.25, flub: 0.8 },
|
{ short: 'JUDGE', line: 'EVERY BALL OF YOURS IS OUT.', wide: 1.25, flub: 0.8 },
|
||||||
{ short: 'UMPIRE', line: 'HE WILL NOT BE OVERRULED.', speed: 1.1, flub: 0.7 },
|
{ short: 'UMPIRE', line: 'HE WILL NOT BE OVERRULED.', speed: 1.1, flub: 0.7 },
|
||||||
@@ -694,11 +695,11 @@ const LADDER_TRAITS = [
|
|||||||
{ short: 'SELLER', line: 'WITH CREAM.', pace: 1.1, speed: 1.1, wide: 1.15 },
|
{ short: 'SELLER', line: 'WITH CREAM.', pace: 1.1, speed: 1.1, wide: 1.15 },
|
||||||
{ short: 'CHAMPION', line: 'HE RETURNS EVERYTHING.', pace: 1.25, speed: 1.3, wide: 1.25, flub: 0.5 },
|
{ short: 'CHAMPION', line: 'HE RETURNS EVERYTHING.', pace: 1.25, speed: 1.3, wide: 1.25, flub: 0.5 },
|
||||||
];
|
];
|
||||||
// Rung N's identity. The chapter roster's own walk is reused wholesale (bossArt), so the champion
|
// Who opponent N is. The chapter roster's own order is reused wholesale (bossArt), so the champion
|
||||||
// lands on rung 10 exactly as he lands on chapter 10 and the two rosters can never drift apart.
|
// lands on opponent 10 exactly as he lands on chapter 10 and the two rosters can never drift apart.
|
||||||
export const matchRung = n => {
|
export const matchFoe = n => {
|
||||||
const art = bossArt(n), i = BOSS_ART.indexOf(art);
|
const art = bossArt(n), i = BOSS_ART.indexOf(art);
|
||||||
return { name: art.name, key: 'L' + i, fam: 'r', ...LADDER_TRAITS[i] };
|
return { name: art.name, key: 'L' + i, fam: 'r', ...OPPONENTS[i] };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- THE SPRITE BENCH's stops (back room only, see beats.startBench). Each is either an ANIMATED
|
// ---- THE SPRITE BENCH's stops (back room only, see beats.startBench). Each is either an ANIMATED
|
||||||
@@ -727,7 +728,9 @@ export const BENCH_STAGES = ['HEALTHY', 'ARM OFF', 'HEADLESS'];
|
|||||||
// needs the names — the preloader, drawZombie's wound lookup, the sprite bench — derives them from
|
// 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
|
// 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.
|
// preload list is 31 chances to typo one and have a body silently vanish for a frame.
|
||||||
export const ZFAMS = ['r'];
|
// 'br' is THE BRUTE, generated with _master.py --variant off the master sheets — the same creature
|
||||||
|
// the gore mode's 'g' shambler is, now playing tennis: twice the width, a racket lost in one fist.
|
||||||
|
export const ZFAMS = ['r', 'br'];
|
||||||
const POSES = ['bounce', 'toss', 'serve', 'fall1', 'fall2', 'fall3', 'rise'];
|
const POSES = ['bounce', 'toss', 'serve', 'fall1', 'fall2', 'fall3', 'rise'];
|
||||||
export const famFrames = f => [
|
export const famFrames = f => [
|
||||||
`z_${f}1`, `z_${f}2`, `z_${f}3`, `z_${f}4`, // stance, shuffle A, shuffle B, forehand
|
`z_${f}1`, `z_${f}2`, `z_${f}3`, `z_${f}4`, // stance, shuffle A, shuffle B, forehand
|
||||||
@@ -765,12 +768,12 @@ export const ENEMIES = {
|
|||||||
for (let i = 0; i < BOSS_ART.length; i++) // the chapter bosses, named where they already are
|
for (let i = 0; i < BOSS_ART.length; i++) // the chapter bosses, named where they already are
|
||||||
ENEMIES['B' + i] = { name: BOSS_ART[i].name, spr: BOSS_ART[i].walk[0], boss: true,
|
ENEMIES['B' + i] = { name: BOSS_ART[i].name, spr: BOSS_ART[i].walk[0], boss: true,
|
||||||
sub: i === BOSS_ART.length - 1 ? 'THE FINAL · EVERY TENTH CHAPTER' : 'CHAPTER BOSS' };
|
sub: i === BOSS_ART.length - 1 ? 'THE FINAL · EVERY TENTH CHAPTER' : 'CHAPTER BOSS' };
|
||||||
// ...and the same roster again as MATCH opponents ('L0'..), because that is who walks on in zombie
|
// ...and the same roster again as MATCH opponents, because that is who walks on in zombie tennis.
|
||||||
// tennis. Separate keys, not shared ones: the two are different fights with different bodies, they
|
// Separate keys, not shared ones: the two are different fights with different bodies, they are
|
||||||
// are counted on different screens now (the mode split in stats.js), and one row cannot mean both.
|
// counted on different screens (the mode split in stats.js), and one row cannot mean both. The 'L'
|
||||||
|
// prefix is kept as-is because it is written into saved run records — renaming it would orphan them.
|
||||||
for (let i = 0; i < BOSS_ART.length; i++)
|
for (let i = 0; i < BOSS_ART.length; i++)
|
||||||
ENEMIES['L' + i] = { name: BOSS_ART[i].name, spr: 'z_r1', ladder: true,
|
ENEMIES['L' + i] = { name: BOSS_ART[i].name, spr: 'z_r1', sub: OPPONENTS[i].line };
|
||||||
sub: LADDER_TRAITS[i].line };
|
|
||||||
|
|
||||||
// Which wound stage a body is in: 0 = unhurt, 1..n = the skins, spread evenly over its full health.
|
// Which wound stage a body is in: 0 = unhurt, 1..n = the skins, spread evenly over its full health.
|
||||||
// This is the rule the chapter boss already used, and it turns out to reproduce the brute's old
|
// This is the rule the chapter boss already used, and it turns out to reproduce the brute's old
|
||||||
|
|||||||
+4
-4
@@ -154,7 +154,7 @@ function doHit(ch, quality, aim) {
|
|||||||
// rungs take it back.
|
// rungs take it back.
|
||||||
// ...and the RUNG's own pace trait rides on top, inside the cap: full pace is still the ceiling,
|
// ...and the RUNG's own pace trait rides on top, inside the cap: full pace is still the ceiling,
|
||||||
// a rung just gets there sooner or later than the flat ladder would (see matchRung in config).
|
// a rung just gets there sooner or later than the flat ladder would (see matchRung in config).
|
||||||
s *= Math.min(1, (TUNE.matchOppPace + (G.match.opp - 1) * TUNE.matchOppPacePer) * (ch.rung?.pace || 1));
|
s *= Math.min(1, (TUNE.matchOppPace + (G.match.opp - 1) * TUNE.matchOppPacePer) * (ch.foe?.pace || 1));
|
||||||
}
|
}
|
||||||
if (G.match) {
|
if (G.match) {
|
||||||
// THE OPPONENT'S WIDTH IS THE LADDER'S THIRD DIAL. Its returns scatter off aimError x (1 - 0.55
|
// THE OPPONENT'S WIDTH IS THE LADDER'S THIRD DIAL. Its returns scatter off aimError x (1 - 0.55
|
||||||
@@ -162,7 +162,7 @@ function doHit(ch, quality, aim) {
|
|||||||
// balls". Opponent one keeps its returns inside matchWideBase of the centre line; each rung
|
// balls". Opponent one keeps its returns inside matchWideBase of the centre line; each rung
|
||||||
// earns matchWidePer more width, so the line-painting is the LATE ladder, not the opening one.
|
// earns matchWidePer more width, so the line-painting is the LATE ladder, not the opening one.
|
||||||
if (ch.side !== 'P') {
|
if (ch.side !== 'P') {
|
||||||
const wide = (TUNE.matchWideBase + (G.match.opp - 1) * TUNE.matchWidePer) * (ch.rung?.wide || 1);
|
const wide = (TUNE.matchWideBase + (G.match.opp - 1) * TUNE.matchWidePer) * (ch.foe?.wide || 1);
|
||||||
tx = Math.max(-wide, Math.min(wide, tx));
|
tx = Math.max(-wide, Math.min(wide, tx));
|
||||||
}
|
}
|
||||||
// THE PLACEMENT, MADE VISIBLE: your aimed rally hit stamps its landing point, and render draws a
|
// THE PLACEMENT, MADE VISIBLE: your aimed rally hit stamps its landing point, and render draws a
|
||||||
@@ -1050,7 +1050,7 @@ function racketAI(rz, dt) {
|
|||||||
// 2.8 m/s lumber was tuned for one sudden-death point against a fresh player, and across a
|
// 2.8 m/s lumber was tuned for one sudden-death point against a fresh player, and across a
|
||||||
// whole game it read as "very weak and slow": opponent one already moves at matchSpeedBase.
|
// whole game it read as "very weak and slow": opponent one already moves at matchSpeedBase.
|
||||||
const spd = TUNE.racketSpeed * (G.match ? (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer)
|
const spd = TUNE.racketSpeed * (G.match ? (TUNE.matchSpeedBase + (G.match.opp - 1) * TUNE.matchSpeedPer)
|
||||||
* (rz.rung?.speed || 1) : 1);
|
* (rz.foe?.speed || 1) : 1);
|
||||||
const step = Math.min(spd * dt, d);
|
const step = Math.min(spd * dt, d);
|
||||||
rz.dist = (rz.dist || 0) + step; // ground covered — his walk cycle steps off this, not the clock
|
rz.dist = (rz.dist || 0) + step; // ground covered — his walk cycle steps off this, not the clock
|
||||||
rz.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt;
|
rz.x += dx / d * step; rz.z += dz / d * step; rz.runT += dt;
|
||||||
@@ -1062,7 +1062,7 @@ function racketAI(rz, dt) {
|
|||||||
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
|
&& ball.y < CONTACT.maxY && ball.y > CONTACT.minY) {
|
||||||
// ...and the match has its OWN flub base: the duel's 0.2 hands a fifth of all returns away,
|
// ...and the match has its OWN flub base: the duel's 0.2 hands a fifth of all returns away,
|
||||||
// which across a game is an opponent that beats itself.
|
// which across a game is an opponent that beats itself.
|
||||||
const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) * (rz.rung?.flub || 1)
|
const flub = G.match ? TUNE.matchFlub * Math.pow(TUNE.matchFlubKeep, G.match.opp - 1) * (rz.foe?.flub || 1)
|
||||||
: TUNE.racketFlub;
|
: TUNE.racketFlub;
|
||||||
if (Math.random() < flub) rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
if (Math.random() < flub) rz.swingCool = 0.5; // whiff — it hesitates (rarer per rung of the ladder)
|
||||||
else { rz.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; trySwing(rz, true, TUNE.racketQMax); rz.swingT = -1; rz.swingCool = 0.3; }
|
else { rz.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; trySwing(rz, true, TUNE.racketQMax); rz.swingT = -1; rz.swingCool = 0.3; }
|
||||||
|
|||||||
+3
-3
@@ -2142,9 +2142,9 @@ export function render() {
|
|||||||
// Silkscreen pixel font — no code-drawn slots or bevels painted on top of the art.
|
// Silkscreen pixel font — no code-drawn slots or bevels painted on top of the art.
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
// ...and the match names the thing across the net rather than calling it THEM. Eight characters,
|
// ...and the match names the thing across the net rather than calling it THEM. Eight characters,
|
||||||
// which is the name slot — see `short` on the ladder rows.
|
// which is the name slot — see `short` on the opponent rows in config.
|
||||||
const rung = G.match && zombies.find(z => z.variant === 'r' && z.rung);
|
const foe = G.match && zombies.find(z => z.variant === 'r' && z.foe);
|
||||||
const names = G.match ? ['YOU', (rung && rung.rung.short) || 'THEM']
|
const names = G.match ? ['YOU', (foe && foe.foe.short) || 'THEM']
|
||||||
: splat ? ['YOU', 'THEM'] : ['YOU', 'CPU'];
|
: splat ? ['YOU', 'THEM'] : ['YOU', 'CPU'];
|
||||||
for (let i = 0; i < 2 && !benchOn; 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
|
const cy = 12 + i * 18, svr = (score.server === 'P') === (i === 0); // rows aligned to the art's two bands
|
||||||
|
|||||||
+1
-1
@@ -232,7 +232,7 @@ export function drawStats(period, v, mode = MODE_GORE) {
|
|||||||
const d = read(period, undefined, mode);
|
const d = read(period, undefined, mode);
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
$('stPeriod').textContent = period === 'last' ? 'LAST RUN' : period === '7d' ? '7 DAYS' : 'ALL TIME';
|
$('stPeriod').textContent = period === 'last' ? 'LAST RUN' : period === '7d' ? '7 DAYS' : 'ALL TIME';
|
||||||
$('stTitle').textContent = mode === MODE_MATCH ? 'THE LADDER'
|
$('stTitle').textContent = mode === MODE_MATCH ? 'OPPONENTS FELLED'
|
||||||
: mode === MODE_CLASSIC ? 'THE RECORD' : 'BODY COUNT';
|
: mode === MODE_CLASSIC ? 'THE RECORD' : 'BODY COUNT';
|
||||||
const figs = mode === MODE_MATCH
|
const figs = mode === MODE_MATCH
|
||||||
? [['FELLED', n(d.kills)], ['DEEPEST', n(d.longest)], ['RUNS', n(d.matches)]]
|
? [['FELLED', n(d.kills)], ['DEEPEST', n(d.longest)], ['RUNS', n(d.matches)]]
|
||||||
|
|||||||
+3
-3
@@ -491,10 +491,10 @@ function killZombie(i, quiet) {
|
|||||||
// down, so it earns a row. The barrel is loot and scores nothing anywhere else in the game —
|
// down, so it earns a row. The barrel is loot and scores nothing anywhere else in the game —
|
||||||
// keep that agreement here too. A chapter boss files under its DESIGN, so the screen can name
|
// keep that agreement here too. A chapter boss files under its DESIGN, so the screen can name
|
||||||
// the one that has been killing you.
|
// the one that has been killing you.
|
||||||
// A MATCH OPPONENT FILES UNDER ITS RUNG, not under the generic body it wears: the ladder screen
|
// A MATCH OPPONENT FILES UNDER WHO IT IS, not under the generic body it wears: the screen is a
|
||||||
// is a list of who you have felled, and 'r' is a body, not a name. zb.rung is set by matchOpponent.
|
// list of who you have felled, and 'r' is a body, not a name. zb.foe is set by matchOpponent.
|
||||||
if (zb.variant !== 'o')
|
if (zb.variant !== 'o')
|
||||||
tally(zb.rung ? zb.rung.key
|
tally(zb.foe ? zb.foe.key
|
||||||
: zb.variant === 'B' ? 'B' + Math.max(0, BOSS_ART.indexOf(zb.art))
|
: zb.variant === 'B' ? 'B' + Math.max(0, BOSS_ART.indexOf(zb.art))
|
||||||
: zb.variant, CAUSE);
|
: zb.variant, CAUSE);
|
||||||
zombies.splice(i, 1);
|
zombies.splice(i, 1);
|
||||||
|
|||||||
Reference in new issue
Block a user