The leagues, phase 4: the duel learns, tin tiers by league, level buffs, the board wears the badge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-29 12:53:12 +02:00
1 parent 76d88030e5
commit 787877bce6
10 files changed
+175 -24

No files matched your search

+64 -2
View File
@@ -89,13 +89,13 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
// ---- game modules (real code) ----------------------------------------------
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { update, forceZombies, startGore, splatPoint, restartRun, leaguePace, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { update, forceZombies, startGore, splatPoint, restartRun, leaguePace, duelSkill, tinLadder, deliverTin, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs } = await import('./src/beats.js');
const { mirror, netMsg, netTest, netTick } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
// player would, so the tests step a live court
const startDrill = (...a) => { const r = _startDrill(...a); G.modal = null; G.modalQ.length = 0; return r; };
const { zombies, spawnZombie, pellets, parts, explode } = await import('./src/zombies.js');
const { zombies, spawnZombie, pellets, parts, explode, hurtPlayer } = await import('./src/zombies.js');
const { killPlayer } = await import('./src/zombies.js');
const { drillQueue, drillLegal, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
const cfg = await import('./src/config.js');
@@ -2268,5 +2268,67 @@ test('THE LEAGUES: the pace tier scales the solo match world and nothing else',
G.league = 1;
});
test('THE DUEL LEARNS: its skill ramps with the run, and game zero is the shipped duel', () => {
G.match = null;
G.runGames = 0;
assert.equal(duelSkill(), 0, 'a fresh run duels the shipped opponent');
G.runGames = Math.round(TUNE.duelRampGames / 2);
const mid = duelSkill();
G.runGames = TUNE.duelRampGames + 10;
assert.ok(mid > 0 && mid < 1, 'the ramp is a ramp');
assert.equal(duelSkill(), 1, '...and it caps');
G.match = { opp: 5 };
assert.equal(duelSkill(), 0, 'the match opponent has its own ladder — the ramp is duel-only');
G.match = null;
// the dial relationships the ramp promises: a full-skill duel still misses and still misreads
assert.ok(TUNE.racketFlub * (1 - TUNE.duelFlubCut) > 0.05, 'a maxed duel still whiffs sometimes');
assert.ok(TUNE.duelReadMax < 1, 'a maxed duel can still be wrong-footed');
assert.ok(TUNE.racketQMax + TUNE.duelQBonus < 1, 'its returns never reach perfect quality');
});
test('THE TIN TIERS: the league deepens the duel pot, and league 5 pays double', () => {
G.runGames = 0;
prof.prof.lg = 1;
assert.ok(!tinLadder().includes('laser') && !tinLadder().includes('rocket'),
'league one, game zero: the shipped pot');
prof.prof.lg = 3;
assert.ok(tinLadder().includes('laser') && !tinLadder().includes('rocket'),
'league three adds the laser, not the rocket');
prof.prof.lg = 4;
assert.ok(tinLadder().includes('rocket'), 'league four adds the rocket');
prof.prof.lg = 1;
G.runGames = 12;
assert.ok(tinLadder().includes('laser'), 'the run\'s own barrel gates still work without a league');
G.runGames = 0;
// the double tin: an empty rack, one delivery, twice the stock at league five
const stockOf = id => { const i = G.rack.findIndex(r => r.id === id); return i < 0 ? 0 : G.rack[i].n; };
G.rack.length = 0; G.temp = null; G.deliveries = 0;
prof.prof.lg = 5;
deliverTin();
const first = G.rack[0];
assert.equal(first.n, cfg.WEAPONS[first.id].tin * 2, 'a league-five duel pays a double tin');
G.rack.length = 0; G.temp = null; G.deliveries = 0;
prof.prof.lg = 1;
deliverTin();
assert.equal(G.rack[0].n, cfg.WEAPONS[G.rack[0].id].tin, 'league one pays the shipped tin');
});
test('THE LEVEL IS YOU: the defensive buffs scale with level and level one is the shipped game', () => {
const xp0 = prof.prof.xp;
prof.prof.xp = 0;
const b1 = prof.bonus();
assert.deepEqual(b1, { melee: 1, parry: 1, grace: 1 }, 'level one changes nothing');
prof.prof.xp = prof.xpFor(prof.LEVEL_CAP);
const bMax = prof.bonus();
assert.ok(bMax.melee < 1 && bMax.melee >= 0.7, 'the swish floor drops, boundedly');
assert.ok(bMax.parry > 1 && bMax.parry <= 1.3, 'the parry reaches further, boundedly');
assert.ok(bMax.grace > 1 && bMax.grace <= 1.4, 'the grace stretches, boundedly');
// ...and the grace actually lands on the player: hurtPlayer writes the scaled window
G.mode = 'splatter'; G.hurtT = 0; G.dodgeI = 0; G.hp = 4;
hurtPlayer(1);
assert.ok(G.hurtT > TUNE.hurtGrace * 1.3, 'a maxed profile\'s grace window is visibly longer');
prof.prof.xp = xp0;
});
console.log(`\n${passed} tests passed`);
process.exit(0); // render.js schedules viewport timers at load; don't let them outlive the report