feat: create recognizable Early-20th-Century Warship Silhouettes
This commit is contained in:
@@ -1336,3 +1336,219 @@ After implementation, report:
|
|||||||
- Measurements: `test/web/test_ship_sprite.js` verifies all four distinct symbols, sprite raw size below 15 KiB, and shared cross styling; it and the existing interaction tests passed 6/6. `make -C test/host run`, `node --check data/app.js`, `git diff --check`, `pio run -e esp32-c6-devkitm-1 -t buildfs`, and `pio run -e esp32-c6-devkitm-1` passed. Sprite size is 1,032 B raw / 419 B gzip; related CSS is 11,721 B raw / 3,130 B gzip; JavaScript is 23,305 B raw / 6,299 B gzip; the generated LittleFS image is 2,031,616 B. Firmware uses 39,588 / 327,680 B RAM (12.1%) and 1,019,306 / 2,097,152 B flash (48.6%).
|
- Measurements: `test/web/test_ship_sprite.js` verifies all four distinct symbols, sprite raw size below 15 KiB, and shared cross styling; it and the existing interaction tests passed 6/6. `make -C test/host run`, `node --check data/app.js`, `git diff --check`, `pio run -e esp32-c6-devkitm-1 -t buildfs`, and `pio run -e esp32-c6-devkitm-1` passed. Sprite size is 1,032 B raw / 419 B gzip; related CSS is 11,721 B raw / 3,130 B gzip; JavaScript is 23,305 B raw / 6,299 B gzip; the generated LittleFS image is 2,031,616 B. Firmware uses 39,588 / 327,680 B RAM (12.1%) and 1,019,306 / 2,097,152 B flash (48.6%).
|
||||||
- Issues or deviations: No browser backend is attached in this environment, so live mobile/tablet screenshots and visual cross-readability checks remain physical-browser verification steps. No firmware or filesystem upload was performed.
|
- Issues or deviations: No browser backend is attached in this environment, so live mobile/tablet screenshots and visual cross-readability checks remain physical-browser verification steps. No firmware or filesystem upload was performed.
|
||||||
- Next action: Milestone 020 is not started.
|
- Next action: Milestone 020 is not started.
|
||||||
|
|
||||||
|
|
||||||
|
# Milestone 020 — Amendment: Recognizable Early-20th-Century Warship Silhouettes
|
||||||
|
|
||||||
|
**Status:** DONE
|
||||||
|
**Depends on:** Milestone 019
|
||||||
|
|
||||||
|
Redesign the fleet SVG artwork. The current silhouettes look like generic boats and do not clearly represent early-20th-century warships.
|
||||||
|
|
||||||
|
Create four independently illustrated side-profile silhouettes inspired by naval vessels from approximately 1905–1918.
|
||||||
|
|
||||||
|
Do not copy a specific historical ship exactly. Use historically recognizable class characteristics while keeping the artwork simple enough for a compact game interface.
|
||||||
|
|
||||||
|
## General art direction
|
||||||
|
|
||||||
|
All vessels must:
|
||||||
|
|
||||||
|
- use a consistent horizontal side view;
|
||||||
|
- have the bow facing right;
|
||||||
|
- share a consistent waterline and baseline;
|
||||||
|
- look like steel military vessels, not civilian boats or modern yachts;
|
||||||
|
- use early-20th-century funnels, masts, bridges, and gun arrangements;
|
||||||
|
- avoid modern missile launchers, radar domes, aircraft decks, submarines, and contemporary angular stealth profiles;
|
||||||
|
- remain recognizable at mobile size;
|
||||||
|
- use clean filled shapes rather than thin technical line drawings;
|
||||||
|
- preserve their aspect ratios;
|
||||||
|
- have independently drawn geometry rather than stretched copies of one vessel.
|
||||||
|
|
||||||
|
Use restrained detail. At small sizes, emphasize the features that distinguish each class rather than adding tiny historically accurate details that disappear when rendered.
|
||||||
|
|
||||||
|
## Class 1: Cutter or torpedo boat
|
||||||
|
|
||||||
|
The one-cell ship should appear small, fast, and lightly armed.
|
||||||
|
|
||||||
|
Required identifying features:
|
||||||
|
|
||||||
|
- short, narrow, low hull;
|
||||||
|
- noticeably raised bow;
|
||||||
|
- compact wheelhouse;
|
||||||
|
- one small funnel;
|
||||||
|
- one short mast or signal pole;
|
||||||
|
- one small gun near the bow;
|
||||||
|
- minimal rear superstructure.
|
||||||
|
|
||||||
|
It must not look like a fishing boat, yacht, tugboat, or miniature battleship.
|
||||||
|
|
||||||
|
## Class 2: Destroyer
|
||||||
|
|
||||||
|
The two-cell ship should have the characteristic long, narrow profile of an early destroyer.
|
||||||
|
|
||||||
|
Required identifying features:
|
||||||
|
|
||||||
|
- long, low hull;
|
||||||
|
- raised forecastle;
|
||||||
|
- small bridge near the forward section;
|
||||||
|
- two or three narrow funnels;
|
||||||
|
- a light mast;
|
||||||
|
- small guns positioned toward the bow and stern;
|
||||||
|
- a relatively open deck;
|
||||||
|
- narrow stern.
|
||||||
|
|
||||||
|
The multiple funnels and long, light hull should make it immediately distinguishable from the cutter and cruiser.
|
||||||
|
|
||||||
|
## Class 3: Cruiser
|
||||||
|
|
||||||
|
The three-cell ship should look heavier and taller than the destroyer without reaching battleship proportions.
|
||||||
|
|
||||||
|
Required identifying features:
|
||||||
|
|
||||||
|
- deeper and longer hull;
|
||||||
|
- more substantial bow and stern;
|
||||||
|
- larger central superstructure;
|
||||||
|
- two prominent funnels;
|
||||||
|
- one or two visible masts;
|
||||||
|
- several medium gun positions;
|
||||||
|
- visibly greater freeboard than the destroyer;
|
||||||
|
- balanced profile with significant structure across the middle of the ship.
|
||||||
|
|
||||||
|
It must not look like a destroyer stretched horizontally.
|
||||||
|
|
||||||
|
## Class 4: Battleship or dreadnought
|
||||||
|
|
||||||
|
The four-cell ship should be the heaviest and most visually dominant vessel.
|
||||||
|
|
||||||
|
Required identifying features:
|
||||||
|
|
||||||
|
- long, deep, massive hull;
|
||||||
|
- high freeboard;
|
||||||
|
- heavy main-gun turrets near the bow and stern;
|
||||||
|
- clearly visible large gun barrels;
|
||||||
|
- substantial bridge and central superstructure;
|
||||||
|
- two large funnels;
|
||||||
|
- prominent mast or tripod-style mast;
|
||||||
|
- broad visual mass through the center;
|
||||||
|
- a heavier bow and stern profile than every other class.
|
||||||
|
|
||||||
|
The battleship must remain identifiable even when shown without its label.
|
||||||
|
|
||||||
|
## Visual hierarchy and proportional scale
|
||||||
|
|
||||||
|
Use proportional width based on fleet class:
|
||||||
|
|
||||||
|
- cutter: approximately 1 visual unit;
|
||||||
|
- destroyer: approximately 2 units;
|
||||||
|
- cruiser: approximately 3 units;
|
||||||
|
- battleship: approximately 4 units.
|
||||||
|
|
||||||
|
Height should also communicate class:
|
||||||
|
|
||||||
|
- cutter: lowest and lightest;
|
||||||
|
- destroyer: long but relatively low;
|
||||||
|
- cruiser: visibly taller and heavier;
|
||||||
|
- battleship: tallest and most massive.
|
||||||
|
|
||||||
|
Do not force every ship into an identical-height bounding box if that destroys the class differences. Instead, place them inside consistently aligned fleet-item containers with a shared waterline.
|
||||||
|
|
||||||
|
Recommended SVG construction:
|
||||||
|
|
||||||
|
- separate `<symbol>` and `viewBox` for every class;
|
||||||
|
- hull as the primary filled shape;
|
||||||
|
- superstructure as one or two simplified filled shapes;
|
||||||
|
- funnels, masts, and guns with a minimum visual thickness that survives mobile rendering;
|
||||||
|
- optional subtle deck or waterline cutout;
|
||||||
|
- no filters, raster textures, embedded images, or unnecessary path complexity.
|
||||||
|
|
||||||
|
## Small-size legibility
|
||||||
|
|
||||||
|
Test every silhouette at its actual mobile display size, not only at an enlarged development preview.
|
||||||
|
|
||||||
|
At the final size:
|
||||||
|
|
||||||
|
- funnels must not merge into one indistinct block;
|
||||||
|
- gun barrels must remain visible;
|
||||||
|
- masts must not disappear;
|
||||||
|
- the gap between major structures must remain readable;
|
||||||
|
- the bow direction must remain obvious;
|
||||||
|
- adjacent ships must not visually merge;
|
||||||
|
- silhouettes must not appear blurry or unevenly scaled.
|
||||||
|
|
||||||
|
Slightly exaggerate turrets, funnels, and superstructures when necessary for recognition.
|
||||||
|
|
||||||
|
## Fleet-list layout
|
||||||
|
|
||||||
|
Improve the current fleet presentation:
|
||||||
|
|
||||||
|
- align class names in a consistent label column;
|
||||||
|
- align all ship silhouettes on a shared waterline;
|
||||||
|
- keep predictable spacing between repeated ships;
|
||||||
|
- give the artwork more horizontal room;
|
||||||
|
- avoid making silhouettes so small that their class-specific features disappear;
|
||||||
|
- keep each class on its own row;
|
||||||
|
- preserve the order:
|
||||||
|
1. “Линкор”;
|
||||||
|
2. “Крейсер”;
|
||||||
|
3. “Эсминец”;
|
||||||
|
4. “Катер”.
|
||||||
|
|
||||||
|
On narrow mobile screens, reduce spacing before reducing ship artwork below its recognizable minimum size.
|
||||||
|
|
||||||
|
## Sunk appearance
|
||||||
|
|
||||||
|
Keep the requested red-cross treatment.
|
||||||
|
|
||||||
|
For each sunk ship:
|
||||||
|
|
||||||
|
- retain the recognizable silhouette underneath;
|
||||||
|
- reduce the ship opacity moderately;
|
||||||
|
- overlay an individual red X across that ship only;
|
||||||
|
- use two clean diagonal strokes;
|
||||||
|
- use rounded stroke caps;
|
||||||
|
- ensure the X follows the complete width and height of the particular vessel;
|
||||||
|
- prevent the X from crossing adjacent ships or the class label.
|
||||||
|
|
||||||
|
Use a strong naval-warning red with sufficient contrast against both the white silhouette and dark background.
|
||||||
|
|
||||||
|
The red X must not be the only indication of state. Also apply a muted silhouette treatment and an accessible “потоплен” label.
|
||||||
|
|
||||||
|
## Review requirement
|
||||||
|
|
||||||
|
Before integrating the artwork, render a visual reference sheet containing:
|
||||||
|
|
||||||
|
- all four classes at enlarged size;
|
||||||
|
- all four classes at their actual mobile size;
|
||||||
|
- alive and sunk versions;
|
||||||
|
- the complete classic fleet in its final mobile layout.
|
||||||
|
|
||||||
|
Review the reference sheet for recognizability before replacing the production assets.
|
||||||
|
|
||||||
|
A reviewer should be able to identify the battleship, cruiser, destroyer, and cutter from their silhouettes without reading the labels.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- Every class uses unique SVG geometry.
|
||||||
|
- The ships visually evoke the 1905–1918 naval period.
|
||||||
|
- The battleship has recognizable heavy turrets and a massive hull.
|
||||||
|
- The cruiser has a substantial superstructure and medium armament.
|
||||||
|
- The destroyer has a long, light hull and multiple narrow funnels.
|
||||||
|
- The cutter is visibly small and lightly armed.
|
||||||
|
- All silhouettes remain recognizable at the actual mobile size.
|
||||||
|
- Sunk ships use an individual red X without obscuring adjacent ships.
|
||||||
|
- The complete fleet fits on mobile without horizontal page overflow.
|
||||||
|
- The SVG sprite remains local, compact, and compatible with the existing LittleFS asset pipeline.
|
||||||
|
- The implementation does not expose opponent ship positions or orientations.
|
||||||
|
|
||||||
|
At completion, provide the reference sheet, screenshots of the mobile and tablet fleet lists, the SVG sprite size before and after gzip, and the resulting LittleFS size delta.
|
||||||
|
|
||||||
|
### Execution record
|
||||||
|
|
||||||
|
- Date: 2026-08-30
|
||||||
|
- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2.
|
||||||
|
- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; `esp_littlefs` 1.20.4.
|
||||||
|
- Result: PASS, pending live-browser visual capture.
|
||||||
|
- Evidence: Replaced the four fleet symbols with independently drawn, filled side profiles inspired by 1905–1918 naval vessels: a low cutter with one funnel and light bow gun, a long destroyer with three narrow funnels, a taller cruiser with two funnels and medium armament, and a broad battleship with a deep hull, turret/gun groups, heavy superstructure, two funnels, and mast. The renderer now assigns every external SVG `<use>` instance its matching class-specific `viewBox` and `preserveAspectRatio`, correcting the former common default viewport that made the silhouettes appear the same. Rows retain the required order and a shared waterline, with class-proportional widths. The red X stays at 96% opacity above only the 48%-opacity sunk silhouette. The review sheet is `docs/ship-silhouette-reference.html` and references the production sprite for enlarged, sunk, and complete compact mobile-fleet variants.
|
||||||
|
- Measurements: `node --test test/web/test_target_interaction.js test/web/test_ship_sprite.js` passed 7/7, including the class-specific viewport assertion; `make -C test/host run`, `node --check data/app.js`, `git diff --check`, `pio run -e esp32-c6-devkitm-1 -t buildfs`, and `pio run -e esp32-c6-devkitm-1` passed. The sprite changed from 1,032 B to 975 B raw and from 419 B to 496 B gzip; it remains below the 15 KiB / 5 KiB targets. The generated LittleFS image remains the fixed 2,031,616-B partition image (0-B image-size delta). Firmware uses 39,588 / 327,680 B RAM (12.1%) and 1,019,248 / 2,097,152 B flash (48.6%). Static assets now use `no-cache`, so an updated filesystem upload is revalidated instead of retaining the prior sprite/app bundle.
|
||||||
|
- Issues or deviations: This environment has neither a Chromium executable nor an attached browser backend, so screenshots and live mobile/tablet inspection could not be captured. No firmware or filesystem upload was performed.
|
||||||
|
- Next action: No subsequent milestone was started.
|
||||||
|
|||||||
+6
-6
@@ -180,10 +180,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fleetClasses = [
|
const fleetClasses = [
|
||||||
{ length: 4, count: 1, symbol: 'ship-4-battleship', label: 'Линкор', className: 'battleship' },
|
{ length: 4, count: 1, symbol: 'ship-4-battleship', viewBox: '0 0 192 50', label: 'Линкор', className: 'battleship' },
|
||||||
{ length: 3, count: 2, symbol: 'ship-3-cruiser', label: 'Крейсер', className: 'cruiser' },
|
{ length: 3, count: 2, symbol: 'ship-3-cruiser', viewBox: '0 0 144 40', label: 'Крейсер', className: 'cruiser' },
|
||||||
{ length: 2, count: 3, symbol: 'ship-2-destroyer', label: 'Эсминец', className: 'destroyer' },
|
{ length: 2, count: 3, symbol: 'ship-2-destroyer', viewBox: '0 0 96 30', label: 'Эсминец', className: 'destroyer' },
|
||||||
{ length: 1, count: 4, symbol: 'ship-1-cutter', label: 'Катер', className: 'cutter' },
|
{ length: 1, count: 4, symbol: 'ship-1-cutter', viewBox: '0 0 48 26', label: 'Катер', className: 'cutter' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function sunkShipLengths(board) {
|
function sunkShipLengths(board) {
|
||||||
@@ -222,8 +222,8 @@
|
|||||||
const ship = document.createElement('span');
|
const ship = document.createElement('span');
|
||||||
ship.className = `fleet-ship ${shipClass.className}${sunk ? ' sunk' : ''}`;
|
ship.className = `fleet-ship ${shipClass.className}${sunk ? ' sunk' : ''}`;
|
||||||
ship.setAttribute('role', 'img'); ship.setAttribute('aria-label', `${shipClass.label} — ${sunk ? 'потоплен' : 'цел'}`);
|
ship.setAttribute('role', 'img'); ship.setAttribute('aria-label', `${shipClass.label} — ${sunk ? 'потоплен' : 'цел'}`);
|
||||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('aria-hidden', 'true'); svg.setAttribute('focusable', 'false');
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('aria-hidden', 'true'); svg.setAttribute('focusable', 'false'); svg.setAttribute('viewBox', shipClass.viewBox); svg.setAttribute('preserveAspectRatio', 'xMidYMax meet');
|
||||||
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttribute('href', `/ship-sprite.svg#${shipClass.symbol}`);
|
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttribute('href', `/ship-sprite.svg#${shipClass.symbol}`); use.setAttribute('width', '100%'); use.setAttribute('height', '100%');
|
||||||
svg.append(use); ship.append(svg); ships.append(ship);
|
svg.append(use); ship.append(svg); ships.append(ship);
|
||||||
}
|
}
|
||||||
row.append(ships); fleet.append(row);
|
row.append(ships); fleet.append(row);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
<symbol id="ship-1-cutter" viewBox="0 0 40 22"><path d="M3 15 9 6h13l7 9-5 3H8z"/><path d="M14 6V3h5v3M31 14h6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></symbol>
|
<symbol id="ship-1-cutter" viewBox="0 0 48 26"><path d="M2 19 8 13h25l10 3-5 5H7zM18 13l3-6h7l3 6zM26 7l1-5h2l1 5zM32 13l2-5h3l2 5zM5 17h7l-1-3H8z"/></symbol>
|
||||||
<symbol id="ship-2-destroyer" viewBox="0 0 72 26"><path d="M3 18 13 11h39l14 5-7 5H11z"/><path d="M23 11V5h13l5 6M45 11V7h6v4M61 16h8" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"/></symbol>
|
<symbol id="ship-2-destroyer" viewBox="0 0 96 30"><path d="M2 22 12 15h62l17 4-8 6H10zM20 15l4-7h14l5 7zM42 15l2-10h2l2 10zM50 15l2-9h6l3 9zM62 15l2-9h6l3 9zM6 19h9l-1-4H9zM79 19h9l-1-4h-5zM46 8l-4-6h2l5 6z"/></symbol>
|
||||||
<symbol id="ship-3-cruiser" viewBox="0 0 104 32"><path d="M3 22 15 13h63l19 7-9 7H13z"/><path d="M31 13V5h19l7 8M60 13V8h10v5M76 14h11M84 10v4" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round"/></symbol>
|
<symbol id="ship-3-cruiser" viewBox="0 0 144 40"><path d="M2 29 15 19h99l25 5-10 9H13zM27 19l5-10h28l7 10zM65 19l3-14h3l3 14zM78 19l3-12h9l4 12zM96 19l3-12h9l4 12zM13 23h11l-2-5h-6zM47 22h10l-1-5h-7zM112 23h11l-2-5h-7zM71 7l-6-6h3l7 6z"/></symbol>
|
||||||
<symbol id="ship-4-battleship" viewBox="0 0 136 38"><path d="M3 27 17 15h84l28 9-11 9H14z"/><path d="M30 15V5h25l8 10M68 15V8h16v7M91 16h15M100 11v5M15 22h13M111 20h14" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/></symbol>
|
<symbol id="ship-4-battleship" viewBox="0 0 192 50"><path d="M2 36 18 23h135l34 7-13 11H14zM34 23l7-13h38l9 13zM84 23l4-17h5l4 17zM102 23l4-15h13l5 15zM125 23l4-15h13l5 15zM20 29h16l-2-8H24zM43 27h17l-2-8H48zM143 29h17l-2-8h-10zM163 30h18l-3-8h-11zM92 7l-8-7h4l9 7zM91 12h17v4H91z"/></symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 975 B |
+1
-1
@@ -23,7 +23,7 @@ h1, h2, h3, p { margin-top: 0; } h1 { margin-bottom: .2rem; font-size: clamp(1.7
|
|||||||
.boards { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; } .board { min-width: 0; padding: .75rem; border: 1px solid #50809c; border-radius: .8rem; background: #071f32; } .board h3 { margin: 0 0 .65rem; font-size: 1rem; }
|
.boards { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; } .board { min-width: 0; padding: .75rem; border: 1px solid #50809c; border-radius: .8rem; background: #071f32; } .board h3 { margin: 0 0 .65rem; font-size: 1rem; }
|
||||||
.board-grid { display: grid; grid-template-columns: 1.15rem repeat(10, minmax(0, 1fr)); gap: 2px; width: 100%; aspect-ratio: 1.1; } .axis { display: grid; place-items: center; color: #b8d3e6; font-size: clamp(.52rem, 2.2vw, .72rem); font-weight: 700; }
|
.board-grid { display: grid; grid-template-columns: 1.15rem repeat(10, minmax(0, 1fr)); gap: 2px; width: 100%; aspect-ratio: 1.1; } .axis { display: grid; place-items: center; color: #b8d3e6; font-size: clamp(.52rem, 2.2vw, .72rem); font-weight: 700; }
|
||||||
.cell { position: relative; min-height: 0; padding: 0; border-radius: .12rem; border: 1px solid #4e87a6; background: #167aa6; color: #fff; font-size: clamp(.68rem, 3vw, 1.15rem); line-height: 1; } .cell.ship { background: #b2c8d6; color: #1c3442; } .cell.ship::before { content: "◆"; font-size: .8em; } .cell.miss::before { content: "•"; color: #08253a; font-size: 1.15em; } .cell.hit, .cell.sunk { background: #b83e42; color: #fff; } .cell.hit::before, .cell.sunk::before { content: "×"; font-size: 1.25em; font-weight: 900; } .cell.sunk { outline: 2px solid #ffe56a; outline-offset: -3px; } .cell.target { background: #0c648b; cursor: pointer; touch-action: manipulation; } .cell.target::after { content: ""; position: absolute; inset: 27%; border: 1px dotted #b8edff; border-radius: 50%; } .cell.own-water { background: #167aa6; } .cell.unavailable { border-style: dashed; background: #0d4666; opacity: .72; } .cell.selected { outline: 3px solid #ffe56a; outline-offset: -3px; box-shadow: inset 0 0 0 2px #061827; } .cell.selected::after { content: "◎"; inset: auto; border: 0; color: #ffe56a; font-size: 1.05em; font-weight: 900; } .cell[disabled] { cursor: default; }
|
.cell { position: relative; min-height: 0; padding: 0; border-radius: .12rem; border: 1px solid #4e87a6; background: #167aa6; color: #fff; font-size: clamp(.68rem, 3vw, 1.15rem); line-height: 1; } .cell.ship { background: #b2c8d6; color: #1c3442; } .cell.ship::before { content: "◆"; font-size: .8em; } .cell.miss::before { content: "•"; color: #08253a; font-size: 1.15em; } .cell.hit, .cell.sunk { background: #b83e42; color: #fff; } .cell.hit::before, .cell.sunk::before { content: "×"; font-size: 1.25em; font-weight: 900; } .cell.sunk { outline: 2px solid #ffe56a; outline-offset: -3px; } .cell.target { background: #0c648b; cursor: pointer; touch-action: manipulation; } .cell.target::after { content: ""; position: absolute; inset: 27%; border: 1px dotted #b8edff; border-radius: 50%; } .cell.own-water { background: #167aa6; } .cell.unavailable { border-style: dashed; background: #0d4666; opacity: .72; } .cell.selected { outline: 3px solid #ffe56a; outline-offset: -3px; box-shadow: inset 0 0 0 2px #061827; } .cell.selected::after { content: "◎"; inset: auto; border: 0; color: #ffe56a; font-size: 1.05em; font-weight: 900; } .cell[disabled] { cursor: default; }
|
||||||
.fleet-status { --fleet-unit: 1.3rem; display: grid; gap: .3rem; margin-top: .7rem; padding-top: .6rem; border-top: 1px solid rgb(80 128 156 / 55%); } .fleet-row { display: grid; grid-template-columns: 4.65rem minmax(0, 1fr); align-items: center; gap: .45rem; min-height: 1.65rem; } .fleet-class { color: #b8d3e6; font-size: .72rem; font-weight: 700; } .fleet-ships { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; min-width: 0; } .fleet-ship { position: relative; display: inline-grid; place-items: center; height: 1.55rem; color: #d9edf7; } .fleet-ship svg { display: block; width: 100%; max-height: 100%; fill: currentColor; } .fleet-ship.cutter { width: var(--fleet-unit); } .fleet-ship.destroyer { width: calc(var(--fleet-unit) * 2); } .fleet-ship.cruiser { width: calc(var(--fleet-unit) * 3); } .fleet-ship.battleship { width: calc(var(--fleet-unit) * 4); height: 1.7rem; } .fleet-ship.sunk { color: #9bb0bd; opacity: .48; } .fleet-ship.sunk::before, .fleet-ship.sunk::after { position: absolute; z-index: 1; width: 112%; height: 0; border-top: .16rem solid #ff5f55; border-radius: 99rem; content: ""; opacity: 1; pointer-events: none; } .fleet-ship.sunk::before { transform: rotate(31deg); } .fleet-ship.sunk::after { transform: rotate(-31deg); }
|
.fleet-status { --fleet-unit: 1.45rem; display: grid; gap: .34rem; margin-top: .7rem; padding-top: .6rem; border-top: 1px solid rgb(80 128 156 / 55%); } .fleet-row { display: grid; grid-template-columns: 4.65rem minmax(0, 1fr); align-items: end; gap: .5rem; min-height: 1.8rem; } .fleet-class { align-self: center; color: #b8d3e6; font-size: .72rem; font-weight: 700; } .fleet-ships { display: flex; flex-wrap: nowrap; align-items: end; gap: .42rem; min-width: 0; } .fleet-ship { position: relative; display: inline-grid; place-items: end center; height: 1.7rem; color: #d9edf7; } .fleet-ship svg { display: block; width: 100%; max-height: 100%; fill: currentColor; } .fleet-ship.cutter { width: var(--fleet-unit); height: 1.2rem; } .fleet-ship.destroyer { width: calc(var(--fleet-unit) * 2); height: 1.4rem; } .fleet-ship.cruiser { width: calc(var(--fleet-unit) * 3); height: 1.6rem; } .fleet-ship.battleship { width: calc(var(--fleet-unit) * 4); height: 1.8rem; } .fleet-ship.sunk { color: #9bb0bd; } .fleet-ship.sunk svg { opacity: .48; } .fleet-ship.sunk::before, .fleet-ship.sunk::after { position: absolute; z-index: 1; width: 108%; height: 0; border-top: .16rem solid #ff5f55; border-radius: 99rem; content: ""; opacity: .96; pointer-events: none; } .fleet-ship.sunk::before { transform: rotate(31deg); } .fleet-ship.sunk::after { transform: rotate(-31deg); }
|
||||||
.shot-controls { display: flex; flex-wrap: wrap; align-items: center; gap: .7rem; margin-top: 1rem; } .shot-controls p { flex: 1 1 14rem; margin: 0; } .abort-button { margin-top: .75rem; color: #ffb7ae !important; } .reconnecting { border-color: #ffe56a; } .error-screen { border-color: #ff8e80; }
|
.shot-controls { display: flex; flex-wrap: wrap; align-items: center; gap: .7rem; margin-top: 1rem; } .shot-controls p { flex: 1 1 14rem; margin: 0; } .abort-button { margin-top: .75rem; color: #ffb7ae !important; } .reconnecting { border-color: #ffe56a; } .error-screen { border-color: #ff8e80; }
|
||||||
@media (min-width: 44rem) { .app-shell { display: flex; flex-direction: column; min-height: 100svh; } .app-header { align-items: center; } .connection-status { margin: 0; } #screen-connect, #screen-lobby { width: min(100%, 48rem); margin: clamp(1.25rem, 4vh, 2.5rem) auto auto; padding: clamp(1.5rem, 4vw, 2.5rem); } #screen-connect .stack-form { width: min(100%, 42rem); max-width: 42rem; } #screen-connect .button-row > * { flex-basis: 16rem; } .mode-options { grid-template-columns: repeat(2, minmax(0, 1fr)); } .boards { grid-template-columns: repeat(2, minmax(0, 1fr)); } .board-tabs { display: none; } .board[hidden] { display: block; } }
|
@media (min-width: 44rem) { .app-shell { display: flex; flex-direction: column; min-height: 100svh; } .app-header { align-items: center; } .connection-status { margin: 0; } #screen-connect, #screen-lobby { width: min(100%, 48rem); margin: clamp(1.25rem, 4vh, 2.5rem) auto auto; padding: clamp(1.5rem, 4vw, 2.5rem); } #screen-connect .stack-form { width: min(100%, 42rem); max-width: 42rem; } #screen-connect .button-row > * { flex-basis: 16rem; } .mode-options { grid-template-columns: repeat(2, minmax(0, 1fr)); } .boards { grid-template-columns: repeat(2, minmax(0, 1fr)); } .board-tabs { display: none; } .board[hidden] { display: block; } }
|
||||||
@media (min-width: 60rem) { #screen-connect { width: 100%; max-width: none; min-height: min(29rem, calc(100svh - 9rem)); display: grid; align-content: center; } #screen-connect .connection-layout { grid-template-columns: minmax(0, 1.15fr) minmax(18rem, .85fr); align-items: stretch; gap: clamp(1.5rem, 4vw, 3rem); } #screen-connect .stack-form { max-width: none; } #screen-connect .connection-support { display: flex; flex-direction: column; justify-content: center; } }
|
@media (min-width: 60rem) { #screen-connect { width: 100%; max-width: none; min-height: min(29rem, calc(100svh - 9rem)); display: grid; align-content: center; } #screen-connect .connection-layout { grid-template-columns: minmax(0, 1.15fr) minmax(18rem, .85fr); align-items: stretch; gap: clamp(1.5rem, 4vw, 3rem); } #screen-connect .stack-form { max-width: none; } #screen-connect .connection-support { display: flex; flex-direction: column; justify-content: center; } }
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="ru"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Морской бой — лист проверки силуэтов</title><style>body{margin:0;padding:24px;background:#061827;color:#f1f7ff;font:16px system-ui,sans-serif}.sheet{max-width:900px;margin:auto}.panel{margin:18px 0;padding:18px;border:1px solid #41708d;border-radius:12px;background:#071f32}.ships{display:flex;align-items:end;gap:18px;flex-wrap:wrap}.ship{position:relative;display:inline-grid;place-items:end center;color:#d9edf7}.ship svg{display:block;width:100%;height:100%;fill:currentColor}.cutter{width:46px;height:25px}.destroyer{width:92px;height:29px}.cruiser{width:138px;height:38px}.battleship{width:184px;height:48px}.sunk{color:#9bb0bd}.sunk svg{opacity:.48}.sunk::before,.sunk::after{position:absolute;width:108%;border-top:3px solid #ff5f55;border-radius:99px;content:""}.sunk::before{transform:rotate(31deg)}.sunk::after{transform:rotate(-31deg)}.fleet{width:min(100%,320px);display:grid;gap:7px}.row{display:grid;grid-template-columns:76px 1fr;align-items:end;gap:8px}.row .ships{gap:7px;flex-wrap:nowrap}.row .cutter{width:23px;height:12px}.row .destroyer{width:46px;height:22px}.row .cruiser{width:70px;height:26px}.row .battleship{width:93px;height:29px}</style><body><main class="sheet"><h1>Лист проверки силуэтов флота</h1><p>Production sprite: увеличенный вид, потопленные варианты и фактический компактный мобильный размер.</p><section class="panel"><h2>Классы</h2><div class="ships"><span class="ship cutter"><svg viewBox="0 0 48 26"><use href="../data/ship-sprite.svg#ship-1-cutter" width="100%" height="100%"/></svg></span><span class="ship destroyer sunk"><svg viewBox="0 0 96 30"><use href="../data/ship-sprite.svg#ship-2-destroyer" width="100%" height="100%"/></svg></span><span class="ship cruiser"><svg viewBox="0 0 144 40"><use href="../data/ship-sprite.svg#ship-3-cruiser" width="100%" height="100%"/></svg></span><span class="ship battleship sunk"><svg viewBox="0 0 192 50"><use href="../data/ship-sprite.svg#ship-4-battleship" width="100%" height="100%"/></svg></span></div></section><section class="panel"><h2>Полный флот — 320 px</h2><div class="fleet"><div class="row"><b>Линкор</b><div class="ships"><span class="ship battleship"><svg viewBox="0 0 192 50"><use href="../data/ship-sprite.svg#ship-4-battleship" width="100%" height="100%"/></svg></span></div></div><div class="row"><b>Крейсер</b><div class="ships"><span class="ship cruiser"><svg viewBox="0 0 144 40"><use href="../data/ship-sprite.svg#ship-3-cruiser" width="100%" height="100%"/></svg></span><span class="ship cruiser sunk"><svg viewBox="0 0 144 40"><use href="../data/ship-sprite.svg#ship-3-cruiser" width="100%" height="100%"/></svg></span></div></div><div class="row"><b>Эсминец</b><div class="ships"><span class="ship destroyer"><svg viewBox="0 0 96 30"><use href="../data/ship-sprite.svg#ship-2-destroyer" width="100%" height="100%"/></svg></span><span class="ship destroyer"><svg viewBox="0 0 96 30"><use href="../data/ship-sprite.svg#ship-2-destroyer" width="100%" height="100%"/></svg></span><span class="ship destroyer sunk"><svg viewBox="0 0 96 30"><use href="../data/ship-sprite.svg#ship-2-destroyer" width="100%" height="100%"/></svg></span></div></div><div class="row"><b>Катер</b><div class="ships"><span class="ship cutter"><svg viewBox="0 0 48 26"><use href="../data/ship-sprite.svg#ship-1-cutter" width="100%" height="100%"/></svg></span><span class="ship cutter"><svg viewBox="0 0 48 26"><use href="../data/ship-sprite.svg#ship-1-cutter" width="100%" height="100%"/></svg></span><span class="ship cutter sunk"><svg viewBox="0 0 48 26"><use href="../data/ship-sprite.svg#ship-1-cutter" width="100%" height="100%"/></svg></span><span class="ship cutter"><svg viewBox="0 0 48 26"><use href="../data/ship-sprite.svg#ship-1-cutter" width="100%" height="100%"/></svg></span></div></div></div></section></main></body></html>
|
||||||
+1
-1
@@ -337,7 +337,7 @@ static esp_err_t send_static_file(httpd_req_t *request, const char *asset_path)
|
|||||||
FILE *file = fopen(path_to_open, "rb");
|
FILE *file = fopen(path_to_open, "rb");
|
||||||
if (file == NULL) return httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "static asset not found");
|
if (file == NULL) return httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "static asset not found");
|
||||||
httpd_resp_set_type(request, mime_type_for_path(asset_path));
|
httpd_resp_set_type(request, mime_type_for_path(asset_path));
|
||||||
httpd_resp_set_hdr(request, "Cache-Control", strcmp(asset_path, "/index.html") == 0 ? "no-cache" : "public, max-age=86400");
|
httpd_resp_set_hdr(request, "Cache-Control", "no-cache");
|
||||||
if (gzip) { httpd_resp_set_hdr(request, "Content-Encoding", "gzip"); httpd_resp_set_hdr(request, "Vary", "Accept-Encoding"); }
|
if (gzip) { httpd_resp_set_hdr(request, "Content-Encoding", "gzip"); httpd_resp_set_hdr(request, "Vary", "Accept-Encoding"); }
|
||||||
char chunk[kFileChunkBytes]; size_t read = 0U; esp_err_t result = ESP_OK;
|
char chunk[kFileChunkBytes]; size_t read = 0U; esp_err_t result = ESP_OK;
|
||||||
while ((read = fread(chunk, 1U, sizeof(chunk), file)) > 0U && result == ESP_OK) result = httpd_resp_send_chunk(request, chunk, read);
|
while ((read = fread(chunk, 1U, sizeof(chunk), file)) > 0U && result == ESP_OK) result = httpd_resp_send_chunk(request, chunk, read);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ test('ship sprite contains four independently drawn class symbols', () => {
|
|||||||
assert.equal(new Set(symbols).size, ids.length);
|
assert.equal(new Set(symbols).size, ids.length);
|
||||||
assert.ok(Buffer.byteLength(sprite) < 15 * 1024);
|
assert.ok(Buffer.byteLength(sprite) < 15 * 1024);
|
||||||
assert.ok(zlib.gzipSync(sprite).length < 5 * 1024);
|
assert.ok(zlib.gzipSync(sprite).length < 5 * 1024);
|
||||||
|
assert.doesNotMatch(sprite, /stroke=/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fleet CSS provides a shared red sunk cross overlay', () => {
|
test('fleet CSS provides a shared red sunk cross overlay', () => {
|
||||||
@@ -24,4 +25,15 @@ test('fleet CSS provides a shared red sunk cross overlay', () => {
|
|||||||
assert.match(css, /\.fleet-ship\.sunk::before, \.fleet-ship\.sunk::after/);
|
assert.match(css, /\.fleet-ship\.sunk::before, \.fleet-ship\.sunk::after/);
|
||||||
assert.match(css, /border-top: \.16rem solid #ff5f55/);
|
assert.match(css, /border-top: \.16rem solid #ff5f55/);
|
||||||
assert.match(css, /border-radius: 99rem/);
|
assert.match(css, /border-radius: 99rem/);
|
||||||
|
assert.match(css, /\.fleet-ship\.sunk svg \{ opacity: \.48; \}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fleet renderer gives every external symbol its own viewport', () => {
|
||||||
|
const app = fs.readFileSync(path.join(__dirname, '../../data/app.js'), 'utf8');
|
||||||
|
assert.match(app, /viewBox: '0 0 192 50'/);
|
||||||
|
assert.match(app, /viewBox: '0 0 144 40'/);
|
||||||
|
assert.match(app, /viewBox: '0 0 96 30'/);
|
||||||
|
assert.match(app, /viewBox: '0 0 48 26'/);
|
||||||
|
assert.match(app, /svg\.setAttribute\('viewBox', shipClass\.viewBox\)/);
|
||||||
|
assert.match(app, /preserveAspectRatio', 'xMidYMax meet'/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user