From e4c917e23d8d998b823acc88662c12f65b7651db Mon Sep 17 00:00:00 2001 From: Oliver Rieger Date: Fri, 14 Aug 2026 10:29:51 +0200 Subject: [PATCH] The menu grows a second step, classic learns difficulty, and the rally feel unifies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oliver's restructure: mode first, then what the mode asks — classic deals EASY / MEDIUM / HARD chips, ZOMBIE TENNIS deals two sub-mode posters (ARCADE, the exhibition ladder as was, and THE TOURNAMENT), and SPLATTER GORE stands alone as the third top card. config.MENU is the grouping, MODES stays the four run modes, and pickedMode stays the run-mode id — start(), stats and the summary never learn the menu got taller. The sub-row sits inset under the posters; sub-cards are the poster at 2/3 height centred on their scene. DIFFICULTY is the one real gameplay addition: config.DIFFS scales the three dials the classic CPU already reads (flub, quality cap, aiSpeed), MEDIUM pinned by test to the shipped classicFlub/classicQualityMax so the middle chip IS yesterday's game. Rides on G.diff next to G.classic, so restarts keep it. AND THE FEEL SPLIT OLIVER REPORTED IS REAL: "in classic tennis I tend to miss balls way more often" — the match's three contact helpers (matchReach 1.35, the matchAssist floor, the short-ball chase) were all G.match-gated, so classic played with a 35% smaller racket and no rescue on the dying drop shot. rallyFeel() = match || classic now carries all three; gore's opener keeps the old prologue feel on purpose. The ARCADE poster took two rolls: the first came back a flat sticker line-up ("very constructed, not artsy"), the second anchors on the zombie poster itself as a MOOD reference — one challenger lit at the net, the queue receding into purple haze. Rolls in _refcheck/modebtn. The cup's stats mapping also joins MODE_MATCH (it banks through endRun as MATCH and read back as GORE). SPRV 117, main.js v233, 66 tests. Co-Authored-By: Claude Fable 5 --- assets/mode_arcade.png | Bin 0 -> 49610 bytes index.html | 29 +++++++++++----- src/config.js | 22 +++++++++++- src/gameplay.js | 24 +++++++++---- src/main.js | 75 ++++++++++++++++++++++++++++++++--------- src/render.js | 2 +- test.js | 23 ++++++++++++- 7 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 assets/mode_arcade.png diff --git a/assets/mode_arcade.png b/assets/mode_arcade.png new file mode 100644 index 0000000000000000000000000000000000000000..efdd97b770446a1b5205a0e5b2d6d5a5684f7591 GIT binary patch literal 49610 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 RY6lP&4OLy0T4lTF{|8`pc;Em4 literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 8485216..62c74a0 100644 --- a/index.html +++ b/index.html @@ -223,18 +223,30 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { the art + a scrim gradient as the background stack) with the caption written over the bottom. Unpicked buttons sit dimmed and desaturated so the chosen mode is the one that glows — the same "one bright thing means selected" rule the menu palette already follows. */ - #modeList button.mode { display: flex; align-items: flex-end; width: 100%; min-height: 96px; + #modeList button.mode, #modeSub button.mode { display: flex; align-items: flex-end; width: 100%; min-height: 96px; margin: 0 0 8px; padding: 7px 9px; background-size: cover; background-position: center bottom; image-rendering: pixelated; border: 2px solid var(--line); color: inherit; font: inherit; text-align: left; cursor: pointer; -webkit-tap-highlight-color: transparent; filter: saturate(0.45) brightness(0.62); transition: filter 0.15s; } - #modeList button.mode.on { border-color: var(--acid); filter: none; } - #modeList b { text-shadow: 0 1px 0 #000, 0 0 6px rgba(0,0,0,0.9); } + #modeList button.mode.on, #modeSub button.mode.on { border-color: var(--acid); filter: none; } + #modeList b, #modeSub b { text-shadow: 0 1px 0 #000, 0 0 6px rgba(0,0,0,0.9); } /* the subtitle carries the mode's one-line rules over busy art, so it gets WHITE at full opacity, a size up from the court rows' 9px, and a solid black underlay — "barely readable" at cream-on-crowd was the report from the first live look */ - #modeList i { color: #fff; font-size: 10px; margin-bottom: 0; + #modeList i, #modeSub i { color: #fff; font-size: 10px; margin-bottom: 0; text-shadow: 0 1px 0 #000, 1px 0 0 #000, -1px 0 0 #000, 0 -1px 0 #000, 0 0 7px #000; } + /* THE SECOND STEP (config.MENU): the picked poster's own choices, inset so they read as its + children. Sub-mode cards are the same poster at ~2/3 height, centred on their scene rather + than bottom-anchored (a 64px window off the bottom of these scenes is all court and shins); + the difficulty chips are plain acid-outline buttons — art for EASY/MEDIUM/HARD would be + forced, and the chip row reads faster anyway. */ + #modeSub { display: flex; flex-wrap: wrap; gap: 8px; padding-left: 14px; margin: 0 0 8px; } + #modeSub:empty { display: none; } + #modeSub button.mode.sm { flex: 1 1 100%; min-height: 64px; margin: 0; background-position: center; } + #modeSub button.chip { flex: 1; padding: 12px 0; background: var(--panel); border: 2px solid var(--line); + color: var(--cream); font: 400 11px Silkscreen, monospace; letter-spacing: 1px; + cursor: pointer; -webkit-tap-highlight-color: transparent; } + #modeSub button.chip.on { border-color: var(--acid); color: var(--acid); background: rgba(188,212,85,0.09); } #courtList button.court { display: flex; align-items: center; gap: 11px; width: 100%; margin: 0 0 8px; padding: 7px; background: var(--panel); border: 2px solid var(--line); color: inherit; font: inherit; text-align: left; @@ -242,9 +254,9 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { #courtList button.court.on { border-color: var(--acid); background: rgba(188,212,85,0.09); } #courtList img { width: 92px; height: 66px; display: block; image-rendering: pixelated; flex: none; object-fit: cover; } - #courtList .cb, #modeList .cb { flex: 1; min-width: 0; } - #courtList b, #modeList b { display: block; font: 400 12px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; } - #courtList i, #modeList i { display: block; margin: 3px 0 6px; font: 400 9px Silkscreen, monospace; color: var(--dim); font-style: normal; } + #courtList .cb, #modeList .cb, #modeSub .cb { flex: 1; min-width: 0; } + #courtList b, #modeList b, #modeSub b { display: block; font: 400 12px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; } + #courtList i, #modeList i, #modeSub i { display: block; margin: 3px 0 6px; font: 400 9px Silkscreen, monospace; color: var(--dim); font-style: normal; } /* The two meters read straight off the court's own bounce numbers (see main.js), so they cannot drift away from what the ball actually does on it. */ .meter { display: flex; align-items: center; gap: 6px; margin-top: 4px; @@ -370,6 +382,7 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {

