The leagues, the polish round: the way-up ribbon, upright chevrons, the breathing badges, the card counts up

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-29 15:51:59 +02:00
1 parent 87ac3294bc
commit fa6db7f77b
4 files changed
+113 -42

No files matched your search

+4 -1
View File
@@ -844,7 +844,10 @@ export const MODES = [
// (`mode`) or carries its own card and the MODES ids it fans out to (`kids`).
export const MENU = [
{ mode: 'classic', diffs: true },
{ name: 'ZOMBIE TENNIS', sub: 'THE DEAD CLUB PLAYS YOU', art: 'mode_zombie', kids: ['zombie', 'cup', 'train'] },
// the WAY-UP ribbon shows on THIS level too, so the click path to the promotion is marked
// from the first screen of the picker (main.banner reads `tag`)
{ name: 'ZOMBIE TENNIS', sub: 'THE DEAD CLUB PLAYS YOU', art: 'mode_zombie', kids: ['zombie', 'cup', 'train'],
tag: 'THE WAY UP' },
{ mode: 'gore' },
// NETPLAY (prototype): a poster with no run mode of its own — its second step is HOST / JOIN,
// and either chip starts classic tennis against the other phone (beats.startNetVs + src/net.js)
+44 -16
View File
@@ -506,7 +506,21 @@ document.getElementById('trBack').addEventListener('click', () => go('setup'));
// honours row is a COLLECTION: every slot always visible, earned badges bright, the rest dark —
// the empty slots are the incentive. The ledger reads the same stats module the BODY COUNT does.
const fig = (label, val) => `<div class="fig"><b>${label}</b><i>${val}</i></div>`;
// THE CARD COUNTS UP (Oliver): bars sweep in and every number climbs fast on entry. setTimeout,
// not rAF — the menu parks the game loop (the bracket's own rule). Timers are cancelled on the
// next build so a re-entry never races itself.
let cardTimers = [];
const countUp = (write, target, dur = 700) => {
const t0 = performance.now();
(function tick() {
const p = Math.min(1, (performance.now() - t0) / dur);
write(Math.round(target * (1 - Math.pow(1 - p, 3)))); // easeOutCubic: fast, then settles
if (p < 1) cardTimers.push(setTimeout(tick, 30));
})();
};
function buildCard() {
for (const t of cardTimers) clearTimeout(t);
cardTimers = [];
const lg = leagueOf(profile.prof.lg), pr = profile.progress();
document.getElementById('cdName').textContent = stats.savedName() || 'THE WALK-ON';
document.getElementById('cdBadge').src = `assets/badge_l${lg.id}.png?v=${SPRV}`;
@@ -515,14 +529,20 @@ function buildCard() {
el.style.color = lg.color;
document.getElementById('cdLevel').textContent =
'LEVEL ' + pr.level + ' OF ' + profile.LEVEL_CAP;
document.getElementById('cdFill').style.width = Math.round(pr.frac * 100) + '%';
document.getElementById('cdXp').textContent = profile.prof.xp.toLocaleString('en-GB') + ' XP'
+ (pr.need ? ' · ' + (pr.need - pr.into).toLocaleString('en-GB') + ' TO LEVEL ' + (pr.level + 1)
: ' · THE CAP');
// the CAREER bar is the long form the splash deliberately does not carry: whole-curve progress,
// honest about how far level 50 is
document.getElementById('cdCareerFill').style.width =
Math.min(100, Math.round(profile.prof.xp / profile.xpFor(profile.LEVEL_CAP) * 100)) + '%';
// both bars sweep from empty to their mark (the 0.7s eased transition in the CSS)
for (const [id, w] of [['cdFill', Math.round(pr.frac * 100)],
['cdCareerFill', Math.min(100, Math.round(profile.prof.xp / profile.xpFor(profile.LEVEL_CAP) * 100))]]) {
const bar = document.getElementById(id);
bar.style.transition = 'none'; bar.style.width = '0%';
void bar.offsetWidth; // commit the empty bar before the sweep
bar.style.transition = '';
cardTimers.push(setTimeout(() => { bar.style.width = w + '%'; }, 40));
}
const xpTail = pr.need
? ' · ' + (pr.need - pr.into).toLocaleString('en-GB') + ' TO LEVEL ' + (pr.level + 1)
: ' · THE CAP';
const cdXp = document.getElementById('cdXp');
countUp(v => cdXp.textContent = v.toLocaleString('en-GB') + ' XP' + xpTail, profile.prof.xp);
const row = document.getElementById('cdBadges');
row.innerHTML = '';
for (const l of LEAGUES) {
@@ -542,14 +562,22 @@ function buildCard() {
const g = stats.read('all', undefined, stats.MODE_GORE),
z = stats.read('all', undefined, stats.MODE_MATCH),
c = stats.read('all', undefined, stats.MODE_CLASSIC);
document.getElementById('cdFigs1').innerHTML =
fig('KILLS', (g.kills + z.kills + c.kills).toLocaleString('en-GB'))
+ fig('MULTI-KILLS', (g.multi + z.multi + c.multi).toLocaleString('en-GB'))
+ fig('DEATHS', (g.deaths + z.deaths + c.deaths).toLocaleString('en-GB'));
document.getElementById('cdFigs2').innerHTML =
fig('BEST RUN', g.longest + ' G')
+ fig('MOST FELLED', z.longest)
+ fig('RUNS', (g.matches + z.matches + c.matches).toLocaleString('en-GB'));
// the ledger climbs: rows are [box id, [label, target, format]...]; every value starts at 0 and
// counts up through the same easing the bars sweep with
for (const [boxId, rows] of [
['cdFigs1', [['KILLS', g.kills + z.kills + c.kills],
['MULTI-KILLS', g.multi + z.multi + c.multi],
['DEATHS', g.deaths + z.deaths + c.deaths]]],
['cdFigs2', [['BEST RUN', g.longest, v => v + ' G'],
['MOST FELLED', z.longest],
['RUNS', g.matches + z.matches + c.matches]]]]) {
const box = document.getElementById(boxId);
box.innerHTML = rows.map(([label]) => fig(label, '0')).join('');
[...box.querySelectorAll('.fig i')].forEach((cell, i) => {
const fmt = rows[i][2] || (v => v.toLocaleString('en-GB'));
countUp(v => cell.textContent = fmt(v), rows[i][1]);
});
}
refreshLook();
}
document.getElementById('plaque').addEventListener('click', () => { buildCard(); go('card'); });