feat(web): render canonical living hedge maze walls
This commit is contained in:
@@ -342,6 +342,11 @@ bits, and projects changed passages only in cells already visible to that role.
|
||||
The browser may correlate a matching server-approved impact stage with an
|
||||
already visible bush wall; it never derives or opens topology locally.
|
||||
|
||||
Browser hedge rendering derives a bounded canonical edge list solely from the
|
||||
current role projection. Its deterministic local SVG selection is presentation
|
||||
only: it cannot create maze edges, alter hit testing, retain hidden cells, or
|
||||
change authoritative movement and visibility.
|
||||
|
||||
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;
|
||||
|
||||
@@ -2591,3 +2591,263 @@ The following planning-time request defines this milestone:
|
||||
breach variants, and the density and palette defined in `docs/UI_STYLE.md`.
|
||||
This satisfies the required owner review. Runtime integration remains a
|
||||
separately planned milestone.
|
||||
|
||||
## Milestone 024 — Runtime living hedge rendering
|
||||
|
||||
**Status:** `IN_PROGRESS`
|
||||
|
||||
### Objective
|
||||
|
||||
Replace the maze's simple green wall strokes and provisional perseverance
|
||||
damage styling with the project-owner-approved Milestone 023 living-hedge SVG
|
||||
family, so intact walls, impacts, and permanent breakthroughs form one varied,
|
||||
legible top-down environment on player and observer screens without changing
|
||||
authoritative gameplay or exposing hidden state.
|
||||
|
||||
### Scope
|
||||
|
||||
- Do not start this milestone until every preceding milestone is `COMPLETE`.
|
||||
Use only the intact families, impact sequence, breakthrough frame, breached
|
||||
variants, density, and palette approved in Milestone 023. Any substantive art
|
||||
redesign requires returning to a separately planned sample-review milestone.
|
||||
- Reconcile `docs/UI_STYLE.md`, `docs/PLAYER_UI.md`, and `ARCHITECTURE.md` with
|
||||
the runtime hedge-rendering contract before changing browser assets.
|
||||
- Derive a compact runtime SVG sprite from
|
||||
`docs/hedge-sample-library.svg`; keep the review source untouched and do not
|
||||
make runtime pages depend on files under `docs/`. Preserve stable semantic
|
||||
symbol identifiers where practical and remove comparison-sheet-only content.
|
||||
- Package the runtime sprite locally with the compressed web application. Do
|
||||
not add remote images, fonts, libraries, network requests, embedded raster
|
||||
data, or runtime SVG scripts.
|
||||
- Replace the four solid CSS cell borders with a dedicated decorative edge
|
||||
layer that draws each server-provided closed wall once. Canonicalize shared
|
||||
edges so two adjacent visible cells do not produce doubled foliage, darker
|
||||
overlaps, seams, or inconsistent variants.
|
||||
- Draw walls at projection boundaries only from the current server-provided
|
||||
cell and passage bits. Do not assume what lies beyond an unseen edge, fill
|
||||
unknown space, retain an explored map, or generate decorative geometry for
|
||||
cells absent from the current projection.
|
||||
- Select among the six approved intact variants with a documented deterministic
|
||||
function of the currently visible canonical wall edge and orientation. The
|
||||
same edge must keep its variant across rerenders and appear consistently to
|
||||
both players and observers when their approved projections include it, with
|
||||
no new protocol or authoritative game-state field.
|
||||
- Rotate or mirror approved symbols for north, east, south, and west while
|
||||
preserving their shared-edge center, non-directional lighting, foliage
|
||||
thickness, and joins. Compose approved corner, crossing, and end treatments
|
||||
only from wall edges explicitly present in the received projection.
|
||||
- Preserve exact maze geometry and hit testing. Decorative foliage may extend
|
||||
visually into neighboring cells by a bounded amount, but it cannot change
|
||||
traversability, pointer targets, player focal-point calculation, map bounds,
|
||||
projection translation, or page overflow.
|
||||
- Keep hedge artwork behind players, keys, doors, artifacts, landmarks, and
|
||||
interaction feedback. Leaves and branches must remain recognizable at 34,
|
||||
40, and 46 CSS-pixel cells without obscuring the passage center or reducing
|
||||
object and player contrast.
|
||||
- Connect Milestone 022 server-approved impact stages one through four to the
|
||||
matching approved static SVG frames on the exact visible wall that received
|
||||
the impact. Preserve the existing bounded timer, event correlation,
|
||||
deduplication, and cleanup behavior; the browser never increments damage or
|
||||
predicts a breakthrough locally.
|
||||
- Present the approved breakthrough frame only when justified by the matching
|
||||
authoritative response and transition to one of the approved permanent
|
||||
breached variants when the received `opened` passage state changes. Derive
|
||||
this transition only from current and immediately preceding visible
|
||||
server-approved state, held in bounded memory and discarded on projection
|
||||
replacement or reset.
|
||||
- Render one canonical permanent breach decoration for a shared opened edge so
|
||||
the gap aligns from both adjacent cells and from both traversal directions.
|
||||
Breached foliage must remain visible while the opened edge is in the current
|
||||
projection and disappear when that edge is no longer projected.
|
||||
- Preserve reduced-motion behavior by using the approved static impact frames
|
||||
and an immediate permanent breach state, with no shaking, swaying, moving
|
||||
leaves, or travel animation. Normal motion may use restrained transforms of
|
||||
the approved shapes but must not allocate unbounded particles or DOM nodes.
|
||||
- Use bounded reusable DOM or CSS structures. A rerender must have a documented
|
||||
maximum number of hedge elements derived from the maximum visible projection,
|
||||
remove obsolete edges, and never accumulate duplicate SVG instances,
|
||||
animation timers, styles, or event listeners.
|
||||
- Apply the same hedge family to the observer's full-map renderer without
|
||||
changing its read-only behavior, pan or zoom controls, minimum viewport, or
|
||||
authoritative snapshot contents. At overview zoom levels, simplify details
|
||||
gracefully while retaining continuous wall and breached-gap silhouettes.
|
||||
- Preserve Russian player-facing copy, Milestone 020 visual feedback,
|
||||
Milestone 021 focal-point positioning, Milestone 022 game rules, offline
|
||||
operation, accessibility, and existing firmware and filesystem budgets.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Updated runtime hedge-rendering, layering, variation, projection-safety, and
|
||||
reduced-motion contracts in canonical and supporting documentation.
|
||||
- Optimized local runtime SVG sprite derived reproducibly from the approved
|
||||
Milestone 023 source, with deterministic size and dependency validation.
|
||||
- A deduplicated decorative edge renderer for player and observer maps using
|
||||
the six intact hedge variants and approved joins and end treatments.
|
||||
- Runtime wiring for the four approved impact stages, breakthrough frame, and
|
||||
three deterministic permanent breached variants.
|
||||
- Deterministic browser tests, asset checks, host regression results, size
|
||||
evidence, and representative mobile, desktop, observer, and physical-device
|
||||
visual verification.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. Every closed edge in the received player or observer projection is rendered
|
||||
with an approved living-hedge SVG variant rather than a solid CSS green
|
||||
border. Visible leaves, twigs, irregular foliage, depth tones, and outline
|
||||
remain recognizable at supported gameplay scales. Horizontal and vertical
|
||||
hedge center lines coincide with their authoritative cell edges, never pass
|
||||
through cell centers, and leave open floor at least as clear as the former
|
||||
solid green wall presentation.
|
||||
2. Every visible shared wall has exactly one canonical decorative edge. Adjacent
|
||||
cells, full rerenders, projection updates, and observer zoom changes produce
|
||||
no doubled foliage, dark overlap, seam, flicker, or mismatched variant.
|
||||
3. The documented deterministic selection maps canonical visible wall edges to
|
||||
all six approved intact variants with representative distribution. The same
|
||||
edge and orientation produce the same variant across rerenders and roles,
|
||||
while no random state, hidden topology, or new protocol field is required.
|
||||
4. North, east, south, and west walls plus continuous runs, corners, crossings,
|
||||
exposed ends, and projection boundaries align to within one CSS pixel at 34,
|
||||
40, and 46 CSS-pixel cells and at representative device pixel ratios.
|
||||
5. The edge layer is derived exclusively from cells and passage/opened bits in
|
||||
the current server-approved projection. Tests confirm no unknown cell,
|
||||
missing edge, hidden route, remote breach, or explored-map decoration is
|
||||
inferred, retained, serialized, or rendered.
|
||||
6. Hedge artwork does not alter authoritative movement, wall collision,
|
||||
visibility, object pickup, exits, results, input hit targets, observer
|
||||
read-only behavior, or player focal positioning. No gameplay or protocol
|
||||
contract changes are introduced.
|
||||
7. Players, keys, exits, artifacts, landmarks, controls, and feedback remain
|
||||
above the hedge layer and clearly legible in normal color, grayscale, and
|
||||
representative color-vision checks. Decorative overhang creates no page or
|
||||
map-shell overflow and captures no pointer input.
|
||||
8. Each server-approved perseverance impact selects the matching approved stage
|
||||
on the exact currently visible wall. Stages do not advance from local input,
|
||||
duplicate or reordered messages, impacts on another edge, or events outside
|
||||
the current projection.
|
||||
9. A matching fifth-impact transition briefly uses the approved breakthrough
|
||||
frame in normal-motion mode and settles on one approved permanent breached
|
||||
variant from the next authoritative opened-edge projection. The visual gap
|
||||
aligns bidirectionally and cannot be mistaken for a closed wall.
|
||||
10. All three approved breached variants appear through a stable deterministic
|
||||
mapping. They remain visible only while their authoritative opened edge is
|
||||
projected, survive ordinary rerenders without changing variant, and leave
|
||||
no stale decoration after reset, match end, disconnect, or projection loss.
|
||||
11. Reduced-motion mode uses static impact stages and changes immediately to the
|
||||
permanent breach without maze shake, foliage sway, moving leaf fragments,
|
||||
or breakthrough travel animation. Meaning does not depend on motion, sound,
|
||||
vibration, text, or green hue alone.
|
||||
12. The renderer has a tested fixed upper bound for hedge elements, transient
|
||||
effects, timers, and memory based on the maximum supported projection.
|
||||
Repeated snapshots and impacts do not accumulate nodes, listeners, styles,
|
||||
or timers, and obsolete edge elements are removed deterministically.
|
||||
13. Observer rendering uses the same approved family and canonical edge logic,
|
||||
keeps full-map pan and zoom responsive, and preserves continuous intact and
|
||||
breached silhouettes at overview and inspection scales.
|
||||
14. Automated browser tests cover variant selection, shared-edge deduplication,
|
||||
every orientation and join, projection boundaries, hidden-state isolation,
|
||||
layering, impact and breach transitions, cleanup, reduced motion, observer
|
||||
zoom, supported cell sizes, and zero viewport overflow.
|
||||
15. The runtime SVG is local, valid, script-free, raster-free, deterministically
|
||||
generated, and included in web-asset reproducibility checks. All host tests
|
||||
pass, compressed web assets remain within the 1.5 MiB budget and LittleFS
|
||||
partition, and firmware remains within the factory partition.
|
||||
16. The project owner confirms on representative mobile and observer screens
|
||||
that the maze resembles a varied living hedge from above, repetition is not
|
||||
distracting, leaves and branches are visible, objects remain easy to find,
|
||||
impacts visibly damage the correct bush, and permanent openings are clear,
|
||||
or explicitly approves a documented evidence deviation.
|
||||
|
||||
### Evidence
|
||||
|
||||
The following planning-time state defines this milestone:
|
||||
|
||||
- On 2026-09-10, the project owner requested a separate future milestone to
|
||||
integrate the living-hedge samples generated and approved by Milestone 023.
|
||||
The approved review source contains six intact variants, four impact stages,
|
||||
one breakthrough frame, three permanent breach variants, and topology
|
||||
examples, but remains outside runtime assets at planning time.
|
||||
- Runtime SVG derivation, browser integration, tests, resource measurements,
|
||||
physical-device verification, and owner acceptance are deferred to this
|
||||
milestone. No implementation was performed while creating the plan.
|
||||
- 2026-09-11 implementation evidence: the local `web/hedges.svg` runtime
|
||||
sprite contains the approved intact, impact, breakthrough, and breach
|
||||
symbols. Player and observer maps derive a bounded canonical hedge-edge list
|
||||
only from their current projections; a full 32 by 32 projection is bounded
|
||||
to 2,112 edge elements. The renderer uses deterministic edge hashes and one
|
||||
restartable breakthrough timer, with immediate reduced-motion settlement.
|
||||
- 2026-09-11 verification: `bash scripts/run_host_tests.sh` passed. `node
|
||||
--test test/web/test_player_app.js test/web/test_hedge_svg.js` passed all 26
|
||||
browser tests. `python3 scripts/verify_web_assets.py` passed with a 29,912 /
|
||||
1,572,864 byte compressed payload.
|
||||
- 2026-09-11 skipped check: `pio run` could not acquire
|
||||
`/home/sasa/.platformio/platforms.lock` because the global PlatformIO home
|
||||
is read-only. Firmware size verification, physical-device visual testing,
|
||||
and project-owner mobile and observer acceptance remain required. Milestone
|
||||
024 therefore remains `IN_PROGRESS`.
|
||||
- 2026-09-11 user-run verification: `./scripts/run_host_tests.sh` passed all
|
||||
host and bundled player browser checks; `node --test
|
||||
test/web/test_player_app.js test/web/test_hedge_svg.js` passed all 26 tests;
|
||||
and `python3 ./scripts/verify_web_assets.py` confirmed deterministic assets
|
||||
at 29,912 / 1,572,864 bytes.
|
||||
- 2026-09-11 physical-device diagnostic: host `192.168.1.103/24` reached the
|
||||
connected ESP at `192.168.1.50` with two ICMP replies and no packet loss, but
|
||||
`curl --noproxy '*' http://192.168.1.50/health` was refused on TCP port 80.
|
||||
The device is reachable but its HTTP server is not listening; no flash or
|
||||
reset was performed. Physical visual acceptance remains pending.
|
||||
- 2026-09-11 physical-device repair and verification: boot logs identified
|
||||
`ESP_ERR_HTTPD_HANDLERS_FULL` after adding the hedge sprite route. The
|
||||
bounded URI handler capacity was raised from 13 to 14. A writable temporary
|
||||
PlatformIO home built and uploaded the application and LittleFS image through
|
||||
`/dev/ttyACM0` to the ESP32-C6FH4; every upload hash was verified. Firmware
|
||||
uses 1,029,600 / 2,097,152 factory bytes (49.1% flash) and 59,940 / 327,680
|
||||
bytes RAM (18.3%); LittleFS uses 2,031,616 / 2,031,616 bytes. After reboot,
|
||||
`GET /health` returned `200` with `{"status":"serving","healthy":true,`
|
||||
`"version":"0.14.0","errorCode":0}`, while GET requests for `/` and
|
||||
`/hedges.svg` both returned `200` (3,378 and 1,106 bytes respectively).
|
||||
Project-owner visual acceptance of the mobile and observer hedge rendering
|
||||
remains required before the milestone can become `COMPLETE`.
|
||||
- 2026-09-11 project-owner mobile review rejected the first runtime hedge
|
||||
presentation because foliage obscured the wall boundaries and made open
|
||||
routes substantially harder to understand than the former green strokes.
|
||||
The corrective contract limits foliage to 20% of a cell, adds a continuous
|
||||
high-contrast spine only to closed edges, and raises the contrast of the
|
||||
inset cell floor. Repeat physical-device visual acceptance remains required.
|
||||
- 2026-09-11 corrective verification: a local rendered maze preview confirmed
|
||||
that the continuous spine restores exact closed-edge continuity while opened
|
||||
edges remain uninterrupted passages. `node --test
|
||||
test/web/test_player_app.js test/web/test_hedge_svg.js` passed all 26 tests,
|
||||
`bash scripts/run_host_tests.sh` passed, and web-asset reproducibility passed
|
||||
at 30,012 / 1,572,864 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;
|
||||
`pio run -t buildfs` also passed for the 2,031,616-byte LittleFS image.
|
||||
- 2026-09-11 deployment was not attempted because no `/dev/ttyACM*` or
|
||||
`/dev/ttyUSB*` device was present and the ESP HTTP endpoint at
|
||||
`192.168.1.50:80` was unreachable from the host. The already-open browser tab
|
||||
still shows its previously loaded assets. Upload and repeat project-owner
|
||||
mobile visual acceptance remain required, so Milestone 024 stays
|
||||
`IN_PROGRESS`.
|
||||
- 2026-09-11 repeat project-owner review rejected the first readability
|
||||
correction: its added bright spine and emphasized floor insets produced a
|
||||
confusing rectangular grid, while the player still appeared to stand on a
|
||||
hedge. Browser geometry inspection found two root causes: rotating a
|
||||
horizontal SVG wrapper into a vertical edge left its center displaced by
|
||||
half a cell on both axes, and SVG instances had no `160 x 40` view box, so
|
||||
source-coordinate foliage overflowed far beyond its owning cell edge. The
|
||||
next correction removes the spine and floor emphasis, restores the quiet cell
|
||||
surface, constrains every SVG to one edge, and aligns vertical wrapper centers
|
||||
with authoritative boundaries. Repeat physical-device visual acceptance
|
||||
remains required.
|
||||
- 2026-09-11 second corrective verification: a rendered 7 by 7 maze at the
|
||||
minimum gameplay cell scale showed each `160 x 40` hedge constrained to one
|
||||
edge, vertical and horizontal centers aligned, the player wholly inside its
|
||||
cell, and continuous open-floor routes without the rejected rectangular
|
||||
overlay. `bash scripts/run_host_tests.sh` passed, including all bundled
|
||||
browser checks, and web-asset reproducibility passed at 29,979 / 1,572,864
|
||||
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`.
|
||||
|
||||
@@ -20,6 +20,7 @@ static const maze32_static_asset_t PLAYER_STYLE_ASSET = {"/littlefs/player.css.g
|
||||
static const maze32_static_asset_t PLAYER_SCRIPT_ASSET = {"/littlefs/player.js.gz",
|
||||
"text/javascript; charset=utf-8"};
|
||||
static const maze32_static_asset_t ICON_ASSET = {"/littlefs/icons.svg.gz", "image/svg+xml"};
|
||||
static const maze32_static_asset_t HEDGE_ASSET = {"/littlefs/hedges.svg.gz", "image/svg+xml"};
|
||||
|
||||
static void preserve_static_context(void* context) {
|
||||
(void)context;
|
||||
@@ -104,7 +105,7 @@ esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
|
||||
}
|
||||
|
||||
httpd_config_t configuration = HTTPD_DEFAULT_CONFIG();
|
||||
configuration.max_uri_handlers = 13;
|
||||
configuration.max_uri_handlers = 14;
|
||||
configuration.max_open_sockets = 9;
|
||||
configuration.lru_purge_enable = false;
|
||||
configuration.recv_wait_timeout = 5;
|
||||
@@ -148,6 +149,12 @@ esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
|
||||
.handler = send_static_asset,
|
||||
.user_ctx = (void*)&ICON_ASSET,
|
||||
};
|
||||
const httpd_uri_t hedges = {
|
||||
.uri = "/hedges.svg",
|
||||
.method = HTTP_GET,
|
||||
.handler = send_static_asset,
|
||||
.user_ctx = (void*)&HEDGE_ASSET,
|
||||
};
|
||||
const httpd_uri_t health_uri = {
|
||||
.uri = "/health",
|
||||
.method = HTTP_GET,
|
||||
@@ -174,6 +181,9 @@ esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
|
||||
if (result == ESP_OK) {
|
||||
result = httpd_register_uri_handler(*server, &icons);
|
||||
}
|
||||
if (result == ESP_OK) {
|
||||
result = httpd_register_uri_handler(*server, &hedges);
|
||||
}
|
||||
if (result == ESP_OK) {
|
||||
result = httpd_register_uri_handler(*server, &health_uri);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,11 @@ blocked-move text. Opened-passage bits in an ordinary cell projection render as
|
||||
a permanent trampled bush gap. Effects have one restartable timer and clear on
|
||||
pause, result, reset, disconnect, and control reset. Reduced motion is static.
|
||||
|
||||
The map draws only a bounded canonical hedge-edge layer generated from current
|
||||
received cells. It hashes each visible edge deterministically to choose the
|
||||
approved local SVG variant, so the browser neither retains unseen geometry nor
|
||||
decides topology. The layer is decorative and cannot receive input.
|
||||
|
||||
All remaining Russian system messages use one `role=status` live message rail
|
||||
above the application top bar. One connection-loss message has highest priority
|
||||
and remains until a successful recovery; other messages deduplicate, replace
|
||||
|
||||
@@ -158,6 +158,35 @@ are not MVP requirements.
|
||||
at 34, 40, and 46 CSS pixels. They contain no coordinates, objects,
|
||||
ownership, hidden topology, animation, or runtime dependencies.
|
||||
|
||||
### Runtime hedge rendering
|
||||
|
||||
- The local `/hedges.svg` sprite is derived from the approved review source.
|
||||
It contains only reusable runtime symbols and no scripts, raster data, or
|
||||
external dependencies. The review sheet in `docs/` is never requested by a
|
||||
browser page.
|
||||
- A map uses one non-interactive hedge-edge element for each canonical closed
|
||||
or server-opened edge in its current projection. Closed shared edges are
|
||||
owned by their north or west cell; east and south edges are rendered only at
|
||||
the current projection boundary. This bounds a full 32 by 32 map to 2,112
|
||||
hedge elements and avoids duplicated foliage.
|
||||
- A stable integer hash of the canonical edge index and orientation selects one
|
||||
of the six approved intact variants. Opened edges select one of the three
|
||||
approved breach variants. The same projection edge always receives the same
|
||||
art without random state, retained exploration, or a protocol field.
|
||||
- Gameplay readability takes priority over foliage density. Hedge foliage is
|
||||
at most 20% of one cell, and the center line of every horizontal or vertical
|
||||
symbol must coincide with its authoritative cell edge. Rotation must preserve
|
||||
that center line instead of shifting a vertical hedge through cell centers.
|
||||
Every runtime SVG instance uses the approved `160 x 40` view box scaled to
|
||||
exactly one cell edge; source-coordinate artwork must never overflow into
|
||||
unrelated cells. Cell interiors remain visually quiet and continuous so
|
||||
passages read as open floor rather than as a grid of highlighted rectangles.
|
||||
- Hedge edges are decorative, have `pointer-events: none`, sit below map
|
||||
objects and players, and may overhang an edge by at most 15% of a cell. A
|
||||
projection replacement removes all obsolete edge nodes. One restartable
|
||||
breakthrough timer may briefly show the approved breakthrough frame; reduced
|
||||
motion settles immediately on the permanent breach.
|
||||
|
||||
## 8. Responsive behavior
|
||||
|
||||
- The minimum supported player viewport is `412 x 916` CSS pixels.
|
||||
|
||||
@@ -6,6 +6,8 @@ const zlib = require("node:zlib");
|
||||
|
||||
const samplePath = path.join(__dirname, "..", "..", "docs", "hedge-sample-library.svg");
|
||||
const sample = fs.readFileSync(samplePath, "utf8");
|
||||
const runtimePath = path.join(__dirname, "..", "..", "web", "hedges.svg");
|
||||
const runtime = fs.readFileSync(runtimePath, "utf8");
|
||||
|
||||
test("hedge review sheet is local editable SVG with complete stable symbols", () => {
|
||||
assert.match(sample, /^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg"/);
|
||||
@@ -25,3 +27,19 @@ test("hedge review sheet is local editable SVG with complete stable symbols", ()
|
||||
assert.ok((sample.match(/<use href=/g) || []).length >= 20);
|
||||
assert.ok(zlib.gzipSync(sample, {level: 9}).length < sample.length);
|
||||
});
|
||||
|
||||
test("runtime hedge sprite remains local and contains approved runtime symbols", () => {
|
||||
assert.match(runtime, /^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg"/);
|
||||
assert.doesNotMatch(runtime, /<(?:script|image|foreignObject|metadata)\b/i);
|
||||
assert.doesNotMatch(runtime, /(?:href="https?:|url\(|data:|@import|<style\b)/i);
|
||||
for (const identifier of [
|
||||
"hedge-intact-1", "hedge-intact-2", "hedge-intact-3", "hedge-intact-4",
|
||||
"hedge-intact-5", "hedge-intact-6", "hedge-impact-1", "hedge-impact-2",
|
||||
"hedge-impact-3", "hedge-impact-4", "hedge-breakthrough", "hedge-breach-1",
|
||||
"hedge-breach-2", "hedge-breach-3"
|
||||
]) {
|
||||
assert.match(runtime, new RegExp(`id="${identifier}"`));
|
||||
assert.match(sample, new RegExp(`id="${identifier}"`));
|
||||
}
|
||||
assert.ok(zlib.gzipSync(runtime, {level: 9}).length < 4096);
|
||||
});
|
||||
|
||||
@@ -167,7 +167,15 @@ test("playful feedback uses bounded maze-only effects and a top message rail", (
|
||||
assert.match(script, /moveDirections: new Map\(\)/);
|
||||
assert.match(script, /if \(message\.payload\.code === "blocked"\) \{ showImpact\(direction, message\.payload\.breakthroughHits\); feedback\(false\); return; \}/);
|
||||
assert.match(css, /\.cell\.bush-impact/);
|
||||
assert.match(css, /\.cell\.trampled-n::before/);
|
||||
assert.match(css, /\.hedge-layer/);
|
||||
assert.match(css, /\.hedge-edge \{[^}]*height: calc\(var\(--cell\) \* \.2\)/);
|
||||
assert.match(css, /\.hedge-edge\.hedge-vertical \{[^}]*left: calc\(var\(--hedge-x\) \* var\(--cell\) - var\(--cell\) \* \.5\);[^}]*top: calc\(var\(--hedge-y\) \* var\(--cell\) \+ var\(--cell\) \* \.4\)/);
|
||||
assert.match(css, /\.observer-maze \.hedge-edge\.hedge-vertical \{[^}]*top: calc\(var\(--hedge-y\) \* var\(--cell\) \+ var\(--cell\) \* \.5 - 1px\)/);
|
||||
assert.match(css, /\.cell::after \{[^}]*background: #1c1b27;[^}]*opacity: \.55/);
|
||||
assert.match(script, /projectedHedgeEdges/);
|
||||
assert.match(script, /svg\.setAttribute\("viewBox", "0 0 160 40"\)/);
|
||||
assert.match(script, /svg\.setAttribute\("preserveAspectRatio", "none"\)/);
|
||||
assert.match(script, /\/hedges\.svg#/);
|
||||
assert.match(script, /if \(message\.payload\.code === "rate_limited"\) \{ showRateLimit\(\); feedback\(false\); return; \}/);
|
||||
assert.doesNotMatch(script, /Там стена\. Очень убедительная\.|Слишком быстро! Сервер просит немного меньше турбо\./);
|
||||
});
|
||||
@@ -260,6 +268,19 @@ test("chunk assembly replaces projection cells without explored-map memory", ()
|
||||
assert.equal(player.snapshotIsValid({width: 32, height: 33, cells: [], objects: []}), false);
|
||||
});
|
||||
|
||||
test("hedge edges are canonical, bounded, and deterministic", () => {
|
||||
const cells = [[0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0]];
|
||||
const first = player.projectedHedgeEdges(cells, 2);
|
||||
const second = player.projectedHedgeEdges(cells, 2);
|
||||
assert.deepEqual(first, second);
|
||||
assert.equal(new Set(first.map(edge => edge.key)).size, first.length);
|
||||
assert.ok(first.length <= cells.length * 2 + 2 * 2);
|
||||
assert.equal(player.hedgeVariant(first[0], 6), player.hedgeVariant(first[0], 6));
|
||||
const opened = player.projectedHedgeEdges([[0, 0, 2], [1, 0, 8]], 2);
|
||||
assert.equal(opened.filter(edge => edge.opened).length, 1);
|
||||
assert.equal(player.hedgeKeyForMove(0, 2, "east"), "v:1:0");
|
||||
});
|
||||
|
||||
test("refresh contract restores only the token and waits for server projection", () => {
|
||||
const restore = player.createEnvelope("restore", 1, "restore-1", {token: "0123456789abcdef0123456789abcdef"});
|
||||
assert.deepEqual(Object.keys(restore.payload), ["token"]);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 40" aria-hidden="true">
|
||||
<defs>
|
||||
<g id="leaf-cluster-a" stroke="#173d2a" stroke-width="1.5" stroke-linejoin="round"><path fill="#296443" d="M2 13C0 6 7 0 13 3c2-7 12-5 14 2 8-1 11 8 5 13 3 8-6 13-12 8-6 5-14 1-13-6-7 0-9-5-5-7Z"/><path fill="#4f9b55" d="M7 13c2-5 7-8 12-7 5 0 8 4 9 8-3 5-8 8-14 7-5 0-8-3-7-8Z"/><path fill="#c7e68a" d="M12 9c3-3 8-3 10 0-2 3-7 4-10 0Z"/></g>
|
||||
<g id="leaf-cluster-b" stroke="#173d2a" stroke-width="1.5" stroke-linejoin="round"><path fill="#296443" d="M1 16C-1 8 5 2 11 4c1-7 11-7 14-1 7-3 13 5 8 11 5 6-2 14-9 11-4 7-15 4-15-3-8 2-12-4-8-6Z"/><path fill="#8cc968" d="M8 14c2-5 8-7 13-5 4 2 6 7 4 11-5 4-13 3-17-2Z"/></g>
|
||||
<g id="twig-fork" fill="none" stroke="#765033" stroke-width="2" stroke-linecap="round"><path d="M3 20 16 8 29 3M16 8l2 12M16 8 7 4"/></g>
|
||||
<g id="hedge-base"><path d="M0 11C9 2 20 4 30 9c12-8 23-7 34 1 10-5 22-5 32 1 11-7 23-6 34 1 9-5 22-4 30 4v17c-10 5-22 5-32 0-11 7-23 6-34 0-10 6-22 6-32 0-11 6-23 6-34 0-10 5-20 4-30-3Z" fill="#173d2a"/><use href="#twig-fork" x="5" y="9"/><use href="#twig-fork" x="54" y="8"/><use href="#twig-fork" x="105" y="9"/><use href="#leaf-cluster-a" x="1" y="5"/><use href="#leaf-cluster-b" x="27" y="4"/><use href="#leaf-cluster-a" x="54" y="6"/><use href="#leaf-cluster-b" x="80" y="4"/><use href="#leaf-cluster-a" x="107" y="5"/><use href="#leaf-cluster-b" x="132" y="5"/></g>
|
||||
<g id="hedge-intact-1"><use href="#hedge-base"/></g>
|
||||
<g id="hedge-intact-2"><use href="#hedge-base"/><path fill="#8cc968" d="M20 7l8-3 4 4-8 4Zm61 3 8-4 4 4-7 4Zm48 12 7-3 3 5-7 3Z"/></g>
|
||||
<g id="hedge-intact-3"><use href="#hedge-base"/><path fill="none" stroke="#765033" stroke-width="2" d="M38 27 49 9m36 18L73 7m47 20 13-17"/></g>
|
||||
<g id="hedge-intact-4"><use href="#hedge-base"/><path fill="#4f9b55" d="M6 27c12-5 20 6 31 0 11-6 20 6 32 0 11-6 21 5 33-1 10-5 19 3 29 0v8H6Z"/></g>
|
||||
<g id="hedge-intact-5"><use href="#hedge-base"/><path fill="#c7e68a" d="m10 17 5-4 4 3-5 4Zm42-8 5-4 4 3-5 4Zm46 11 5-4 4 3-5 4Zm38-8 5-4 4 3-5 4Z"/></g>
|
||||
<g id="hedge-intact-6"><use href="#hedge-base"/><path fill="none" stroke="#173d2a" stroke-width="2" d="M19 30 32 14l14 13m33 3 12-15 13 14m17 1 11-12 12 10"/></g>
|
||||
<g id="hedge-impact-1"><use href="#hedge-intact-1"/><path fill="#8cc968" d="m70 2 8 3-3 6-8-3Zm12 28 7 2-3 5-7-2Z"/></g>
|
||||
<g id="hedge-impact-2"><use href="#hedge-intact-1"/><path fill="#173d2a" d="M64 0h30v40H64Z" opacity=".18"/></g>
|
||||
<g id="hedge-impact-3"><use href="#hedge-intact-1" transform="skewX(-4)"/><path fill="none" stroke="#765033" stroke-width="3" d="m71 28 11-18m1 16 11-13"/></g>
|
||||
<g id="hedge-impact-4"><use href="#hedge-intact-1" transform="translate(0 4) scale(1 .86)"/><path fill="#c7e68a" d="m61 2 8 3-3 6-8-3Zm13 7 8 3-3 6-8-3Zm14-4 8 3-3 6-8-3Z"/></g>
|
||||
<g id="hedge-breakthrough"><use href="#hedge-intact-1"/><path fill="#11131a" stroke="#173d2a" stroke-width="2" d="M62 0h36v40H62Z"/><path fill="#765033" d="m60 6 10 8-4 5-11-8Zm40 12 10-8 5 5-10 9Z"/></g>
|
||||
<g id="hedge-breach-1"><use href="#hedge-breakthrough"/><path fill="#4f9b55" d="M44 28c8-7 14-5 18 0v8H42Zm56 0c7-6 13-5 18 0v8H98Z"/></g>
|
||||
<g id="hedge-breach-2"><use href="#hedge-breakthrough"/><path fill="#296443" d="M35 25c11-8 18-4 27 3v10H33Zm65 3c10-7 18-11 27-3v13H98Z"/></g>
|
||||
<g id="hedge-breach-3"><use href="#hedge-breakthrough"/><path fill="none" stroke="#765033" stroke-width="3" d="m38 33 17-12m50 12 17-12"/></g>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
+8
-11
@@ -112,17 +112,12 @@ input:focus { border-color: var(--aqua); box-shadow: 0 0 0 3px #43ead229; }
|
||||
.unknown-message { position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; gap: 12px; padding: 24px; color: var(--muted); text-align: center; }
|
||||
.unknown-message svg { width: 54px; height: 54px; color: var(--aqua); animation: radar 1.8s linear infinite; }
|
||||
.maze { position: absolute; left: 50%; top: 50%; display: grid; transform: translate(calc(-50% + var(--map-offset-x, 0px)), calc(-50% + var(--map-offset-y, 0px))); }
|
||||
.cell { position: relative; width: var(--cell); height: var(--cell); background: #11131a; border: 4px solid transparent; }
|
||||
.cell.wall-n { border-top-color: var(--plant); box-shadow: inset 0 3px 0 var(--plant-dark); }
|
||||
.cell.wall-e { border-right-color: var(--plant); }
|
||||
.cell.wall-s { border-bottom-color: var(--plant); }
|
||||
.cell.wall-w { border-left-color: var(--plant); }
|
||||
.cell { position: relative; width: var(--cell); height: var(--cell); background: #11131a; }
|
||||
.cell::after { content: ""; position: absolute; inset: 7px; background: #1c1b27; opacity: .55; }
|
||||
.cell.trampled-n::before, .cell.trampled-e::before, .cell.trampled-s::before, .cell.trampled-w::before { content: ""; position: absolute; z-index: 2; background: repeating-linear-gradient(90deg, #254d2d 0 4px, #6f8d43 4px 7px); opacity: .9; }
|
||||
.cell.trampled-n::before { left: 4px; right: 4px; top: -3px; height: 6px; }
|
||||
.cell.trampled-e::before { top: 4px; right: -3px; bottom: 4px; width: 6px; }
|
||||
.cell.trampled-s::before { right: 4px; bottom: -3px; left: 4px; height: 6px; }
|
||||
.cell.trampled-w::before { top: 4px; bottom: 4px; left: -3px; width: 6px; }
|
||||
.hedge-layer { position: absolute; z-index: 1; inset: 0; pointer-events: none; overflow: visible; }
|
||||
.hedge-edge { position: absolute; width: var(--cell); height: calc(var(--cell) * .2); left: calc(var(--hedge-x) * var(--cell)); top: calc(var(--hedge-y) * var(--cell) - var(--cell) * .1); transform-origin: 50% 50%; pointer-events: none; }
|
||||
.hedge-edge svg { display: block; width: 100%; height: 100%; overflow: visible; }
|
||||
.hedge-edge.hedge-vertical { left: calc(var(--hedge-x) * var(--cell) - var(--cell) * .5); top: calc(var(--hedge-y) * var(--cell) + var(--cell) * .4); transform: rotate(90deg); }
|
||||
.cell.bush-impact { z-index: 5; animation: bush-resist .34s steps(3, end); }
|
||||
.cell.bush-stage-2 { filter: saturate(1.25); }
|
||||
.cell.bush-stage-3 { filter: brightness(1.2) saturate(1.4); }
|
||||
@@ -234,8 +229,10 @@ input:focus { border-color: var(--aqua); box-shadow: 0 0 0 3px #43ead229; }
|
||||
.observer-stage { position: absolute; inset: 92px 0 0; overflow: hidden; border: 2px solid #2f2041; background: #050408; cursor: grab; touch-action: none; user-select: none; }
|
||||
.observer-stage.dragging { cursor: grabbing; }
|
||||
.observer-maze { position: absolute; left: 50%; top: 50%; display: grid; transform-origin: center; will-change: transform; }
|
||||
.observer-maze .cell { width: 8px; height: 8px; border-width: 1px; }
|
||||
.observer-maze .cell { width: 8px; height: 8px; }
|
||||
.observer-maze .cell::after { inset: 2px; }
|
||||
.observer-maze .hedge-edge { height: 2px; top: calc(var(--hedge-y) * var(--cell) - 1px); }
|
||||
.observer-maze .hedge-edge.hedge-vertical { left: calc(var(--hedge-x) * var(--cell) - var(--cell) * .5); top: calc(var(--hedge-y) * var(--cell) + var(--cell) * .5 - 1px); }
|
||||
.observer-maze .object { inset: 5%; }
|
||||
.observer-maze .object-key::after { display: none; }
|
||||
.observer-maze .object-landmark { font-size: 6px; }
|
||||
|
||||
+100
-19
@@ -302,6 +302,48 @@
|
||||
return snapshot && Array.isArray(snapshot.cells) ? snapshot.cells.map(cell => cell[0]) : [];
|
||||
}
|
||||
|
||||
function hedgeEdgeKey(x, y, vertical) {
|
||||
return `${vertical ? "v" : "h"}:${x}:${y}`;
|
||||
}
|
||||
|
||||
function hedgeVariant(edge, count) {
|
||||
const seed = ((edge.x + 1) * 1103515245 + (edge.y + 1) * 12345 + (edge.vertical ? 97 : 13)) >>> 0;
|
||||
return seed % count + 1;
|
||||
}
|
||||
|
||||
function projectedHedgeEdges(cells, width) {
|
||||
if (!Array.isArray(cells) || !Number.isInteger(width) || width < 1) return [];
|
||||
const present = new Set(cells.map(cell => cell[0]));
|
||||
const edges = new Map();
|
||||
const add = (x, y, vertical, opened) => {
|
||||
const key = hedgeEdgeKey(x, y, vertical);
|
||||
const existing = edges.get(key);
|
||||
edges.set(key, {key, x, y, vertical, opened: !!opened || !!(existing && existing.opened)});
|
||||
};
|
||||
cells.forEach(cell => {
|
||||
if (!Array.isArray(cell) || cell.length < 3 || !Number.isInteger(cell[0])) return;
|
||||
const index = cell[0]; const passages = cell[1]; const opened = cell[2];
|
||||
const x = index % width; const y = Math.floor(index / width);
|
||||
const eastVisible = present.has(index + 1) && (index + 1) % width !== 0;
|
||||
const southVisible = present.has(index + width);
|
||||
if (!(passages & 1) || (opened & 1)) add(x, y, false, opened & 1);
|
||||
if (!(passages & 8) || (opened & 8)) add(x, y, true, opened & 8);
|
||||
if ((!eastVisible && !(passages & 2)) || (opened & 2)) add(x + 1, y, true, opened & 2);
|
||||
if ((!southVisible && !(passages & 4)) || (opened & 4)) add(x, y + 1, false, opened & 4);
|
||||
});
|
||||
return Array.from(edges.values());
|
||||
}
|
||||
|
||||
function hedgeKeyForMove(cellIndex, width, direction) {
|
||||
if (!Number.isInteger(cellIndex) || !Number.isInteger(width) || width < 1) return null;
|
||||
const x = cellIndex % width; const y = Math.floor(cellIndex / width);
|
||||
if (direction === "north") return hedgeEdgeKey(x, y, false);
|
||||
if (direction === "east") return hedgeEdgeKey(x + 1, y, true);
|
||||
if (direction === "south") return hedgeEdgeKey(x, y + 1, false);
|
||||
if (direction === "west") return hedgeEdgeKey(x, y, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
function clampObserverZoom(value) {
|
||||
return Math.min(6, Math.max(.25, value));
|
||||
}
|
||||
@@ -313,7 +355,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, rail: {priority: 0, key: null, persistent: false}, effects: {impactTimer: null, rateTimer: null}, moveDirections: new Map(), 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, avatar: {pointerId: null, startX: null, startY: null, direction: null, repeatTimer: null, repeatMs: null}, dpad: {pointerId: null, direction: null, repeatTimer: 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, rail: {priority: 0, key: null, persistent: false}, effects: {impactTimer: null, rateTimer: null, breakthroughTimer: null}, hedge: {openedKeys: new Set()}, moveDirections: new Map(), 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, avatar: {pointerId: null, startX: null, startY: null, direction: null, repeatTimer: null, repeatMs: null}, dpad: {pointerId: null, direction: null, repeatTimer: null}, observer: {scale: 1, x: 0, y: 0, dragging: null}};
|
||||
state.moveFlow = new LatestIntentFlow((direction, element) => sendMoveNow(direction, element));
|
||||
|
||||
function showNotice(text, kind, priority, persistent, key) {
|
||||
@@ -332,8 +374,11 @@
|
||||
function clearEffects() {
|
||||
windowObject.clearTimeout(state.effects.impactTimer);
|
||||
windowObject.clearTimeout(state.effects.rateTimer);
|
||||
windowObject.clearTimeout(state.effects.breakthroughTimer);
|
||||
state.effects.impactTimer = null;
|
||||
state.effects.rateTimer = null;
|
||||
state.effects.breakthroughTimer = null;
|
||||
state.hedge.openedKeys.clear();
|
||||
byId("maze").classList.remove("impact-north", "impact-east", "impact-south", "impact-west", "rate-limited");
|
||||
byId("wall-bump").classList.remove("show", "impact-north", "impact-east", "impact-south", "impact-west");
|
||||
documentObject.querySelectorAll(".cell.bush-impact").forEach(cell => cell.classList.remove("bush-impact", "bush-stage-1", "bush-stage-2", "bush-stage-3", "bush-stage-4"));
|
||||
@@ -353,7 +398,11 @@
|
||||
const stage = Math.min(4, Math.max(1, Number.isInteger(hits) ? hits : 1));
|
||||
const target = state.snapshot && documentObject.querySelector(`.cell[data-cell-index="${state.snapshot.self}"]`);
|
||||
if (target && Number.isInteger(hits) && hits > 0) target.classList.add("bush-impact", `bush-stage-${stage}`);
|
||||
state.effects.impactTimer = windowObject.setTimeout(() => { maze.classList.remove("impact-north", "impact-east", "impact-south", "impact-west"); bump.classList.remove("show", "impact-north", "impact-east", "impact-south", "impact-west"); if (target) target.classList.remove("bush-impact", `bush-stage-${stage}`); }, 340);
|
||||
const hedgeKey = state.snapshot && hedgeKeyForMove(state.snapshot.self, state.snapshot.width, direction);
|
||||
const hedge = hedgeKey && maze.querySelector(`[data-hedge-key="${hedgeKey}"]`);
|
||||
const hedgeUse = hedge && hedge.querySelector("use");
|
||||
if (hedgeUse && Number.isInteger(hits) && hits > 0) hedgeUse.setAttribute("href", `/hedges.svg#hedge-impact-${stage}`);
|
||||
state.effects.impactTimer = windowObject.setTimeout(() => { maze.classList.remove("impact-north", "impact-east", "impact-south", "impact-west"); bump.classList.remove("show", "impact-north", "impact-east", "impact-south", "impact-west"); if (target) target.classList.remove("bush-impact", `bush-stage-${stage}`); if (hedgeUse) hedgeUse.setAttribute("href", `/hedges.svg#${hedge.dataset.hedgeSymbol}`); }, 340);
|
||||
}
|
||||
|
||||
function showRateLimit() {
|
||||
@@ -470,6 +519,51 @@
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function hedgeSymbol(edge) {
|
||||
return edge.opened ? `hedge-breach-${hedgeVariant(edge, 3)}` : `hedge-intact-${hedgeVariant(edge, 6)}`;
|
||||
}
|
||||
|
||||
function renderHedgeEdges(maze, cells, width, minX, minY) {
|
||||
const edges = projectedHedgeEdges(cells, width);
|
||||
const openedKeys = new Set(edges.filter(edge => edge.opened).map(edge => edge.key));
|
||||
const newOpenedKeys = new Set([...openedKeys].filter(key => !state.hedge.openedKeys.has(key)));
|
||||
const layer = documentObject.createElement("span");
|
||||
layer.className = "hedge-layer";
|
||||
edges.forEach(edge => {
|
||||
const wrapper = documentObject.createElement("span");
|
||||
const symbol = hedgeSymbol(edge);
|
||||
wrapper.className = `hedge-edge ${edge.vertical ? "hedge-vertical" : "hedge-horizontal"}`;
|
||||
wrapper.dataset.hedgeKey = edge.key;
|
||||
wrapper.dataset.hedgeSymbol = symbol;
|
||||
wrapper.style.setProperty("--hedge-x", String(edge.x - minX));
|
||||
wrapper.style.setProperty("--hedge-y", String(edge.y - minY));
|
||||
const svg = documentObject.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("viewBox", "0 0 160 40");
|
||||
svg.setAttribute("preserveAspectRatio", "none");
|
||||
const use = documentObject.createElementNS("http://www.w3.org/2000/svg", "use");
|
||||
use.setAttribute("href", `/hedges.svg#${symbol}`);
|
||||
svg.appendChild(use);
|
||||
wrapper.appendChild(svg);
|
||||
layer.appendChild(wrapper);
|
||||
});
|
||||
maze.appendChild(layer);
|
||||
state.hedge.openedKeys = openedKeys;
|
||||
if (newOpenedKeys.size && !state.reducedMotion) {
|
||||
newOpenedKeys.forEach(key => {
|
||||
const edge = layer.querySelector(`[data-hedge-key="${key}"]`);
|
||||
const use = edge && edge.querySelector("use");
|
||||
if (use) use.setAttribute("href", "/hedges.svg#hedge-breakthrough");
|
||||
});
|
||||
windowObject.clearTimeout(state.effects.breakthroughTimer);
|
||||
state.effects.breakthroughTimer = windowObject.setTimeout(() => {
|
||||
layer.querySelectorAll(".hedge-edge").forEach(edge => {
|
||||
const use = edge.querySelector("use");
|
||||
if (use) use.setAttribute("href", `/hedges.svg#${edge.dataset.hedgeSymbol}`);
|
||||
});
|
||||
}, 340);
|
||||
}
|
||||
}
|
||||
|
||||
function addObject(cell, object) {
|
||||
const [kind, owner, variant] = object;
|
||||
let marker;
|
||||
@@ -527,19 +621,12 @@
|
||||
cell.className = "cell";
|
||||
cell.dataset.cellIndex = String(data.index);
|
||||
cell.style.gridColumn = String(data.x - minX + 1); cell.style.gridRow = String(data.y - minY + 1);
|
||||
if (!(data.passages & 1)) cell.classList.add("wall-n");
|
||||
if (!(data.passages & 2)) cell.classList.add("wall-e");
|
||||
if (!(data.passages & 4)) cell.classList.add("wall-s");
|
||||
if (!(data.passages & 8)) cell.classList.add("wall-w");
|
||||
if (data.opened & 1) cell.classList.add("trampled-n");
|
||||
if (data.opened & 2) cell.classList.add("trampled-e");
|
||||
if (data.opened & 4) cell.classList.add("trampled-s");
|
||||
if (data.opened & 8) cell.classList.add("trampled-w");
|
||||
(objects.get(data.index) || []).forEach(object => addObject(cell, object));
|
||||
const slot = state.session ? state.session.slot : 1;
|
||||
if (data.index === snapshot.opponent) addPawn(cell, slot === 1 ? 2 : 1, true);
|
||||
maze.appendChild(cell);
|
||||
});
|
||||
renderHedgeEdges(maze, snapshot.cells, snapshot.width, minX, minY);
|
||||
byId("map-placeholder").hidden = true;
|
||||
}
|
||||
|
||||
@@ -601,6 +688,7 @@
|
||||
setScreen("observer");
|
||||
const maze = byId("observer-maze");
|
||||
maze.replaceChildren();
|
||||
maze.style.setProperty("--cell", `${OBSERVER_CELL_SIZE}px`);
|
||||
maze.style.gridTemplateColumns = `repeat(${snapshot.width}, ${OBSERVER_CELL_SIZE}px)`;
|
||||
maze.style.gridTemplateRows = `repeat(${snapshot.height}, ${OBSERVER_CELL_SIZE}px)`;
|
||||
const objects = new Map();
|
||||
@@ -612,14 +700,6 @@
|
||||
cell.className = "cell";
|
||||
cell.style.gridColumn = String(index % snapshot.width + 1);
|
||||
cell.style.gridRow = String(Math.floor(index / snapshot.width) + 1);
|
||||
if (!(passages & 1)) cell.classList.add("wall-n");
|
||||
if (!(passages & 2)) cell.classList.add("wall-e");
|
||||
if (!(passages & 4)) cell.classList.add("wall-s");
|
||||
if (!(passages & 8)) cell.classList.add("wall-w");
|
||||
if (opened & 1) cell.classList.add("trampled-n");
|
||||
if (opened & 2) cell.classList.add("trampled-e");
|
||||
if (opened & 4) cell.classList.add("trampled-s");
|
||||
if (opened & 8) cell.classList.add("trampled-w");
|
||||
(objects.get(index) || []).forEach(object => addObject(cell, object));
|
||||
players.forEach((player, playerIndex) => {
|
||||
if (player.cell === index && !player.exited) {
|
||||
@@ -629,6 +709,7 @@
|
||||
});
|
||||
maze.appendChild(cell);
|
||||
});
|
||||
renderHedgeEdges(maze, snapshot.cells, snapshot.width, 0, 0);
|
||||
players.forEach((player, index) => {
|
||||
const key = player.key == null ? "без ключа" : `ключ P${player.key + 1}`;
|
||||
byId(`observer-p${index + 1}`).textContent = !player.connected ? "нет связи" : player.exited ? "снаружи" : key;
|
||||
@@ -935,7 +1016,7 @@
|
||||
return {state, handleMessage, move, resetAvatarControl, latencySamples: () => state.latency.snapshot(), pointerFeedbackSamples: () => state.latency.pointerSnapshot()};
|
||||
}
|
||||
|
||||
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});
|
||||
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, hedgeEdgeKey, hedgeKeyForMove, hedgeVariant, projectedHedgeEdges, 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); });
|
||||
|
||||
Reference in New Issue
Block a user