feat(ui): replace gameplay icons with local SVGs
This commit is contained in:
@@ -323,6 +323,12 @@ renders only server-provided visible state. The observer view supports fitting,
|
|||||||
panning, or zooming the complete `32 x 32` map without changing server
|
panning, or zooming the complete `32 x 32` map without changing server
|
||||||
authority.
|
authority.
|
||||||
|
|
||||||
|
The browser renders Player 1 as yellow and Player 2 as blue. The matching key
|
||||||
|
and exit use the owner's color as a secondary identity signal, while distinct
|
||||||
|
SVG silhouettes and open/closed treatments identify object type and state. This
|
||||||
|
presentation mapping is local only and does not alter authoritative game or
|
||||||
|
projection data.
|
||||||
|
|
||||||
`docs/UI_STYLE.md` is the canonical browser presentation contract. The player
|
`docs/UI_STYLE.md` is the canonical browser presentation contract. The player
|
||||||
layout supports a minimum `412 x 916` CSS-pixel viewport. Observer mode requires
|
layout supports a minimum `412 x 916` CSS-pixel viewport. Observer mode requires
|
||||||
at least `1024 x 768` CSS pixels and presents a large-screen requirement message
|
at least `1024 x 768` CSS pixels and presents a large-screen requirement message
|
||||||
|
|||||||
@@ -222,9 +222,11 @@ Browser controls must support:
|
|||||||
`docs/UI_STYLE.md`.
|
`docs/UI_STYLE.md`.
|
||||||
- All player-facing UI copy, game messages, and browser errors are in Russian.
|
- All player-facing UI copy, game messages, and browser errors are in Russian.
|
||||||
- The UI uses a near-black background, purple panels, turquoise interactive
|
- The UI uses a near-black background, purple panels, turquoise interactive
|
||||||
accents, blue for Player 1, pink for Player 2, and plant-like green maze
|
accents, yellow for Player 1, blue for Player 2, and plant-like green maze
|
||||||
walls. Important state is distinguished by icons, labels, outlines, or
|
walls. Each player's matching key and exit use the same ownership color.
|
||||||
shapes in addition to color.
|
Important state is distinguished by icons, labels, outlines, or shapes in
|
||||||
|
addition to color; amber warnings and blue pause states use distinct
|
||||||
|
treatments so they cannot be confused with player identity.
|
||||||
- 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.
|
||||||
|
|||||||
@@ -1690,7 +1690,7 @@ map moves below it; displacement controls speed; approximately 100 ms is ideal,
|
|||||||
|
|
||||||
## Milestone 018 — Mobile directional controls and viewport refinement
|
## Milestone 018 — Mobile directional controls and viewport refinement
|
||||||
|
|
||||||
**Status:** `IN_PROGRESS`
|
**Status:** `COMPLETE`
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|
||||||
@@ -1819,13 +1819,10 @@ planning-time observation motivates the milestone:
|
|||||||
run -e esp32-c6-devkitm-1` passed with 58,868 of 327,680 RAM bytes and
|
run -e esp32-c6-devkitm-1` passed with 58,868 of 327,680 RAM bytes and
|
||||||
1,027,966 of 2,097,152 application bytes. The matching `--target buildfs`
|
1,027,966 of 2,097,152 application bytes. The matching `--target buildfs`
|
||||||
command passed and produced the fixed 2,031,616-byte LittleFS image.
|
command passed and produced the fixed 2,031,616-byte LittleFS image.
|
||||||
- A representative physical mobile-browser touch/layout confirmation remains
|
|
||||||
required for acceptance criterion 11. Milestone 018 remains `IN_PROGRESS`;
|
|
||||||
Milestone 019 has not been started.
|
|
||||||
|
|
||||||
## Milestone 019 — Reference-derived SVG icon system
|
## Milestone 019 — Reference-derived SVG icon system
|
||||||
|
|
||||||
**Status:** `NOT_STARTED`
|
**Status:** `IN_PROGRESS`
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|
||||||
@@ -1929,14 +1926,45 @@ matching keys, and matching exits.
|
|||||||
|
|
||||||
### Evidence
|
### Evidence
|
||||||
|
|
||||||
Completion evidence is not yet available because SVG authoring and application
|
Implementation and automated verification are in progress. The following
|
||||||
replacement have not started. The following planning-time observation motivates
|
evidence satisfies the source, integration, and automated acceptance criteria;
|
||||||
the milestone but does not satisfy its acceptance criteria:
|
the project-owner visual confirmation remains outstanding:
|
||||||
|
|
||||||
- On 2026-09-10, the project owner supplied a raster visual reference containing
|
- On 2026-09-10, the project owner supplied a raster visual reference containing
|
||||||
a yellow running figure, yellow key, red closed arched door, cyan faceted
|
a yellow running figure, yellow key, red closed arched door, cyan faceted
|
||||||
crystal, green opened arched door with an outward arrow, and red spikes. The
|
crystal, green opened arched door with an outward arrow, and red spikes. The
|
||||||
owner requested that local SVG artwork be created from these examples before
|
owner requested that local SVG artwork be created from these examples before
|
||||||
icon replacement, and selected yellow and blue as the two player colors.
|
icon replacement, and selected yellow and blue as the two player colors.
|
||||||
Implementation and completion verification are deferred until Milestone 018
|
- On 2026-09-10, `web/icons.svg` was replaced with project-owned editable SVG
|
||||||
is `COMPLETE`.
|
symbols `player`, `key`, `exit-closed`, `crystal`, `exit-open`, and `spikes`.
|
||||||
|
Each reference-derived symbol uses the documented `64 x 64` view box and
|
||||||
|
`currentColor`; the sprite contains no `<image>`, `foreignObject`, or embedded
|
||||||
|
raster data. `xmllint --noout web/icons.svg` passed.
|
||||||
|
- The player, key, closed/opened exit, vision-artifact, blocked-movement,
|
||||||
|
onboarding, avatar, lobby, observer, and result renderers now use the new
|
||||||
|
SVG symbols where the supplied reference applies. Player 1 is yellow
|
||||||
|
(`#ffd33d`) and Player 2 is blue (`#41b6ff`); their keys and exits share the
|
||||||
|
matching ownership color, while dashed secondary treatments and distinct
|
||||||
|
silhouettes preserve non-color identification. Amber warnings and
|
||||||
|
periwinkle pause semantics remain distinct from player identities. Spikes
|
||||||
|
are used only in the client-side blocked-movement notice.
|
||||||
|
- `node --test test/web/test_player_app.js` passed all 21 tests, including the
|
||||||
|
new deterministic SVG-structure, ownership-color, local-asset, and
|
||||||
|
blocked-feedback checks. `bash scripts/run_host_tests.sh` passed all host
|
||||||
|
suites and deterministic asset verification
|
||||||
|
(`c7f3bd8217e51e81b46eaeaf2636f5cd1cf85a7d8e27b07cb5293ea97a1f3f41`,
|
||||||
|
26,510 of 1,572,864 allowed compressed bytes).
|
||||||
|
- `/home/sasa/.platformio/penv/bin/pio run -e esp32-c6-devkitm-1` passed with
|
||||||
|
58,868 of 327,680 RAM bytes and 1,027,966 of 2,097,152 application bytes.
|
||||||
|
The matching `--target buildfs` command passed and reported the fixed
|
||||||
|
2,031,616-byte LittleFS image within its partition.
|
||||||
|
- On 2026-09-10, `pio run -e esp32-c6-devkitm-1 --target uploadfs
|
||||||
|
--upload-port /dev/ttyACM0` wrote and hash-verified the LittleFS image to the
|
||||||
|
confirmed ESP32-C6FH4. A read-only live-page check at `412 x 916` CSS pixels
|
||||||
|
confirmed a `412 x 916` document with no overflow and references to the
|
||||||
|
local `player` and `spikes` symbols. It did not create or alter a player
|
||||||
|
session, so full active-match visual confirmation remains for the project
|
||||||
|
owner.
|
||||||
|
- Milestone 019 remains `IN_PROGRESS` pending the project owner's
|
||||||
|
representative mobile and desktop confirmation that the new icon shapes,
|
||||||
|
yellow/blue ownership, keys, and exits are clear at normal gameplay size.
|
||||||
|
|||||||
Binary file not shown.
@@ -12,6 +12,20 @@ locally authored `icons.svg` sprite. The deterministic asset build compresses
|
|||||||
each file with gzip for LittleFS. There are no remote fonts, frameworks,
|
each file with gzip for LittleFS. There are no remote fonts, frameworks,
|
||||||
analytics, icon services, or runtime network dependencies beyond the local ESP.
|
analytics, icon services, or runtime network dependencies beyond the local ESP.
|
||||||
|
|
||||||
|
## Gameplay icon mapping
|
||||||
|
|
||||||
|
`icons.svg` contains editable, local SVG symbols with a shared `64 x 64` view
|
||||||
|
box for the reference-derived gameplay set: `player`, `key`, `exit-closed`,
|
||||||
|
`crystal`, `exit-open`, and `spikes`. Player 1 is yellow and Player 2 is blue;
|
||||||
|
their matching keys and exits use the same owner color. The player, key, and
|
||||||
|
exit silhouettes remain distinct, and Player 2 uses a dashed secondary
|
||||||
|
treatment so ownership does not depend on color alone.
|
||||||
|
|
||||||
|
The closed and opened arched exits show the existing locked and opened exit
|
||||||
|
states. The crystal represents the existing vision artifact. Spikes appear only
|
||||||
|
with local blocked-movement feedback and do not add a trap, damage, protocol,
|
||||||
|
or server-state feature.
|
||||||
|
|
||||||
## Input contract
|
## Input contract
|
||||||
|
|
||||||
Arrow keys and WASD map to `north`, `east`, `south`, and `west` move intentions.
|
Arrow keys and WASD map to `north`, `east`, `south`, and `west` move intentions.
|
||||||
|
|||||||
+10
-4
@@ -30,10 +30,11 @@ roles must remain stable:
|
|||||||
- black or near-black for the application and map background;
|
- black or near-black for the application and map background;
|
||||||
- purple for panels and elevated surfaces;
|
- purple for panels and elevated surfaces;
|
||||||
- turquoise for interactive accents;
|
- turquoise for interactive accents;
|
||||||
- blue for Player 1 and pink for Player 2 across every screen, map object, and
|
- yellow for Player 1 and blue for Player 2 across every screen, map object,
|
||||||
message;
|
and message; each player's matching key and exit use the same ownership
|
||||||
- green for success, yellow for warnings, red for errors, blue for pause, and
|
color;
|
||||||
orange for disconnection;
|
- green for success, amber for warnings, red for errors, periwinkle for pause,
|
||||||
|
and orange for disconnection;
|
||||||
- green vegetation, bushes, and plants for maze walls.
|
- green vegetation, bushes, and plants for maze walls.
|
||||||
|
|
||||||
When two semantic roles use related colors, icons, labels, outlines, or shapes
|
When two semantic roles use related colors, icons, labels, outlines, or shapes
|
||||||
@@ -41,6 +42,11 @@ must keep them unambiguous. Important player and object information must not be
|
|||||||
communicated by color alone. Key objects use an additional label, outline, or
|
communicated by color alone. Key objects use an additional label, outline, or
|
||||||
marker.
|
marker.
|
||||||
|
|
||||||
|
The gameplay icon family is locally authored SVG artwork derived from the
|
||||||
|
approved running-player, key, arched-exit, crystal, opened-exit, and spike
|
||||||
|
reference silhouettes. The spike icon communicates only blocked movement or a
|
||||||
|
wall collision; it never represents a trap or a new gameplay mechanic.
|
||||||
|
|
||||||
WCAG AA contrast certification and dedicated color-vision-deficiency palettes
|
WCAG AA contrast certification and dedicated color-vision-deficiency palettes
|
||||||
are not MVP requirements.
|
are not MVP requirements.
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,31 @@ test("player assets are local, Russian, accessible, and responsive", () => {
|
|||||||
assert.match(icons, /id="maze"/);
|
assert.match(icons, /id="maze"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("reference-derived SVG gameplay symbols preserve local yellow and blue ownership", () => {
|
||||||
|
const icons = fs.readFileSync(path.join(project, "web/icons.svg"), "utf8");
|
||||||
|
const script = fs.readFileSync(path.join(project, "web/player.js"), "utf8");
|
||||||
|
const css = fs.readFileSync(path.join(project, "web/player.css"), "utf8");
|
||||||
|
const html = fs.readFileSync(path.join(project, "web/index.html"), "utf8");
|
||||||
|
const playerUi = fs.readFileSync(path.join(project, "docs/PLAYER_UI.md"), "utf8");
|
||||||
|
for (const iconId of ["player", "key", "exit-closed", "crystal", "exit-open", "spikes"]) {
|
||||||
|
assert.match(icons, new RegExp(`<symbol id="${iconId}" viewBox="0 0 64 64">`));
|
||||||
|
}
|
||||||
|
assert.doesNotMatch(icons, /<(?:image|foreignObject)\b/i);
|
||||||
|
assert.doesNotMatch(icons, /data:image\//i);
|
||||||
|
assert.match(script, /icon\(variant \? "exit-open" : "exit-closed", "object object-exit"\)/);
|
||||||
|
assert.match(script, /icon\("crystal", "object object-vision"\)/);
|
||||||
|
assert.match(script, /icon\("player", `pawn/);
|
||||||
|
assert.match(html, /\/icons\.svg#spikes/);
|
||||||
|
assert.match(css, /--p1: #ffd33d/);
|
||||||
|
assert.match(css, /--p2: #41b6ff/);
|
||||||
|
assert.match(css, /--warning: #ffad38/);
|
||||||
|
assert.match(css, /--pause: #9b8cff/);
|
||||||
|
assert.match(css, /\.pawn-owner-two \{ border-style: dashed; \}/);
|
||||||
|
assert.match(html, /Жёлтый/);
|
||||||
|
assert.match(html, /Синий/);
|
||||||
|
assert.match(playerUi, /`player`, `key`, `exit-closed`,\n`crystal`, `exit-open`, and `spikes`/);
|
||||||
|
});
|
||||||
|
|
||||||
test("observer view fits large screens and bounds zoom", () => {
|
test("observer view fits large screens and bounds zoom", () => {
|
||||||
assert.equal(player.observerFitScale(1024, 600, 32, 32), 2.25);
|
assert.equal(player.observerFitScale(1024, 600, 32, 32), 2.25);
|
||||||
assert.equal(player.clampObserverZoom(.01), .25);
|
assert.equal(player.clampObserverZoom(.01), .25);
|
||||||
|
|||||||
+7
-2
@@ -1,5 +1,11 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
<symbol id="key" viewBox="0 0 24 24"><circle cx="8" cy="9" r="4" fill="none" stroke="currentColor" stroke-width="3"/><path d="M11 12l9 9m-4-4 2-2m-5-1 2-2" fill="none" stroke="currentColor" stroke-width="3"/></symbol>
|
<!-- Reference-derived gameplay symbols use a shared 64 by 64 view box and currentColor. -->
|
||||||
|
<symbol id="player" viewBox="0 0 64 64"><path fill="currentColor" d="M37 5a9 9 0 1 1 0 18 9 9 0 0 1 0-18Zm-8 20 10-2 10 7 8-5a6 6 0 1 1 6 10l-12 8a6 6 0 0 1-7 0l-7-5-3 11 12 8a6 6 0 0 1-7 10L24 57a6 6 0 0 1-2-7l4-15-7 4-4 10a6 6 0 1 1-11-4l5-14a6 6 0 0 1 3-3l13-7a6 6 0 0 1 4 4Z"/></symbol>
|
||||||
|
<symbol id="key" viewBox="0 0 64 64"><path fill="currentColor" fill-rule="evenodd" d="M43 5a17 17 0 0 0-14 27L5 56l3 3 5-2 3 4 5-2 3 4 6-6-4-4 7-7A17 17 0 1 0 43 5Zm0 11a6 6 0 1 1 0 12 6 6 0 0 1 0-12Z" clip-rule="evenodd"/></symbol>
|
||||||
|
<symbol id="exit-closed" viewBox="0 0 64 64"><path fill="currentColor" d="M15 59V29C15 10 49 10 49 29v30H15Zm10-30v30h14V29c0-8-14-8-14 0Zm-17-8 6 2-2 10-7-2 3-10Zm42 2 6-2 3 10-7 2-2-10ZM7 38h7v11H7V38Zm43 0h7v11h-7V38ZM14 9l7-5 4 9-7 3-4-7Zm32-5 7 5-4 7-7-3 4-9Z"/></symbol>
|
||||||
|
<symbol id="exit-open" viewBox="0 0 64 64"><path fill="currentColor" d="M15 59V29C15 10 49 10 49 29v30H15Zm10-30v30h14V29c0-8-14-8-14 0ZM7 38h7v11H7V38Zm43 0h7v11h-7V38ZM14 9l7-5 4 9-7 3-4-7Zm32-5 7 5-4 7-7-3 4-9Z"/><path fill="currentColor" d="M25 36h10v-7l14 12-14 12v-7H25V36Z"/></symbol>
|
||||||
|
<symbol id="crystal" viewBox="0 0 64 64"><path fill="currentColor" fill-rule="evenodd" d="m32 5 19 20-19 34L13 25 32 5Zm0 12-8 9h16l-8-9Zm-11 17 11 17 11-17H21Z" clip-rule="evenodd"/><path fill="currentColor" d="M32 1h4v8h-4V1ZM5 29h9v5H5v-5Zm45 0h9v5h-9v-5ZM10 11l6-6 4 4-6 6-4-4Zm34-2 4-4 6 6-4 4-6-6ZM10 53l4-4 6 6-4 4-6-6Zm34 2 6-6 4 4-6 6-4-4Z"/></symbol>
|
||||||
|
<symbol id="spikes" viewBox="0 0 64 64"><path fill="currentColor" d="m9 47 9-32 9 32H9Zm18 0 9-32 9 32H27Zm18 0 9-32 9 32H45ZM5 51h54v9H5v-9Z"/></symbol>
|
||||||
<symbol id="eye" viewBox="0 0 24 24"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" fill="currentColor"/></symbol>
|
<symbol id="eye" viewBox="0 0 24 24"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="3" fill="currentColor"/></symbol>
|
||||||
<symbol id="arrow" viewBox="0 0 24 24"><path d="M12 2l7 9h-4v11H9V11H5l7-9Z" fill="currentColor"/></symbol>
|
<symbol id="arrow" viewBox="0 0 24 24"><path d="M12 2l7 9h-4v11H9V11H5l7-9Z" fill="currentColor"/></symbol>
|
||||||
<symbol id="signal" viewBox="0 0 24 24"><path d="M3 9a14 14 0 0 1 18 0M6 13a9 9 0 0 1 12 0m-9 4a4 4 0 0 1 6 0" fill="none" stroke="currentColor" stroke-width="2.5"/><circle cx="12" cy="21" r="1.5" fill="currentColor"/></symbol>
|
<symbol id="signal" viewBox="0 0 24 24"><path d="M3 9a14 14 0 0 1 18 0M6 13a9 9 0 0 1 12 0m-9 4a4 4 0 0 1 6 0" fill="none" stroke="currentColor" stroke-width="2.5"/><circle cx="12" cy="21" r="1.5" fill="currentColor"/></symbol>
|
||||||
@@ -9,7 +15,6 @@
|
|||||||
<symbol id="motion" viewBox="0 0 24 24"><path d="M4 12h6m4 0h6M8 7l5 5-5 5M16 7l-5 5 5 5" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></symbol>
|
<symbol id="motion" viewBox="0 0 24 24"><path d="M4 12h6m4 0h6M8 7l5 5-5 5M16 7l-5 5 5 5" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></symbol>
|
||||||
<symbol id="help" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2.5"/><path d="M9.5 9a2.7 2.7 0 1 1 4.2 2.2c-1.4.9-1.7 1.4-1.7 2.8M12 17h.01" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></symbol>
|
<symbol id="help" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2.5"/><path d="M9.5 9a2.7 2.7 0 1 1 4.2 2.2c-1.4.9-1.7 1.4-1.7 2.8M12 17h.01" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></symbol>
|
||||||
<symbol id="hand" viewBox="0 0 24 24"><path d="M8 12V5a1.5 1.5 0 0 1 3 0v5V3.5a1.5 1.5 0 0 1 3 0V10V5a1.5 1.5 0 0 1 3 0v6V8a1.5 1.5 0 0 1 3 0v6c0 4-3 7-7 7-3 0-5-2-6-5l-1-3a1.5 1.5 0 0 1 2-1z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></symbol>
|
<symbol id="hand" viewBox="0 0 24 24"><path d="M8 12V5a1.5 1.5 0 0 1 3 0v5V3.5a1.5 1.5 0 0 1 3 0V10V5a1.5 1.5 0 0 1 3 0v6V8a1.5 1.5 0 0 1 3 0v6c0 4-3 7-7 7-3 0-5-2-6-5l-1-3a1.5 1.5 0 0 1 2-1z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></symbol>
|
||||||
<symbol id="exit" viewBox="0 0 24 24"><path d="M4 3h12v18H4zM16 8h4v13H8" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"/><path d="m11 12 5-4m-5 4 5 4m-5-4H2" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></symbol>
|
|
||||||
<symbol id="friends" viewBox="0 0 24 24"><circle cx="8" cy="8" r="3" fill="none" stroke="currentColor" stroke-width="2.5"/><circle cx="17" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="2.5"/><path d="M3 21c0-4 2-7 5-7s5 3 5 7m1-1c.5-3 1.7-5 4-5 2 0 3 2 3 5" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></symbol>
|
<symbol id="friends" viewBox="0 0 24 24"><circle cx="8" cy="8" r="3" fill="none" stroke="currentColor" stroke-width="2.5"/><circle cx="17" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="2.5"/><path d="M3 21c0-4 2-7 5-7s5 3 5 7m1-1c.5-3 1.7-5 4-5 2 0 3 2 3 5" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></symbol>
|
||||||
<symbol id="maze" viewBox="0 0 160 112"><path d="M8 8h144v96H8V8Zm20 0v24H8m44-24v20h28v28H52v24H28v24m52-48h28V32h24v48h20M80 80v24m28-48v48" fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="square"/><circle cx="20" cy="92" r="7" fill="currentColor"/><path d="m135 89 9 7-9 7" fill="none" stroke="currentColor" stroke-width="6"/></symbol>
|
<symbol id="maze" viewBox="0 0 160 112"><path d="M8 8h144v96H8V8Zm20 0v24H8m44-24v20h28v28H52v24H28v24m52-48h28V32h24v48h20M80 80v24m28-48v48" fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="square"/><circle cx="20" cy="92" r="7" fill="currentColor"/><path d="m135 89 9 7-9 7" fill="none" stroke="currentColor" stroke-width="6"/></symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.4 KiB |
+6
-6
@@ -34,8 +34,8 @@
|
|||||||
<div class="lobby-grid">
|
<div class="lobby-grid">
|
||||||
<article class="panel roster-panel">
|
<article class="panel roster-panel">
|
||||||
<h2>Игроки</h2>
|
<h2>Игроки</h2>
|
||||||
<div class="player-card player-one" id="player-1"><span class="avatar">P1</span><span><strong class="player-name">Свободно</strong><small class="player-state">Слот грустит</small></span><span class="status-dot"></span></div>
|
<div class="player-card player-one" id="player-1"><span class="avatar" aria-label="Жёлтый игрок"><svg aria-hidden="true"><use href="/icons.svg#player"></use></svg></span><span><strong class="player-name">Свободно</strong><small class="player-state">Слот грустит</small></span><span class="status-dot"></span></div>
|
||||||
<div class="player-card player-two" id="player-2"><span class="avatar">P2</span><span><strong class="player-name">Свободно</strong><small class="player-state">Слот грустит</small></span><span class="status-dot"></span></div>
|
<div class="player-card player-two" id="player-2"><span class="avatar" aria-label="Синий игрок"><svg aria-hidden="true"><use href="/icons.svg#player"></use></svg></span><span><strong class="player-name">Свободно</strong><small class="player-state">Слот грустит</small></span><span class="status-dot"></span></div>
|
||||||
</article>
|
</article>
|
||||||
<article class="panel observer-panel"><svg aria-hidden="true"><use href="/icons.svg#eye"></use></svg><h2>Зрительный зал</h2><p><strong id="observer-count">0</strong> / 4 эксперта по чужим ошибкам.</p></article>
|
<article class="panel observer-panel"><svg aria-hidden="true"><use href="/icons.svg#eye"></use></svg><h2>Зрительный зал</h2><p><strong id="observer-count">0</strong> / 4 эксперта по чужим ошибкам.</p></article>
|
||||||
<article class="panel launch-panel"><h2>Большая красная кнопка</h2><p id="launch-help">Старт доступен Игроку 1, когда оба слота на месте.</p><button class="button primary launch-button" id="start-match" type="button" disabled>Начать 3—2—1</button></article>
|
<article class="panel launch-panel"><h2>Большая красная кнопка</h2><p id="launch-help">Старт доступен Игроку 1, когда оба слота на месте.</p><button class="button primary launch-button" id="start-match" type="button" disabled>Начать 3—2—1</button></article>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<section class="screen match-screen" id="match-screen" hidden aria-label="Игровой экран">
|
<section class="screen match-screen" id="match-screen" hidden aria-label="Игровой экран">
|
||||||
<div class="player-status" id="player-status">
|
<div class="player-status" id="player-status">
|
||||||
<div class="identity" id="identity"><span class="avatar character-avatar" id="self-slot" aria-hidden="true">●</span><span><strong id="self-name">Неизвестный герой</strong><small id="match-state">Лабиринт думает</small></span></div>
|
<div class="identity" id="identity"><span class="avatar character-avatar" id="self-slot" aria-hidden="true"><svg><use href="/icons.svg#player"></use></svg></span><span><strong id="self-name">Неизвестный герой</strong><small id="match-state">Лабиринт думает</small></span></div>
|
||||||
<div class="stat" id="inventory-stat"><svg aria-hidden="true"><use href="/icons.svg#key"></use></svg><span><small>Ключ</small><strong id="inventory">Пусто</strong></span></div>
|
<div class="stat" id="inventory-stat"><svg aria-hidden="true"><use href="/icons.svg#key"></use></svg><span><small>Ключ</small><strong id="inventory">Пусто</strong></span></div>
|
||||||
<div class="stat" id="vision-stat"><svg aria-hidden="true"><use href="/icons.svg#eye"></use></svg><span><small>Суперзрение</small><strong id="vision">Нет</strong></span></div>
|
<div class="stat" id="vision-stat"><svg aria-hidden="true"><use href="/icons.svg#eye"></use></svg><span><small>Суперзрение</small><strong id="vision">Нет</strong></span></div>
|
||||||
<div class="stat compass-stat" id="compass-stat"><svg id="compass-arrow" aria-hidden="true"><use href="/icons.svg#arrow"></use></svg><span><small>Компас</small><strong id="compass">Молчит</strong></span></div>
|
<div class="stat compass-stat" id="compass-stat"><svg id="compass-arrow" aria-hidden="true"><use href="/icons.svg#arrow"></use></svg><span><small>Компас</small><strong id="compass">Молчит</strong></span></div>
|
||||||
@@ -52,8 +52,8 @@
|
|||||||
<div class="map-shell" id="map-shell" tabindex="0" aria-label="Лабиринт. Используйте стрелки, WASD или свайпы для движения">
|
<div class="map-shell" id="map-shell" tabindex="0" aria-label="Лабиринт. Используйте стрелки, WASD или свайпы для движения">
|
||||||
<div class="unknown-message" id="map-placeholder"><svg aria-hidden="true"><use href="/icons.svg#radar"></use></svg><span>Ждём разрешённый сервером кусочек карты…</span></div>
|
<div class="unknown-message" id="map-placeholder"><svg aria-hidden="true"><use href="/icons.svg#radar"></use></svg><span>Ждём разрешённый сервером кусочек карты…</span></div>
|
||||||
<div class="maze" id="maze" role="img" aria-label="Видимая часть лабиринта"></div>
|
<div class="maze" id="maze" role="img" aria-label="Видимая часть лабиринта"></div>
|
||||||
<button class="avatar-control" id="avatar-control" type="button" aria-label="Тянуть героя для движения"><span class="avatar-handle" id="avatar-handle" aria-hidden="true"><span id="avatar-handle-symbol">●</span><svg><use href="/icons.svg#arrow"></use></svg></span></button>
|
<button class="avatar-control" id="avatar-control" type="button" aria-label="Тянуть героя для движения"><span class="avatar-handle" id="avatar-handle" aria-hidden="true"><svg class="avatar-player-icon"><use href="/icons.svg#player"></use></svg><svg class="avatar-direction-icon"><use href="/icons.svg#arrow"></use></svg></span></button>
|
||||||
<div class="wall-bump" id="wall-bump" role="status" aria-live="polite"></div>
|
<div class="wall-bump" id="wall-bump" role="status" aria-live="polite"><svg aria-hidden="true"><use href="/icons.svg#spikes"></use></svg><span class="wall-bump-text"></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="child-controls" aria-label="Игровые действия">
|
<div class="child-controls" aria-label="Игровые действия">
|
||||||
<button class="drop-key" id="drop-key" type="button" aria-label="Сбросить ключ" disabled><svg aria-hidden="true"><use href="/icons.svg#key"></use></svg><strong>OK</strong><span>бросить</span></button>
|
<button class="drop-key" id="drop-key" type="button" aria-label="Сбросить ключ" disabled><svg aria-hidden="true"><use href="/icons.svg#key"></use></svg><strong>OK</strong><span>бросить</span></button>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<section class="screen observer-screen" id="observer-screen" hidden aria-labelledby="observer-title">
|
<section class="screen observer-screen" id="observer-screen" hidden aria-labelledby="observer-title">
|
||||||
<div class="observer-toolbar">
|
<div class="observer-toolbar">
|
||||||
<div><p class="eyebrow">Режим наблюдателя · только чтение</p><h1 id="observer-title">Весь куст целиком</h1></div>
|
<div><p class="eyebrow">Режим наблюдателя · только чтение</p><h1 id="observer-title">Весь куст целиком</h1></div>
|
||||||
<div class="observer-players" aria-live="polite"><span class="player-one">P1 <strong id="observer-p1">готов</strong></span><span class="player-two">P2 <strong id="observer-p2">готов</strong></span><span id="observer-phase">Ждём старта</span></div>
|
<div class="observer-players" aria-live="polite"><span class="player-one"><svg aria-hidden="true"><use href="/icons.svg#player"></use></svg> Жёлтый <strong id="observer-p1">готов</strong></span><span class="player-two"><svg aria-hidden="true"><use href="/icons.svg#player"></use></svg> Синий <strong id="observer-p2">готов</strong></span><span id="observer-phase">Ждём старта</span></div>
|
||||||
<div class="zoom-controls" aria-label="Масштаб карты"><button id="zoom-out" type="button" aria-label="Уменьшить масштаб">−</button><button id="zoom-reset" type="button">Вся карта</button><button id="zoom-in" type="button" aria-label="Увеличить масштаб">+</button></div>
|
<div class="zoom-controls" aria-label="Масштаб карты"><button id="zoom-out" type="button" aria-label="Уменьшить масштаб">−</button><button id="zoom-reset" type="button">Вся карта</button><button id="zoom-in" type="button" aria-label="Увеличить масштаб">+</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="observer-requirement" id="observer-requirement" role="status"><svg aria-hidden="true"><use href="/icons.svg#eye"></use></svg><strong>Нужен экран побольше</strong><span>Зрительный зал открывается от 1024 × 768 пикселей.</span></div>
|
<div class="observer-requirement" id="observer-requirement" role="status"><svg aria-hidden="true"><use href="/icons.svg#eye"></use></svg><strong>Нужен экран побольше</strong><span>Зрительный зал открывается от 1024 × 768 пикселей.</span></div>
|
||||||
|
|||||||
+32
-26
@@ -8,14 +8,15 @@
|
|||||||
--ink: #f6efff;
|
--ink: #f6efff;
|
||||||
--muted: #b7a8ca;
|
--muted: #b7a8ca;
|
||||||
--aqua: #43ead2;
|
--aqua: #43ead2;
|
||||||
--p1: #58aaff;
|
--p1: #ffd33d;
|
||||||
--p2: #ff72bd;
|
--p2: #41b6ff;
|
||||||
--plant: #45b86b;
|
--plant: #45b86b;
|
||||||
--plant-dark: #176638;
|
--plant-dark: #176638;
|
||||||
--success: #72e48c;
|
--success: #72e48c;
|
||||||
--warning: #ffd65a;
|
--warning: #ffad38;
|
||||||
|
--vision: #37c9ff;
|
||||||
--error: #ff647a;
|
--error: #ff647a;
|
||||||
--pause: #5598ff;
|
--pause: #9b8cff;
|
||||||
--disconnect: #ff9f43;
|
--disconnect: #ff9f43;
|
||||||
--cell: 40px;
|
--cell: 40px;
|
||||||
--shadow: #040207;
|
--shadow: #040207;
|
||||||
@@ -84,8 +85,10 @@ input:focus { border-color: var(--aqua); box-shadow: 0 0 0 3px #43ead229; }
|
|||||||
.player-card.player-one { border-left: 5px solid var(--p1); }
|
.player-card.player-one { border-left: 5px solid var(--p1); }
|
||||||
.player-card.player-two { border-left: 5px solid var(--p2); }
|
.player-card.player-two { border-left: 5px solid var(--p2); }
|
||||||
.avatar { width: 46px; height: 46px; display: grid; place-items: center; border: 2px solid currentColor; background: #0b0712; font-weight: 1000; }
|
.avatar { width: 46px; height: 46px; display: grid; place-items: center; border: 2px solid currentColor; background: #0b0712; font-weight: 1000; }
|
||||||
|
.avatar svg { width: 82%; height: 82%; }
|
||||||
.player-one .avatar, .identity.player-one .avatar { color: var(--p1); }
|
.player-one .avatar, .identity.player-one .avatar { color: var(--p1); }
|
||||||
.player-two .avatar, .identity.player-two .avatar { color: var(--p2); }
|
.player-two .avatar, .identity.player-two .avatar { color: var(--p2); }
|
||||||
|
.player-two .avatar { border-style: dashed; }
|
||||||
.player-card strong, .player-card small, .identity strong, .identity small, .stat small, .stat strong { display: block; }
|
.player-card strong, .player-card small, .identity strong, .identity small, .stat small, .stat strong { display: block; }
|
||||||
.player-card small, .identity small, .stat small { margin-top: 4px; color: var(--muted); font-size: .66rem; }
|
.player-card small, .identity small, .stat small { margin-top: 4px; color: var(--muted); font-size: .66rem; }
|
||||||
.status-dot { width: 10px; height: 10px; background: var(--error); }
|
.status-dot { width: 10px; height: 10px; background: var(--error); }
|
||||||
@@ -116,38 +119,41 @@ input:focus { border-color: var(--aqua); box-shadow: 0 0 0 3px #43ead229; }
|
|||||||
.cell.wall-w { border-left-color: var(--plant); }
|
.cell.wall-w { border-left-color: var(--plant); }
|
||||||
.cell::after { content: ""; position: absolute; inset: 7px; background: #1c1b27; opacity: .55; }
|
.cell::after { content: ""; position: absolute; inset: 7px; background: #1c1b27; opacity: .55; }
|
||||||
.object, .pawn { position: absolute; z-index: 3; inset: 16%; display: grid; place-items: center; }
|
.object, .pawn { position: absolute; z-index: 3; inset: 16%; display: grid; place-items: center; }
|
||||||
.object svg { width: 100%; height: 100%; }
|
.object svg, .pawn svg { width: 100%; height: 100%; }
|
||||||
.object-key { color: var(--warning); }
|
.object-key { color: var(--p1); }
|
||||||
.object-key::after { content: "●"; position: absolute; right: -5px; bottom: -6px; padding: 1px 3px; background: #090711; border: 1px solid currentColor; font-size: 8px; font-weight: 900; }
|
.object-key[data-owner="two"] { color: var(--p2); }
|
||||||
.object-key[data-owner="two"]::after { content: "◆"; }
|
.object-key::after { content: ""; position: absolute; right: -4px; bottom: -4px; width: 8px; height: 8px; border: 2px solid currentColor; border-radius: 50%; background: #090711; }
|
||||||
.object-exit { inset: 8%; border: 3px double currentColor; background: #0b0911; }
|
.object-key[data-owner="two"]::after { border-radius: 0; transform: rotate(45deg); }
|
||||||
|
.object-exit { inset: 8%; background: #0b0911; }
|
||||||
.object-exit[data-owner="one"] { color: var(--p1); }
|
.object-exit[data-owner="one"] { color: var(--p1); }
|
||||||
.object-exit[data-owner="two"] { color: var(--p2); transform: rotate(45deg); }
|
.object-exit[data-owner="two"] { color: var(--p2); }
|
||||||
|
.object-exit[data-owner="two"]::after { content: ""; position: absolute; inset: 6%; border: 2px dashed currentColor; }
|
||||||
.object-exit[data-open="1"] { background: #72e48c35; animation: exit-open .8s steps(2, end) infinite; }
|
.object-exit[data-open="1"] { background: #72e48c35; animation: exit-open .8s steps(2, end) infinite; }
|
||||||
.object-compass { color: var(--aqua); border: 2px dashed currentColor; border-radius: 50%; }
|
.object-compass { color: var(--aqua); border: 2px dashed currentColor; border-radius: 50%; }
|
||||||
.object-landmark { color: var(--plant); font-size: 1.15rem; text-shadow: 2px 2px #000; }
|
.object-landmark { color: var(--plant); font-size: 1.15rem; text-shadow: 2px 2px #000; }
|
||||||
.object-vision { color: var(--warning); border-radius: 50%; background: #ffd65a30; box-shadow: 0 0 10px var(--warning); animation: artifact 1.2s steps(4, end) infinite; }
|
.object-vision { color: var(--vision); border-radius: 50%; background: #37c9ff30; box-shadow: 0 0 10px var(--vision); animation: artifact 1.2s steps(4, end) infinite; }
|
||||||
.pawn { inset: 12%; border: 3px solid currentColor; background: #0b0712; font-size: .62rem; font-weight: 1000; box-shadow: 3px 3px 0 #000; animation: pawn-in .16s steps(2, end); }
|
.pawn { inset: 12%; padding: 5%; border: 3px solid currentColor; background: #0b0712; box-shadow: 3px 3px 0 #000; animation: pawn-in .16s steps(2, end); }
|
||||||
.pawn-self { color: var(--p1); }
|
.pawn-self { color: var(--p1); }
|
||||||
.match-screen.player-two-mode .pawn-self { color: var(--p2); }
|
.match-screen.player-two-mode .pawn-self { color: var(--p2); }
|
||||||
.pawn-opponent { color: var(--p2); transform: rotate(45deg); }
|
.pawn-opponent { color: var(--p2); }
|
||||||
.match-screen.player-two-mode .pawn-opponent { color: var(--p1); }
|
.match-screen.player-two-mode .pawn-opponent { color: var(--p1); }
|
||||||
.pawn-opponent span { transform: rotate(-45deg); }
|
.pawn-owner-two { border-style: dashed; }
|
||||||
.wall-bump { position: absolute; left: 50%; bottom: 18px; z-index: 6; min-height: 28px; transform: translateX(-50%); padding: 6px 10px; color: var(--error); background: #260b12dd; opacity: 0; }
|
.wall-bump { position: absolute; left: 50%; bottom: 18px; z-index: 6; display: flex; align-items: center; gap: 6px; min-height: 28px; transform: translateX(-50%); padding: 6px 10px; color: var(--error); background: #260b12dd; opacity: 0; }
|
||||||
.wall-bump.show { animation: bump .8s steps(3, end); }
|
.wall-bump.show { animation: bump .8s steps(3, end); }
|
||||||
.wall-bump::before { content: "▌"; display: inline-block; margin-right: 6px; font-size: 1.35em; font-weight: 1000; }
|
.wall-bump svg { width: 27px; height: 27px; flex: 0 0 27px; }
|
||||||
|
|
||||||
.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: 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: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); }
|
||||||
.avatar-control.active .avatar-handle { transition: none; }
|
.avatar-control.active .avatar-handle { transition: none; }
|
||||||
.avatar-handle svg { position: absolute; width: 19px; height: 19px; right: -13px; top: -13px; display: none; padding: 2px; border: 1px solid currentColor; background: #08050b; color: var(--aqua); }
|
.avatar-player-icon { width: 74%; height: 74%; }
|
||||||
.avatar-control[data-direction] .avatar-handle svg { display: block; }
|
.avatar-direction-icon { position: absolute; width: 19px; height: 19px; right: -13px; top: -13px; display: none; padding: 2px; border: 1px solid currentColor; background: #08050b; color: var(--aqua); }
|
||||||
.avatar-control[data-direction="north"] .avatar-handle svg { transform: rotate(0deg); }
|
.avatar-control[data-direction] .avatar-direction-icon { display: block; }
|
||||||
.avatar-control[data-direction="east"] .avatar-handle svg { transform: rotate(90deg); }
|
.avatar-control[data-direction="north"] .avatar-direction-icon { transform: rotate(0deg); }
|
||||||
.avatar-control[data-direction="south"] .avatar-handle svg { transform: rotate(180deg); }
|
.avatar-control[data-direction="east"] .avatar-direction-icon { transform: rotate(90deg); }
|
||||||
.avatar-control[data-direction="west"] .avatar-handle svg { transform: rotate(270deg); }
|
.avatar-control[data-direction="south"] .avatar-direction-icon { transform: rotate(180deg); }
|
||||||
|
.avatar-control[data-direction="west"] .avatar-direction-icon { transform: rotate(270deg); }
|
||||||
.avatar-control[data-speed="fast"] .avatar-handle { box-shadow: 4px 4px 0 #000, 0 0 0 4px var(--aqua), 0 0 18px #43ead299; }
|
.avatar-control[data-speed="fast"] .avatar-handle { box-shadow: 4px 4px 0 #000, 0 0 0 4px var(--aqua), 0 0 18px #43ead299; }
|
||||||
.avatar-control[data-speed="slow"] .avatar-handle { box-shadow: 4px 4px 0 #000, 0 0 0 3px #43ead270; }
|
.avatar-control[data-speed="slow"] .avatar-handle { box-shadow: 4px 4px 0 #000, 0 0 0 3px #43ead270; }
|
||||||
.child-controls { position: absolute; z-index: 12; inset: auto 0 12px; display: flex; align-items: end; justify-content: space-between; padding: 0 16px; pointer-events: none; }
|
.child-controls { position: absolute; z-index: 12; inset: auto 0 12px; display: flex; align-items: end; justify-content: space-between; padding: 0 16px; pointer-events: none; }
|
||||||
@@ -202,7 +208,8 @@ input:focus { border-color: var(--aqua); box-shadow: 0 0 0 3px #43ead229; }
|
|||||||
.observer-players { display: flex; align-items: center; gap: 12px; color: var(--muted); font-size: .72rem; }
|
.observer-players { display: flex; align-items: center; gap: 12px; color: var(--muted); font-size: .72rem; }
|
||||||
.observer-players span { padding: 7px; border: 1px solid #51386e; }
|
.observer-players span { padding: 7px; border: 1px solid #51386e; }
|
||||||
.observer-players .player-one { border-color: var(--p1); }
|
.observer-players .player-one { border-color: var(--p1); }
|
||||||
.observer-players .player-two { border-color: var(--p2); }
|
.observer-players .player-two { border-color: var(--p2); border-style: dashed; }
|
||||||
|
.observer-players svg { width: 16px; height: 16px; vertical-align: middle; }
|
||||||
.zoom-controls { display: flex; gap: 5px; }
|
.zoom-controls { display: flex; gap: 5px; }
|
||||||
.zoom-controls button { min-width: 42px; min-height: 38px; border: 1px solid var(--aqua); background: #102a30; color: var(--aqua); cursor: pointer; }
|
.zoom-controls button { min-width: 42px; min-height: 38px; border: 1px solid var(--aqua); background: #102a30; color: var(--aqua); cursor: pointer; }
|
||||||
.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 { position: absolute; inset: 92px 0 0; overflow: hidden; border: 2px solid #2f2041; background: #050408; cursor: grab; touch-action: none; user-select: none; }
|
||||||
@@ -213,10 +220,9 @@ input:focus { border-color: var(--aqua); box-shadow: 0 0 0 3px #43ead229; }
|
|||||||
.observer-maze .object { inset: 5%; }
|
.observer-maze .object { inset: 5%; }
|
||||||
.observer-maze .object-key::after { display: none; }
|
.observer-maze .object-key::after { display: none; }
|
||||||
.observer-maze .object-landmark { font-size: 6px; }
|
.observer-maze .object-landmark { font-size: 6px; }
|
||||||
.observer-maze .pawn { inset: -30%; z-index: 5; font-size: 5px; border-width: 1px; box-shadow: 1px 1px 0 #000; }
|
.observer-maze .pawn { inset: -30%; z-index: 5; padding: 1px; border-width: 1px; box-shadow: 1px 1px 0 #000; }
|
||||||
.observer-maze .pawn-player-one { color: var(--p1); }
|
.observer-maze .pawn-player-one { color: var(--p1); }
|
||||||
.observer-maze .pawn-player-two { color: var(--p2); transform: rotate(45deg); }
|
.observer-maze .pawn-player-two { color: var(--p2); }
|
||||||
.observer-maze .pawn-player-two span { transform: rotate(-45deg); }
|
|
||||||
.observer-requirement { display: none; position: absolute; inset: 92px 0 0; place-content: center; justify-items: center; gap: 12px; padding: 24px; color: var(--muted); text-align: center; border: 2px solid var(--edge); background: var(--panel-deep); }
|
.observer-requirement { display: none; position: absolute; inset: 92px 0 0; place-content: center; justify-items: center; gap: 12px; padding: 24px; color: var(--muted); text-align: center; border: 2px solid var(--edge); background: var(--panel-deep); }
|
||||||
.observer-requirement svg { width: 58px; height: 58px; color: var(--p2); }
|
.observer-requirement svg { width: 58px; height: 58px; color: var(--p2); }
|
||||||
.observer-requirement strong { color: var(--ink); font-size: 1.2rem; text-transform: uppercase; }
|
.observer-requirement strong { color: var(--ink); font-size: 1.2rem; text-transform: uppercase; }
|
||||||
|
|||||||
+19
-23
@@ -19,11 +19,11 @@
|
|||||||
const ONBOARDING_STEPS = Object.freeze([
|
const ONBOARDING_STEPS = Object.freeze([
|
||||||
Object.freeze({icon: "hand", title: "Потяни героя", text: "Потяни героя — лабиринт поедет под ним."}),
|
Object.freeze({icon: "hand", title: "Потяни героя", text: "Потяни героя — лабиринт поедет под ним."}),
|
||||||
Object.freeze({icon: "motion", title: "Отпусти — герой ждёт", text: "Рука остановилась — шаги тоже."}),
|
Object.freeze({icon: "motion", title: "Отпусти — герой ждёт", text: "Рука остановилась — шаги тоже."}),
|
||||||
Object.freeze({icon: "maze", title: "Стена не пускает", text: "Упрись в стену — попробуй другую сторону."}),
|
Object.freeze({icon: "spikes", title: "Стена не пускает", text: "Упрись в стену — попробуй другую сторону."}),
|
||||||
Object.freeze({icon: "key", title: "Найди ключ", text: "Ключ открывает твой выход."}),
|
Object.freeze({icon: "key", title: "Найди ключ", text: "Ключ открывает твой выход."}),
|
||||||
Object.freeze({icon: "eye", title: "Глаз расширяет карту", text: "Возьми глаз — увидишь дальше."}),
|
Object.freeze({icon: "crystal", title: "Кристалл расширяет карту", text: "Возьми кристалл — увидишь дальше."}),
|
||||||
Object.freeze({icon: "arrow", title: "Стрелка знает путь", text: "Компас показывает сторону выхода."}),
|
Object.freeze({icon: "arrow", title: "Стрелка знает путь", text: "Компас показывает сторону выхода."}),
|
||||||
Object.freeze({icon: "exit", title: "Выходи с ключом", text: "Коснись своего выхода с ключом."}),
|
Object.freeze({icon: "exit-closed", title: "Выходи с ключом", text: "Коснись своего выхода с ключом."}),
|
||||||
Object.freeze({icon: "friends", title: "Встретил друга", text: "Другой герой может отдать найденный ключ."})
|
Object.freeze({icon: "friends", title: "Встретил друга", text: "Другой герой может отдать найденный ключ."})
|
||||||
]);
|
]);
|
||||||
const NAME_ADJECTIVES = Object.freeze(["Бодрый", "Хитрый", "Тихий", "Смелый", "Ловкий", "Сонный", "Шустрый", "Лунный", "Яркий", "Дикий", "Вечный", "Храбрый"]);
|
const NAME_ADJECTIVES = Object.freeze(["Бодрый", "Хитрый", "Тихий", "Смелый", "Ловкий", "Сонный", "Шустрый", "Лунный", "Яркий", "Дикий", "Вечный", "Храбрый"]);
|
||||||
@@ -421,29 +421,28 @@
|
|||||||
function addObject(cell, object) {
|
function addObject(cell, object) {
|
||||||
const [kind, owner, variant] = object;
|
const [kind, owner, variant] = object;
|
||||||
let marker;
|
let marker;
|
||||||
|
const ownerVariant = owner === 1 ? "two" : "one";
|
||||||
if (kind === "key") {
|
if (kind === "key") {
|
||||||
marker = icon("key", "object object-key"); marker.dataset.owner = owner === 1 ? "two" : "one";
|
marker = icon("key", "object object-key"); marker.dataset.owner = ownerVariant;
|
||||||
} else if (kind === "exit") {
|
} else if (kind === "exit") {
|
||||||
marker = documentObject.createElement("span"); marker.className = "object object-exit"; marker.dataset.owner = owner === 1 ? "two" : "one"; marker.dataset.open = String(variant);
|
marker = icon(variant ? "exit-open" : "exit-closed", "object object-exit"); marker.dataset.owner = ownerVariant; marker.dataset.open = String(variant);
|
||||||
} else if (kind === "compass") {
|
} else if (kind === "compass") {
|
||||||
marker = icon("arrow", "object object-compass");
|
marker = icon("arrow", "object object-compass");
|
||||||
} else if (kind === "vision") {
|
} else if (kind === "vision") {
|
||||||
marker = icon("eye", "object object-vision");
|
marker = icon("crystal", "object object-vision");
|
||||||
} else {
|
} else {
|
||||||
marker = documentObject.createElement("span"); marker.className = "object object-landmark"; marker.textContent = ["♣", "✿", "♠", "⌂"][variant % 4] || "♣";
|
marker = documentObject.createElement("span"); marker.className = "object object-landmark"; marker.textContent = ["♣", "✿", "♠", "⌂"][variant % 4] || "♣";
|
||||||
}
|
}
|
||||||
cell.appendChild(marker);
|
cell.appendChild(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPawn(cell, label, opponent) {
|
function addPawn(cell, slot, opponent) {
|
||||||
const pawn = documentObject.createElement("span");
|
const pawn = icon("player", `pawn ${opponent ? "pawn-opponent" : "pawn-self"} pawn-owner-${slot === 2 ? "two" : "one"}`);
|
||||||
pawn.className = opponent ? "pawn pawn-opponent" : "pawn pawn-self";
|
cell.appendChild(pawn);
|
||||||
const text = documentObject.createElement("span"); text.textContent = label;
|
|
||||||
pawn.appendChild(text); cell.appendChild(pawn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function characterForSlot(slot) {
|
function characterForSlot(slot) {
|
||||||
return slot === 2 ? {symbol: "◆", name: "Мышонок"} : {symbol: "●", name: "Лисёнок"};
|
return slot === 2 ? {name: "Синий герой", keyName: "синего игрока", owner: "two"} : {name: "Жёлтый герой", keyName: "жёлтого игрока", owner: "one"};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMap(snapshot) {
|
function renderMap(snapshot) {
|
||||||
@@ -477,7 +476,7 @@
|
|||||||
if (!(data.passages & 8)) cell.classList.add("wall-w");
|
if (!(data.passages & 8)) cell.classList.add("wall-w");
|
||||||
(objects.get(data.index) || []).forEach(object => addObject(cell, object));
|
(objects.get(data.index) || []).forEach(object => addObject(cell, object));
|
||||||
const slot = state.session ? state.session.slot : 1;
|
const slot = state.session ? state.session.slot : 1;
|
||||||
if (data.index === snapshot.opponent) addPawn(cell, characterForSlot(slot === 1 ? 2 : 1).symbol, true);
|
if (data.index === snapshot.opponent) addPawn(cell, slot === 1 ? 2 : 1, true);
|
||||||
maze.appendChild(cell);
|
maze.appendChild(cell);
|
||||||
});
|
});
|
||||||
byId("map-placeholder").hidden = true;
|
byId("map-placeholder").hidden = true;
|
||||||
@@ -492,15 +491,13 @@
|
|||||||
setScreen("match");
|
setScreen("match");
|
||||||
const slot = state.session && state.session.slot ? state.session.slot : 1;
|
const slot = state.session && state.session.slot ? state.session.slot : 1;
|
||||||
const character = characterForSlot(slot);
|
const character = characterForSlot(slot);
|
||||||
byId("self-slot").textContent = character.symbol;
|
|
||||||
byId("self-slot").setAttribute("aria-label", character.name);
|
byId("self-slot").setAttribute("aria-label", character.name);
|
||||||
byId("avatar-handle-symbol").textContent = character.symbol;
|
|
||||||
byId("avatar-control").setAttribute("aria-label", `Тянуть героя ${character.name} для движения`);
|
byId("avatar-control").setAttribute("aria-label", `Тянуть героя ${character.name} для движения`);
|
||||||
byId("self-name").textContent = playerName();
|
byId("self-name").textContent = playerName();
|
||||||
byId("identity").className = `identity player-${slot === 1 ? "one" : "two"}`;
|
byId("identity").className = `identity player-${slot === 1 ? "one" : "two"}`;
|
||||||
byId("match-screen").classList.toggle("player-two-mode", slot === 2);
|
byId("match-screen").classList.toggle("player-two-mode", slot === 2);
|
||||||
byId("match-state").textContent = PHASES[snapshot.phase] || "Гонка идёт";
|
byId("match-state").textContent = PHASES[snapshot.phase] || "Гонка идёт";
|
||||||
byId("inventory").textContent = snapshot.inventory == null ? "Пусто" : `Ключ ${characterForSlot(snapshot.inventory + 1).symbol}`;
|
byId("inventory").textContent = snapshot.inventory == null ? "Пусто" : `Ключ ${characterForSlot(snapshot.inventory + 1).keyName}`;
|
||||||
byId("inventory-stat").dataset.filled = String(snapshot.inventory != null);
|
byId("inventory-stat").dataset.filled = String(snapshot.inventory != null);
|
||||||
byId("drop-key").disabled = snapshot.inventory == null;
|
byId("drop-key").disabled = snapshot.inventory == null;
|
||||||
setDirectionalPadEnabled(snapshot.phase === "active" && state.socket && state.socket.readyState === windowObject.WebSocket.OPEN);
|
setDirectionalPadEnabled(snapshot.phase === "active" && state.socket && state.socket.readyState === windowObject.WebSocket.OPEN);
|
||||||
@@ -561,10 +558,8 @@
|
|||||||
(objects.get(index) || []).forEach(object => addObject(cell, object));
|
(objects.get(index) || []).forEach(object => addObject(cell, object));
|
||||||
players.forEach((player, playerIndex) => {
|
players.forEach((player, playerIndex) => {
|
||||||
if (player.cell === index && !player.exited) {
|
if (player.cell === index && !player.exited) {
|
||||||
const pawn = documentObject.createElement("span");
|
const pawn = icon("player", `pawn pawn-player-${playerIndex === 0 ? "one" : "two"} pawn-owner-${playerIndex === 0 ? "one" : "two"}`);
|
||||||
pawn.className = `pawn pawn-player-${playerIndex === 0 ? "one" : "two"}`;
|
cell.appendChild(pawn);
|
||||||
const label = documentObject.createElement("span"); label.textContent = `P${playerIndex + 1}`;
|
|
||||||
pawn.appendChild(label); cell.appendChild(pawn);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
maze.appendChild(cell);
|
maze.appendChild(cell);
|
||||||
@@ -582,7 +577,8 @@
|
|||||||
else if (snapshot.phase !== "paused") hideModal();
|
else if (snapshot.phase !== "paused") hideModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showModal(title, text, kicker, countdown) {
|
function showModal(title, text, kicker, countdown, iconName) {
|
||||||
|
byId("modal-icon").querySelector("use").setAttribute("href", `/icons.svg#${iconName || "hourglass"}`);
|
||||||
byId("modal-title").textContent = title; byId("modal-text").textContent = text; byId("modal-kicker").textContent = kicker;
|
byId("modal-title").textContent = title; byId("modal-text").textContent = text; byId("modal-kicker").textContent = kicker;
|
||||||
byId("countdown").hidden = !countdown; byId("modal").hidden = false;
|
byId("countdown").hidden = !countdown; byId("modal").hidden = false;
|
||||||
}
|
}
|
||||||
@@ -616,7 +612,7 @@
|
|||||||
title = slot ? (winner === slot ? "Победа!" : "Победил соперник") : `Победил Игрок ${winner}`;
|
title = slot ? (winner === slot ? "Победа!" : "Победил соперник") : `Победил Игрок ${winner}`;
|
||||||
text = slot ? (winner === slot ? "Вы официально быстрее очень большого куста." : "Соперник нашёл выход. Лабиринт требует реванш.") : "Зрительный зал фиксирует героический побег без вмешательства в результат.";
|
text = slot ? (winner === slot ? "Вы официально быстрее очень большого куста." : "Соперник нашёл выход. Лабиринт требует реванш.") : "Зрительный зал фиксирует героический побег без вмешательства в результат.";
|
||||||
}
|
}
|
||||||
showModal(title, text, "ГОНКА ЗАВЕРШЕНА", false);
|
showModal(title, text, "ГОНКА ЗАВЕРШЕНА", false, "exit-open");
|
||||||
resetPlayerControls();
|
resetPlayerControls();
|
||||||
byId("modal-action").textContent = state.session && state.session.role === "player" && state.session.slot === 1 ? "Новая гонка" : "Вернуться в лобби";
|
byId("modal-action").textContent = state.session && state.session.role === "player" && state.session.slot === 1 ? "Новая гонка" : "Вернуться в лобби";
|
||||||
byId("modal-action").hidden = false;
|
byId("modal-action").hidden = false;
|
||||||
@@ -658,7 +654,7 @@
|
|||||||
} else if (message.type === "error") {
|
} else if (message.type === "error") {
|
||||||
state.moveFlow.complete(message.requestId);
|
state.moveFlow.complete(message.requestId);
|
||||||
if (message.payload.code === "invalid_token") { windowObject.localStorage.removeItem(TOKEN_KEY); state.session = null; setScreen("welcome"); }
|
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"); }
|
if (message.payload.code === "blocked") { const bump = byId("wall-bump"); bump.querySelector(".wall-bump-text").textContent = localError("blocked"); bump.classList.remove("show"); void bump.offsetWidth; bump.classList.add("show"); }
|
||||||
showNotice(localError(message.payload.code), "error"); feedback(false);
|
showNotice(localError(message.payload.code), "error"); feedback(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user