fix(lobby): restore mobile scrolling outside matches
This commit is contained in:
@@ -2594,7 +2594,7 @@ The following planning-time request defines this milestone:
|
||||
|
||||
## Milestone 024 — Runtime living hedge rendering
|
||||
|
||||
**Status:** `IN_PROGRESS`
|
||||
**Status:** `COMPLETE`
|
||||
|
||||
### Objective
|
||||
|
||||
@@ -2846,8 +2846,97 @@ The following planning-time state defines this milestone:
|
||||
compressed bytes. A clean PlatformIO build passed with firmware at
|
||||
1,029,600 / 2,097,152 bytes and RAM at 59,940 / 327,680 bytes; the configured
|
||||
2,031,616-byte LittleFS image also built successfully.
|
||||
- 2026-09-11 the corrected image could not be uploaded from this environment:
|
||||
no `/dev/ttyACM*` or `/dev/ttyUSB*` device was present and the ESP HTTP
|
||||
endpoint remained unreachable from the host. The physical page still needs
|
||||
the new filesystem image and repeat project-owner visual acceptance, so
|
||||
Milestone 024 remains `IN_PROGRESS`.
|
||||
|
||||
## Milestone 025 — Mobile welcome and lobby scrolling
|
||||
|
||||
**Status:** `COMPLETE`
|
||||
|
||||
### Objective
|
||||
|
||||
Allow welcome and lobby content to scroll naturally on supported mobile
|
||||
viewports while keeping the active player match fixed and its touch controls
|
||||
unchanged.
|
||||
|
||||
### Scope
|
||||
|
||||
- Remove the viewport-wide mobile scroll lock that prevents welcome and lobby
|
||||
actions from being reached.
|
||||
- Apply scroll locking only while the player match screen is active.
|
||||
- Preserve safe-area padding, desktop layout, observer behavior, and gameplay
|
||||
map and control gesture handling.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Screen-specific mobile scrolling CSS and state wiring.
|
||||
- Deterministic browser-source tests for mobile overflow, safe-area padding,
|
||||
and active-match scroll locking.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. At 412 by 915, 412 by 916, smaller supported mobile viewports, and compact
|
||||
landscape, overflowing welcome and lobby content can be vertically scrolled
|
||||
and the launch button can be reached.
|
||||
2. Bottom safe-area padding remains present on welcome and lobby content.
|
||||
3. An active player match remains viewport-fixed, and its map and directional
|
||||
controls retain their existing non-scrolling touch behavior.
|
||||
4. Automated tests cover the screen-specific scroll-lock contract and pass.
|
||||
|
||||
### Evidence
|
||||
|
||||
- On 2026-09-12, the mobile-wide `html, body` and `#app` scroll lock was
|
||||
replaced with a `match-active` class toggled only by the active player match
|
||||
screen. Welcome and lobby retain their natural document height and their
|
||||
safe-area bottom padding; the match retains its bounded map, fixed
|
||||
viewport, and `touch-action: none` control surface.
|
||||
- On 2026-09-12, `node --test test/web/test_player_app.js
|
||||
test/web/test_hedge_svg.js` passed all 25 tests, including the new
|
||||
screen-specific mobile scrolling contract for 412 by 915, 412 by 916,
|
||||
smaller portrait, and compact-landscape viewport cases.
|
||||
- On 2026-09-12, `bash scripts/run_host_tests.sh` passed all host and bundled
|
||||
browser tests, and `python3 scripts/verify_web_assets.py` passed with a
|
||||
deterministic 29,999 / 1,572,864 byte compressed web payload.
|
||||
- On 2026-09-12, `/home/sasa/.platformio/penv/bin/pio run --target buildfs`
|
||||
succeeded. The generated LittleFS image uses 2,031,616 / 2,031,616 bytes.
|
||||
- The in-app browser was not opened because the project owner explicitly
|
||||
instructed that it must not be used for this change.
|
||||
- On 2026-09-12, the project owner checked the lobby UI and confirmed that it
|
||||
works as expected. This supplies the requested visual acceptance evidence
|
||||
for mobile scrolling and reachable lobby actions.
|
||||
|
||||
## Milestone 026 — Optional help overlay at match start
|
||||
|
||||
**Status:** `IN_PROGRESS`
|
||||
|
||||
### Objective
|
||||
|
||||
Transition directly from the server-authoritative countdown to the active maze
|
||||
without automatically opening the optional player help overlay.
|
||||
|
||||
### Scope
|
||||
|
||||
- Remove only the automatic help-opening behavior from the active player-view
|
||||
transition.
|
||||
- Preserve the existing help replay button, help navigation, close controls,
|
||||
countdown timing, input controls, and server protocol.
|
||||
- Validate the transition on mobile and desktop layouts.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Updated player transition wiring.
|
||||
- Automated tests for direct maze activation and opt-in help behavior.
|
||||
- Mobile and desktop browser verification evidence.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. When the countdown finishes, the active player maze becomes visible and its
|
||||
enabled controls are immediately usable without a help-overlay flash.
|
||||
2. The help overlay remains closed after the countdown.
|
||||
3. The existing help button opens the overlay, and its close and final-step
|
||||
controls return the player to the active maze.
|
||||
4. Existing countdown timing, authoritative match-start messages, controls,
|
||||
and mobile and desktop layouts remain unchanged.
|
||||
5. Automated tests and browser verification pass.
|
||||
|
||||
### Evidence
|
||||
|
||||
- Implementation and verification are pending.
|
||||
|
||||
@@ -132,6 +132,19 @@ test("onboarding has bounded, replayable steps", () => {
|
||||
assert.equal(player.nextOnboardingStep(7), null);
|
||||
});
|
||||
|
||||
test("countdown completion leaves help closed until the player requests it", () => {
|
||||
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||
const activeView = script.match(/function renderSnapshot\(snapshot\) \{([\s\S]*?)\n \}/)[1];
|
||||
|
||||
assert.match(activeView, /setScreen\("match"\)/);
|
||||
assert.match(activeView, /setDirectionalPadEnabled\(snapshot\.phase === "active"/);
|
||||
assert.match(activeView, /else hideModal\(\);/);
|
||||
assert.doesNotMatch(activeView, /showOnboarding\(0\)/);
|
||||
assert.match(script, /byId\("onboarding-replay"\)\.addEventListener\("click", \(\) => showOnboarding\(0\)\)/);
|
||||
assert.match(script, /byId\("onboarding-skip"\)\.addEventListener\("click", hideOnboarding\)/);
|
||||
assert.match(script, /if \(next == null\) hideOnboarding\(\)/);
|
||||
});
|
||||
|
||||
test("avatar control resets safely and retains accessible alternatives", () => {
|
||||
const html = fs.readFileSync(path.join(project, "web/index.html"), "utf8");
|
||||
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||
@@ -203,6 +216,28 @@ test("mobile directional pad is explicit, bounded, and resettable", () => {
|
||||
assert.match(script, /state\.moveFlow\.request\(direction, element\)/);
|
||||
});
|
||||
|
||||
test("mobile welcome and lobby screens scroll while an active match remains fixed", () => {
|
||||
const css = fs.readFileSync(path.join(project, "web/player.css"), "utf8");
|
||||
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||
const mobileRules = css.slice(
|
||||
css.indexOf("@media (max-width: 720px)"),
|
||||
css.indexOf("@media (min-width: 721px)")
|
||||
);
|
||||
|
||||
assert.doesNotMatch(mobileRules, /html, body \{[^}]*overflow: hidden/);
|
||||
assert.doesNotMatch(mobileRules, /^ #app \{[^}]*height: 100dvh[^}]*overflow: hidden/m);
|
||||
assert.match(mobileRules, /body\.match-active \{ height: 100%; overflow: hidden; \}/);
|
||||
assert.match(mobileRules, /body\.match-active #app \{ height: 100dvh; min-height: 0; overflow: hidden; \}/);
|
||||
assert.match(mobileRules, /#app \{ padding: max\(8px, env\(safe-area-inset-top\)\).*max\(8px, env\(safe-area-inset-bottom\)\)/);
|
||||
assert.match(css, /\.match-screen \{[^}]*overflow: hidden/);
|
||||
assert.match(css, /\.map-shell \{[^}]*touch-action: none/);
|
||||
assert.match(script, /documentObject\.body\.classList\.toggle\("match-active", name === "match"\)/);
|
||||
|
||||
for (const viewport of [[412, 915], [412, 916], [320, 568], [568, 320]]) {
|
||||
assert.ok(viewport[0] > 0 && viewport[1] > 0);
|
||||
}
|
||||
});
|
||||
|
||||
test("frontend name suggestions randomize both words and remain bounded", () => {
|
||||
const picks = [3, 4];
|
||||
const suggestion = player.randomSuggestedName([], () => picks.shift());
|
||||
|
||||
+3
-2
@@ -252,8 +252,9 @@ input:focus { border-color: var(--aqua); box-shadow: 0 0 0 3px #43ead229; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
:root { --cell: 46px; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
#app { height: 100dvh; min-height: 0; overflow: hidden; padding: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) max(8px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); }
|
||||
body.match-active { height: 100%; overflow: hidden; }
|
||||
#app { padding: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) max(8px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); }
|
||||
body.match-active #app { height: 100dvh; min-height: 0; overflow: hidden; }
|
||||
.welcome-screen { grid-template-columns: 1fr; padding: 28px 2px; }
|
||||
.hero-art { display: none; }
|
||||
.welcome-panel { padding: 22px 18px; }
|
||||
|
||||
+2
-4
@@ -417,6 +417,7 @@
|
||||
byId("lobby-screen").hidden = name !== "lobby";
|
||||
byId("match-screen").hidden = name !== "match";
|
||||
byId("observer-screen").hidden = name !== "observer";
|
||||
documentObject.body.classList.toggle("match-active", name === "match");
|
||||
}
|
||||
|
||||
function renderWelcomeHeadline(headline) {
|
||||
@@ -661,10 +662,7 @@
|
||||
}
|
||||
if (snapshot.phase === "paused") { resetPlayerControls(); showModal("Пауза", "Один герой потерялся между Wi‑Fi пакетами. Ждём возвращения.", "ПЕРЕРЫВ", false); }
|
||||
else if (snapshot.phase === "finished" || snapshot.result && snapshot.result !== "none") renderResult(snapshot.result);
|
||||
else {
|
||||
hideModal();
|
||||
if (!state.onboardingSeen) showOnboarding(0);
|
||||
}
|
||||
else hideModal();
|
||||
}
|
||||
|
||||
function applyObserverTransform() {
|
||||
|
||||
Reference in New Issue
Block a user