MATCH SETUP

01 · MODE

+

02 · PLAYER

03 · COURT

@@ -443,6 +456,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') { - + diff --git a/src/config.js b/src/config.js index 6f16fea..4b56389 100644 --- a/src/config.js +++ b/src/config.js @@ -635,11 +635,31 @@ export const COURTS = [ // baked in — the button's own name/sub render over a scrim, so the caption stays crisp. export const MODES = [ { id: 'classic', name: 'CLASSIC TENNIS', sub: 'REAL TENNIS · BEST OF THREE SETS', art: 'mode_classic' }, - { id: 'zombie', name: 'ZOMBIE TENNIS', sub: 'ONE GAME PER OPPONENT · BLOOD ONLY', art: 'mode_zombie' }, + { id: 'zombie', name: 'ARCADE', sub: 'ONE GAME PER OPPONENT · BLOOD ONLY', art: 'mode_arcade' }, { id: 'cup', name: 'THE TOURNAMENT', sub: 'FOUR ROUNDS · THE CHAMPION WAITS', art: 'mode_cup' }, { id: 'gore', name: 'SPLATTER GORE', sub: 'THE MATCH TURNS · SURVIVE IT', art: 'mode_gore' }, ]; +// THE SETUP SCREEN'S TOP ROW (Oliver's two-step menu, 2026-08-14): three posters, and picking one +// opens its own second step — classic deals the difficulty chips, zombie tennis deals its two +// sub-mode cards (arcade / the cup), gore just IS gore. A row either points at a run mode in 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'] }, + { mode: 'gore' }, +]; + +// CLASSIC's difficulty, the one thing the modes screen adds to gameplay: a scaling on the three +// dials the classic CPU already reads (flub = error rate, quality = shot cap, speed = aiSpeed +// multiplier). MEDIUM is the game exactly as it shipped — classicFlub/classicQualityMax repeated +// here so the middle chip can never drift from what "no difficulty" used to play like. +export const DIFFS = [ + { id: 'easy', name: 'EASY', flub: 0.22, quality: 0.72, speed: 0.88 }, + { id: 'medium', name: 'MEDIUM', flub: 0.10, quality: 0.86, speed: 1.0 }, + { id: 'hard', name: 'HARD', flub: 0.04, quality: 0.95, speed: 1.12 }, +]; + // THE TOURNAMENT (Oliver's pick over the endless ladder, 2026-08-13): four rounds up the club's // dead hierarchy, each round DRAWING one rung from its band so runs differ, the final always THE // CHAMPION. `need` is games to take the round — round one is a single game so a run opens fast — diff --git a/src/gameplay.js b/src/gameplay.js index b984cdc..b73e2b8 100644 --- a/src/gameplay.js +++ b/src/gameplay.js @@ -1056,11 +1056,19 @@ function onBounce() { // which points are reachable, and computing that with autoAssist alone meant a desktop match's // intercept thought NOTHING was reachable and sent the player home every time — he had legs but // his destination was always the baseline centre. -export const assistK = () => G.match ? Math.max(TUNE.autoAssist, TUNE.matchAssist) : TUNE.autoAssist; +// THE RALLY MODES SHARE ONE PLAYER FEEL (Oliver, 2026-08-14: "in classic tennis I tend to miss +// balls way more often — zombie tennis is the good one"). The match's three contact helpers — +// assist floor, matchReach, the short-ball chase — were tuned in the match and classic never got +// 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; function intercept(side) { const ch = side === 'O' ? O : P; - const spd = side === 'O' ? TUNE.aiSpeed : TUNE.playerSpeed * assistK(); + // G.diff is CLASSIC's difficulty chip (config.DIFFS) — null everywhere else, so only the + // classic CPU speeds up or slows down. Same guard on every dial it scales. + const spd = side === 'O' ? TUNE.aiSpeed * (G.diff ? G.diff.speed : 1) : TUNE.playerSpeed * assistK(); let { x, y, z, vx, vy, vz } = ball; const g = ball.g || TUNE.gravity, dt = 0.01; const inZone = side === 'O' ? zz => zz <= ZONE_DEPTH : zz => zz >= COURT.L - ZONE_DEPTH; @@ -1074,7 +1082,7 @@ function intercept(side) { // legal contact height by then) and the point went to the zombie. Seven of seven recorded // losses. Remember the earliest reachable point ON HIS HALF where the ball is still at hitting // height: running forward onto the short ball is the tennis answer, and now it is the assist's. - if (side === 'P' && G.match && !anyHit && i > 5 && z > COURT.netZ && y < 1.6 && y > 0.35) { + if (side === 'P' && rallyFeel() && !anyHit && i > 5 && z > COURT.netZ && y < 1.6 && y > 0.35) { const pt = { x, z: Math.min(24.5, z) }; if (Math.hypot(pt.x - ch.x, pt.z - ch.z) / spd <= i * dt) anyHit = pt; } @@ -1146,7 +1154,7 @@ function trySwing(ch, isAI, qCap = TUNE.aiQualityMax) { // sampled 1.3-1.6 m — centimetres outside the 1.3 m post-bounce reach, point after point. The // assist can deliver you NEAR the ball but not INSIDE a phone-tuned reach; a rally mode lives or // dies on contact feeling fair, and the gore modes are untouched. - const reach = isAI ? TUNE.aiReach : TUNE.reach * (volley ? VOLLEY.reach : 1) * (G.match ? TUNE.matchReach : 1); + const reach = isAI ? TUNE.aiReach : TUNE.reach * (volley ? VOLLEY.reach : 1) * (rallyFeel() ? TUNE.matchReach : 1); const win = TUNE.swingWindow * (volley ? VOLLEY.window : 1); const t = ch.swingT; if (t < TUNE.swingWindup || t > TUNE.swingWindup + win) return; @@ -1712,7 +1720,7 @@ export function update(dt) { G.aiReactT += dt; const incoming = ball.active && ball.lastHit === 'P' && G.aiReactT >= TUNE.aiReaction; const tgt = incoming ? intercept('O') : HOME.O; - const spd = TUNE.aiSpeed * (incoming ? 1 : 0.5); // slow recovery: wrong-footing wins points + const spd = TUNE.aiSpeed * (G.diff ? G.diff.speed : 1) * (incoming ? 1 : 0.5); // slow recovery: wrong-footing wins points const dx = tgt.x - O.x, dz = tgt.z - O.z, d = Math.hypot(dx, dz); if (d > 0.1) { O.x += dx / d * Math.min(spd * dt, d); @@ -1731,11 +1739,13 @@ export function update(dt) { // CLASSIC TENNIS turns the band OFF and flubs less: the band exists to get a new player // PAST the opener, and a mode whose whole promise is a real match cannot be won by an // opponent that falls apart the moment he goes ahead. - const flub = G.classic ? TUNE.classicFlub : TUNE.aiFlub + score.games[1] * TUNE.aiFlubLoss; + const flub = G.classic ? (G.diff ? G.diff.flub : TUNE.classicFlub) + : TUNE.aiFlub + score.games[1] * TUNE.aiFlubLoss; if (Math.random() < flub) G.aiSwingCool = 0.5; // whiff: hesitates and misses else { O.swingT = TUNE.swingWindup + TUNE.swingWindow / 2; - trySwing(O, true, G.classic ? TUNE.classicQualityMax : TUNE.aiQualityMax); O.swingT = -1; + trySwing(O, true, G.classic ? (G.diff ? G.diff.quality : TUNE.classicQualityMax) + : TUNE.aiQualityMax); O.swingT = -1; G.aiSwingCool = ball.lastHit === 'O' ? 0.4 : 0.12; } } diff --git a/src/main.js b/src/main.js index 6461c63..c7d8d7f 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,7 @@ // Module map: config (tuning/constants) · state (mutable game state) · gameplay (logic/update) // render (camera/drawing) · input (keys/touch) · audio (sfx/umpire) · panel (T tuning UI) -import { TUNE, COURTS, TOURS, MODES, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt } from './config.js'; +import { TUNE, COURTS, TOURS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt } from './config.js'; import { P, O, ball, score, G, setupPoint, selectCourt, selectTour } from './state.js'; import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, flickTarget, dragServe, toggleTestMode } from './gameplay.js'; import { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall } from './beats.js'; @@ -72,27 +72,63 @@ let picked = remembered('court') || COURTS[0].id; let pickedTour = remembered('tour') || TOURS[0].id; const go = stage => { intro.dataset.stage = stage; menuMusic(); }; // every stage change is a gesture, so it is also a safe place to (re)start the theme -// --- MODE tiles, built from config.MODES. The setup screen now picks WHAT, WHO and WHERE, and this -// is the first of the three because it is the one that changes the other two's meaning. Same -// "tap selects, SERVE UP starts" rule as everything else on the screen; the row markup is the court -// row's, reused wholesale (see the shared selectors in index.html). +// --- MODE selection in TWO STEPS (config.MENU + config.MODES): three posters on top, and the +// picked one deals its own second step underneath — classic deals the difficulty chips, zombie +// tennis deals its two sub-mode cards, gore just is gore. `pickedMode` stays the RUN mode id +// (classic/zombie/cup/gore) so start(), stats and the summary never learn about the grouping. let pickedMode = remembered('mode') || MODES[0].id; +let pickedDiff = remembered('diff') || 'medium'; +const modeOf = id => MODES.find(x => x.id === id); +const topOf = m => MENU.find(t => t.mode === m || (t.kids && t.kids.includes(m))) || MENU[0]; const modeList = document.getElementById('modeList'); -for (const m of MODES) { +const modeSub = document.getElementById('modeSub'); +const banner = (card, cls, on) => { const b = document.createElement('button'); - b.className = 'mode' + (m.id === pickedMode ? ' on' : ''); + b.className = cls + (on ? ' on' : ''); // the scrim rides IN the background stack (gradient over art), so the caption needs no extra box b.style.backgroundImage = - `linear-gradient(180deg, rgba(0,0,0,0) 34%, rgba(0,0,0,0.78)), url(assets/${m.art}.png?v=${SPRV})`; - b.innerHTML = `${m.name}${m.sub}`; + `linear-gradient(180deg, rgba(0,0,0,0) 34%, rgba(0,0,0,0.78)), url(assets/${card.art}.png?v=${SPRV})`; + b.innerHTML = `${card.name}${card.sub}`; + return b; +}; +const pickMode = id => { pickedMode = id; try { localStorage.setItem('mode', id); } catch {} }; +function buildSub() { + modeSub.innerHTML = ''; + const t = topOf(pickedMode); + if (t.diffs) for (const d of DIFFS) { + const b = document.createElement('button'); + b.className = 'chip' + (d.id === pickedDiff ? ' on' : ''); + b.textContent = d.name; + b.addEventListener('click', () => { + pickedDiff = d.id; + try { localStorage.setItem('diff', d.id); } catch {} + for (const el of modeSub.children) el.classList.toggle('on', el === b); + summary(); + }); + modeSub.appendChild(b); + } + else if (t.kids) for (const id of t.kids) { + const b = banner(modeOf(id), 'mode sm', id === pickedMode); + b.addEventListener('click', () => { + pickMode(id); + for (const el of modeSub.children) el.classList.toggle('on', el === b); + summary(); + }); + modeSub.appendChild(b); + } +} +for (const t of MENU) { + const b = banner(t.mode ? modeOf(t.mode) : t, 'mode', topOf(pickedMode) === t); b.addEventListener('click', () => { - pickedMode = m.id; - try { localStorage.setItem('mode', m.id); } catch {} // come back to what you were playing + // a group remembers which child you were on; entering it fresh lands on its first card + pickMode(t.mode || (t.kids.includes(pickedMode) ? pickedMode : t.kids[0])); for (const el of modeList.children) el.classList.toggle('on', el === b); + buildSub(); summary(); }); modeList.appendChild(b); } +buildSub(); // --- tour tiles, built from the table. Same "tap selects, SERVE UP starts" rule as the court rows — // the setup screen picks WHO and WHERE, and nothing on it commits you. @@ -144,13 +180,17 @@ function summary() { const c = COURTS.find(x => x.id === picked) || COURTS[0]; const m = MODES.find(x => x.id === pickedMode) || MODES[0]; // the mark to beat is the PICKED MODE's own, because the three of them do not measure the same - // thing — see the record-shape note in stats.endRun - const mode = m.id === 'zombie' ? stats.MODE_MATCH : m.id === 'classic' ? stats.MODE_CLASSIC : stats.MODE_GORE; + // thing — see the record-shape note in stats.endRun. The cup banks through endRun as MODE_MATCH + // (see cupWon), so its summary reads the same book it writes. + const mode = m.id === 'zombie' || m.id === 'cup' ? stats.MODE_MATCH + : m.id === 'classic' ? stats.MODE_CLASSIC : stats.MODE_GORE; const best = stats.read('all', undefined, mode).longest; - const unit = m.id === 'zombie' ? (best === 1 ? ' FELLED' : ' FELLED') + const unit = m.id === 'zombie' || m.id === 'cup' ? ' FELLED' : best === 1 ? ' GAME' : ' GAMES'; + const name = m.id === 'classic' + ? m.name + ' · ' + (DIFFS.find(d => d.id === pickedDiff) || DIFFS[1]).name : m.name; document.getElementById('sSum').innerHTML = - `${m.name} · ${t.name.replace(' SINGLES', '')} · ${c.name}` + `${name} · ${t.name.replace(' SINGLES', '')} · ${c.name}` + `${best ? best + unit + ' TO BEAT' : 'FIRST RUN'}`; } @@ -178,6 +218,8 @@ function start(modeId = pickedMode) { // turn switched off, and zombie tennis is the match taking the court. Everything else about a mode // lives in the code that reads G.classic / G.match, never here. G.classic = modeId === 'classic'; + // the difficulty rides on G like G.classic does, so a mid-run restart keeps the CPU you chose + G.diff = G.classic ? (DIFFS.find(d => d.id === pickedDiff) || DIFFS[1]) : null; if (modeId === 'zombie') { intoSplatter(); startMatch(); } if (modeId === 'cup') { intoSplatter(); startMatch(true); } } @@ -185,7 +227,8 @@ let stPeriod = 'all', stMode = stats.MODE_GORE; // the stats screen opens on the mode you last PICKED to play — that is the one whose numbers you // came to look at, and it saves a tap on the way in every single time const openStats = () => { - stMode = pickedMode === 'zombie' ? stats.MODE_MATCH : pickedMode === 'classic' ? stats.MODE_CLASSIC : stats.MODE_GORE; + stMode = pickedMode === 'zombie' || pickedMode === 'cup' ? stats.MODE_MATCH + : pickedMode === 'classic' ? stats.MODE_CLASSIC : stats.MODE_GORE; for (const el of document.getElementById('stModes').children) el.classList.toggle('on', el.dataset.m === stMode); stats.drawStats(stPeriod, SPRV, stMode); go('stats'); }; diff --git a/src/render.js b/src/render.js index 7420d8a..c1320f4 100644 --- a/src/render.js +++ b/src/render.js @@ -221,7 +221,7 @@ function farB(z) { // Asset cache-buster — bump with main.js?v=N (see the img.src note below). EXPORTED because the // menu screens hang tags on the same files, and two copies of this number is one copy too // many: the one that does not get bumped is the one that serves a cached 404. -export const SPRV = 116; +export const SPRV = 117; // The boot bar in main.js watches this. Nothing here CHANGES when things load — every sprite was // already requested at import — it just makes the wait visible and holds the menu back until the // last sheet has landed. See the onerror note below for the other half of "sometimes missing". diff --git a/test.js b/test.js index 7156663..f3314c6 100644 --- a/test.js +++ b/test.js @@ -83,7 +83,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' }); // ---- game modules (real code) ---------------------------------------------- const { G, P, O, score, ball, setupPoint } = await import('./src/state.js'); -const { update, forceZombies, splatPoint, restartRun, flickTarget, keyPressed, classicGame, serveApexY } = await import('./src/gameplay.js'); +const { update, forceZombies, splatPoint, restartRun, flickTarget, keyPressed, classicGame, serveApexY, assistK, rallyFeel } = await import('./src/gameplay.js'); const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN } = await import('./src/beats.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 @@ -1104,6 +1104,27 @@ test('classic tennis: losing the match ends it the same way, and counts as a los assert.equal(runs[runs.length - 1].dead, 1, 'a match lost IS the run ending badly'); }); +test('classic wears the match feel, and the difficulty table is a ladder', () => { + // THE RALLY MODES SHARE ONE PLAYER FEEL: classic gets the match's assist floor (with matchReach + // and the short-ball chase riding the same rallyFeel() switch) — "in classic tennis I tend to + // miss balls way more often" was these dials being match-only. + G.classic = false; G.match = null; + assert.ok(!rallyFeel(), 'gore alone keeps the old feel'); + G.classic = true; + assert.ok(rallyFeel(), 'classic is a rally mode'); + assert.ok(assistK() >= TUNE.matchAssist, 'classic gets the match assist floor'); + G.classic = false; + // ...and DIFFS climbs monotonically, with MEDIUM pinned to the shipped classic dials so the + // middle chip can never drift from what "no difficulty" used to play like. + const [e, m, h] = cfg.DIFFS; + assert.ok(e.flub > m.flub && m.flub > h.flub, 'easier flubs more'); + assert.ok(e.quality < m.quality && m.quality < h.quality, 'harder hits better'); + assert.ok(e.speed < m.speed && m.speed < h.speed, 'harder runs faster'); + assert.equal(m.flub, TUNE.classicFlub, 'MEDIUM flub IS classicFlub'); + assert.equal(m.quality, TUNE.classicQualityMax, 'MEDIUM quality IS classicQualityMax'); + assert.equal(m.speed, 1.0, 'MEDIUM leaves aiSpeed alone'); +}); + // stats.js ships its own fixture-based check — the period filter, the mode split and the name // normalisation that has to agree with server.py. It was exported and never called by anything. test("stats.js's own check", () => { assert.equal(statsCheck(), 'ok'); });