feat(player): reposition mobile player focal point
This commit is contained in:
@@ -341,6 +341,12 @@ already visible projection in that attempted direction, while `rate_limited`
|
|||||||
briefly darkens and blurs the maze only. Neither effect changes or derives game
|
briefly darkens and blurs the maze only. Neither effect changes or derives game
|
||||||
state, and both use bounded, resettable local timers.
|
state, and both use bounded, resettable local timers.
|
||||||
|
|
||||||
|
Player projections are translated only within their received cells so the
|
||||||
|
authoritative `self` cell aligns with the responsive local focal point. That
|
||||||
|
point is centered horizontally and 30% down the usable portrait map region;
|
||||||
|
compact landscape uses a left-biased unobstructed region. It changes no server
|
||||||
|
projection, visibility, or hidden-state handling.
|
||||||
|
|
||||||
All application assets are local and precompressed. Browser startup and play
|
All application assets are local and precompressed. Browser startup and play
|
||||||
must not depend on internet access, third-party CSS, remote fonts, icons,
|
must not depend on internet access, third-party CSS, remote fonts, icons,
|
||||||
analytics, or content services. The complete compressed web payload is limited
|
analytics, or content services. The complete compressed web payload is limited
|
||||||
|
|||||||
@@ -230,6 +230,10 @@ Browser controls must support:
|
|||||||
- The player map consumes all space not required by fixed status and control
|
- The player map consumes all space not required by fixed status and control
|
||||||
panels. The player view supports a minimum `412 x 916` CSS-pixel viewport
|
panels. The player view supports a minimum `412 x 916` CSS-pixel viewport
|
||||||
without page scrolling.
|
without page scrolling.
|
||||||
|
- On portrait phones, the authoritative player focal point is horizontally
|
||||||
|
centered at approximately 30% of the usable map height, above the operating
|
||||||
|
hand zone. Compact landscape uses a stable left-biased focal point in the
|
||||||
|
largest unobstructed map area.
|
||||||
- Textual system notices use one Russian live message rail above the top bar.
|
- Textual system notices use one Russian live message rail above the top bar.
|
||||||
A connection-loss notice remains there until recovery; movement blocked by a
|
A connection-loss notice remains there until recovery; movement blocked by a
|
||||||
wall and rate limiting use maze-only visual feedback and never show text.
|
wall and rate limiting use maze-only visual feedback and never show text.
|
||||||
|
|||||||
@@ -1971,7 +1971,7 @@ the project-owner visual confirmation remains outstanding:
|
|||||||
|
|
||||||
## Milestone 020 — Playful visual feedback and top message rail
|
## Milestone 020 — Playful visual feedback and top message rail
|
||||||
|
|
||||||
**Status:** `IN_PROGRESS`
|
**Status:** `COMPLETE`
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|
||||||
@@ -2119,7 +2119,151 @@ The following planning-time observation motivated the milestone:
|
|||||||
RAM `58868/327680` bytes and factory flash `1027966/2097152` bytes.
|
RAM `58868/327680` bytes and factory flash `1027966/2097152` bytes.
|
||||||
`/home/sasa/.platformio/penv/bin/pio run -t buildfs` passed and produced the
|
`/home/sasa/.platformio/penv/bin/pio run -t buildfs` passed and produced the
|
||||||
configured `2031616/2031616` byte LittleFS image.
|
configured `2031616/2031616` byte LittleFS image.
|
||||||
- Milestone 020 remains `IN_PROGRESS`: representative mobile/desktop visual,
|
- On 2026-09-10, the project owner completed a live UI check and confirmed that
|
||||||
reduced-motion, accessibility, and project-owner confirmation evidence for
|
the feedback, message rail, and animations work as expected. This satisfies
|
||||||
acceptance criteria 7 and 13 has not been recorded. Milestone 021 has not
|
the remaining visual-confirmation criterion; Milestone 020 is `COMPLETE`.
|
||||||
been started.
|
|
||||||
|
## Milestone 021 — Mobile player focal-point repositioning
|
||||||
|
|
||||||
|
**Status:** `IN_PROGRESS`
|
||||||
|
|
||||||
|
### Objective
|
||||||
|
|
||||||
|
Move the player avatar away from the geometric center of the mobile maze into
|
||||||
|
a responsive, unobstructed focal position so hands operating the lower controls
|
||||||
|
do not hide the player or the most useful nearby maze context.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
- Before implementation, reconcile `MVP.md`, `ARCHITECTURE.md`,
|
||||||
|
`docs/UI_STYLE.md`, and supporting player-UI documentation with the new
|
||||||
|
mobile player-anchor contract. This supersedes the current requirement to
|
||||||
|
keep the player at the exact center of the map viewport.
|
||||||
|
- Define a responsive safe play area that excludes the fixed player-status
|
||||||
|
panel, Milestone 020 top message rail, directional pad, key-drop action,
|
||||||
|
utility controls, safe-area insets, and other persistent overlays.
|
||||||
|
- In portrait mobile layouts, place the authoritative player focal point above
|
||||||
|
the maze's geometric center while retaining useful visible context on every
|
||||||
|
side. At the canonical compact viewport, target approximately 30% of the map
|
||||||
|
region's height from its top rather than 50%.
|
||||||
|
- In compact landscape layouts, choose the player focal point from the largest
|
||||||
|
unobstructed map region and bias it away from the directional-pad hand zone.
|
||||||
|
Do not apply the portrait offset blindly when it would reduce usable context.
|
||||||
|
- Translate only the server-provided projection so its authoritative `self`
|
||||||
|
cell appears beneath the responsive focal point. Do not request, synthesize,
|
||||||
|
retain, or infer extra maze cells to fill space created by the offset.
|
||||||
|
- Keep the visible player icon aligned exactly with the authoritative `self`
|
||||||
|
cell after every projection, resize, orientation change, safe-area change,
|
||||||
|
and projection-edge case.
|
||||||
|
- Preserve the four-button directional pad as the primary phone input. Avatar
|
||||||
|
drag remains an optional compatibility input, but normal mobile play must not
|
||||||
|
require touching or covering the player icon.
|
||||||
|
- Keep the optional avatar-drag deflection relative to the new focal point and
|
||||||
|
return it there on release, cancellation, pause, disconnect, blur, hidden
|
||||||
|
document, lost pointer capture, and application reset.
|
||||||
|
- Integrate the new focal point with Milestone 020 maze shake and blur effects.
|
||||||
|
Temporary feedback transforms must compose with the projection offset and
|
||||||
|
must not cause the player or map to jump when an effect begins or ends.
|
||||||
|
- Preserve observer full-map positioning, desktop behavior where appropriate,
|
||||||
|
reduced-motion behavior, authoritative server ownership, hidden-state
|
||||||
|
protection, offline assets, and existing memory and Flash budgets.
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
|
||||||
|
- Updated player-anchor, responsive-layout, input, and projection-presentation
|
||||||
|
contracts in canonical and supporting UI documentation.
|
||||||
|
- A bounded responsive focal-point calculation for portrait and compact
|
||||||
|
landscape mobile layouts.
|
||||||
|
- Player and server-projection rendering aligned to the new focal point,
|
||||||
|
including edge projections and orientation changes.
|
||||||
|
- Compatibility with the directional pad, optional avatar drag, top message
|
||||||
|
rail, blocked-movement shake, and rate-limit blur.
|
||||||
|
- Deterministic layout tests plus representative mobile portrait and landscape
|
||||||
|
visual and touch verification evidence.
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
|
||||||
|
1. At `412 x 915` and `412 x 916` CSS pixels in portrait orientation, the
|
||||||
|
resting player center is horizontally centered and positioned at
|
||||||
|
approximately 30% of the usable map region's height from its top, with a
|
||||||
|
documented tolerance of no more than five percentage points. It is not at
|
||||||
|
the map's 50% geometric center.
|
||||||
|
2. The player focal point lies completely outside the directional pad,
|
||||||
|
key-drop, utility-control, top-message, and status-panel exclusion zones,
|
||||||
|
including safe-area insets. The visible player icon and its outline do not
|
||||||
|
overlap any persistent control.
|
||||||
|
3. While a representative thumb operates every directional-pad button, the
|
||||||
|
player icon and its current cell remain visible and enough adjacent
|
||||||
|
server-provided maze context remains unobstructed to understand the next
|
||||||
|
orthogonal move.
|
||||||
|
4. Compact landscape orientation computes a stable focal point within the
|
||||||
|
largest unobstructed map region and away from the directional-pad hand zone.
|
||||||
|
Rotating between portrait and landscape does not leave the player beneath a
|
||||||
|
control or outside the visible map shell.
|
||||||
|
5. The rendered player stays aligned with the authoritative `self` cell to
|
||||||
|
within one CSS pixel after movement projections, viewport resize,
|
||||||
|
orientation changes, dynamic safe-area changes, and projections touching
|
||||||
|
each maze edge or corner.
|
||||||
|
6. Repositioning translates only cells contained in the current authoritative
|
||||||
|
projection. The browser does not expose, retain, infer, decorate, or fill
|
||||||
|
hidden maze geometry, and no protocol or server projection changes.
|
||||||
|
7. Direction-pad, swipe, keyboard, and optional avatar-drag inputs continue to
|
||||||
|
send the same bounded cardinal intentions. Avatar drag deflects relative to
|
||||||
|
the new focal point and every existing reset path returns it exactly there.
|
||||||
|
8. Milestone 020 directional shake and rate-limit blur compose with the focal
|
||||||
|
offset without changing the underlying authoritative position, accumulating
|
||||||
|
transforms, exposing hidden cells, or producing a jump when feedback clears.
|
||||||
|
9. The player layout requires no horizontal or vertical page scrolling at the
|
||||||
|
supported mobile viewports. The maze remains the dominant visual region and
|
||||||
|
the shifted projection does not create a distracting inactive void.
|
||||||
|
10. Browser tests cover focal-point calculation, exclusion-zone avoidance,
|
||||||
|
portrait and landscape placement, safe areas, resize and rotation, all map
|
||||||
|
edges and corners, one-pixel player/self alignment, input compatibility,
|
||||||
|
drag reset paths, Milestone 020 transform composition, zero overflow, and
|
||||||
|
absence of hidden-state expansion.
|
||||||
|
11. All host tests pass, compressed web assets remain within the 1.5 MiB budget
|
||||||
|
and LittleFS partition, firmware remains within the factory partition, and
|
||||||
|
observer layout and authority remain unchanged.
|
||||||
|
12. The project owner confirms on a representative mobile browser that the new
|
||||||
|
player position is comfortable, the operating hand no longer hides the
|
||||||
|
player or critical nearby maze context during directional-button play, and
|
||||||
|
portrait and landscape layouts remain easy to understand, or explicitly
|
||||||
|
approves a documented evidence deviation.
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
|
||||||
|
The following planning-time observation motivated the milestone:
|
||||||
|
|
||||||
|
- On 2026-09-10, the project owner reported that the exactly centered mobile
|
||||||
|
player is uncomfortable because fingers hide the maze and the player. A
|
||||||
|
read-only inspection of the open player page at `412 x 915` CSS pixels found
|
||||||
|
the `96 x 96` avatar target centered at `(206, 525.5)` and the map shell also
|
||||||
|
centered at `(206, 525.5)`, confirming exact geometric overlap. The player
|
||||||
|
requested a planning milestone only; implementation and completion
|
||||||
|
verification were deferred until Milestone 020 was `COMPLETE`.
|
||||||
|
- On 2026-09-10, `MVP.md`, `ARCHITECTURE.md`, `docs/UI_STYLE.md`, and
|
||||||
|
`docs/PLAYER_UI.md` were reconciled to define a 30%-height portrait focal
|
||||||
|
point and a compact-landscape left-biased unobstructed focal point. The
|
||||||
|
browser now composes the received-projection translation with that focal
|
||||||
|
offset, places the optional avatar drag target at the same point, and
|
||||||
|
recalculates both after resize and orientation change. It continues to render
|
||||||
|
only received projection cells; the Milestone 020 feedback transform uses
|
||||||
|
these composed map-offset variables unchanged.
|
||||||
|
- `node --test test/web/test_player_app.js` passed: 23 tests, 0 failures.
|
||||||
|
The added deterministic focal-point checks cover portrait and compact
|
||||||
|
landscape placement plus projection-offset composition at a projection edge.
|
||||||
|
- `bash scripts/run_host_tests.sh` passed all portable host suites, all 23
|
||||||
|
browser tests, and the deterministic asset check:
|
||||||
|
`web asset reproducibility: PASS (..., 27367/1572864 bytes)`.
|
||||||
|
- `/home/sasa/.platformio/penv/bin/pio run` passed for `esp32-c6-devkitm-1`:
|
||||||
|
RAM `58868/327680` bytes and factory flash `1027966/2097152` bytes.
|
||||||
|
`/home/sasa/.platformio/penv/bin/pio run -t buildfs` passed and produced the
|
||||||
|
configured `2031616/2031616` byte LittleFS image.
|
||||||
|
- Milestone 021 remains `IN_PROGRESS`: the project-owner representative mobile
|
||||||
|
portrait and compact-landscape touch confirmation required by acceptance
|
||||||
|
criterion 12 has not been recorded. Milestone 022 has not been started.
|
||||||
|
- On 2026-09-10, a project-owner live UI check found that the original 40%
|
||||||
|
map-region target still appeared centered on the full phone screen because
|
||||||
|
the map begins below the status area. The owner requested a more elevated
|
||||||
|
player position, so the active milestone target and implementation were
|
||||||
|
adjusted to 30% of map height; mobile confirmation remains required.
|
||||||
|
|||||||
+5
-2
@@ -48,8 +48,11 @@ drag changes into one latest intention; release discards that unsent intention.
|
|||||||
The deflection is local input feedback only. The maze redraws only after a
|
The deflection is local input feedback only. The maze redraws only after a
|
||||||
server-authoritative projection and the browser never predicts a cell change.
|
server-authoritative projection and the browser never predicts a cell change.
|
||||||
For every projection, the browser translates only the received cells so the
|
For every projection, the browser translates only the received cells so the
|
||||||
authoritative `self` cell stays under the centered avatar; this also keeps the
|
authoritative `self` cell stays under the responsive avatar focal point. On a
|
||||||
avatar off a wall when the visible projection reaches a maze boundary.
|
portrait phone this is horizontally centered at 30% of the map height; compact
|
||||||
|
landscape uses a stable left-biased unobstructed point away from the directional
|
||||||
|
pad. This also keeps the avatar off a wall when the visible projection reaches
|
||||||
|
a maze boundary, and is recalculated on resize and rotation.
|
||||||
|
|
||||||
## Feedback and message rail
|
## Feedback and message rail
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -113,8 +113,11 @@ are not MVP requirements.
|
|||||||
|
|
||||||
## 7. Player map presentation
|
## 7. Player map presentation
|
||||||
|
|
||||||
- Keep the player near the center of the map viewport except where maze
|
- Keep the portrait player horizontally centered at approximately 30% of the
|
||||||
boundaries prevent it.
|
usable map height, above the directional-pad hand zone. In compact landscape,
|
||||||
|
use the stable left-biased focal point in the largest unobstructed map area.
|
||||||
|
Recalculate it after resize and rotation without moving the authoritative
|
||||||
|
player away from its received `self` cell.
|
||||||
- Render unknown space as a solid background. Never infer, retain, animate, or
|
- Render unknown space as a solid background. Never infer, retain, animate, or
|
||||||
decorate maze geometry outside the server-provided visible projection.
|
decorate maze geometry outside the server-provided visible projection.
|
||||||
- Compute cell size from the current server projection and available map area.
|
- Compute cell size from the current server projection and available map area.
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ test("authoritative self cell remains below the centered avatar at map edges", (
|
|||||||
assert.deepEqual(player.playerMapOffset(-1, 7, 0, 0, 7, 7, 46), {x: 0, y: 0});
|
assert.deepEqual(player.playerMapOffset(-1, 7, 0, 0, 7, 7, 46), {x: 0, y: 0});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("responsive player focal point stays above phone controls and composes with map offsets", () => {
|
||||||
|
assert.deepEqual(player.playerFocalPoint(400, 800), {x: 200, y: 240});
|
||||||
|
assert.deepEqual(player.playerFocalPoint(900, 400), {x: 378, y: 180});
|
||||||
|
assert.deepEqual(player.playerFocalMapOffset(24, 7, 1, 1, 5, 5, 46, 400, 800), {x: 0, y: -160});
|
||||||
|
assert.deepEqual(player.playerFocalMapOffset(0, 7, 0, 0, 7, 7, 46, 400, 800), {x: 138, y: -22});
|
||||||
|
});
|
||||||
|
|
||||||
test("onboarding has bounded, replayable steps", () => {
|
test("onboarding has bounded, replayable steps", () => {
|
||||||
assert.equal(player.ONBOARDING_STEPS.length, 8);
|
assert.equal(player.ONBOARDING_STEPS.length, 8);
|
||||||
assert.equal(player.nextOnboardingStep(0), 1);
|
assert.equal(player.nextOnboardingStep(0), 1);
|
||||||
@@ -136,6 +143,8 @@ test("avatar control resets safely and retains accessible alternatives", () => {
|
|||||||
assert.match(script, /snapshot\.phase === "paused"\) \{ resetPlayerControls\(\)/);
|
assert.match(script, /snapshot\.phase === "paused"\) \{ resetPlayerControls\(\)/);
|
||||||
assert.match(script, /state\.avatar\.startX = event\.clientX/);
|
assert.match(script, /state\.avatar\.startX = event\.clientX/);
|
||||||
assert.match(script, /event\.clientX - state\.avatar\.startX/);
|
assert.match(script, /event\.clientX - state\.avatar\.startX/);
|
||||||
|
assert.match(script, /windowObject\.addEventListener\("resize", refreshPlayerFocalPoint\)/);
|
||||||
|
assert.match(script, /windowObject\.addEventListener\("orientationchange", refreshPlayerFocalPoint\)/);
|
||||||
assert.match(script, /setMuted\(!state\.muted\)/);
|
assert.match(script, /setMuted\(!state\.muted\)/);
|
||||||
assert.match(script, /setReducedMotion\(!state\.reducedMotion\)/);
|
assert.match(script, /setReducedMotion\(!state\.reducedMotion\)/);
|
||||||
assert.match(html, /id="avatar-control"/);
|
assert.match(html, /id="avatar-control"/);
|
||||||
|
|||||||
+1
-1
@@ -153,7 +153,7 @@ input:focus { border-color: var(--aqua); box-shadow: 0 0 0 3px #43ead229; }
|
|||||||
[data-reduced-motion="true"] .wall-bump.impact-south { border-bottom-color: var(--error); }
|
[data-reduced-motion="true"] .wall-bump.impact-south { border-bottom-color: var(--error); }
|
||||||
[data-reduced-motion="true"] .wall-bump.impact-west { border-left-color: var(--error); }
|
[data-reduced-motion="true"] .wall-bump.impact-west { border-left-color: var(--error); }
|
||||||
|
|
||||||
.avatar-control { position: absolute; z-index: 8; left: 50%; top: 50%; width: 96px; height: 96px; margin: -48px; padding: 0; border: 0; border-radius: 50%; background: transparent; cursor: grab; touch-action: none; user-select: none; }
|
.avatar-control { position: absolute; z-index: 8; left: var(--player-focal-x, 50%); top: var(--player-focal-y, 50%); width: 96px; height: 96px; margin: -48px; padding: 0; border: 0; border-radius: 50%; background: transparent; cursor: grab; touch-action: none; user-select: none; }
|
||||||
.avatar-control:active, .avatar-control.active { cursor: grabbing; }
|
.avatar-control:active, .avatar-control.active { cursor: grabbing; }
|
||||||
.avatar-handle { position: absolute; left: 50%; top: 50%; width: 52px; height: 52px; display: grid; place-items: center; border: 3px solid currentColor; border-radius: 50%; background: #0b0712; color: var(--p1); box-shadow: 4px 4px 0 #000, 0 0 0 3px #43ead236; font-size: 1.25rem; font-weight: 1000; transform: translate(-50%, -50%); transition: transform .12s steps(3, end); }
|
.avatar-handle { position: absolute; left: 50%; top: 50%; width: 52px; height: 52px; display: grid; place-items: center; border: 3px solid currentColor; border-radius: 50%; background: #0b0712; color: var(--p1); box-shadow: 4px 4px 0 #000, 0 0 0 3px #43ead236; font-size: 1.25rem; font-weight: 1000; transform: translate(-50%, -50%); transition: transform .12s steps(3, end); }
|
||||||
.player-two-mode .avatar-handle { color: var(--p2); }
|
.player-two-mode .avatar-handle { color: var(--p2); }
|
||||||
|
|||||||
+22
-2
@@ -190,6 +190,18 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playerFocalPoint(stageWidth, stageHeight) {
|
||||||
|
if (![stageWidth, stageHeight].every(value => Number.isFinite(value) && value > 0)) return {x: 0, y: 0};
|
||||||
|
if (stageWidth > stageHeight) return {x: Math.round(stageWidth * .42), y: Math.round(stageHeight * .45)};
|
||||||
|
return {x: Math.round(stageWidth / 2), y: Math.round(stageHeight * .3)};
|
||||||
|
}
|
||||||
|
|
||||||
|
function playerFocalMapOffset(selfIndex, width, minX, minY, columns, rows, cellSize, stageWidth, stageHeight) {
|
||||||
|
const centered = playerMapOffset(selfIndex, width, minX, minY, columns, rows, cellSize);
|
||||||
|
const focal = playerFocalPoint(stageWidth, stageHeight);
|
||||||
|
return {x: centered.x + focal.x - stageWidth / 2, y: centered.y + focal.y - stageHeight / 2};
|
||||||
|
}
|
||||||
|
|
||||||
function nextOnboardingStep(currentStep) {
|
function nextOnboardingStep(currentStep) {
|
||||||
return currentStep >= ONBOARDING_STEPS.length - 1 ? null : currentStep + 1;
|
return currentStep >= ONBOARDING_STEPS.length - 1 ? null : currentStep + 1;
|
||||||
}
|
}
|
||||||
@@ -495,12 +507,15 @@
|
|||||||
const rows = maxY - minY + 1;
|
const rows = maxY - minY + 1;
|
||||||
const mapShell = byId("map-shell");
|
const mapShell = byId("map-shell");
|
||||||
const cellSize = playerCellSize(mapShell.clientWidth, mapShell.clientHeight, columns, rows);
|
const cellSize = playerCellSize(mapShell.clientWidth, mapShell.clientHeight, columns, rows);
|
||||||
const offset = playerMapOffset(snapshot.self, snapshot.width, minX, minY, columns, rows, cellSize);
|
const focal = playerFocalPoint(mapShell.clientWidth, mapShell.clientHeight);
|
||||||
|
const offset = playerFocalMapOffset(snapshot.self, snapshot.width, minX, minY, columns, rows, cellSize, mapShell.clientWidth, mapShell.clientHeight);
|
||||||
maze.style.setProperty("--cell", `${cellSize}px`);
|
maze.style.setProperty("--cell", `${cellSize}px`);
|
||||||
maze.style.gridTemplateColumns = `repeat(${columns}, var(--cell))`;
|
maze.style.gridTemplateColumns = `repeat(${columns}, var(--cell))`;
|
||||||
maze.style.gridTemplateRows = `repeat(${rows}, var(--cell))`;
|
maze.style.gridTemplateRows = `repeat(${rows}, var(--cell))`;
|
||||||
maze.style.setProperty("--map-offset-x", `${offset.x}px`);
|
maze.style.setProperty("--map-offset-x", `${offset.x}px`);
|
||||||
maze.style.setProperty("--map-offset-y", `${offset.y}px`);
|
maze.style.setProperty("--map-offset-y", `${offset.y}px`);
|
||||||
|
mapShell.style.setProperty("--player-focal-x", `${focal.x}px`);
|
||||||
|
mapShell.style.setProperty("--player-focal-y", `${focal.y}px`);
|
||||||
const objects = new Map();
|
const objects = new Map();
|
||||||
snapshot.objects.forEach(object => { const list = objects.get(object[3]) || []; list.push(object); objects.set(object[3], list); });
|
snapshot.objects.forEach(object => { const list = objects.get(object[3]) || []; list.push(object); objects.set(object[3], list); });
|
||||||
coordinates.forEach(data => {
|
coordinates.forEach(data => {
|
||||||
@@ -837,6 +852,11 @@
|
|||||||
byId("avatar-control").addEventListener("pointercancel", stopAvatarControl);
|
byId("avatar-control").addEventListener("pointercancel", stopAvatarControl);
|
||||||
byId("avatar-control").addEventListener("lostpointercapture", stopAvatarControl);
|
byId("avatar-control").addEventListener("lostpointercapture", stopAvatarControl);
|
||||||
windowObject.addEventListener("blur", resetPlayerControls);
|
windowObject.addEventListener("blur", resetPlayerControls);
|
||||||
|
const refreshPlayerFocalPoint = () => {
|
||||||
|
if (state.snapshot && state.snapshot.role === "player") windowObject.requestAnimationFrame(() => renderMap(state.snapshot));
|
||||||
|
};
|
||||||
|
windowObject.addEventListener("resize", refreshPlayerFocalPoint);
|
||||||
|
windowObject.addEventListener("orientationchange", refreshPlayerFocalPoint);
|
||||||
documentObject.addEventListener("visibilitychange", () => { if (documentObject.hidden) resetPlayerControls(); });
|
documentObject.addEventListener("visibilitychange", () => { if (documentObject.hidden) resetPlayerControls(); });
|
||||||
documentObject.querySelectorAll("[data-direction]").forEach(button => {
|
documentObject.querySelectorAll("[data-direction]").forEach(button => {
|
||||||
button.addEventListener("pointerdown", event => {
|
button.addEventListener("pointerdown", event => {
|
||||||
@@ -902,7 +922,7 @@
|
|||||||
return {state, handleMessage, move, resetAvatarControl, latencySamples: () => state.latency.snapshot(), pointerFeedbackSamples: () => state.latency.pointerSnapshot()};
|
return {state, handleMessage, move, resetAvatarControl, latencySamples: () => state.latency.snapshot(), pointerFeedbackSamples: () => state.latency.pointerSnapshot()};
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, ONBOARDING_STEPS, LATENCY_SAMPLE_LIMIT, AVATAR_TOUCH_TARGET_SIZE, AVATAR_MAX_DEFLECTION, AVATAR_DEAD_ZONE, AVATAR_DIRECTION_HYSTERESIS, AVATAR_SLOW_REPEAT_MS, AVATAR_FAST_REPEAT_MS, DIRECTIONAL_PAD_REPEAT_MS, LatestIntentFlow, LatencyTracker, directionFromKey, directionFromSwipe, avatarDirection, avatarRepeatMs, avatarVector, playerCellSize, playerMapOffset, nextOnboardingStep, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot});
|
const api = Object.freeze({TOKEN_KEY, MAX_VIEW_CHUNKS, NAME_ADJECTIVES, NAME_NOUNS, NAME_COMBINATION_COUNT, LOGIN_HEADLINES, ONBOARDING_STEPS, LATENCY_SAMPLE_LIMIT, AVATAR_TOUCH_TARGET_SIZE, AVATAR_MAX_DEFLECTION, AVATAR_DEAD_ZONE, AVATAR_DIRECTION_HYSTERESIS, AVATAR_SLOW_REPEAT_MS, AVATAR_FAST_REPEAT_MS, DIRECTIONAL_PAD_REPEAT_MS, LatestIntentFlow, LatencyTracker, directionFromKey, directionFromSwipe, avatarDirection, avatarRepeatMs, avatarVector, playerCellSize, playerMapOffset, playerFocalPoint, playerFocalMapOffset, nextOnboardingStep, randomIndex, randomSuggestedName, randomLoginHeadline, createEnvelope, snapshotIsValid, SnapshotAssembler, projectedCellIndices, clampObserverZoom, observerFitScale, boot});
|
||||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||||
root.Maze32Player = api;
|
root.Maze32Player = api;
|
||||||
if (root.document) root.document.addEventListener("DOMContentLoaded", () => { root.maze32PlayerApp = boot(root.document, root); });
|
if (root.document) root.document.addEventListener("DOMContentLoaded", () => { root.maze32PlayerApp = boot(root.document, root); });
|
||||||
|
|||||||
Reference in New Issue
Block a user