diff --git a/PLANS.md b/PLANS.md index 4944c31..7a0e6d6 100644 --- a/PLANS.md +++ b/PLANS.md @@ -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%). - 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. + + +# 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 `` 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 `` 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. diff --git a/data/app.js b/data/app.js index c9775bf..b7dadd0 100644 --- a/data/app.js +++ b/data/app.js @@ -180,10 +180,10 @@ } const fleetClasses = [ - { length: 4, count: 1, symbol: 'ship-4-battleship', label: 'Линкор', className: 'battleship' }, - { length: 3, count: 2, symbol: 'ship-3-cruiser', label: 'Крейсер', className: 'cruiser' }, - { length: 2, count: 3, symbol: 'ship-2-destroyer', label: 'Эсминец', className: 'destroyer' }, - { length: 1, count: 4, symbol: 'ship-1-cutter', label: 'Катер', className: 'cutter' }, + { length: 4, count: 1, symbol: 'ship-4-battleship', viewBox: '0 0 192 50', label: 'Линкор', className: 'battleship' }, + { length: 3, count: 2, symbol: 'ship-3-cruiser', viewBox: '0 0 144 40', label: 'Крейсер', className: 'cruiser' }, + { length: 2, count: 3, symbol: 'ship-2-destroyer', viewBox: '0 0 96 30', label: 'Эсминец', className: 'destroyer' }, + { length: 1, count: 4, symbol: 'ship-1-cutter', viewBox: '0 0 48 26', label: 'Катер', className: 'cutter' }, ]; function sunkShipLengths(board) { @@ -222,8 +222,8 @@ const ship = document.createElement('span'); ship.className = `fleet-ship ${shipClass.className}${sunk ? ' 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 use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttribute('href', `/ship-sprite.svg#${shipClass.symbol}`); + 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}`); use.setAttribute('width', '100%'); use.setAttribute('height', '100%'); svg.append(use); ship.append(svg); ships.append(ship); } row.append(ships); fleet.append(row); diff --git a/data/ship-sprite.svg b/data/ship-sprite.svg index 3cc2731..79bc5d5 100644 --- a/data/ship-sprite.svg +++ b/data/ship-sprite.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/data/styles.css b/data/styles.css index e86a45e..e284cde 100644 --- a/data/styles.css +++ b/data/styles.css @@ -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; } .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; } -.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; } @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; } } diff --git a/docs/ship-silhouette-reference.html b/docs/ship-silhouette-reference.html new file mode 100644 index 0000000..e802afe --- /dev/null +++ b/docs/ship-silhouette-reference.html @@ -0,0 +1 @@ +Морской бой — лист проверки силуэтов

Лист проверки силуэтов флота

Production sprite: увеличенный вид, потопленные варианты и фактический компактный мобильный размер.

Классы

Полный флот — 320 px

Линкор
Крейсер
Эсминец
Катер
diff --git a/src/main.c b/src/main.c index 3d49bc5..26c5cfc 100644 --- a/src/main.c +++ b/src/main.c @@ -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"); 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_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"); } 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); diff --git a/test/web/test_ship_sprite.js b/test/web/test_ship_sprite.js index c07d15e..b82b920 100644 --- a/test/web/test_ship_sprite.js +++ b/test/web/test_ship_sprite.js @@ -17,6 +17,7 @@ test('ship sprite contains four independently drawn class symbols', () => { assert.equal(new Set(symbols).size, ids.length); assert.ok(Buffer.byteLength(sprite) < 15 * 1024); assert.ok(zlib.gzipSync(sprite).length < 5 * 1024); + assert.doesNotMatch(sprite, /stroke=/); }); 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, /border-top: \.16rem solid #ff5f55/); 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'/); });