diff --git a/.gitignore b/.gitignore
index 3a8cc3e..683da74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,10 @@
orca-*.png
tests/shot-*.png
+# Встроенный emoji-шрифт (tools/emoji-font.py): исходник Noto Color Emoji и venv с fonttools
+tools/fonts-src/
+tools/.venv/
+
# Служебное
.po-handoff.md
*.log
diff --git a/AGENTS.md b/AGENTS.md
index c34d364..3b5cbd3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,13 +11,13 @@
npm install # puppeteer-core
npm test # = node test.js
```
- Ждать все PASS и exit 0. Сейчас проверок 162 (включая §Y5 i18n и §Y6 паузу).
+ Ждать все PASS и exit 0. Сейчас проверок 164 (включая §Y5 i18n, §Y6 паузу и §Y7 шрифт).
3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI):
```bash
node tests/orca-playtest.js # из каталога репо; требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND)
```
Проходит тот же геймплей «вживую» в браузере (не headless) и пишет `orca-*.png` в каталог запуска. Если Orca app не запущен — пропустить и НЕ блокировать (главный рубеж — пункт 2). Подробности и подводные камни: `docs/BROWSER-TESTING.md`.
-4. Скриншоты: `node tests/shots.js` пишет `shot-*.png` в каталог запуска (запускать из `tests/`); README ссылается на `screenshots/` в корне — свежие скопировать туда. В headless нужен шрифт emoji (Noto Color Emoji), иначе в скриншотах квадраты.
+4. Скриншоты: `node tests/shots.js` пишет `shot-*.png` в каталог запуска (запускать из `tests/`); README ссылается на `screenshots/` в корне — свежие скопировать туда. Emoji рисует встроенный шрифт (раздел «Встроенный emoji-шрифт») — системные emoji-шрифты для скриншотов больше не нужны.
5. Публикация в Яндекс Играх: `node tools/pack.mjs` (сборка dist/fishing-yandex.zip + самопроверка 5 пунктов) → `node tests/pack-smoke.js` (smoke играбельности КОПИИ из архива, 6 чек) → загрузить zip в консоль. Доки: раздел README «Публикация в Яндекс Играх», запись Этап 27 в DEVLOG.
## Подводные камни
@@ -44,6 +44,7 @@
- Реклама (Y2): константы `AD_FIRST_MS`/`AD_COOLDOWN_MS` (секция Y1), точка interstitial — `tryInterstitial()` в хендлере `btnCardOk` (гейт: первый показ ≥2 мин сессии, кулдаун 3 мин; `lastAdTs` — осознанно «попытка», не показ — при троттлинге платформы wasShown=false кулдаун сгорает), RV-кнопка `#btnRv` «📺 Реклама: ×2 за этот улов» на карточке улова (`showCard` сбрасывает `rvVal`/`rvDone`), методы `YG.showFullscreen/showRewarded` (+watchdog 90 с), `endAd()`; карточка улова БЕЗ `gameplayStop()` — осознанно (часть цикла улова); реклама не персистится в сейв (только `persist()` перед показом и после бонуса).
- i18n (треб. 2.14/2.2): таблица `T={ru,en}` (единая точка строк UI), `t(key)`/`tt(key,o)`, `LANG` (дефолт `ru`), `forceLang` (тест-форс), `setLang()` (обновляет ``/`
` + пере-рендер меню/статичного UI и открытых оверлеев — магазина/карточки), `resolveLang(ysdk)` — резолюция: `forceLang` → `ysdk.environment.i18n.lang` → `ru`; стаб (file:// без SDK) — всегда `ru` (`navigator.language` НЕ используется — ломает headless-тесты). `trName`/`trDesc`/`trRarity` — выбор по `nameEn`/`descEn`/`RARITY_NAMES_EN` в таблицах данных. Меню — `renderMenu()` (device-aware: тач/мышь, `IS_TOUCH`). EN-ветка `questText()` (RU-ветка с `pluralN` не меняется).
- Пауза (треб. 6.3/1.3): HUD-кнопка `#btnPause` (⏸) → `openPause()` (гарды MENU/uiOpen/adShowing/pauseOpen; `YG.onPause()` + оверлей `#pause`); `#btnPauseResume` → `closePause()`, `#btnPauseMenu` → MENU (с `YG.onResume()`). Гард `pauseOpen` в `YG.onResume` (после `adShowing`) — blur/focus/visibilitychange во время открытой паузы не снимают паузу (снимут только `closePause()`/«В меню»). `window blur` → `YG.onPause()`, `focus` → `YG.onResume()` (треб. 1.3, дополнение к visibilitychange). Видимость `#btnPause` и `#btnRecall` — в `syncDom()` (вызывается каждый кадр в `frame()` вне `YG.paused` — без этого `#btnPause` «залипает» видимым за оверлеями).
+- Встроенный emoji-шрифт (Э28): все emoji UI и canvas рисует вшитый шрифт — субсет Noto Color Emoji (COLRv1, OFL) как base64 `@font-face font-family:'EmojiEmbedded'` в index.html между маркерами `` (внешний .woff2 невозможен: публикуется только один index.html). В font-stack `html,body` и во всех `ctx.font` (константа `UI_FONT`, секция «ОТРИСОВКА») стоит ПЕРВЫМ — единый Google-стиль emoji на всех платформах; headless-скриншоты не зависят от системных шрифтов. Первый кадр ждёт `document.fonts.load('16px "EmojiEmbedded"')` с timeout-фолбэком 1.5 с. Генератор `tools/emoji-font.py` (venv: `python3 -m venv tools/.venv && tools/.venv/bin/pip install fonttools brotli`; исходник `tools/fonts-src/NotoColorEmoji-Regular.ttf` из google/fonts, всё в gitignore): `python3 tools/emoji-font.py` — перегенерация по текущим emoji index.html (ОБЯЗАТЕЛЬНО при добавлении новых emoji), `--check` — падает, если субсет не покрывает index.html (✓ № ● ○ ↩ — в NCE их нет, это ок, рисуются системными шрифтами). Тест §Y7 (tests/test.js) гоняет `document.fonts.check('16px "EmojiEmbedded"','🪙')`; семейство переименовывать нельзя (тест + @font-face + UI_FONT).
## Запуск и публикация
diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md
index 86d90bc..88924f8 100644
--- a/docs/DEVLOG.md
+++ b/docs/DEVLOG.md
@@ -542,3 +542,28 @@ git add -A && git commit -m "что изменилось" && git push
**Заметки (осознанные решения и ловушки):**
- Самопроверка распаковывает встроенным zip-ридером, а не `unzip` — у скрипта ноль бинарных зависимостей помимо самого сборщика (`zip` → fallback `python3`).
- python3-фолбэк тоже deflate (проверено на Python 3.12) — встроенный ридер самопроверки понимает и stored, и deflate; по размеру разница несущественна (40.6 против 40.7 KB).
+
+## Этап 28. Встроенный emoji-шрифт (субсет Noto Color Emoji)
+
+**Проблема:** все emoji игры (UI-строки, таблицы данных, canvas `fillText` — 💥🌀⚡💪🐟💤) рисовались системным шрифтом платформы. На системах без emoji-шрифта (headless Linux) — квадраты/монохром (известный подводный камень для скриншотов), а вид emoji разнился между платформами (Segoe/Apple/Android). Публикация в Яндекс Игры — строго один `index.html` (треб. 1.22, `tools/pack.mjs`), поэтому внешний `.woff2` невозможен — шрифт встраивается в HTML.
+
+**Что сделано:**
+
+1. **`tools/emoji-font.py`** (Python; dev-зависимости fonttools+brotli в `tools/.venv`) — генератор встроенного шрифта: вытаскивает все emoji-кодопоинты из `index.html` (одна точка истины — сам файл игры; диапазонные правила по Unicode-категориям + явные одиночные: стрелки, U+FE0F), строит субсет через `fontTools.subset` (woff2, без hinting; legacy-таблица SVG отбрасывается — цвет только через COLRv1) и вписывает base64 `@font-face` в `index.html` между маркерами ``. Режим `--check` — регрессия: падает с листом недостающих кодопоинтов, если в `index.html` появился emoji, которого нет в субсете (требует только пересечение с cmap исходника — см. заметки).
+2. **index.html** — блок `@font-face font-family:'EmojiEmbedded'` (57 глифов, 78 КБ woff2 → +105 КБ base64; index.html 155→260 КБ). Шрифт стоит ПЕРВЫМ в font-stack `html,body` и во всех 10 `ctx.font` (константа `UI_FONT` в секции «ОТРИСОВКА») — единый Google-стиль emoji на всех платформах. Перед первым кадром — `document.fonts.load('16px "EmojiEmbedded"')` с timeout-фолбэком 1.5 с (canvas-emoji рисуются загруженным шрифтом; data: URI грузится за миллисекунды, фолбэк страхует от «зависания» первого кадра на file://).
+3. **tests/test.js** — секция §Y7: @font-face зарегистрирован в `document.fonts` + `document.fonts.check('16px "EmojiEmbedded"','🪙')` (шрифт реально резолвит glyph, а не молча падает в системный фоллбек).
+4. **`.gitignore`** — `tools/fonts-src/` (исходник Noto Color Emoji, 25 МБ) и `tools/.venv/`.
+
+**Почему Noto Color Emoji COLRv1 (2022+):** в игре есть emoji из Emoji 15.0 (2022) — 🪙 U+1FA99, 🪱 U+1FAB1, 🪝 U+1FA9D; старые версии шрифта их не знают. COLRv1 поддерживается всеми актуальными браузерами (Chrome/Firefox/Safari с 2023–2024).
+
+**Результат прогона:** `python3 tools/emoji-font.py` → 57 глифов, woff2 78 КБ; `python3 tools/emoji-font.py --check` — OK; `node tools/pack.mjs` — 5 PASS, zip 110.9 КБ (было ~40 КБ до шрифта; лимит требования 1.22 — 100 МБ).
+
+**Тесты:** `node tools/check.mjs` — OK; `cd tests && npm test` — **164/164 PASS, exit 0** (+2 новых §Y7); `node tests/pack-smoke.js` — **6/6**. Рендер-пробы (headless, canvas `fillText`): 🪙/⚠️/⏸/⭐ шрифтом «EmojiEmbedded» — цветные пиксели (не фоллбек).
+
+**Визуальная проверка:** все 14 кадров `node tests/shots.js` пересняты, `screenshots/` обновлены. Среда чекаута — системный emoji-шрифт Emoji One (не Noto): на кадрах виден именно встроенный Google-стиль (🎣🖱️ в меню, ⚡🐟💪 на канвасе вываживания, 🐠🪙🏅📺 в карточке, 🐋 у босса, 🌧 в HUD-пилюле, 🏆 в тосте ачивки) — доказательство, что рисуется встроенный шрифт, а не системный.
+
+**Заметки (осознанные решения и ловушки):**
+- ✓ (U+2713), №, ●, ○, ↩ — в Noto Color Emoji их нет (текстовые глифы) — рисуются системными шрифтами, как и раньше; `--check` это учитывает (требует только `wanted ∩ cmap(исходник)`).
+- ⚡ ⭐ ☀ ⚠ ⏸ ⚖ ⛵ ⚒ стали цветными emoji на всех платформах (на части систем раньше — монохромные) — осознанная унификация вида.
+- U+FE0F (variation selector, 18 вхождений в «⚠️») в субсете нет: NCE мапит U+26A0 прямо в цветной глиф, FE0F остаётся нулевым формат-знаком — «⚠️» рисуется цветным (рендер-проба).
+- Перегенерация субсета обязательна при добавлении новых emoji в index.html: `python3 tools/emoji-font.py` (реэкз в `tools/.venv`-интерпретатор встроен в скрипт); `--check` — защита от «добавил emoji, забыл перегенерировать».
diff --git a/index.html b/index.html
index 34e722b..8ffe675 100644
--- a/index.html
+++ b/index.html
@@ -6,9 +6,21 @@
Рыбалка
+
+
+
\n'
+ f'{END}'
+ )
+ if re.search(re.escape(BEGIN) + r'.*?' + re.escape(END), html, re.S):
+ html = re.sub(re.escape(BEGIN) + r'.*?' + re.escape(END), lambda _m: block, html, flags=re.S)
+ else:
+ html = html.replace('Рыбалка', f'Рыбалка\n{block}', 1)
+ if 'Рыбалка' not in html:
+ sys.exit('не удалось вставить блок: не найден Рыбалка и нет маркеров EMOJI-FONT')
+ HTML.write_text(html, encoding='utf-8')
+ print(f'OK: субсет {len(have)} глифов → woff2 {len(data)//1024} КБ → index.html +{(len(b64)//1024)} КБ base64')
+ print(f' кодопоинты: {", ".join(f"U+{cp:04X}" for cp in sorted(have & required))}')
+
+
+if __name__ == '__main__':
+ main()