diff --git a/PLANS.md b/PLANS.md index 89ab2ba..7da8de2 100644 --- a/PLANS.md +++ b/PLANS.md @@ -1267,7 +1267,7 @@ confirmation remains pending. ## Milestone 015 — Pre-reader child-friendly game experience -**Status:** `IN_PROGRESS` +**Status:** `COMPLETE` ### Objective @@ -1363,9 +1363,9 @@ rules. ### Evidence -The implementation and automated verification are complete; the required -representative pre-reader observation or owner-approved evidence deviation -remains pending. +The implementation and automated verification are complete. The project owner +approved the required evidence deviation after manual gameplay, as recorded +below. - The player screen now uses circle and diamond character shapes instead of `P1`/`P2`, icon-and-shape ownership markers for keys and exits, dynamic @@ -1398,7 +1398,273 @@ remains pending. `pio run -e esp32-c6-devkitm-1 --target buildfs` passed; the fixed LittleFS image used 2,031,616 of 2,031,616 bytes and SHA-256 `ea6813e603d883a1f6b79a9f9efbd20e7d02f010052210716a89d7e863716415`. -- Acceptance criterion 11 still requires the facilitated representative - pre-reader observation from `docs/PRE_READER_USABILITY.md`, or a - project-owner-approved documented deviation. Therefore Milestone 015 remains - `IN_PROGRESS`; Milestone 016 has not been started. +- On 2026-09-09, the project owner manually exercised the gameplay and + confirmed that all host, firmware, and filesystem checks above pass. The + owner also found the separate on-screen joystick imitation uncomfortable and + insufficiently responsive for real play. The owner explicitly approved the + criterion-11 evidence deviation: no representative pre-reader observation is + required for this milestone. Movement-latency diagnosis is deferred to + Milestone 016, and the resulting avatar-centered control refinement is + deferred to Milestone 017. Criteria 1--10 have the recorded implementation + and automated evidence above; criterion 11 has this documented owner + approval. Milestone 015 is therefore `COMPLETE`. + +## Milestone 016 — End-to-end movement latency diagnosis + +**Status:** `IN_PROGRESS` + +### Objective + +Measure and attribute the delay from a player's touch to the corresponding +authoritative map update, determine whether the unacceptable delays originate +in the local Wi-Fi network, browser input and rendering, ESP firmware, or their +interaction under load, and produce evidence that selects the remediation for +Milestone 017. + +### Scope + +- Define separate latency intervals for local pointer feedback, browser command + scheduling, WebSocket uplink, ESP receive/parse/game processing, outbound + queueing and serialization, WebSocket downlink, complete-view assembly, DOM + update, and the next painted frame. +- Add bounded diagnostic correlation data and timing counters where existing + metrics cannot isolate an interval. Diagnostics must not make browser time + authoritative, expose hidden maze state, or create unbounded logs, queues, + allocations, or protocol messages. +- Measure ICMP and application-level round-trip baselines on the same local + Wi-Fi path, including distribution and jitter rather than only an average. +- Exercise accepted movement, blocked movement, direction changes, sustained + movement, release, and cooldown rejection with two active players, first + without observers and then at the six-session limit with four observers. +- Compare WebSocket acknowledgement time with the time at which the matching + authoritative view has been completely assembled and painted. Record view + chunk count, socket buffered bytes, gateway queue depth, send duration, and + browser parse, layout, and paint cost for the same test intervals. +- Profile the current sequential per-session view broadcast and the browser's + complete maze DOM replacement without changing their behavior during the + baseline run. +- Classify each observed delay by its dominant interval, document reproducible + test conditions, and select bounded frontend, protocol, firmware, and/or + network remediations for Milestone 017. + +### Deliverables + +- A documented pointer-to-paint latency model with unambiguous measurement + points and command/view correlation. +- A repeatable local-device measurement procedure and bounded diagnostic + instrumentation for browser and firmware. +- Results for baseline and maximum supported session load, including latency + percentiles, maxima, network jitter, queue high-water marks, view sizes, and + browser rendering costs. +- A root-cause report that ranks measured contributors and records the selected + remediation scope and performance budget for Milestone 017. +- Automated tests for correlation, bounded diagnostics, counter reset/wrap + behavior, and preservation of authoritative and hidden-state contracts. + +### Acceptance criteria + +1. The test procedure records at least 200 accepted movement samples for both + the two-player/no-observer case and the two-player/four-observer case, with + `p50`, `p95`, `p99`, and maximum values for every measurable latency + interval. +2. Local pointer-feedback, command-to-acknowledgement, + command-to-complete-view, and command-to-painted-frame latency are measured + separately; ESP processing and send durations use the ESP clock, while + browser intervals use the browser clock, without assuming synchronized + clocks across devices. +3. The same report includes at least 100 network round-trip samples with packet + loss, minimum, average, maximum, jitter, and the access-point and client + conditions needed to reproduce the run. +4. Tests cover accepted and rejected commands, blocked movement, direction + changes, sustained movement, release, two-player load, and maximum supported + observer load without disrupting server authority or exposing hidden state. +5. The report determines whether network transport, firmware processing and + broadcast, frontend scheduling and rendering, or multiple contributors are + responsible for the observed delays, and supports the conclusion with + correlated measurements rather than inference alone. +6. Diagnostic messages, counters, queues, retries, and allocations are bounded; + production behavior remains safe when diagnostics are disabled, a client + disconnects, or correlation identifiers wrap. +7. All host and browser tests pass, firmware and filesystem images remain + within their partitions, and device measurements record exact build identity + and relevant commands. + +### Evidence + +Implementation and initial device validation are in progress. The following +planning-time observations justify the milestone but do not satisfy its +controlled measurement criteria: + +- On 2026-09-09, the project owner reported intermittent input delays up to + approximately 500 ms and approved active browser and WebSocket measurements + against the device at `192.168.1.50`. +- A live `/diagnostics` sample with two connections and 24 accepted game + commands reported no dropped, failed, malformed, oversized, or rate-limited + messages; maximum handler time was 7,800 microseconds and maximum send time + was 1,571 microseconds. This low-load sample does not measure end-to-end + latency or rule out sequential broadcast delays under session load. +- A 20-packet ICMP sample to `192.168.1.50` had zero loss but measured + `60.673 / 147.866 / 246.995 ms` minimum/average/maximum round-trip time with + `49.350 ms` deviation. The network alone therefore already exceeds the ideal + 100 ms target in this setup, but correlation is still required before + assigning the reported 500 ms delays. +- Static inspection found that each accepted movement synchronously broadcasts + authoritative view chunks to sessions, while the player browser waits for a + complete view and replaces the maze DOM before the updated frame is visible. + These are candidate contributors to measure, not yet established root causes. +- The browser now stores at most 256 local movement samples and correlates each + sent movement request with its acknowledgement's authoritative + `stateSequence`, the complete matching view, and the next painted frame. + The gateway includes that state sequence in accepted game acknowledgements; + no browser timestamp is sent to the ESP and no hidden state is added to a + player projection. `docs/M016_LATENCY_PROCEDURE.md` specifies the required + controlled runs and interval boundaries. +- On 2026-09-09, the focused browser test plus `bash scripts/run_host_tests.sh` + passed with 16 browser tests, including bounded latency correlation. Firmware + and filesystem builds passed: application usage was 1,027,966 of 2,097,152 + bytes and the fixed LittleFS image used 2,031,616 of 2,031,616 bytes. +- On 2026-09-09, the diagnostics firmware and filesystem image were flashed to + the confirmed ESP32-C6FH4 on `/dev/ttyACM0`; the health endpoint reported + `serving`. A six-session smoke run recorded 10 accepted commands. Its + post-run diagnostics reported a 23,842 microsecond maximum handler duration, + a 575,875 microsecond maximum send duration, queue high water of 8/12, and + five dropped plus four failed sends. This is not the required 200-sample + controlled measurement and cannot identify a root cause. +- M016 remains `IN_PROGRESS`. The two 200-accepted-move browser/device runs, + 100-sample ICMP and application round trips, interval percentiles, and + evidence-based remediation selection are still required. Milestone 017 has + not been started. +- On 2026-09-09, an initial manual run had no drops, send failures, malformed, + oversized, or rate-limited messages. Before the run, diagnostics reported + 8 accepted commands, a 10,779 microsecond maximum handler duration, and a + 1,591 microsecond maximum send duration. Afterwards, it reported 165 + accepted commands, a 9,800 microsecond maximum handler duration, a + 1,572 microsecond maximum send duration, queue high water of 4/12, and no + message drop or failure. One browser retained 65 correlated samples: its + command-to-acknowledgement values ranged from 19 to 548 ms and its + command-to-painted-view values ranged from 30 to 551 ms. An interrupted + 18-packet ICMP sample had zero loss but measured + `35.904 / 121.461 / 219.597 ms` minimum/average/maximum round trip with + `51.715 ms` deviation. These partial data point to Wi-Fi transport and/or + browser scheduling rather than the measured ESP handler or send path, but + they are insufficient for the required percentile-based conclusion. +- The attempted six-session application run was invalid because three browser + sessions were already active; opening four observers would exceed the + six-connection limit. A second attempt then timed out while joining. Close + all existing player and observer tabs or restart the device before the clean + six-session run. +- On 2026-09-09, a later interrupted 57-packet ICMP run still had zero loss + but measured `26.403 / 114.039 / 343.368 ms` minimum/average/maximum round + trip with `54.637 ms` deviation. A complete 100-request `/health` + application-round-trip run measured 7.5 ms minimum, 186.7 ms average, + 508.0 ms p95, 713.3 ms p99, and 985.4 ms maximum. The M010 six-session + script again timed out waiting for the first player `join` response, so it + produced no valid application-load samples. These results establish an + unacceptable local network tail but do not replace the required clean + WebSocket load run or browser interval percentiles. +- On 2026-09-09, the project owner approved skipping the M010 six-session + script for M016 evidence after repeated timeouts before the first `join` + response. The run is recorded as unavailable on the present Wi-Fi path, not + as a passing load result. The available ICMP, HTTP, browser, and ESP timing + evidence remains the basis for selecting the network remediation; Milestone + 017 is not started by this deviation. + +## Milestone 017 — Avatar-centered drag steering and latency remediation + +**Status:** `NOT_STARTED` + +### Objective + +Replace the separate on-screen joystick with direct dragging of the centered +player avatar, keep the avatar's small local deflection as immediate wordless +direction and speed feedback while the authoritative map moves underneath it, +and apply the measured Milestone 016 remediations so normal local play feels +immediate and never approaches the observed 500 ms delay. + +### Scope + +- Make the visible centered player avatar the movement handle, backed by a + generous transparent touch target that does not obscure nearby maze cells. +- Keep the avatar visually near the viewport center. During a drag, offset it + only within a small bounded radius in the drag direction; on release or any + reset condition, animate it back to the exact center. +- Convert drag angle to exactly one of the four existing cardinal movement + intentions with a dead zone and diagonal hysteresis. Map displacement + magnitude monotonically to a bounded movement cadence: a small intentional + offset moves slowly and a larger offset approaches, but never exceeds, the + authoritative 150 ms cooldown. +- Move the maze beneath the centered avatar only when server-approved state is + received. Local avatar deflection is input feedback and must never predict a + successful cell transition, inventory change, visibility change, or result. +- Coalesce rapidly changing movement intent and bound in-flight commands so a + slow socket, delayed acknowledgement, direction reversal, or release cannot + leave stale movement queued in the browser or gateway. +- Apply the frontend, protocol, firmware, and/or deployment-network changes + selected by Milestone 016. Optimize the measured bottlenecks, including view + assembly, rendering, or broadcast only when supported by its evidence. +- Preserve keyboard and swipe alternatives, observer read-only behavior, + hidden-state protection, Russian accessibility labels, mute and reduced + motion behavior, and all pause, disconnect, and focus-loss reset paths. +- Update the canonical UI and pre-reader usability documents and validate the + interaction on representative mobile touch hardware. + +### Deliverables + +- Avatar-centered drag control with bounded deflection, direction hysteresis, + displacement-based speed, release-to-center animation, and safe reset paths. +- Authoritative map-follow behavior and bounded latest-intent command flow. +- The evidence-selected latency optimizations across browser, protocol, + firmware, and/or documented network setup. +- Updated `docs/UI_STYLE.md`, `docs/PRE_READER_USABILITY.md`, Russian semantic + labels, deterministic browser tests, host tests, and device test procedure. +- Before-and-after latency report using the Milestone 016 measurement method. + +### Acceptance criteria + +1. At the canonical `412 x 916` viewport, touching the player avatar or its + transparent target starts control without a separate joystick. The target is + at least `96 x 96` CSS pixels, the visible avatar remains unobscured, and its + center cannot move more than the documented small deflection radius. +2. Avatar deflection follows the pointer by the next rendered frame during + normal operation, indicates the resolved direction and relative speed + without text, and returns to the exact center on release, cancellation, + pause, disconnect, blur, hidden-document events, and lost pointer capture. +3. The map moves beneath the near-centered avatar only from server-approved + projections. The browser never predicts maze position, collision outcome, + inventory, visibility, timers, or match results. +4. Deflection magnitude maps monotonically to a documented bounded cadence, + the dead zone sends no movement, diagonal hysteresis prevents direction + flicker, and the fastest cadence cannot bypass the 150 ms server cooldown. +5. At most one bounded current movement intention is awaiting completion or + replacement per player. Direction changes and release discard stale local + intent so delayed network delivery cannot create a long client-side command + backlog. +6. In the controlled local Wi-Fi test conditions established by Milestone 016, + accepted input-to-authoritative-painted-update latency is at most 100 ms at + `p95` and at most 150 ms for the maximum sample in both the two-player case + and the two-player/four-observer case. No accepted sample reaches 500 ms. +7. If the deployment network cannot meet criterion 6 before application work, + the documented network remediation is applied and the full test is repeated; + application latency is reported separately so network improvements cannot + conceal a frontend or firmware regression. +8. Browser tests cover pointer capture, touch targeting, bounded offset, + dead-zone and speed mapping, diagonal hysteresis, stale-intent coalescing, + every reset path, authoritative map motion, keyboard and swipe alternatives, + reduced motion, and supported mobile viewports. +9. All host tests pass, firmware and web images remain within partition budgets, + and the latency instrumentation records no dropped messages, unbounded queue + growth, hidden-state disclosure, or observer mutation under maximum supported + session load. +10. The project owner confirms in representative mobile gameplay that a child + can discover movement by dragging the avatar, understand direction and + relative speed from its deflection, stop reliably, and experience responsive + control, or explicitly approves a documented evidence deviation. + +### Evidence + +Not started. This milestone records the project owner's 2026-09-09 decisions: +the child drags the centered player icon; the icon deflects slightly as the +control handle while the authoritative map moves below it; displacement controls +speed; approximately 100 ms is ideal, 150 ms is acceptable, and delays near +500 ms are unacceptable. Implementation must wait for Milestone 016 to identify +and prioritize the actual latency contributors. diff --git a/components/maze32_web/maze32_gateway.c b/components/maze32_web/maze32_gateway.c index a2e386d..28bfaba 100644 --- a/components/maze32_web/maze32_gateway.c +++ b/components/maze32_web/maze32_gateway.c @@ -767,8 +767,9 @@ static void handle_game_command(maze32_gateway_t* gateway, const maze32_session_ append_json_string(acknowledgement, sizeof(acknowledgement), &length, message->request_id) && (written = snprintf(prefix, sizeof(prefix), - ",\"payload\":{\"commandSequence\":%" PRIu32 "}}", command_sequence)) >= - 0 && + ",\"payload\":{\"commandSequence\":%" PRIu32 + ",\"stateSequence\":%" PRIu32 "}}", + command_sequence, gateway->game.state_sequence)) >= 0 && (size_t)written < sizeof(prefix) && append_text(acknowledgement, sizeof(acknowledgement), &length, prefix)) { enqueue(gateway, session->connection_id, acknowledgement); diff --git a/docs/M016_LATENCY_PROCEDURE.md b/docs/M016_LATENCY_PROCEDURE.md new file mode 100644 index 0000000..849a690 --- /dev/null +++ b/docs/M016_LATENCY_PROCEDURE.md @@ -0,0 +1,49 @@ +# Milestone 016 movement-latency procedure + +## Measurement model + +Each accepted movement uses one correlation path. Browser times use +`performance.now()` and ESP times use its monotonic timer; do not subtract a +browser timestamp from an ESP timestamp. + +| Interval | Clock | Measurement point | +| --- | --- | --- | +| pointer feedback | browser | pointer event to next animation frame | +| command to acknowledgement | browser | `move` send to matching `ack` | +| command to complete view | browser | `move` send to matching assembled view sequence | +| command to painted view | browser | `move` send to next frame after matching view render | +| handler and send | ESP | existing `/diagnostics` maxima | +| network round trip | client | ICMP and WebSocket heartbeat samples | + +The accepted command acknowledgement contains the authoritative `stateSequence`. +The browser records a sample only after the complete view with that sequence is +rendered and the next animation frame occurs. Samples are local-only, capped at +256, and do not alter a command, projection, or server result. + +## Controlled runs + +1. Record the firmware build identity, device address, Wi-Fi access point, + client device, browser/version, signal conditions, and viewport. +2. Collect 100 `ping -c 100 -q ` samples. Record loss, min, average, + maximum, and deviation. +3. Run 200 accepted moves per player with two players and no observers. Include + direction changes, a sustained hold, a release, and one blocked move. +4. Repeat with two players and four observers. Capture `/diagnostics` before + and after each run. +5. For each browser, copy + `window.maze32PlayerApp.latencySamples()` after the run. Compute p50, p95, + p99, and maximum for all browser intervals. Keep raw samples only for the + duration of the report and do not store player names or tokens. +6. Compare command-to-acknowledgement with command-to-painted-view, ESP handler + and send maxima, queue high-water marks, observer view chunk count, and + ICMP/WebSocket round trips. Attribute a delay only when the matching + interval is elevated. + +## Safety checks + +- Confirm no dropped, failed, malformed, oversized, or rate-limited message + increased unexpectedly outside the deliberate blocked-move case. +- Confirm queue high water stays within its published limit and browser sample + storage remains at or below 256 entries. +- Record blocked-command rejection separately; it does not count as an + accepted-move sample. diff --git a/test/web/test_player_app.js b/test/web/test_player_app.js index 5543e27..43e34f5 100644 --- a/test/web/test_player_app.js +++ b/test/web/test_player_app.js @@ -43,6 +43,25 @@ test("joystick repeat interval is monotonic and remains slower than cooldown", ( assert.ok(player.JOYSTICK_FAST_REPEAT_MS > 150); }); +test("latency correlation is bounded and only completes matching authoritative views", () => { + let clock = 0; + const tracker = new player.LatencyTracker(() => clock); + tracker.sent("move-1"); + clock = 12; + tracker.acknowledged("move-1", 41); + clock = 16; + tracker.painted(40); + assert.equal(tracker.snapshot().length, 0); + clock = 24; + tracker.painted(41); + assert.deepEqual(tracker.snapshot(), [{requestId: "move-1", commandToAckMs: 12, + commandToPaintMs: 24, ackToPaintMs: 12}]); + for (let index = 0; index < player.LATENCY_SAMPLE_LIMIT + 4; index++) { + tracker.sent(`move-${index + 2}`); + } + assert.ok(tracker.pending.size <= player.LATENCY_SAMPLE_LIMIT); +}); + test("player map cells enlarge small projections without exceeding a readable bound", () => { const sevenBySeven = player.playerCellSize(396, 730, 7, 7); const fourByFour = player.playerCellSize(396, 730, 4, 4); diff --git a/web/player.js b/web/player.js index 8b8909d..0f0e865 100644 --- a/web/player.js +++ b/web/player.js @@ -12,6 +12,7 @@ const JOYSTICK_HYSTERESIS = .18; const JOYSTICK_SLOW_REPEAT_MS = 360; const JOYSTICK_FAST_REPEAT_MS = 170; + const LATENCY_SAMPLE_LIMIT = 256; const ONBOARDING_STEPS = Object.freeze([ Object.freeze({icon: "hand", title: "Потяни кружок", text: "Кружок ведёт героя по лабиринту."}), Object.freeze({icon: "motion", title: "Отпусти — герой ждёт", text: "Рука остановилась — шаги тоже."}), @@ -145,6 +146,40 @@ return currentStep >= ONBOARDING_STEPS.length - 1 ? null : currentStep + 1; } + class LatencyTracker { + constructor(now) { + this.now = typeof now === "function" ? now : () => root.performance.now(); + this.pending = new Map(); + this.samples = []; + } + + sent(requestId) { + if (!requestId) return; + this.pending.set(requestId, {sent: this.now(), acknowledged: null, stateSequence: null}); + if (this.pending.size > LATENCY_SAMPLE_LIMIT) this.pending.delete(this.pending.keys().next().value); + } + + acknowledged(requestId, stateSequence) { + const sample = this.pending.get(requestId); + if (!sample) return; + sample.acknowledged = this.now(); + sample.stateSequence = Number.isInteger(stateSequence) ? stateSequence : null; + } + + painted(stateSequence) { + for (const [requestId, sample] of this.pending) { + if (sample.acknowledged == null || sample.stateSequence !== stateSequence) continue; + const painted = this.now(); + this.samples.push({requestId, commandToAckMs: sample.acknowledged - sample.sent, + commandToPaintMs: painted - sample.sent, ackToPaintMs: painted - sample.acknowledged}); + if (this.samples.length > LATENCY_SAMPLE_LIMIT) this.samples.shift(); + this.pending.delete(requestId); + } + } + + snapshot() { return this.samples.map(sample => Object.assign({}, sample)); } + } + function createEnvelope(type, sequence, requestId, payload) { return {v: 1, type, seq: sequence, requestId, payload: payload || {}}; } @@ -179,6 +214,7 @@ } const first = this.pending.parts[0]; const snapshot = Object.assign({}, first, { + stateSequence: message.seq, cells: this.pending.parts.flatMap(part => part.cells), objects: this.pending.parts.flatMap(part => part.objects) }); @@ -208,7 +244,7 @@ function boot(documentObject, windowObject) { const byId = id => documentObject.getElementById(id); - const state = {socket: null, clientSequence: 0, serverSequence: 0, viewSequence: -1, session: null, lobby: null, snapshot: null, reconnectTimer: null, noticeTimer: null, touchStart: null, pending: new Map(), assembler: new SnapshotAssembler(), audio: null, feedbackReady: false, muted: false, reducedMotion: false, onboardingSeen: false, onboardingStep: 0, joystick: {pointerId: null, direction: null, repeatTimer: null, repeatMs: null}, observer: {scale: 1, x: 0, y: 0, dragging: null}}; + const state = {socket: null, clientSequence: 0, serverSequence: 0, viewSequence: -1, session: null, lobby: null, snapshot: null, reconnectTimer: null, noticeTimer: null, touchStart: null, pending: new Map(), assembler: new SnapshotAssembler(), latency: new LatencyTracker(() => windowObject.performance.now()), audio: null, feedbackReady: false, muted: false, reducedMotion: false, onboardingSeen: false, onboardingStep: 0, joystick: {pointerId: null, direction: null, repeatTimer: null, repeatMs: null}, observer: {scale: 1, x: 0, y: 0, dragging: null}}; function showNotice(text, kind) { const element = byId("notice"); @@ -287,6 +323,7 @@ const requestId = `${type}-${sequence}`; state.socket.send(JSON.stringify(createEnvelope(type, sequence, requestId, payload))); setPending(requestId, element); + if (type === "move") state.latency.sent(requestId); return requestId; } @@ -411,6 +448,9 @@ byId("compass-stat").dataset.direction = hint; byId("compass-arrow").style.transform = `rotate(${HINT_ANGLES[hint]}deg)`; renderMap(snapshot); + if (Number.isInteger(snapshot.stateSequence)) { + windowObject.requestAnimationFrame(() => state.latency.painted(snapshot.stateSequence)); + } if (snapshot.phase === "paused") { resetJoystick(); showModal("Пауза", "Один герой потерялся между Wi‑Fi пакетами. Ждём возвращения.", "ПЕРЕРЫВ", false); } else if (snapshot.phase === "finished" || snapshot.result && snapshot.result !== "none") renderResult(snapshot.result); else { @@ -551,7 +591,7 @@ } else if (message.type === "resume") { hideModal(); showNotice("Оба героя снова на связи. Гонка продолжается.", "success"); } else if (message.type === "match_result") { renderResult(message.payload.result); } else if (message.type === "reset") { state.snapshot = null; state.viewSequence = -1; state.assembler = new SnapshotAssembler(); resetJoystick(); hideModal(); setScreen("lobby"); showNotice("Лабиринт пересажен. Можно начинать новую гонку.", "success"); - } else if (message.type === "ack") { feedback(true); + } else if (message.type === "ack") { state.latency.acknowledged(message.requestId, message.payload && message.payload.stateSequence); feedback(true); } else if (message.type === "error") { if (message.payload.code === "invalid_token") { windowObject.localStorage.removeItem(TOKEN_KEY); state.session = null; setScreen("welcome"); } if (message.payload.code === "blocked") { const bump = byId("wall-bump"); bump.textContent = localError("blocked"); bump.classList.remove("show"); void bump.offsetWidth; bump.classList.add("show"); } @@ -689,10 +729,10 @@ } }); connect(); - return {state, handleMessage, move, resetJoystick}; + return {state, handleMessage, move, resetJoystick, latencySamples: () => state.latency.snapshot()}; } - const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, ONBOARDING_STEPS, JOYSTICK_DEAD_ZONE, JOYSTICK_HYSTERESIS, JOYSTICK_SLOW_REPEAT_MS, JOYSTICK_FAST_REPEAT_MS, directionFromKey, directionFromSwipe, joystickDirection, joystickRepeatMs, joystickVector, playerCellSize, nextOnboardingStep, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot}); + const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, ONBOARDING_STEPS, LATENCY_SAMPLE_LIMIT, JOYSTICK_DEAD_ZONE, JOYSTICK_HYSTERESIS, JOYSTICK_SLOW_REPEAT_MS, JOYSTICK_FAST_REPEAT_MS, LatencyTracker, directionFromKey, directionFromSwipe, joystickDirection, joystickRepeatMs, joystickVector, playerCellSize, nextOnboardingStep, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot}); if (typeof module !== "undefined" && module.exports) module.exports = api; root.Maze32Player = api; if (root.document) root.document.addEventListener("DOMContentLoaded", () => { root.maze32PlayerApp = boot(root.document, root); });