28 KiB
KODA.md — инструкционный контекст проекта «Рыбалка»
Этот файл — рабочая шпаргалка для ИИ-агентов и разработчиков: что за проект, как его собирать, проверять и правильно править. Основной источник истины по деталям —
AGENTS.mdиdocs/DEVLOG.md; документ, который вы читаете, сводит их воедино.
1. Обзор проекта
«Рыбалка» — браузерная мобильная игра про рыбалку, опубликованная как автономный файл index.html (HTML + CSS + JS в одном <script>, ~2940 строк). Ни сборки, ни зависимостей, ни бэкенда: файл открывается двойным кликом (file://) или раздаётся любым статическим хостингом. Целевая площадка публикации — Яндекс Игры (SDK подхватывается автоматически, локально работает встроенный стаб).
Ключевые архитектурные решения
| Решение | Зачем |
|---|---|
Один самодостаточный index.html |
Нет сборки и зависимостей; требование площадки — строго один файл в архиве |
Вся графика рисуется на <canvas> |
Работает офлайн, грузится мгновенно, нет внешних ассетов |
Вшитый emoji-шрифт (base64 @font-face) |
Единый вид emoji на всех платформах; внешний .woff2 невозможен (один файл) |
| HTML-оверлеи для UI, Canvas — только мир | Резкие доступные кнопки и текст; игровой мир — 60 fps |
Звук — генеративный WebAudio, вибрация — navigator.vibrate |
Ноль внешних ресурсов |
Прогресс в localStorage (ключ fishing-save-v1) |
Мобильная игра без аккаунтов и сервера; миграция старых сейвов обязательна |
| Портрет, управление одним пальцем, Pointer Events | Работают и палец, и мышь — на этом держатся автотесты |
Игровой цикл
бросок (удержание = мощность) → ожидание → клёв («!») → подсечка (тап в окно)
→ вываживание (тяни / отпускай) → {улов / обрыв шнура / срыв} → монеты → лавка
Машина состояний: MENU → IDLE → CHARGE → CAST → WAIT → APPROACH → BITE → REEL → SNAP / CARD → IDLE.
Основные технологии
- «Чистый» JavaScript современного стандарта (
'use strict', стрелочные функции), без фреймворков и сборщиков. - Canvas 2D — весь мир: небо, вода, лодка, поплавок, оснастка, рыбы, частицы, метры.
- WebAudio API — 12 генеративных эффектов;
navigator.vibrate— тактильный отклик. - SDK Яндекс Игр (
ysdk) через единственную обёрткуYG+ собственный стаб-режим. - Инструментарий проверки: Node.js (без зависимостей), Chromium +
puppeteer-core, Python +fonttools/brotli.
Структура index.html (секции помечены комментариями-разделителями)
| Секция | О чём |
|---|---|
ДАННЫЕ |
SPECIES, SPECIES_NIGHT, LOCATIONS (5 локаций, этап 45 — +Болото), RIVER_* / LAKE_* / SEA_* / OCEAN_* / SWAMP_* + табличный SPECIES_BY_LOCATION(_NIGHT), BOSS_BY_LOCATION (босс каждой локации, этап 37; +Болото, этап 45), VARIANTS, WEATHER, RODS, LURES, апгрейды, скины. Здесь крутится баланс |
СОХРАНЕНИЕ |
save, freshSave(), loadSave() (со всеми миграциями), persist() |
M3: ЦИКЛ ДНЯ/НОЧИ |
DAY_LENGTH, dayTime, dayPhase, setDayTime() |
M8: ПОГОДА |
WEATHER, таймеры, weatherActive(), updateWeather() |
M2: НАГРАДЫ |
квесты дня (QUEST_TPLS), стрик (STREAK_REWARDS), 38 достижений (ACHIEVEMENTS), achBonus() |
ХОЛСТ И РАЗМЕРЫ |
resize(), DPR, layout (waterY, boatX) |
ЗВУК |
ac(), beep(), noiseBurst(), sfx(), vib() |
СОСТОЯНИЕ ИГРЫ |
все мутабельные глобалы геймплея |
ЛОГИКА |
pickSpecies(), biteChance(), rollWeight(), hook(), fishForce(), updateReel(), catchFish(), updateGame() |
ОТРИСОВКА |
UI_FONT, drawSky/Water/Scenery/AmbientFish/Boat/Bobber/Meters/Weather(), свечение Болота (drawGlowAura()/fishGlow()/GLOW_RGBA), декор drawSwampDecor() (pads/frogs), draw() |
АКВАРИУМ (Этап 46) |
aqOpen, initAquarium()/updateAquarium()/drawAquarium(), aqTap()/aqReleaseSel(), слоты (AQ_BASE_SLOTS/AQ_MAX_SLOTS/AQ_SLOT_COSTS), DOM-слой портретов #aqFishLayer (aqBuildFishEl()/aqPlaceFish()/aqFishEls, вариант .var<id>/--f1) |
Y1: SDK Яндекс Игр |
обёртка YG, реклама (tryInterstitial(), #btnRv), пауза платформы |
Y5: i18n RU/EN |
таблица T, t()/tt(), setLang(), resolveLang(), renderMenu() |
ЦИКЛ |
frame() — requestAnimationFrame, дельта-тайм, пауза |
ВВОД |
pointerdown / pointerup |
UI: МАГАЗИН / ДНЕВНИК / КАРТОЧКА |
6 вкладок лавки, карточка улова |
КНОПКИ, СТАРТ |
обработчики, инициализация, ожидание emoji-шрифта |
Контент игры (кратко)
- 37 видов рыб (0.05–100 кг, 5 уровней редкости, цена 4–1500 🪙/кг; этап 38 — лестница веса: Озёра ×8, Море ×6, Океан ×4, кит ×2; этап 39 — цены Озёр/Моря/Океана урезаны), пять локаций со СВОЕЙ ихтиофауной (без пересечений, этапы 35/36/45; по цене воды: Река → Озёра → Болото → Море → Океан): Река — 8 базовых (бесплатно), Озёра — 10 озёрных (65 536 🪙; zander/chuch/crayfish этап 5, bleak/roundel/eelpout/tench/navaga этап 33, rudd/cisco этап 35), Море — 5 морских (131 072 🪙 = ×2 Озёр; herring/mackerel/flounder/tuna/swordfish этап 35), Океан — 9 глубоководных (196 608 🪙 = ×1.5 Моря; moray/grouper/turtle/angler/octopus/manta/shark/squid/whale этап 36), Волшебное болото — 5 светящихся (90 000 🪙; glowfin/moonfish/emberfin/violetlure/spiritfin этап 45, ночью светятся разными цветами).
- 8 вариантов рыбы: обычная, медная ×1.5, серебряная ×2, кварцевая ×2.5, золотая ×3, изумрудная ×4, алмазная ×4.5, радужная ×5 (топ). P(особой) ≈ 31%, шанс усиливают наживка и дождь.
- 4 удочки и 4 приманки, каждая с апгрейдами до 4 уровня (фонарик для ночи, леска, тормоз, катушка, клёв, редкие/крупные).
- Цикл суток ~5 минут, погода (дождь 🌧️ ×3 к особым, туман 🌫️ ×1.5 к клёву), босс в каждой локации (Река — осётр, Озёра — гигантский рак, Море — рыба-меч, Океан — синий кит, Болото — дух болота; босс весит 20–50 кг) в 3 фазы (награда ×5).
- Награды: 3 квеста дня (детерминированы от даты), стрик входов до 7 дней со «щитом», 38 достижений с постоянными бонусами.
- Лавка с вкладками: Лавка 🛒, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️. Плюс хаб «Меню»
#gamemenuиз паузы. - Аквариум 🐠 (этап 46): декоративная сцена коллекции экземпляров (вид+вариант+вес), вход из хаба «Меню»; кнопка «В аквариум» на карточке улова кладёт бесплатную копию (модель C — экономика не затронута), слоты 6→12 за монеты (1500…100000 🪙), тап по рыбе — поповер с весом/редкостью и «Выпустить» (без монет).
- Монетизация: interstitial (2 мин сессии, кулдаун 3 мин) и rewarded «×2 за улов».
- Локализация RU/EN через
ysdk.environment.i18n.lang.
2. Сборка, запуск и проверка
Запуск игры
# Просто открыть в браузере
xdg-open index.html # или открыть файл руками (file:// работает)
# Локальный dev-сервер
python3 -m http.server 8000 # → http://localhost:8000
# Отладка с реальным SDK Яндекса локально
# открыть с ?sdk=1 и положить sdk.js рядом с index.html
Линт и типы
Отсутствуют. Сборки, package.json в корне, ESLint/TypeScript/Prettier нет. Единственная статическая проверка — синтаксис JS, вынутый из HTML:
node tools/check.mjs # блок-за-блоком прогоняет JS через node --check
# при ошибке печатает номер строки в index.html
Автотесты (главный рубеж)
cd tests
npm install # ставит puppeteer-core
npm test # = node test.js → ждать 222/222 PASS и exit 0
- Браузер берётся из
process.env.CHROMIUM || '/usr/bin/chromium-browser'— при необходимости поправьте путь. - URL игры резолвится от каталога репо; если тест падает на старте — проверьте путь к Chromium.
- Граница полуночи: если прогон стартует в пределах 5 минут от 00:00,
test.jsсам ждёт безопасного окна (иначе падают date-ассерты квестов/стрика). Это не баг. - Тест управляет игрой мышью (Pointer Events) и читает/подталкивает внутренние глобалы через
page.evaluate.
Прочие проверки и артефакты
node tests/gen-screenshots.js [каталог-сборки] [папка-вывода] [язык]
# 15 кадров × 3 вьюпорта (десктоп 1280×720, моб. портрет 360×640@2x,
# моб. ландшафт 800×450@1.6x) = 45 PNG, 24-бит без альфа.
# Без аргументов → screenshots-wiki/ (RU); с 'en' → screenshots-wiki-en/.
# README ссылается на screenshots-wiki/mobp-* (мобильный портрет).
node tests/orca-playtest.js # браузерный плейтест через Orca CLI (76 проверок).
# Требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND).
# Если Orca не запущен — шаг ПРОПУСКАЕТСЯ и НЕ блокирует коммит.
# Пишет orca-*.png в каталог запуска. Только stdlib Node.
node tools/pack.mjs # → dist/fishing-yandex.zip + самопроверка 5 пунктов
node tests/pack-smoke.js # smoke играбельности КОПИИ из архива, 6 проверок
Перегенерация встроенного emoji-шрифта
python3 -m venv tools/.venv && tools/.venv/bin/pip install fonttools brotli
python3 tools/emoji-font.py # ОБЯЗАТЕЛЬНО при добавлении новых emoji
python3 tools/emoji-font.py --check # падает, если субсет не покрывает index.html
Исходник шрифта — tools/fonts-src/NotoColorEmoji-Regular.ttf (в .gitignore). Блок @font-face в index.html заключён в маркеры <!-- EMOJI-FONT:BEGIN/END --> и руками не редактируется.
Публикация
node tools/pack.mjs # сборка архива (только index.html в корне) + 5 самопроверок
node tests/pack-smoke.js # прогон игры из распакованной копии
# затем загрузить dist/fishing-yandex.zip в консоль Яндекс Игр:
# монетизация — включена, облачные сейвы — выключены, платформы — без Android TV,
# debug-mode=16 — для проверки лоадера
Публикация в Gitea (git.shstk.ru): git push; токен — кратковременный, после push отзывается, в репозиторий и документы попадают только плейсхолдеры <ТОКЕН>.
3. Правила разработки
Стиль и соглашения
- Весь UI, комментарии и документация — на русском. При правках сохраняйте этот стиль (EN-строки живут только в i18n-таблицах
*En/T.en). - Один файл, один
<script>,'use strict'; новый код размещать внутри соответствующей помеченной секции. - Компактный «плотный» синтаксис (короткие имена, однострочные ветки) — исторически сложившийся стиль проекта; новые функции именовать понятно и в духе соседнего кода (
updateReel,variantChance,drawMeters). - Каждая фича помечена меткой этапа (
M2,M3,M4,M5,M6,M8,Y1…Y5,Этап 5/22/28/29/…) — новые правки тоже подписывать, откуда они. - Ввод — только Pointer Events (
pointerdown/pointerup); заменять на touch/mouse-обработчики нельзя (на них держатся тесты). - Любая новая величина, влияющая на баланс (шансы, силы, цены), — в константах секции
ДАННЫЕ, а не разбросана по логике. Единственные «точки истины»:variantChance()— все вероятности вариантов,weatherActive()— все чтения погоды,fishForce()/updateReel()— физика боя,effRod()/effLure()— эффективные статы снастей.
Обязательный цикл изменений
# 1. поправить index.html
node tools/check.mjs # 2. синтаксис
cd tests && npm install && npm test # 3. главный рубеж: ждать 222/222 PASS, exit 0
node tools/emoji-font.py # (если добавляли emoji) + --check
node tests/gen-screenshots.js # 4. переснять скриншоты, если менялась картинка
node tools/pack.mjs && node tests/pack-smoke.js # 5. перед публикацией
git add -A && git commit -m "…" && git push
После правок index.html или tests/test.js плейтест запускать обязательно.
Тесты и их ограничения
- Тесты гоняют игру через внутренние глобалы в
page.evaluate:state,save,biteTimer,reel,SPECIES,SPECIES_NIGHT,rod(),lure(),SHINY_CHANCE,forceBoss,forceBite,forceVariant,VARIANTS,rollVariant(),variantChance(),fishForce(),stateBoss,bossPhase,dayPhase,pickSpecies(),setDayTime(),todayStr(),dateShift(),isoWeek(),achBonus(),forceWeather,weather,weatherT,weatherGap,WEATHER,WEATHER_GAP_MIN/MAX,WEATHER_DURATION,weatherActive(),BOAT_SKINS,BOBBER_SKINS,LOCATIONS,RIVER_SPECIES(_NIGHT),LAKE_SPECIES(_NIGHT),SEA_SPECIES(_NIGHT),OCEAN_SPECIES(_NIGHT),SWAMP_SPECIES(_NIGHT)(+*_IDS/*_WEIGHTSлокаций),SAVE_KEY,loadSave,freshSave,ACHIEVEMENTS,checkAchievements,questText,YG(+YG.counts),forceLang,setLang(),LANG,trName(),trRarity(),pauseOpen,uiOpen,dayTimeи имена состоянийMENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD; аквариум (этап 46):aqOpen,aqFishRt,aqSel,aqTank,AQ_BASE_SLOTS,AQ_MAX_SLOTS,AQ_SLOT_COSTS,AQ_VARIANT_RECOLOR,AQ_VARIANT_CLASS,aqApplyVariant,aqFishEls,aqBuildFishEl,aqPlaceFish,initAquarium,updateAquarium,drawAquarium,aqFishSize,aqTap,showAqPop,hideAqPop,renderAqBar,aqReleaseSel,openAquarium,closeAquarium,syncKeepBtn,cardSpec,save.aqFish/save.aqSlots. Переименование любого из них ломает тесты молча — имена держать, либо обновлять тест в том же коммите. - Числа проверок зафиксированы в README (
222/222вtests/test.js,76/76в Orca,6/6в pack-smoke) — при добавлении проверок обновлять доки в том же коммите. - Изменение физики вываживания требует Monte-Carlo-проверки точными формулами (см.
docs/PHYSICS-RESEARCH.md, §4.7) и синхронной правки тестов. - Пины шансов в тестах снимаются на нейтральной снасти (снапшот/рестор
save.lure/lureLvвнутри eval). - В браузере Orca (живой плейтест) нельзя использовать фиксированные
sleepдля ожидания состояний — rAF троттлится; только polling с таймаутом.
Сейвы и обратная совместимость
- Ключ
localStorage—fishing-save-v1, не менять. - Любое изменение формата сейва → новая миграция в
loadSave(). Старые сейвы обязаны оставаться читаемыми; новые счётчики желательно читать через||0(тогда миграция не нужна). - Не менять
idсуществующих записей (SPECIES,ACHIEVEMENTS,VARIANTS,QUEST_TPLS) — на них завязаны старые сейвы и пины тестов. - Река (
RIVER_SPECIES(_NIGHT)) — только 8 базовых видов: новые виды добавлять лишь в таблицы СВОЕЙ воды (*_IDS+*_DAY_WEIGHTS/*_NIGHT_WEIGHTS, как Болото этапа 45), иначе сломаются роллы и пины тестов. - Новые виды — только в КОНЕЦ
SPECIES(послеspiritfin, этап 45):SPECIES[0..7]запинены тестами и квестами, вставка в середину ломает их молча. Новому id обязательны ключи вNIGHT_WEIGHTSи в дневной/ночной таблице весов его локации (*_DAY_WEIGHTS/*_NIGHT_WEIGHTS); тест lc4 (3000 роллов локации) вызываетpickSpecies()без аргумента — штрафminDistне действует, так что для гарантии выпадения нужен вес ≥ ~2.2. Новая локация → запись вLOCATIONS, табличныйSPECIES_BY_LOCATION(_NIGHT)(порядок =LOCATIONS) и guard «вне 0..4 → Река» вpickSpecies()(этап 45). Новый emoji → перегенерация субсетаtools/emoji-font.py. - Баланс «стало дороже» правится сдвигом долей, а не масштабом.
pickSpecies()нормирует веса → ожидаемая выручка за улов (Σ(w·V)/Σw, V = середина веса × цена) не меняется от умножения всех весов на константу. Пины при нейтральной снасти: Река 65.1/125.9 · Озёра 194.1/249.0 · Болото 240.3/328.9 · Море 297.3/476.7 · Океан 329.1/552.7 🪙 (день/ночь) — считать черезtools/balance.mjs(10 таблиц, сводка по ЦЕНЕ воды: Река → Озёра → Болото → Море → Океан); при правке*_WEIGHTSдержать инвариант «новая (по цене) вода доходнее предыдущей в ОБЕ фазы» и мин. вес вида ≥ ~2.2 (иначе lc4 начинает плавать).
Известные «грабли»
#gamemenuидёт в DOM после#shop/#resetDlgпри том жеz-index— при открытии лавки/сброса из «Меню» гменю надо прятать (флагshopFromMenu).- Видимость
#btnPause/#btnRecallсинхронится вsyncDom()каждый кадр — иначе кнопка «залипает» под оверлеями. catchFish()обнуляетreel— все обращения кreelвupdateGame()под гардомif(state==='REEL'&&reel).shadowBlur(неоновый поплавок, свечение босса) всегда в пареsave/restore.save.caught[id]— объект{n, best, cp, sv, qz, sn, em, dm, rb}(не число);snисторически = «shiny» = gold-вариант.- Аквариум:
#aqBar{pointer-events:none}+.aqpill{pointer-events:auto}(иначе невидимая полоса перехватывает тапы по сцене),body.aq-openпрячет#hud, сентинелaqSel=-1, «выпустить» — без монет (продажу из банки не вводить). Жильцы — DOM-слой#aqFishLayer(pointer-events:none,body.aq-open,clip-pathпо банке), индексaqFishElsобязан совпадать сsave.aqFish/aqFishRt(вinitAquariumбитый вид даётnull, но не сдвигает индексы;aqReleaseSelудаляет DOM точечно) — иначе рыбы «перепутаются». - Пустой клёв и «ночь без фонарика» — легальные состояния ветки
WAIT, а не ошибки;forceBiteне работает ночью безhasLight(). - Headless Chromium в этом чекауте может не видеть
/tmpпоfile://(smoke авто-откатывается во временный каталог под$HOME).
Гигиена секретов
- Секреты (
.gitea_token, логи, PDF) — в.gitignore; в код, коммиты и документы попадают только плейсхолдеры. - Токены Gitea — кратковременные, с минимальными правами, после push отзываются; если токен передавался в URL — удалять его из
git remoteсразу после push.
4. Карта файлов
| Путь | Назначение |
|---|---|
index.html |
Вся игра: HTML + CSS + JS + встроенный emoji-шрифт |
README.md |
Описание игры для игрока/читателя, «как играть», команды запуска и публикации |
AGENTS.md |
Инструкция для агентов: порядок проверок, подводные камни, «где крутить» баланс |
docs/DEVLOG.md |
Журнал разработки: этапы 1–46, найденные баги, решения, Monte-Carlo |
docs/BROWSER-TESTING.md |
Плейтест через Orca CLI: таблица шагов, 13 подводных камней, справочник CLI |
docs/PHYSICS-RESEARCH.md |
Исследование физики вываживания и формулы баланса |
tests/test.js |
Главный автотест (headless Chromium + puppeteer-core), 222 проверок |
tests/gen-screenshots.js |
Генератор скриншотов wiki: 15 кадров × 3 вьюпорта, RU/EN |
tests/orca-playtest.js |
Плейтест в живом браузере Orca через CLI, 76 проверок |
tests/pack-smoke.js |
Smoke-прогон игры из распакованного архива, 6 проверок |
tests/package.json |
Зависимость puppeteer-core; скрипты test, shots |
tools/check.mjs |
Синтаксическая проверка JS из index.html (node --check) |
tools/pack.mjs |
Сборка dist/fishing-yandex.zip + 5 самопроверок (архив, размер, имена, sha256, синтаксис) |
tools/emoji-font.py |
Генерация/проверка встроенного субсета Noto Color Emoji |
screenshots-wiki/, screenshots-wiki-en/ |
Кадры для README (RU и EN): desk-*, mobl-*, mobp-* |
dist/ |
Артефакт сборки (в .gitignore) |
5. Быстрые ориентиры «где менять»
| Задача | Куда смотреть |
|---|---|
| Баланс рыбы/удилищ/приманок | SPECIES, RODS, LURES, цены апгрейдов upgCost() / UPG_MULT |
| Шансы особых рыб | VARIANTS, variantChance(), variantBoost(), SHINY_CHANCE |
| Сложность и физика боя | updateReel(), fishForce(), константы внутри hook() (d0, st0, drag) |
| Ночь и фонарик | DAY_LENGTH, NIGHT_WEIGHTS / LAKE_NIGHT_WEIGHTS / SWAMP_NIGHT_WEIGHTS, hasLight(), ветка WAIT |
| Погода | WEATHER, updateWeather(), weatherActive() |
| Квесты / стрик / достижения | QUEST_TPLS, STREAK_REWARDS, ACHIEVEMENTS, achBonus() |
| Скины и локации | BOAT_SKINS, BOBBER_SKINS, LOCATIONS, RIVER_* / LAKE_* / SEA_* / OCEAN_* / SWAMP_*, свечение/декор Болота (glow, drawGlowAura(), drawSwampDecor()) |
| Аквариум | секция АКВАРИУМ (Этап 46), AQ_BASE_SLOTS/AQ_MAX_SLOTS/AQ_SLOT_COSTS, initAquarium()/drawAquarium(), #btnKeep/syncKeepBtn(), openAquarium() |
| Тексты интерфейса | таблица T = {ru, en}, t() / tt(), renderMenu() |
| SDK и реклама | секция Y1: SDK Яндекс Игр, tryInterstitial(), YG.showRewarded |
| Миграции сейвов | loadSave() |
| Отрисовка | секция ОТРИСОВКА, draw(), UI_FONT |