The level is you, in the league: quicker legs, a wider window, and a curve that pays on the way up
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7dc72009eb
commit
7e2649c888
5 files changed
+82
-16
No files matched your search
+21
@@ -491,6 +491,27 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu
|
||||
`RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud
|
||||
FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone.
|
||||
|
||||
## Session log: THE LEVEL IS YOU, IN THE LEAGUE — legs and window (2026-08-29, after the merge)
|
||||
|
||||
Oliver's deep-test question ("wie wirkt sich mein level aus? werde ich schneller, genauer?")
|
||||
exposed two honest gaps: the level's buffs were defensive-only (the plan's serve/accuracy dials
|
||||
never landed) and linearly spread over 49 levels — imperceptible where he actually plays
|
||||
(level 20, league 4: "autorun schlicht zu langsam, das timing wird zu tight"). His directive:
|
||||
gore aside, the league must reward xp SUBTLY but really; level 1 stays the untouchable baseline.
|
||||
|
||||
- **profile.bonus() is FRONT-LOADED now**: sqrt of the level fraction (the xp curve's own
|
||||
fast-early logic). Level 20 carries ~0.62 of each cap value; linear gave 0.39.
|
||||
- **THE LEAGUE PAIR**, league modes only (`G.match && !G.net` — classic keeps its picker,
|
||||
netplay stays a fair human match, gore untouched): `run` (cap +15%) multiplies INSIDE
|
||||
`assistK()`, so the mover's legs and intercept's reachability can never disagree (the
|
||||
rzChaseSpeed rule, player-side); `window` (cap +20%) widens trySwing's player contact window
|
||||
AND the update() swing-clock expiry that would otherwise close over it — and since quality is
|
||||
centre-normalised, the same millisecond error also scores HIGHER: quicker to the ball, cleaner
|
||||
when there. At level 20 that is ~+9% legs, ~+12% window.
|
||||
- Suite 125: level-1 identity, cap bounds, the front-load ratio, the league/netplay/classic
|
||||
gating — and freshSplatter zeroes xp now, because the level feeds real dials and accumulated
|
||||
test awards would silently buff every later test.
|
||||
|
||||
## THE LEAGUES LAND ON MAIN (2026-08-29, end of the build day)
|
||||
|
||||
Oliver's phone verdict after the full staging day: "super — sehr gute session. bitte merge nach
|
||||
|
||||
+1
-1
@@ -864,6 +864,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=332"></script>
|
||||
<script type="module" src="src/main.js?v=333"></script>
|
||||
</body>
|
||||
</html>
|
||||
+14
-4
@@ -1315,7 +1315,16 @@ function onBounce() {
|
||||
// them, so the same swing whiffed centimetres short there. Gore's opener keeps the old feel: its
|
||||
// tennis is a prologue, not the mode.
|
||||
export const rallyFeel = () => !!(G.match || G.classic);
|
||||
export const assistK = () => rallyFeel() ? Math.max(TUNE.autoAssist, TUNE.matchAssist) : TUNE.autoAssist;
|
||||
// THE LEVEL IS YOU, in the league (2026-08-29): the player's legs and contact window grow subtly
|
||||
// with the level — LEAGUE modes only (the climb, the cup, the training court). Classic keeps its
|
||||
// own difficulty picker, netplay is a fair human match, gore is untouched (its trio lives in
|
||||
// profile.bonus too, read elsewhere). Level 1 is factor 1: the shipped baseline is the floor.
|
||||
export const legLevel = () => G.match && !G.net ? bonus().run : 1;
|
||||
export const winLevel = () => G.match && !G.net ? bonus().window : 1;
|
||||
// legLevel rides INSIDE assistK, so the mover's legs and intercept's reachability can never
|
||||
// disagree about what those legs can get to (the rzChaseSpeed rule, player-side).
|
||||
export const assistK = () => (rallyFeel() ? Math.max(TUNE.autoAssist, TUNE.matchAssist) : TUNE.autoAssist)
|
||||
* legLevel();
|
||||
|
||||
function intercept(side) {
|
||||
const ch = side === 'O' ? O : P;
|
||||
@@ -1409,7 +1418,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) {
|
||||
// dies on contact feeling fair, and the gore modes are untouched.
|
||||
const reach = isAI ? (G.diff ? G.diff.reach : TUNE.aiReach)
|
||||
: TUNE.reach * (volley ? VOLLEY.reach : 1) * (rallyFeel() ? TUNE.matchReach : 1);
|
||||
const win = TUNE.swingWindow * (volley ? VOLLEY.window : 1);
|
||||
const win = TUNE.swingWindow * (volley ? VOLLEY.window : 1) * (isAI ? 1 : winLevel());
|
||||
const t = ch.swingT;
|
||||
if (t < TUNE.swingWindup || t > TUNE.swingWindup + win) return;
|
||||
if (!ball.active || G.phase !== 'rally') return;
|
||||
@@ -1986,12 +1995,13 @@ export function update(dt) {
|
||||
const volley = ball.bounces === 0;
|
||||
(G.swingSnaps ||= []).push({ t: P.swingT, d: Math.hypot(ball.x - P.x, (ball.z - P.z) * 0.8), y: ball.y,
|
||||
bz: ball.z, bounces: ball.bounces,
|
||||
r: TUNE.reach * (volley ? VOLLEY.reach : 1), w: TUNE.swingWindow * (volley ? VOLLEY.window : 1) });
|
||||
r: TUNE.reach * (volley ? VOLLEY.reach : 1), w: TUNE.swingWindow * (volley ? VOLLEY.window : 1) * winLevel() });
|
||||
}
|
||||
trySwing(P, false); // a contact resets swingT to -1 inside trySwing
|
||||
// volley-length window while a hostile shot is in the air too, or the swing clock expires at
|
||||
// 0.24 s and closes TUNE.parryWindow (0.34 s) before it has had its chance
|
||||
const win = TUNE.swingWindow * ((shots.length || (ball.active && ball.bounces === 0)) ? VOLLEY.window : 1);
|
||||
const win = TUNE.swingWindow * ((shots.length || (ball.active && ball.bounces === 0)) ? VOLLEY.window : 1)
|
||||
* winLevel(); // the clock must outlive the widened window or the level's gift is dead time
|
||||
if (P.swingT > TUNE.swingWindup + win + 0.02) { P.swingT = -1; if (G.phase === 'rally') swish(); logWhiff(); } // log even if the point ended mid-swing
|
||||
}
|
||||
expirePose(P, dt);
|
||||
|
||||
+15
-7
@@ -89,14 +89,22 @@ export function award(kind, n = 1) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the level is you ----
|
||||
// Phase 4's defensive buffs, through existing dials only: level 1 is the shipped game (factor 1)
|
||||
// and the cap stays honest — a maxed profile survives volleys better, it does not stop dying.
|
||||
// Read at the dial sites (meleeCooldown's floor, the parry's reach/arm, hurtPlayer's grace).
|
||||
// The level's buffs, through existing dials only: level 1 is the shipped game (factor 1, the
|
||||
// untouchable baseline) and the caps stay honest. FRONT-LOADED (sqrt of the level fraction,
|
||||
// 2026-08-29): Oliver plays at level ~20 and the buffs must be felt on the way up, not only at
|
||||
// the cap — the xp curve itself is fast-early for the same reason. At level 20 the fraction is
|
||||
// ~0.62 of the cap value; linear gave 0.39.
|
||||
// Read at the dial sites: meleeCooldown's floor, the parry's reach/arm, hurtPlayer's grace (the
|
||||
// gore trio), and — THE LEAGUE PAIR (his league-4 report: "autorun schlicht zu langsam, timing
|
||||
// zu tight") — gameplay.assistK's legs and trySwing's contact window, league modes only.
|
||||
export function bonus() {
|
||||
const f = (levelAt(prof.xp) - 1) / (LEVEL_CAP - 1);
|
||||
return { melee: 1 - 0.25 * f, // the swish floor drops to 75% — panic scales with mastery
|
||||
parry: 1 + 0.25 * f, // the parry reaches a quarter further
|
||||
grace: 1 + 0.35 * f }; // a volley needs a third longer to land twice
|
||||
const f = Math.sqrt((levelAt(prof.xp) - 1) / (LEVEL_CAP - 1));
|
||||
return { melee: 1 - 0.25 * f, // the swish floor drops to 75% — panic scales with mastery
|
||||
parry: 1 + 0.25 * f, // the parry reaches a quarter further
|
||||
grace: 1 + 0.35 * f, // a volley needs a third longer to land twice
|
||||
run: 1 + 0.15 * f, // the league autorun's legs, up to +15%
|
||||
window: 1 + 0.20 * f }; // the league contact window, up to +20% — cleaner timing too:
|
||||
// quality is centre-normalised, so the same ms error scores higher
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the promotion ----
|
||||
|
||||
@@ -89,7 +89,7 @@ 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, duelSkill, tinLadder, deliverTin, 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, legLevel: _legLevel, winLevel: _winLevel, assistK: _assistK, 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
|
||||
@@ -111,8 +111,10 @@ const DT = 1 / 60;
|
||||
function freshSplatter() {
|
||||
restartRun();
|
||||
// THE LEAGUES: a won cup PROMOTES the profile (by design), which would leak league 2 into every
|
||||
// test after the tournament walkthrough — each test opens in league 1 with no ceremony pending
|
||||
prof.prof.lg = 1; prof.prof.ch = 0; prof.takePromo();
|
||||
// test after the tournament walkthrough — each test opens in league 1 with no ceremony pending.
|
||||
// XP is zeroed too: the level feeds real dials now (legs, window), and accumulated awards from
|
||||
// earlier tests would silently buff every later one.
|
||||
prof.prof.lg = 1; prof.prof.ch = 0; prof.prof.xp = 0; prof.takePromo();
|
||||
G.mode = 'splatter';
|
||||
G.everTransitioned = true;
|
||||
setupPoint();
|
||||
@@ -2325,12 +2327,18 @@ test('THE LEVEL IS YOU: the defensive buffs scale with level and level one is th
|
||||
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');
|
||||
assert.deepEqual(b1, { melee: 1, parry: 1, grace: 1, run: 1, window: 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');
|
||||
assert.ok(bMax.run > 1 && bMax.run <= 1.15, 'the legs quicken, boundedly');
|
||||
assert.ok(bMax.window > 1 && bMax.window <= 1.2, 'the window widens, boundedly');
|
||||
// FRONT-LOADED: level 20 must already carry more than half the cap's gift (Oliver plays there)
|
||||
prof.prof.xp = prof.xpFor(20);
|
||||
assert.ok((prof.bonus().run - 1) / (bMax.run - 1) > 0.5, 'the curve pays on the way up');
|
||||
prof.prof.xp = prof.xpFor(prof.LEVEL_CAP); // back to the cap for the grace-landing check below
|
||||
// ...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);
|
||||
@@ -2338,5 +2346,24 @@ test('THE LEVEL IS YOU: the defensive buffs scale with level and level one is th
|
||||
prof.prof.xp = xp0;
|
||||
});
|
||||
|
||||
test('THE LEVEL IS YOU, in the league: legs and window grow with xp — baseline, classic and netplay untouched', () => {
|
||||
const legLevel = _legLevel, winLevel = _winLevel, assistK = _assistK;
|
||||
prof.prof.xp = 0;
|
||||
G.match = { opp: 5 };
|
||||
const legs0 = assistK();
|
||||
assert.equal(legLevel(), 1, 'level one IS the shipped game');
|
||||
assert.equal(winLevel(), 1, '...window too');
|
||||
prof.prof.xp = prof.xpFor(20);
|
||||
assert.ok(legLevel() > 1.05 && assistK() > legs0, 'level 20 legs are visibly quicker in the league');
|
||||
assert.ok(winLevel() > 1.05, '...and the contact window visibly wider');
|
||||
G.net = { on: true };
|
||||
assert.equal(legLevel(), 1, 'netplay is a fair human match — no level legs');
|
||||
assert.equal(winLevel(), 1, '...and no level window');
|
||||
G.net = null; G.match = null; G.classic = true;
|
||||
assert.equal(legLevel(), 1, 'classic keeps its own difficulty picker');
|
||||
G.classic = false;
|
||||
prof.prof.xp = 0;
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
process.exit(0); // render.js schedules viewport timers at load; don't let them outlive the report
|
||||
Reference in new issue
Block a user