The ladder has names, the bodies play tennis, and the stats screen splits
THE SPRITES. The match opponent borrowed the turned CPU's art — a body drawn to SHAMBLE at the camera with its racket hanging at its side, which is what was playing tennis points against you. It has its own family now, 'r', drawn off the LIVING opponent's poses: a ready stance, a shuffle pair and a real forehand, at every wound stage, men's and women's — 20 frames, six batches. 'c' keeps its shamble untouched, because in the gore mode shambling is exactly what the turned CPU should be doing. The pose comes from racketAI's own `pose`, so the picture cannot drift from the behaviour, and a one-armed opponent still plays tennis instead of reverting to a walk. Two pipeline bugs found doing it, both fixed at the root in assets/: - _fine.py keyed EVERY magenta pixel, not just the background, and the generator paints racket strings in the backdrop colour more often than not — so frame after frame installed a man holding an empty hoop. Background is now the region connected to the border; magenta left INSIDE a body is repainted pale grey rather than punched out. - the flood fill silently did nothing, because Image.fromarray images are read-only and PIL's floodfill swallows the write error. .copy() plus a real error if it ever fills zero pixels. THE LADDER (handoff slice 2). Rung N takes design N's name off the chapter roster — the club's dead staff play you at tennis here and come for you in gore, same joke twice — plus a personality that is DIALS, not new mechanics: pace, width, flub and speed scalings multiplying numbers racketAI already reads. A rung is a data row. Name plate on the walk-on, and the kill files under the rung rather than under the generic body. THE STATS SCREEN follows the mode split: BODY COUNT / THE LADDER / THE RECORD behind a mode strip, each with its own three figures, its own roster heading and its own footer. The weapon shelf and the roster hide in the modes that have neither, rather than printing a line about something the mode cannot do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c89d4a3ff9
commit
c629ae1108
16 files changed
+138
-33
No files matched your search
+49
-16
@@ -209,19 +209,14 @@ const wIcon = id => id === 'ball' ? 'w_ball' : WEAPONS[id] ? WEAPONS[id].icon :
|
||||
// ----------------------------------------------------------------- screens ----
|
||||
// Both screens are built here rather than in main.js: they are the only readers of the shapes
|
||||
// above, and main.js is the boot file, not a view layer.
|
||||
export function drawStats(period, v) {
|
||||
const d = read(period);
|
||||
document.getElementById('stPeriod').textContent =
|
||||
period === 'last' ? 'LAST MATCH' : period === '7d' ? '7 DAYS' : 'ALL TIME';
|
||||
document.getElementById('stFigs').innerHTML = [
|
||||
['KILLS', n(d.kills)], ['MULTI-KILLS', n(d.multi)], ['DEATHS', n(d.deaths)],
|
||||
].map(([k, val], i) => `<div class="fig${i === 2 ? ' bad' : ''}"><b>${k}</b><i>${val}</i></div>`).join('');
|
||||
|
||||
// sorted by count, and only bodies you have actually met — an empty roster of everything would
|
||||
// read as a checklist of things to go and do, which this screen is not
|
||||
const rows = Object.entries(d.z).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]);
|
||||
// ONE SCREEN, THREE MODES. The three do not share a currency — see the record-shape note in
|
||||
// endRun — so they do not share a layout either: gore counts bodies and weapons, zombie tennis
|
||||
// counts NAMED opponents felled, classic tennis counts matches won and lost. What they do share is
|
||||
// the furniture (the period tabs, the figure strip, the roster list, the footer), so this is one
|
||||
// function with three sets of words rather than three screens to keep in step.
|
||||
const roster = (el, rows, v, empty) => {
|
||||
const top = rows.length ? rows[0][1] : 1;
|
||||
document.getElementById('stZ').innerHTML = rows.length ? rows.map(([k, c]) => {
|
||||
el.innerHTML = rows.length ? rows.map(([k, c]) => {
|
||||
// a key with no row is a save written by an older build: name it off the key, stripped, because
|
||||
// this is the one string on the screen that does not come from our own tables
|
||||
const e = ENEMIES[k] || { name: k.replace(/[^A-Za-z0-9]/g, '').toUpperCase() || '?', spr: 'z_a1', sub: '' };
|
||||
@@ -230,15 +225,53 @@ export function drawStats(period, v) {
|
||||
<div class="zb"><b>${e.name}</b><span>${n(c)}</span>
|
||||
<div class="pb"><s style="width:${Math.max(3, c / top * 100)}%"></s></div>
|
||||
<i>${e.sub}</i></div></div>`;
|
||||
}).join('') : '<p class="none">NOTHING YET. GO AND EARN A ROW.</p>';
|
||||
}).join('') : `<p class="none">${empty}</p>`;
|
||||
};
|
||||
|
||||
export function drawStats(period, v, mode = MODE_GORE) {
|
||||
const d = read(period, undefined, mode);
|
||||
const $ = id => document.getElementById(id);
|
||||
$('stPeriod').textContent = period === 'last' ? 'LAST RUN' : period === '7d' ? '7 DAYS' : 'ALL TIME';
|
||||
$('stTitle').textContent = mode === MODE_MATCH ? 'THE LADDER'
|
||||
: mode === MODE_CLASSIC ? 'THE RECORD' : 'BODY COUNT';
|
||||
const figs = mode === MODE_MATCH
|
||||
? [['FELLED', n(d.kills)], ['DEEPEST', n(d.longest)], ['RUNS', n(d.matches)]]
|
||||
: mode === MODE_CLASSIC
|
||||
? [['PLAYED', n(d.matches)], ['WON', n(d.matches - d.deaths)], ['LOST', n(d.deaths)]]
|
||||
: [['KILLS', n(d.kills)], ['MULTI-KILLS', n(d.multi)], ['DEATHS', n(d.deaths)]];
|
||||
// the third figure is the RED one in gore and in classic (deaths, matches lost); in the match it
|
||||
// is just how many runs you have played, so nothing on that strip is bad news
|
||||
const badIdx = mode === MODE_MATCH ? -1 : 2;
|
||||
$('stFigs').innerHTML = figs.map(([k, val], i) =>
|
||||
`<div class="fig${i === badIdx ? ' bad' : ''}"><b>${k}</b><i>${val}</i></div>`).join('');
|
||||
|
||||
// sorted by count, and only bodies you have actually met — an empty roster of everything would
|
||||
// read as a checklist of things to go and do, which this screen is not
|
||||
const rows = Object.entries(d.z).filter(([, c]) => c > 0).sort((a, b) => b[1] - a[1]);
|
||||
$('stZHdr').firstChild.textContent = mode === MODE_MATCH ? 'OPPONENTS FELLED' : 'BY OPPONENT';
|
||||
roster($('stZ'), rows, v, mode === MODE_MATCH ? 'NOBODY HAS GONE DOWN YET.' : 'NOTHING YET. GO AND EARN A ROW.');
|
||||
|
||||
// THE WEAPON SHELF IS THE GORE MODE'S ALONE. Both tennis modes are played with plain balls (the
|
||||
// rack is hidden in both), so the section is hidden rather than printed empty — a shelf that says
|
||||
// NO WEAPONS FIRED for a mode that has none reads as something you failed to do.
|
||||
const ws = Object.entries(d.w).filter(([id, c]) => c > 0 && wIcon(id)).sort((a, b) => b[1] - a[1]);
|
||||
document.getElementById('stW').innerHTML = ws.length ? ws.map(([id, c]) =>
|
||||
// style.display, not the `hidden` attribute: #stW carries a display rule of its own and any
|
||||
// declared display beats [hidden]'s, so the shelf stayed on screen under a mode with no weapons.
|
||||
const off = mode !== MODE_GORE ? 'none' : '';
|
||||
$('stWHdr').style.display = off; $('stW').style.display = off;
|
||||
// ...and CLASSIC TENNIS has no roster either: nothing dies in it. A section printing that is a
|
||||
// line about something the mode does not do, which is worse than the section not being there.
|
||||
const noZ = mode === MODE_CLASSIC ? 'none' : '';
|
||||
$('stZHdr').style.display = noZ; $('stZ').style.display = noZ;
|
||||
$('stW').innerHTML = ws.length ? ws.map(([id, c]) =>
|
||||
`<div class="wtile"><img src="assets/${wIcon(id)}.png?v=${v}" alt=""><b>${n(c)}</b></div>`).join('')
|
||||
: '<p class="none">NO WEAPONS FIRED.</p>';
|
||||
|
||||
document.getElementById('stFoot').innerHTML =
|
||||
`<b>LONGEST RUN</b><i>${d.longest} GAME${d.longest === 1 ? '' : 'S'}</i>`;
|
||||
$('stFoot').innerHTML = mode === MODE_MATCH
|
||||
? `<b>DEEPEST RUN</b><i>${d.longest} FELLED</i>`
|
||||
: mode === MODE_CLASSIC
|
||||
? `<b>LONGEST MATCH</b><i>${d.longest} GAME${d.longest === 1 ? '' : 'S'}</i>`
|
||||
: `<b>LONGEST RUN</b><i>${d.longest} GAME${d.longest === 1 ? '' : 'S'}</i>`;
|
||||
}
|
||||
|
||||
// The real board. Async, and every failure lands on a line of copy rather than an empty screen —
|
||||
|
||||
Reference in new issue
Block a user