47 lines
12 KiB
Markdown
47 lines
12 KiB
Markdown
# AGENTS.md
|
||
|
||
Игра — один файл `index.html` (HTML+CSS+JS, ~2200 строк): без сборки, зависимостей и бэкенда. Весь код в одном `<script>`, секции помечены комментариями. UI, комментарии и доки — на русском; при правках сохраняйте этот стиль.
|
||
|
||
## Проверка (lint/typecheck отсутствуют)
|
||
|
||
1. Синтаксис: `node tools/check.mjs` — вынимает JS из `index.html` и прогоняет `node --check` (при ошибке показывает номер строки index.html).
|
||
2. Главный рубеж — плейтест:
|
||
```bash
|
||
cd tests
|
||
npm install # puppeteer-core
|
||
npm test # = node test.js
|
||
```
|
||
Ждать все PASS и exit 0. Сейчас проверок 129.
|
||
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), иначе в скриншотах квадраты.
|
||
|
||
## Подводные камни
|
||
|
||
- `tests/test.js` берёт браузер из `process.env.CHROMIUM || '/usr/bin/chromium-browser'`, а URL игры резолвит динамически от каталога репо (тот же паттерн, что в `shots.js`). Если тест падает на старте — поправьте имя/путь chromium под свой чекаут (переменная `CHROMIUM`).
|
||
- Граница полуночи: если прогон стартует в пределах 5 минут от 00:00, test.js сам ждёт безопасное окно (дата не должна смениться посреди прогона — падают date-ассерты квестов/стрика). Это нормально, не баг.
|
||
- Тесты гоняют игру через внутренние глобалы в `page.evaluate`: `state`, `save`, `biteTimer`, `reel`, `SPECIES`, `SPECIES_NIGHT`, `rod()`, `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`, `WEATHER_GAP_MAX`, `WEATHER_DURATION`, `weatherActive()`, `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `RIVER_SPECIES`, `RIVER_SPECIES_NIGHT`, `LAKE_SPECIES`, `LAKE_SPECIES_NIGHT`, `SAVE_KEY`, `loadSave`, `freshSave`, `ACHIEVEMENTS`, `checkAchievements`, `questText` и имена состояний `MENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD`. Переименование любого из них ломает тесты молча — имена держать, либо обновлять тест в том же коммите.
|
||
- Ввод на Pointer Events (`pointerdown`/`pointerup`), а не touch/mouse — так тест управляет игрой мышью. Не заменять на тач-обработчики.
|
||
- Сейв: ключ localStorage `fishing-save-v1`. При изменении формата — миграцию добавлять в `loadSave()` (старые сейвы обязаны оставаться читаемыми).
|
||
|
||
## Где крутить
|
||
|
||
- Баланс (рыба/удилища/приманки): константы `SPECIES`, `RODS`, `LURES` в начале скрипта (базы: удочки 875/4500/15000, наживки 625/3000/11250). Апгрейды снастей (уровни 1..4): цена перехода — `upgCost(type,i,lv)`: id 0 — фикс `ROD_UPG_COST`/`LURE_UPG_COST` (70/210/500 и 60/160/400), id>0 — база×`UPG_MULT` (0.2/0.6/1.2, округление до 10); подписи уровней `ROD_UPG_NEXT`/`LURE_UPG_NEXT`, уровни `save.rodLv`/`save.lureLv`, эффективные статы — `effRod()`/`effLure()` (глобалы `rod()`/`lure()` отдают эффективные), гейтинг открытия — в `itemRow()`/`shopClick()`.
|
||
- Ночь (M3): длительность суток `DAY_LENGTH` (сек), ночная таблица рыб `SPECIES_NIGHT`, фонарик — апгрейд 1 любого удилища (`save.rodLv[save.rod]>=2`, проверка `hasLight()` в ветке WAIT, секция «M3: Цикл дня/ночи»).
|
||
- Босс (M4): тест-форс появления `forceBoss`, фазы боя `stateBoss`/`bossPhase`, босс-ветки в `updateReel()` (обрыв фазы) и `SNAP` в `updateGame()` (авто-переход), награда ×5 в `catchFish()`.
|
||
- Варианты рыб (M6): таблица `VARIANTS` (порядок ролла: gold → silver → rainbow, иначе normal), `variantChance()` — единственная точка вероятностей (дождь M8 уже здесь — ×2,5, см. строку «Погода (M8)»; gold — живая `SHINY_CHANCE`), тест-форс `forceVariant`, босс всегда normal. Рыбу нельзя улучшать: «Улучшить вариант» (M6) убрано в этапе 21 — карточка улова только бейдж/оформление по варианту.
|
||
- Погода (M8): таблица `WEATHER` (rain — `variantMult` ×2,5; fog — `biteMult` ×1,5), таймеры в реальном времени — `updateWeather()` в `frame()` (gap `WEATHER_GAP_MIN..MAX` 120–240 с, длительность `WEATHER_DURATION` 120 с, под форсом не гоняются), `weatherActive()` — единственная точка чтения (форс приоритетнее), эффекты только в `variantChance()` (rain) и `biteChance()` (fog), тест-форс `forceWeather` ('rain'/'fog'/null, паттерн `forceBoss`); погода не персистится в сейв. Заметка: туман может дать p>1 → насыщение 100% клёва (осознанно).
|
||
- Скины (M-skins): таблицы `BOAT_SKINS`/`BOBBER_SKINS` (лодка: Обычная 0 / «Красная лодка» 800 / «Лодка-домик» 2000 🪙; поплавок: Обычный 0 / Красный 500 / Неоновый 1500 🪙), сейв `save.boatSkin(s)`/`save.bobberSkin(s)` + миграция в `loadSave()` (включая гарантию бесплатного скина 0), вкладка «Скины» 🎨 (`#tabSkins`, строка `skinRow(list,i,owned,equipped,group)`, ветки buyskin/select в `shopClick()`: покупка со списанием и авто-экипировкой, выбор — только купленное), отрисовка `drawBoat()`/`drawBobber()` с clamp 0..2 (скин 2: каюта у лодки, неоновое свечение `shadowBlur` в save/restore у поплавка); чистая косметика — без эффектов на геймплей.
|
||
- Локации (этап 5): таблица `LOCATIONS` (Река 0 — бесплатно / «Озёра» 100000 🪙; `water` — 6 hex [dayTop,dayMid,dayBot,nightTop,nightMid,nightBot] для градиента `drawWater()`, nightMix-логика та же), сейв `save.locations[]`/`save.location` + миграция в `loadSave()` (гарантия бесплатной Реки, паттерн скинов), вкладка «Локации» 🗺️ (`#tabLocations`, строка `locationRow(loc,i,owned,selected)`, ветки buylocation/selectlocation в `shopClick()`: покупка — списание, push, АВТО-ПЕРЕХОД `save.location`, тост «Добро пожаловать на Озёра!»; выбор — только купленное). Таблицы рыб: `RIVER_SPECIES(_NIGHT)` — ТОЛЬКО 8 базовых видов (роллы реки не меняются: pin-тесты §9j(b)/M4/M6 завязаны на веса и порядок — новые виды в реку НЕ добавлять!), `LAKE_SPECIES(_NIGHT)` — все 11 видов (веса `LAKE_DAY_WEIGHTS`/`LAKE_NIGHT_WEIGHTS`); `SPECIES`/`SPECIES_NIGHT` — полный словарь 11 видов (новые: zander/chuch/crayfish, ночные веса 6/10/2.5). `pickSpecies()` выбирает таблицу по `save.location` и dayPhase (логика ролла та же); босс (sturge) есть в обеих локациях; ачивка species_8 — теперь «Поймай все виды» (все 11, id не менять — старые сейвы).
|
||
- Сложность вываживания: `updateReel()`.
|
||
- Физика боя (спринт 4): `fishForce()` (сила рыбы: рывок `(16+34·st)·(0.5+0.9·ag)·(0.7+0.5·ov)`, затишье `5+7·ag`, уставшая ×0.45) и `updateReel()` — выносливость `st` (старт `st0`, флаг `tired` при `st≤0.12`: рывки ×0.45, втягивание быстрее до ×1.6), тормоз катушки (черта `drag` в `RODS`: в рывке выше тормоза леска скользит `over·0.22`), натяжение (`10+over·1.1+22·ag`, ×0.35 в затишье), втягивание `(11+3·reel)·(1+0.6·(1−st))`, свободный бег `F·0.32`, escape-бар растёт только при затишье без удержания (отпускание в рывок безопасно), near-miss `1.45·d0`, `d0=45+7·w`. Пустые клёвы: ролл `biteChance()` в ветке WAIT (тест-форс `forceBite`). Тесты (§10) завязаны на эти константы — при ребалансе гонять Monte-Carlo точными формулами (docs/PHYSICS-RESEARCH.md, §4.7) и править тесты в том же коммите.
|
||
- Награды (M2): шаблоны квестов `QUEST_TPLS`, награды стрика `STREAK_REWARDS`, достижения `ACHIEVEMENTS` — секция «M2. Награды» после сейва. Этап 22: ачивок 29 (идут `ACHIEVEMENTS`-записями по темам: снасти у экономики, варианты/погода/вес/ночь у рыболовных, скины/локации у магазинных); новые счётчики сейва `save.kgTotal`/`nightCatch`/`wxRainV`/`wxFog` (чтение через `||0` — без миграции) ставятся в `catchFish()` до `checkAchievements()` (rain-счётчик — только вариант ≠ normal, босс всегда normal); тест-пин §10c(8): сумма `achBonus().coinPct` (все 8 видов × 4 варианта) = 0.16.
|
||
|
||
## Запуск и публикация
|
||
|
||
- Dev-сервер: `python3 -m http.server 8000`; игра так же работает с `file://`.
|
||
- Процесс разработки, публикация в Gitea, гигиена секретов: `docs/DEVLOG.md`.
|