diff --git a/PROJECT.md b/PROJECT.md index 119e36d..48f91d3 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -636,6 +636,34 @@ Each phase ships to staging and gets a phone verdict, and tests ride along with 5. **LATER.** The metal bed (when Oliver has a track: start at the transformation, gore only) and Members' Night (option C). +## Session log: the ladder is flatter — the width cliff (2026-10-15, v387) +Oliver: *"ab Runner ist das Zombie-Game schon sehr schwer — können wir die Schwierigkeitsleiter ein wenig +abflachen?"* **Measured first**: cup mode, the ring bot, 6-8 seeds (`tools/rally-sim.mjs`, `T_` sweeps). +- **The cliff.** Roller (rung 5) 90-98% of points, then Steward 54-79%, Mascot ~50, Runner 41-55. At level + 10 it fell again at the Seller (29%). +- **What it is NOT:** sweeps of the legs (`legKnee` 5.3, `matchSpeedPer` 0.07) and of the flub decay + (`matchFlubKeep` 0.84) all moved it within the noise. +- **What it IS: the width.** `matchWideBase + step·matchWidePer` × the foe's `wide` grows without bound. + Wherever it crosses ~3.3 m (the auto-run's reach from the centre) the win rate falls off. That happens + at the Steward, and with a slower slope it just moved to the Seller. From there every line can be + painted. +- **The put-away chance** sets the top: without it, rungs 9-18 sat flat at ~60%. +- **Shipped (variant G):** + - `TUNE.matchWideMax` 3.0 m caps every match opponent's spread (Climb, cup, story cups, training). + - `punishMin`/`punishMax` go from 0.2/0.85 to 0.15/0.8. +- **Bot, level 10, cup** (before → after): + + | rung | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 18 | + |---|---|---|---|---|---|---|---|---|---|---| + | before | 57 | 51 | 44 | 29 | 31 | 27 | 26 | 34 | 22 | 24 | + | after | 72 | 73 | 68 | 53 | 56 | 57 | 42 | 42 | 36 | 33 | + + The rushers stay their usual bumps. Level 26: Runner 43 → 83, Champion 29 → 41. Climb, level 26: + 85 → 44, smooth. +- 3.1 m was tried as a middle way. It barely helped at level 10, because the cliff sits between 3.0 and + 3.3. +- Test: rungs 8, 12 and 18 never land wider than 3.35 m (it reads 4.05 at the Runner without the cap). + ## Session log: no story boss crosses the net (2026-10-15, v385, STAGING ONLY) Oliver: *"strawberry seller as first endboss is way too hard — or it should not cross the net"*. The seller is the arcade's chapter-9 design. His move is `charge`, the one style deliberately exempt from diff --git a/index.html b/index.html index 632d01c..a1cdbaa 100644 --- a/index.html +++ b/index.html @@ -1201,6 +1201,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/config.js b/src/config.js index 19b2e92..2bb3237 100644 --- a/src/config.js +++ b/src/config.js @@ -169,8 +169,8 @@ export const TUNE = { // its window closes — the follow-through, q 0.05 (a weak short ball, not a whiff). // 0 = the old edge: a tap ~70 ms early whiffed (step 4, see gameplay.trySwing) punishBelow: 0.5, // a player ball under this q is a SITTER the CPU may put away (gameplay.punishes) - punishMin: 0.2, // ...the chance it does at the bottom of the ladder - punishMax: 0.85, // ...and at the CHAMPION (classic: DIFFS[].punish) + punishMin: 0.15, // ...the chance it does at the bottom of the ladder (0.2 until the flattening) + punishMax: 0.8, // ...and at the CHAMPION (0.85 until 2026-10-15; classic: DIFFS[].punish) punishQ: 0.95, // the put-away's quality, played to the open court... punishX: 3.5, // ...at this x, inside the singles line (4.1), bounded by the rung's width dial punishRead: 0.5, // s the player's auto-run stands flat-footed on a put-away of his own sitter @@ -656,6 +656,12 @@ export const TUNE = { // auto-run player can actually cover. Its scatter painted lines from game one // ("still a LOT of unreachable balls"); now line-painting is the late ladder matchWidePer: 0.25, // ...earning this much more width per opponent felled (softened with speedPer) + // ...and NEVER WIDER THAN THIS (Oliver, 2026-10-15: "ab Runner ist das Zombie-Game schon sehr schwer — + // die Schwierigkeitsleiter ein wenig abflachen"). Measured: the ladder fell off a cliff wherever its + // width crossed ~3.3 m — the auto-run's reach from the centre — at the Steward and again at the + // Seller, and from there every line could be painted. Capped at 3.0, the late ladder climbs through + // put-aways, flub, pace and open-court aim instead (bot, level 10: Runner 44 -> 68%, Champion 24 -> 33%) + matchWideMax: 3.0, matchHeal: 2, // hp back for a game won (the duel's healPerDuel rate) netWinBurst: 1.4, // s the friend's burst plays before the win screen covers it. update() freezes // the world on G.done, so this beat is the only thing that lets the gore fly. diff --git a/src/gameplay.js b/src/gameplay.js index 571b4b5..da29f4f 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -345,7 +345,7 @@ function doHit(ch, quality, aim) { // 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. if (ch.side !== 'P') { - const wide = (TUNE.matchWideBase + matchStep() * TUNE.matchWidePer) * (ch.foe?.wide || 1); + const wide = Math.min(TUNE.matchWideMax, (TUNE.matchWideBase + matchStep() * TUNE.matchWidePer) * (ch.foe?.wide || 1)); tx = Math.max(-wide, Math.min(wide, tx)); } } diff --git a/test.js b/test.js index ff8112f..0346d76 100644 --- a/test.js +++ b/test.js @@ -3662,6 +3662,31 @@ function winStoryCup() { G.modal = null; G.modalQ.length = 0; } +test('THE LADDER IS FLATTER: no opponent paints wider than matchWideMax (the cliff at ~3.3 m)', () => { + // Oliver, 2026-10-15: "ab Runner ist das Zombie-Game schon sehr schwer". The win rate fell off a cliff + // wherever the rung's width crossed the auto-run's reach from the centre; the width is capped now. + startMatch(); G.interlude.introT = 0; + const zb = oppOf(); + for (const n of [8, 12, 18]) { + G.match.opp = n; zb.foe = cfg.matchFoe(n); + let widest = 0; + for (let i = 0; i < 600; i++) { + G.phase = 'rally'; G.wasServe = false; G.match.surprised = G.match.expected = false; + P.x = i % 2 ? 3 : -3; P.z = 23; P.swingT = -1; + zb.x = 0; zb.z = 2; zb.swingT = -1; zb.swingCool = 0; zb.serveHold = 0; + Object.assign(ball, { active: true, lastHit: 'P', pow: 1, bounces: 1, rx: null, spin: 1, soft: false, + x: 0, z: 2.4, y: 1.2, vx: 0, vy: 0, vz: -1 }); + update(DT); + if (ball.lastHit !== 'O' || ball.soft) { ball.active = false; continue; } + const t = (ball.vy + Math.sqrt(ball.vy * ball.vy + 2 * ball.g * ball.y)) / ball.g; + widest = Math.max(widest, Math.abs(ball.x + ball.vx * t)); + ball.active = false; + } + assert.ok(widest > 2.2 && widest <= 3.0 + 0.35, `rung ${n}: widest landing ${widest.toFixed(2)} m (the flattening set 3.0)`); + } + endMatch(); +}); + test('THE STORY: every act\'s cup final is the body that becomes its boss, and the stock grows act by act', () => { const famOf = n => cfg.matchFoe(n).fam; const bossFam = { 0: 'gk', 5: 'ma', 2: 'cu', 3: 'hc', 9: 'ch' }; // design -> the ladder body it grew from